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 }