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 }