1 ///
2 module isodi.pack_list;
3 
4 import std.conv;
5 import std.file;
6 import std.path;
7 import std.array;
8 import std.string;
9 import std.random;
10 import std.typecons;
11 import std.exception;
12 import std.container;
13 
14 import isodi.bind;
15 import isodi.pack;
16 import isodi.model;
17 import isodi.resource;
18 import isodi.exceptions;
19 
20 
21 @safe:
22 
23 
24 /// Represents a pack list.
25 abstract class PackList {
26 
27     /// Underlying pack list.
28     Pack[] packList;
29     alias packList this;
30 
31     /// Result of globbing functions.
32     ///
33     /// $(UL
34     ///     $(LI `matches` — Matched objects)
35     ///     $(LI `pack` — Pack the files come from)
36     /// )
37     alias GlobResult(T) = Tuple!(
38         T[],   "matches",
39         Pack*, "pack",
40     );
41 
42     alias Resource = Pack.Resource;
43 
44     private {
45 
46         GlobResult!string[string] packGlobCache;
47         Resource!(SkeletonNode[])[string] getSkeletonCache;
48         Resource!(AnimationPart[])[string] getAnimationCache;
49 
50     }
51 
52     ~this() {
53 
54         clearCache();
55 
56     }
57 
58     /// Create a pack list for the current renderer.
59     static PackList make() {
60 
61         return Renderer.createPackList();
62 
63     }
64 
65     /// Create a pack list for the current renderer.
66     /// Params:
67     ///     packs = Preload the list with given packs.
68     static PackList make(Pack[] packs...) {
69 
70         auto list = make();
71         list.packList = packs.dup;
72         list.clearCache();
73         return list;
74 
75     }
76 
77     /// Clear resource cache. Call when the list contents were changed or reordered.
78     ///
79     /// When overriding, make sure to call `super.clearCache()`.
80     abstract void clearCache() @trusted {
81 
82         // Note on @trusted: cached values are pointers, and nothing should directly refer to the cache.
83         // We want to ensure children are also @safe.
84 
85         packGlobCache.clear();
86         getSkeletonCache.clear();
87 
88     }
89 
90     /// List matching files in the first matching pack.
91     ///
92     /// This function is `@system` because it returns a pointer to a `Pack` struct from this list, which might become
93     /// invalidated when manipulating the pack list.
94     ///
95     /// Params:
96     ///     path = File path to look for.
97     /// Returns: A `GlobResult` tuple with the result.
98     /// Throws: `IsodiException` if the path wasn't found in any of the packs.
99     GlobResult!string packGlob(string path) @system {
100 
101         // Attempt to read from the cache
102         if (auto cached = path in packGlobCache) {
103             return *cached;
104         }
105 
106         // Not in the cache, load it
107         foreach (ref pack; packList) {
108 
109             auto glob = pack.glob(path);
110 
111             // Found a match
112             if (glob.length) {
113 
114                 return packGlobCache[path] = GlobResult!string(glob, &pack);
115 
116             }
117 
118         }
119 
120         throw new PackException(path.format!"Texture %s wasn't found in any pack");
121 
122     }
123 
124     // Barely a unittest, needs more packs to work
125     @system unittest {
126 
127         auto packs = PackList.make(
128             getPack("res/samerion-retro/pack.json")
129         );
130 
131         // Get a list of grass textures
132         auto glob = packs.packGlob("cells/grass/tile/*.png");
133         assert(glob.pack.name == "SamerionRetro");
134         assert(glob.matches.length);
135 
136         // Check all
137         foreach (file; glob.matches) {
138 
139             assert(file.endsWith(".png"));
140 
141         }
142 
143     }
144 
145     /// Get a random resource under a file matching the pattern.
146     /// Params:
147     ///     path = File path to look for.
148     ///     seed = Seed for the RNG.
149     /// Returns: A tuple with path to the file and options of the resource.
150     /// Throws: `IsodiException` if the path wasn't found in any of the packs.
151     Resource!string randomGlob(RNG)(string path, RNG rng) @trusted
152     if (isUniformRNG!RNG) {
153 
154         auto result = packGlob(path);
155         auto resource = result.matches.choice(rng);
156 
157         return Resource!string(
158             resource,
159             result.pack.getOptions(resource),
160         );
161 
162     }
163 
164     /// Load the given skeleton.
165     /// Params:
166     ///     name = Name of the skeleton.
167     /// Returns:
168     ///     A `Resource` tuple, first item is a list of the skeleton's nodes.
169     Resource!(SkeletonNode[]) getSkeleton(string name) {
170 
171         // Attempt to read from the cache
172         if (auto cached = name in getSkeletonCache) {
173             return *cached;
174         }
175 
176         return packSearch!"getSkeleton"(
177             name,
178             name.format!"Skeleton %s wasn't found in any listed pack"
179         );
180 
181     }
182 
183     /// Load the given animation.
184     /// Params:
185     ///     name = Name of the animation.
186     ///     frameCount = Frame count of the animation.
187     /// Returns: A `Resource` tuple, first item is a list of animation parts.
188     Resource!(AnimationPart[]) getAnimation(string name, out uint frameCount) {
189 
190         if (auto cached = name in getAnimationCache) {
191             return *cached;
192         }
193 
194         return packSearch!"getAnimation"(
195             name, frameCount,
196             name.format!"Animation '%s' wasn't found in any listed pack"
197         );
198 
199     }
200 
201     private auto packSearch(string method, Args...)(ref Args args, lazy string fail) {
202 
203         /// Check each pack
204         foreach (ref pack; packList) {
205 
206             // Attempt to load the method
207             try return mixin("pack." ~ method ~ "(args)");
208 
209             // If failed, continue to the next pack
210             catch (PackException) continue;
211 
212         }
213 
214         throw new PackException(fail);
215 
216     }
217 
218     /// List cells available in all the packs.
219     /// Returns: A `RedBlackTree!string`, guarantying unique elements.
220     auto listCells() const @trusted {
221 
222         auto rbtree = redBlackTree!string;
223 
224         foreach (ref pack; packList) {
225 
226             // Add all cells from the pack
227             rbtree.insert(pack.listCells);
228 
229         }
230 
231         return rbtree;
232 
233     }
234 
235     unittest {
236 
237         import std.algorithm;
238 
239         auto packList = PackList.make(
240             getPack("res/samerion-retro/pack.json")
241         );
242         assert(packList.listCells[].canFind("grass"));
243 
244     }
245 
246 }