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 }