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 }