1 /// This module implements basic pack loading. 2 /// 3 /// See_Also: 4 /// `isodi.pack` 5 module isodi.pack_json; 6 7 import std; // Too many imports, stopped making sense to list them. Sorry. // Probably not true anymore? TODO 8 import rcdata.json; 9 10 import isodi.pack; 11 import isodi.internal; 12 import isodi.exceptions; 13 14 15 @safe: 16 17 18 /// Read the pack directly from the JSON parser. 19 /// 20 /// Note, this will not fill out the `path` property of the `Pack` struct, which is required to read resources. 21 /// Use the other overload to fill it automatically. 22 /// 23 /// Params: 24 /// json = `JSONParser` instance to fetch data from. 25 /// Throws: `rcdata.json.JSONException` on type mismatch or type error 26 Pack getPack(ref JSONParser json) @trusted { 27 28 JSONParser[wstring] options; 29 30 auto pack = json.getStruct!Pack((ref Pack obj, wstring key) { 31 32 // Global options 33 if (key == "options") { 34 35 // Alias to fileOptions[""] 36 options[""] = json.save; 37 json.skipValue(); 38 39 } 40 41 // Local options 42 else if (key == "fileOptions") { 43 44 // Check each path 45 foreach (path; json.getObject) { 46 47 // Save the state 48 options[path.strip("/")] = json.save; 49 json.skipValue(); 50 51 } 52 53 } 54 55 // Unknown field, crash instead 56 else enforce!PackException(0, key.format!"Unknown pack key \"%s\""); 57 58 }); 59 60 // Handle inheritance — iterate on items sorted by length 61 foreach (path; options.byKey.array.sort!`a.length < b.length`) { 62 63 ResourceOptions builder; 64 65 // Get all ancestors of this item 66 foreach (ancestorPath; path.Ancestors) { 67 68 // If the ancestor exists 69 if (auto p = ancestorPath in options) { 70 71 // Restore state 72 auto state = *p; 73 74 // Update the struct 75 builder = state.updateStruct(builder); 76 77 } 78 79 } 80 81 // Save it 82 pack.fileOptions[path.to!string] = builder; 83 84 } 85 86 // Before ending, make sure at least the root resource exists 87 pack.fileOptions.require("", ResourceOptions()); 88 89 return pack; 90 91 } 92 93 /// Read the pack data from a JSON file. 94 /// Params: 95 /// filename = Name of the file to read from. 96 /// Throws: `rcdata.json.JSONException` on type mismatch or type error 97 Pack getPack(string filename) { 98 99 // It might be a directory 100 if (filename.isDir) { 101 102 // Read pack.json by default 103 filename = filename.buildPath("pack.json"); 104 105 } 106 107 // Get the pack 108 auto json = filename.readText.JSONParser(); 109 auto pack = json.getPack; 110 pack.path = filename.dirName; 111 return pack; 112 113 } 114 115 unittest { 116 117 // Load the pack 118 auto pack = getPack("res/samerion-retro/pack.json"); 119 120 // Access properties 121 assert(pack.name == "SamerionRetro"); 122 123 // Check the options of 124 const rootOptions = pack.fileOptions[""]; 125 assert(!rootOptions.interpolate); 126 assert(rootOptions.tileSize == 32); 127 128 const grassOptions = pack.fileOptions["cells/grass"]; 129 assert(!grassOptions.interpolate); 130 assert(grassOptions.tileSize == 32); 131 assert(grassOptions.decorationWeight == 20); 132 133 } 134 135 /// Read options of the given resource. 136 /// Params: 137 /// pack = Pack to read from. 138 /// path = Relative path to the resource. 139 /// Returns: A pointer to the resource's options. 140 ResourceOptions* getOptions(Pack pack, string path) { 141 142 /// Search for the closest matching resource 143 foreach (file; path.stripRight("/").DeepAncestors) { 144 145 // Return the first one found 146 if (auto p = file in pack.fileOptions) return p; 147 148 } 149 150 assert(0, pack.name.format!"Internal error: Root options missing for pack %s"); 151 152 } 153 154 /// 155 unittest { 156 157 // Load the pack 158 auto pack = getPack("res/samerion-retro/pack.json"); 159 160 // Check root options 161 const rootOptions = pack.getOptions(""); 162 assert(!rootOptions.interpolate); 163 assert(rootOptions.tileSize == 32); 164 165 // Check if getOptions correctly handles resources that don't have any options set directly 166 assert(pack.getOptions("cells/grass") is pack.getOptions("cells/grass/not-existing")); 167 168 }