1 /** 2 Grimoire 3 Copyright (c) 2017 Enalye 4 5 This software is provided 'as-is', without any express or implied warranty. 6 In no event will the authors be held liable for any damages arising 7 from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute 11 it freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; 14 you must not claim that you wrote the original software. 15 If you use this software in a product, an acknowledgment 16 in the product documentation would be appreciated but 17 is not required. 18 19 2. Altered source versions must be plainly marked as such, 20 and must not be misrepresented as being the original software. 21 22 3. This notice may not be removed or altered from any source distribution. 23 */ 24 25 module common.resource; 26 27 import std.typecons; 28 import std.file; 29 import std.path; 30 import std.algorithm: count; 31 import std.conv: to; 32 33 import core.util; 34 import core.json; 35 import core.vec2; 36 import render.all; 37 import audio.all; 38 39 private { 40 void*[string] _caches; 41 string[string] _cachesSubFolder; 42 string _dataFolder = "./"; 43 } 44 45 void loadResources() { 46 //Path to 'data/' 47 auto path = buildNormalizedPath(absolutePath(_dataFolder)); 48 49 auto textureCache = new DataCache!Texture(path, getResourceSubFolder!Texture, "*.{png,bmp,jpg}"); 50 auto fontCache = new DataCache!Font(path, getResourceSubFolder!Font, "*.{ttf}"); 51 auto spriteCache = new SpriteCache!Sprite(path, getResourceSubFolder!Sprite, "*.{sprite}", textureCache); 52 auto tilesetCache = new TilesetCache!Tileset(path, getResourceSubFolder!Tileset, "*.{tileset}", textureCache); 53 auto soundCache = new DataCache!Sound(path, getResourceSubFolder!Sound, "*.{wav,ogg}"); 54 auto musicCache = new DataCache!Music(path, getResourceSubFolder!Music, "*.{wav,ogg,mp3}"); 55 56 setResourceCache!Texture(textureCache); 57 setResourceCache!Font(fontCache); 58 setResourceCache!Sprite(spriteCache); 59 setResourceCache!Tileset(tilesetCache); 60 setResourceCache!Sound(soundCache); 61 setResourceCache!Music(musicCache); 62 } 63 64 void setResourceFolder(string dataFolder) { 65 _dataFolder = dataFolder; 66 } 67 68 string getResourceFolder() { 69 return buildNormalizedPath(absolutePath(_dataFolder)); 70 } 71 72 void setResourceSubFolder(T)(string subFolder) { 73 _cachesSubFolder[T.stringof] = subFolder; 74 } 75 76 string getResourceSubFolder(T)() { 77 auto subFolder = T.stringof in _cachesSubFolder; 78 if(!subFolder) 79 return ""; 80 return *subFolder; 81 } 82 83 void setResourceCache(T)(ResourceCache!T cache) { 84 _caches[T.stringof] = cast(void*)cache; 85 } 86 87 void getResourceCache(T)() { 88 auto cache = T.stringof in _caches; 89 if(!cache) 90 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 91 return cast(ResourceCache!T)(*cache); 92 } 93 94 bool canFetch(T)(string name) { 95 auto cache = T.stringof in _caches; 96 if(!cache) 97 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 98 return (cast(ResourceCache!T)*cache).canGet(name); 99 } 100 101 bool canFetchPack(T)(string name = ".") { 102 auto cache = T.stringof in _caches; 103 if(!cache) 104 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 105 return (cast(ResourceCache!T)*cache).canGetPack(name); 106 } 107 108 T fetch(T)(string name) { 109 auto cache = T.stringof in _caches; 110 if(!cache) 111 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 112 return (cast(ResourceCache!T)*cache).get(name); 113 } 114 115 T[] fetchPack(T)(string name = ".") { 116 auto cache = T.stringof in _caches; 117 if(!cache) 118 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 119 return (cast(ResourceCache!T)*cache).getPack(name); 120 } 121 122 string[] fetchPackNames(T)(string name = ".") { 123 auto cache = T.stringof in _caches; 124 if(!cache) 125 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 126 return (cast(ResourceCache!T)*cache).getPackNames(name); 127 } 128 129 Tuple!(T, string)[] fetchPackTuples(T)(string name = ".") { 130 auto cache = T.stringof in _caches; 131 if(!cache) 132 throw new Exception("No cache of type \'" ~ T.stringof ~ "\' has been declared"); 133 return (cast(ResourceCache!T)*cache).getPackTuples(name); 134 } 135 136 private class ResourceCache(T) { 137 protected { 138 Tuple!(T, string)[] _data; 139 uint[string] _ids; 140 uint[][string] _packs; 141 } 142 143 protected this() {} 144 145 bool canGet(string name) { 146 return (buildNormalizedPath(name) in _ids) !is null; 147 } 148 149 bool canGetPack(string pack = ".") { 150 return (buildNormalizedPath(pack) in _packs) !is null; 151 } 152 153 T get(string name) { 154 name = buildNormalizedPath(name); 155 156 auto p = (name in _ids); 157 if(p is null) 158 throw new Exception("Resource: no \'" ~ name ~ "\' loaded"); 159 return _data[*p][0]; 160 } 161 162 T[] getPack(string pack = ".") { 163 pack = buildNormalizedPath(pack); 164 165 auto p = (pack in _packs); 166 if(p is null) 167 throw new Exception("Resource: no pack \'" ~ pack ~ "\' loaded"); 168 169 T[] result; 170 foreach(i; *p) 171 result ~= _data[i][0]; 172 return result; 173 } 174 175 string[] getPackNames(string pack = ".") { 176 pack = buildNormalizedPath(pack); 177 178 auto p = (pack in _packs); 179 if(p is null) 180 throw new Exception("Resource: no pack \'" ~ pack ~ "\' loaded"); 181 182 string[] result; 183 foreach(i; *p) 184 result ~= _data[i][1]; 185 return result; 186 } 187 188 Tuple!(T, string)[] getPackTuples(string pack = ".") { 189 pack = buildNormalizedPath(pack); 190 191 auto p = (pack in _packs); 192 if(p is null) 193 throw new Exception("Resource: no pack \'" ~ pack ~ "\' loaded"); 194 195 Tuple!(T, string)[] result; 196 foreach(i; *p) 197 result ~= _data[i]; 198 return result; 199 } 200 } 201 202 class DataCache(T): ResourceCache!T { 203 this(string path, string sub, string filter) { 204 path = buildPath(path, sub); 205 206 if(!exists(path) || !isDir(path)) 207 throw new Exception("The specified path is not a valid directory: \'" ~ path ~ "\'"); 208 auto files = dirEntries(path, filter, SpanMode.depth); 209 foreach(file; files) { 210 string relativeFileName = stripExtension(relativePath(file, path)); 211 string folder = dirName(relativeFileName); 212 uint id = cast(uint)_data.length; 213 214 _packs[folder] ~= id; 215 _ids[relativeFileName] = id; 216 _data ~= tuple(new T(file), relativeFileName); 217 } 218 } 219 } 220 221 private class SpriteCache(T): ResourceCache!T { 222 this(string path, string sub, string filter, ResourceCache!Texture cache) { 223 path = buildPath(path, sub); 224 225 if(!exists(path) || !isDir(path)) 226 throw new Exception("The specified path is not a valid directory: \'" ~ path ~ "\'"); 227 auto files = dirEntries(path, filter, SpanMode.depth); 228 foreach(file; files) { 229 string relativeFileName = stripExtension(relativePath(file, path)); 230 string folder = dirName(relativeFileName); 231 232 auto texture = cache.get(relativeFileName); 233 loadJson(file, texture); 234 } 235 } 236 237 private void loadJson(string file, Texture texture) { 238 auto sheetJson = parseJSON(readText(file)); 239 foreach(string tag, JSONValue value; sheetJson.object) { 240 if((tag in _ids) !is null) 241 throw new Exception("Duplicate sprite defined \'" ~ tag ~ "\' in \'" ~ file ~ "\'"); 242 T sprite = T(texture); 243 244 //Clip 245 sprite.clip.x = getJsonInt(value, "x"); 246 sprite.clip.y = getJsonInt(value, "y"); 247 sprite.clip.z = getJsonInt(value, "w"); 248 sprite.clip.w = getJsonInt(value, "h"); 249 250 //Size/scale 251 sprite.size = to!Vec2f(sprite.clip.zw); 252 sprite.size *= Vec2f(getJsonFloat(value, "scalex", 1f), getJsonFloat(value, "scaley", 1f)); 253 254 //Flip 255 bool flipH = getJsonBool(value, "fliph", false); 256 bool flipV = getJsonBool(value, "flipv", false); 257 258 if(flipH && flipV) 259 sprite.flip = Flip.BothFlip; 260 else if(flipH) 261 sprite.flip = Flip.HorizontalFlip; 262 else if(flipV) 263 sprite.flip = Flip.VerticalFlip; 264 else 265 sprite.flip = Flip.NoFlip; 266 267 //Center expressed in texels, it does the same thing as Anchor 268 Vec2f center = Vec2f(getJsonFloat(value, "centerx", -1f), getJsonFloat(value, "centery", -1f)); 269 if(center.x > -.5f) //Temp 270 sprite.anchor.x = center.x / cast(float)(sprite.clip.z); 271 if(center.y > -.5f) 272 sprite.anchor.y = center.y / cast(float)(sprite.clip.w); 273 274 //Anchor, same as Center but uses a relative coordinate system where [.5,.5] is the center 275 if(center.x < 0f) //Temp 276 sprite.anchor.x = getJsonFloat(value, "anchorx", .5f); 277 if(center.y < 0f) 278 sprite.anchor.y = getJsonFloat(value, "anchory", .5f); 279 280 //Type 281 string type = getJsonStr(value, "type", "."); 282 283 //Register sprite 284 uint id = cast(uint)_data.length; 285 _packs[type] ~= id; 286 _ids[tag] = id; 287 _data ~= tuple(sprite, tag); 288 } 289 } 290 } 291 292 private class TilesetCache(T): ResourceCache!T { 293 this(string path, string sub, string filter, ResourceCache!Texture cache) { 294 path = buildPath(path, sub); 295 296 if(!exists(path) || !isDir(path)) 297 throw new Exception("The specified path is not a valid directory: \'" ~ path ~ "\'"); 298 auto files = dirEntries(path, filter, SpanMode.depth); 299 foreach(file; files) { 300 string relativeFileName = stripExtension(relativePath(file, path)); 301 string folder = dirName(relativeFileName); 302 303 auto texture = cache.get(relativeFileName); 304 loadJson(file, texture); 305 } 306 } 307 308 private void loadJson(string file, Texture texture) { 309 auto sheetJson = parseJSON(readText(file)); 310 foreach(string tag, JSONValue value; sheetJson.object) { 311 if((tag in _ids) !is null) 312 throw new Exception("Duplicate tileset defined \'" ~ tag ~ "\' in \'" ~ file ~ "\'"); 313 Vec2i grid, offset, tileSize; 314 int nbTiles; 315 316 //Max number of tiles the tileset cannot exceeds 317 nbTiles = getJsonInt(value, "tiles", -1); 318 319 //Upper left border of the tileset 320 offset.x = getJsonInt(value, "x", 0); 321 offset.y = getJsonInt(value, "y", 0); 322 323 //Tile size 324 tileSize.x = getJsonInt(value, "w"); 325 tileSize.y = getJsonInt(value, "h"); 326 327 grid.x = getJsonInt(value, "columns", 1); 328 grid.y = getJsonInt(value, "lines", 1); 329 330 string type = getJsonStr(value, "type", "."); 331 332 T tileset = T(texture, offset, grid, tileSize, nbTiles); 333 tileset.scale = Vec2f(getJsonFloat(value, "scalex", 1f), getJsonFloat(value, "scaley", 1f)); 334 335 //Flip 336 bool flipH = getJsonBool(value, "fliph", false); 337 bool flipV = getJsonBool(value, "flipv", false); 338 339 if(flipH && flipV) 340 tileset.flip = Flip.BothFlip; 341 else if(flipH) 342 tileset.flip = Flip.HorizontalFlip; 343 else if(flipV) 344 tileset.flip = Flip.VerticalFlip; 345 else 346 tileset.flip = Flip.NoFlip; 347 348 uint id = cast(uint)_data.length; 349 _packs[type] ~= id; 350 _ids[tag] = id; 351 _data ~= tuple(tileset, tag); 352 } 353 } 354 }