1 /// This module implements pack loading.
2 ///
3 /// See_Also:
4 ///     `isodi.resources.pack`
5 module isodi.resources.pack_json;
6 
7 import raylib;
8 import rcdata.json;
9 
10 import std.conv;
11 import std.file;
12 import std.path;
13 import std.array;
14 import std.string;
15 import std.algorithm;
16 
17 import isodi.utils;
18 import isodi.skeleton;
19 import isodi.exception;
20 import isodi.resources.pack;
21 import isodi.resources.loader;
22 
23 
24 @safe:
25 
26 
27 /// Read the pack directly from the JSON parser.
28 ///
29 /// Note, this will not fill out the `path` property of the `Pack` struct, which is required to read resources.
30 /// Use the other overload to fill it automatically.
31 ///
32 /// Params:
33 ///     json = `JSONParser` instance to fetch data from.
34 /// Throws: `rcdata.json.JSONException` on type mismatch or type error
35 Pack getPack(ref JSONParser json) @trusted {
36 
37     JSONParser[wstring] options;
38 
39     auto pack = json.getStruct!Pack((ref Pack obj, wstring key) {
40 
41         // Global options
42         if (key == "options") {
43 
44             // Alias to fileOptions[""]
45             options[""] = json.save;
46             json.skipValue();
47 
48         }
49 
50         // Local options
51         else if (key == "fileOptions") {
52 
53             // Check each path
54             foreach (path; json.getObject) {
55 
56                 // Save the state
57                 options[path.strip("/").stripExtension] = json.save;
58                 json.skipValue();
59 
60             }
61 
62         }
63 
64         // Unknown field, crash instead
65         else enforce!PackException(0, key.format!"Unknown pack key \"%s\"");
66 
67     });
68 
69     // Handle inheritance — iterate on items sorted by length
70     foreach (path; options.byKey.array.sort!`a.length < b.length`) {
71 
72         ResourceOptions builder;
73 
74         // Get all ancestors of this item
75         foreach (ancestorPath; path.Ancestors) {
76 
77             // If the ancestor exists
78             if (auto p = ancestorPath in options) {
79 
80                 // Restore state
81                 auto state = *p;
82 
83                 // Update the struct
84                 builder = state.updateStruct(builder);
85 
86             }
87 
88         }
89 
90         // Save it
91         pack.fileOptions[path.to!string] = builder;
92 
93     }
94 
95     // Before ending, make sure at least the root resource exists
96     pack.fileOptions.require("", ResourceOptions());
97 
98     return pack;
99 
100 }
101 
102 /// Read the pack data from a JSON file.
103 /// Params:
104 ///     filename = Name of the file to read from.
105 /// Throws: `rcdata.json.JSONException` on type mismatch or type error
106 Pack getPack(string filename) {
107 
108     // It might be a directory
109     if (filename.isDir) {
110 
111         // Read pack.json by default
112         filename = filename.buildPath("pack.json");
113 
114     }
115 
116     // Get the pack
117     auto json = filename.readText.JSONParser();
118     auto pack = json.getPack;
119     pack.path = filename.dirName;
120     return pack;
121 
122 }
123 
124 unittest {
125 
126     // Load the pack
127     auto pack = getPack("res/samerion-retro/pack.json");
128 
129     // Access properties
130     assert(pack.name == "SamerionRetro");
131 
132     // Check the options of
133     const rootOptions = pack.fileOptions[""];
134     assert(!rootOptions.interpolate);
135     assert(rootOptions.tileSize == 32);
136 
137     const grassOptions = pack.fileOptions["block"];
138     assert(!grassOptions.interpolate);
139     assert(grassOptions.tileSize == 32);
140     assert(grassOptions.sideArea == [0, 32, 128, 96]);
141 
142 }
143 
144 /// Parse a bone set from a JSON string.
145 BoneUV[BoneType] parseBoneSet(string json, BoneType delegate(wstring) @safe bonePicker) @trusted {
146 
147     auto parser = JSONParser(json);
148     BoneUV[BoneType] result;
149 
150     // Get each
151     foreach (key; parser.getObject) {
152 
153         // Get the bone type
154         const type = bonePicker(key);
155 
156         // Load the value
157         const uv = parser.get!(int[4]).reinterpret!RectangleI;
158 
159         // Save the UV
160         result[type] = BoneUV(uv);
161         // TODO support variants
162 
163     }
164 
165     return result;
166 
167 }
168 
169 unittest {
170 
171     auto pack = new Pack();
172     auto boneSet = parseBoneSet(q{
173         {
174             "torso": [1, 1, 56, 16],
175             "head": [1, 18, 40, 14],
176             "thigh": [43, 18, 20, 12],
177             "abdomen": [1, 33, 31, 7],
178             "upper-arm": [43, 31, 20, 13],
179             "lower-leg": [1, 41, 12, 9],
180             "hips": [1, 52, 40, 6],
181             "hand": [43, 45, 20, 3],
182             "forearm": [51, 49, 12, 9],
183             "foot": [11, 59, 52, 4]
184         }
185     }, bone => pack.boneType("model", bone.to!string));
186 
187     assert(boneSet[BoneType(0)] == BoneUV(RectangleI(1, 1, 56, 16)));
188 
189     const forearm = pack.boneType("model", "forearm");
190     assert(boneSet[forearm] == BoneUV(RectangleI(51, 49, 12, 9)));
191 
192 }
193 
194 Bone[] parseSkeleton(BoneType delegate(wstring) @safe bonePicker, string json) {
195 
196     auto parser = JSONParser(json);
197     return parseSkeletonImpl(bonePicker, parser, 0, 0);
198 
199 }
200 
201 Bone[] parseSkeletonImpl(BoneType delegate(wstring) @safe bonePicker, ref JSONParser parser, size_t parent,
202 size_t index, float divisor = 1) @trusted
203 do {
204 
205     auto result = [Bone(index, BoneType.init, parent, MatrixIdentity)];
206     size_t childIndex = index + 1;
207 
208     foreach (key; parser.getObject) {
209 
210         switch (key) {
211 
212             case "name":
213                 result[0].type = bonePicker(parser.getString);
214                 break;
215 
216             case "matrix":
217                 result[0].transform = parser.get!(float[16]).reinterpret!Matrix;
218                 break;
219 
220             case "transform":
221 
222                 // Get the transform matrix
223                 auto rhs = parser.parseTransforms;
224 
225                 // Reduce the translations
226                 rhs.m12 /= divisor;
227                 rhs.m13 /= divisor;
228                 rhs.m14 /= divisor;
229 
230                 result[0].transform = mul(
231                     result[0].transform,
232                     rhs,
233                 );
234                 break;
235 
236             case "vector":
237                 result[0].vector = parser.get!(float[3]).reinterpret!Vector3 / divisor;
238                 break;
239 
240             case "children":
241                 foreach (child; parser.getArray) {
242 
243                     // Add the child
244                     const ret = parseSkeletonImpl(bonePicker, parser, index, childIndex, divisor);
245                     result ~= ret;
246 
247                     // Advance the index
248                     childIndex += ret.length;
249 
250                 }
251                 break;
252 
253             case "divisor":
254 
255                 // TODO It would be nice to remove those restrictions
256                 enforce!PackException(result[0].transform == MatrixIdentity,
257                     "`divisor` must precede `matrix` & `transform` fields");
258                 enforce!PackException(result[0].vector == Vector3.init,
259                     "`divisor` must precede the `vector` field");
260 
261                 divisor = parser.get!float;
262                 break;
263 
264             default:
265                 throw new PackException(format!"Unknown key '%s' on line %s"(key, parser.lineNumber));
266 
267         }
268 
269     }
270 
271     return result;
272 
273 }
274 
275 /// Parse a JSON transform list.
276 ///
277 /// Available transforms:
278 /// * `["translate", float x, float y, float z]`
279 /// * `["rotate", float angle, float x, float y, float z]`; angle in degrees, x,y,z multiplier
280 /// * `["rotateX", float angle]`, `["rotateY", float]`, `["rotateZ", float]`; angle in degrees
281 /// * `["scale", float x, float y, float z]`
282 Matrix parseTransforms(ref JSONParser parser) @trusted {
283 
284     auto result = MatrixIdentity;
285 
286     enum TransformType {
287         translate,
288         rotate,
289         rotateX,
290         rotateY,
291         rotateZ,
292         scale,
293     }
294 
295     // Get each option in the array
296     foreach (_; parser.getArray) {
297 
298         TransformType type;
299         float[4] values;
300 
301         // Run the transform
302         foreach (i; parser.getArray) {
303 
304             // First entry: transform type
305             if (i == 0) {
306 
307                 // Set the type
308                 type = parser.get!string.to!TransformType;
309                 continue;
310 
311             }
312 
313             assert(i < 5, format!"Too many arguments for '%s'"(type));
314 
315             // Other entries, set vector value
316             values[i-1] = parser.get!float;
317 
318         }
319 
320         // Perform the transform
321         with (TransformType) {
322 
323             const rhs = type.predSwitch(
324                 translate, MatrixTranslate(values.structof.tupleof[0..3]),
325                 rotate,    MatrixRotate(Vector3(values.structof.tupleof[1..4]), values[0] * 180 / PI),
326                 rotateX,   MatrixRotateX(values[0] * 180 / PI),
327                 rotateY,   MatrixRotateY(values[0] * 180 / PI),
328                 rotateZ,   MatrixRotateZ(values[0] * 180 / PI),
329                 scale,     MatrixScale(values.structof.tupleof[0..3]),
330             );
331 
332             result = mul(result, rhs);
333 
334         }
335 
336     }
337 
338     return result;
339 
340 }