1 /// This module contains structs containing data relating to packs.
2 module isodi.resources.pack;
3 
4 import raylib;
5 import rcdata.json;
6 
7 import std.conv;
8 import std.path;
9 import std.file;
10 import std.string;
11 
12 import isodi.chunk;
13 import isodi.utils;
14 import isodi.skeleton;
15 import isodi.exception;
16 import isodi.resources.loader;
17 
18 public import isodi.resources.pack_json;
19 
20 
21 @safe:
22 
23 
24 /// Represents a pack.
25 ///
26 /// To read a pack from a JSON file, use `getPack`. This must be used from the renderer thread.
27 class Pack : ResourceLoader {
28 
29     public {
30 
31         /// Path to the pack directory in the filesystem.
32         @JSONExclude
33         string path;
34 
35         /// Name of the pack.
36         string name;
37 
38         /// Description of the pack.
39         string description;
40 
41         /// Version of the pack.
42         string packVersion;
43 
44         /// Targeted Isodi version
45         string isodiVersion;
46 
47         /// License of the pack.
48         string license;
49 
50         /// Block types registered in the pack.
51         @JSONExclude
52         BlockType[string] blockTypes;
53 
54         /// Bone types registered in the pack.
55         @JSONExclude
56         BoneType[AbsoluteBoneType] boneTypes;
57 
58         /// Next block type ID to use.
59         @JSONExclude
60         size_t nextBlockType;
61 
62         /// Next bone type ID to use.
63         @JSONExclude
64         size_t nextBoneType;
65 
66         /// Option fields applied to specific files. Filename extensions are to be omitted in keys.
67         ///
68         /// If no option is found for a file, the entry of its parent directory will be checked, then the grandparent
69         /// and so on until an empty string.
70         ///
71         /// In JSON, the `options` property is an alias to an empty string key. Additionally, the JSON parser will
72         /// inherit fields from the parent entries instead of using `.init` values.
73         ///
74         /// Fields missing in the JSON will be inherited from parent directories or will use the default value.
75         @JSONExclude
76         ResourceOptions[string] fileOptions;
77 
78 
79     }
80 
81     // Caches.
82     static private {
83 
84         struct CacheEntry(UV, Type) {
85 
86             Texture2D texture;
87             UV[Type] uv;
88 
89         }
90 
91         /// Image cache. `path => image`
92         Image[string] imageCache;
93 
94         /// Block texture cache
95         CacheEntry!(BlockUV, BlockType)[string[]] blockCache;
96 
97         /// Bone texture cache.
98         CacheEntry!(BoneUV, BoneType)[string[]] boneCache;
99 
100     }
101 
102     private {
103 
104         /// Bone set cache
105         BoneUV[BoneType][string] boneSetCache;
106 
107     }
108 
109     static ~this() @trusted {
110 
111         // OpenGL enforces thread-local context access. GPU texture references are not usable from other threads, so
112         // destroying those textures should be safe, as references on other threads are invalid anyway, so as a result,
113         // no live references will be invalidated.
114 
115         destroyGlobalCache();
116 
117     }
118 
119     /// Free all textures loaded into the GPU and clear the image cache. Make sure to remove all references to those
120     /// textures before calling.
121     ///
122     /// This is automatically done when the rendering thread is freed.
123     static void destroyGlobalCache() @system {
124 
125         import std.range, std.algorithm;
126 
127         // Empty the cache when done
128         scope (success) {
129             imageCache = null;
130             blockCache = null;
131             boneCache = null;
132         }
133 
134         // If the window isn't open, there aren't any textures to free.
135         if (!IsWindowReady) return;
136 
137         // Unload images
138         foreach (image; imageCache) {
139 
140             UnloadImage(image);
141 
142         }
143 
144         // Unload all textures
145         foreach (texture; chain(blockCache.byValue.map!"a.texture", boneCache.byValue.map!"a.texture")) {
146 
147             UnloadTexture(texture);
148 
149         }
150 
151     }
152 
153     /// Destroy the bone set cache.
154     void destroyLocalCache() {
155 
156         boneSetCache = null;
157 
158     }
159 
160     /// Destroy both global and local cache.
161     void destroyAllCache() @system {
162 
163         destroyGlobalCache();
164         destroyLocalCache();
165 
166     }
167 
168     /// Load an image by a filesystem path (from cache or filesystem).
169     ///
170     /// The image will be loaded into cache. Stored data must not be freed.
171     static Image loadImageStatic(string path) {
172 
173         // Load from cache
174         if (auto image = path in imageCache) {
175 
176             return *image;
177 
178         }
179 
180         // Load from filesystem
181         else {
182 
183             // Load the texture
184             auto image = (() @trusted => LoadImage(path.toStringz))();
185 
186             // Write to cache
187             imageCache[path] = image;
188 
189             return image;
190 
191         }
192 
193     }
194 
195     /// Get a filesystem path given a pack path.
196     string globalPath(string file) const => buildPath(path, file);
197 
198     /// Load an image by a path relative to the pack. (from cache or filesystem).
199     ///
200     /// The image will be loaded into the cache. Image data must not be freed.
201     Image loadImage(string file) const {
202 
203         return loadImageStatic(globalPath(file));
204 
205     }
206 
207     /// Glob search within the pack.
208     string[] glob(string file) @trusted const {
209 
210         import std.array : array;
211 
212         // Get paths to the resource
213         const resPath = buildPath(path, file);
214         const resDir = resPath.dirName;
215 
216         // Return an empty set if the directory doesn't exist
217         if (!resDir.exists || !resDir.isDir) return null;
218 
219         // List all files inside
220         return resDir.dirEntries(resPath.baseName, SpanMode.shallow).array.to!(string[]);
221 
222     }
223 
224     /// Load a block.
225     ///
226     /// Returned texture is stored in the cache. Texture data must not be freed.
227     Texture blockTexture(string[] names, out BlockUV[BlockType] uv)
228     in (false)  // inherit contracts
229          => modelTexture!(BlockUV, BlockType, "block/%s.png", blockCache, blockAtlas)(names, uv);
230 
231     /// Load a bone.
232     ///
233     /// Returned texture is stored in the cache. Texture data must not be freed.
234     Texture boneSetTexture(string[] name, out BoneUV[BoneType] uv)
235     in (false)  // inherit contracts
236         => modelTexture!(BoneUV, BoneType, "bone/%s.png", boneCache, boneSetAtlas)(name, uv);
237 
238 
239     /// Load a model texture.
240     Texture2D modelTexture(UV, Type, string path, alias cache, alias atlasLoader)(string[] names, out UV[Type] uv) {
241 
242         // This texture has already been loaded
243         if (auto entry = names in cache) {
244 
245             uv = entry.uv;
246             return entry.texture;
247 
248         }
249 
250         Image[] images;
251         UV[Type][] maps;
252 
253         images.reserve(names.length);
254         maps.reserve(names.length);
255 
256         // Check each texture
257         foreach (i, name; names) {
258 
259             // Load the image
260             images ~= loadImage(format!path(name));
261 
262             // Load the options
263             maps ~= atlasLoader(name);
264 
265         }
266 
267         // Pack the images
268         auto image = packImages(maps, images, uv);
269         auto texture = (() @trusted => LoadTextureFromImage(image))();
270 
271         cache[cast(const) names] = CacheEntry!(UV, Type)(texture, uv);
272 
273         return texture;
274 
275     }
276 
277     /// Load a chunk UV map.
278     BlockUV[BlockType] blockAtlas(string name) => [
279         blockType(name): options(ResourceType.block, name).blockUV
280     ];
281 
282     /// Load a bone UV map.
283     BoneUV[BoneType] boneSetAtlas(string name) {
284 
285         // Check the cache first
286         if (auto set = name in boneSetCache) return *set;
287 
288         const resPath = globalPath(format!"bone/%s.json"(name));
289 
290         // Try to read from file
291         try {
292 
293             auto set = parseBoneSet(resPath.readText, bone => boneType(name, bone.to!string));
294 
295             // Write to cache
296             boneSetCache[name] = set;
297 
298             return set;
299 
300         }
301 
302         // Oops.
303         catch (Exception exc) {
304 
305             // Convert all exceptions to PackException.
306             throw new PackException(format!"Failed to load boneSet %s from file '%s'; %s"(name, resPath, exc.msg));
307 
308         }
309 
310     }
311 
312     /// Get the block type for the given block string. Registers a new block type if the path doesn't exist.
313     BlockType blockType(string block)
314 
315         => blockTypes.require(
316             block,
317             BlockType(nextBlockType++),
318         );
319 
320 
321     /// Get the bone type for the given model/bone strings. Registers a new bone type if the specified one wasn't
322     /// registered before.
323     BoneType boneType(string boneSet, string bone)
324 
325         => boneTypes.require(
326             AbsoluteBoneType(boneSet, bone),
327             BoneType(nextBoneType++),
328         );
329 
330 
331     /// Load a skeleton.
332     /// Params:
333     ///     name = Name for the skeleton.
334     ///     boneSet = Bone set to use.
335     ///     bonePicker = Delegate to determine what bone type to use for each bone.
336     Bone[] skeleton(string name, string boneSet)
337 
338         => skeleton(name, bone => boneType(boneSet, bone.to!string));
339 
340 
341     Bone[] skeleton(string name, BoneType delegate(wstring) @safe bonePicker) const {
342 
343         const resPath = globalPath(format!"skeleton/%s.json"(name));
344 
345         // Try to read & parse the file
346         try return parseSkeleton(bonePicker, resPath.readText);
347 
348         // Oops.
349         catch (Exception exc) {
350 
351             // Convert all exceptions to PackException.
352             throw new PackException(format!"Failed to load skeleton %s from file '%s'; %s"(name, resPath, exc.msg));
353 
354         }
355 
356     }
357 
358     ///
359     const(ResourceOptions)* options(ResourceType type, string res) const {
360 
361         const path = format!"%s/%s"(type, res);
362 
363         /// Search for the closest matching resource
364         foreach (file; path.stripRight("/").DeepAncestors) {
365 
366             // Return the first one found
367             if (auto p = file in fileOptions) return p;
368 
369         }
370 
371         assert(0, name.format!"Internal error: Root options missing for pack %s");
372 
373     }
374 
375     ///
376     unittest {
377 
378         // Load the pack
379         auto pack = getPack("res/samerion-retro/pack.json");
380 
381         enum blockType = ResourceType.block;
382 
383         // Check root options
384         const rootOptions = pack.options(blockType, "");
385         assert(!rootOptions.interpolate);
386         assert(rootOptions.tileSize == 32);
387 
388         // Check if `options` correctly handles resources that don't have any options set directly
389         assert(pack.options(blockType, "grass") is pack.options(blockType, "grass/not-existing"));
390 
391     }
392 
393 }
394 
395 struct AbsoluteBoneType {
396 
397     string boneset;
398     string bone;
399 
400 }