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 }