1 /*
2  *******************************************************************************************
3  * Dgame (a D game framework) - Copyright (c) Randy Schütt
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 from
7  * 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 it
11  * freely, subject to the following restrictions:
12  * 
13  * 1. The origin of this software must not be misrepresented; you must not claim
14  *    that you wrote the original software. If you use this software in a product,
15  *    an acknowledgment in the product documentation would be appreciated but is
16  *    not required.
17  * 
18  * 2. Altered source versions must be plainly marked as such, and must not be
19  *    misrepresented as being the original software.
20  * 
21  * 3. This notice may not be removed or altered from any source distribution.
22  *******************************************************************************************
23  */
24 module Dgame.Graphics.TileMap;
25 
26 private {
27 	import std.math : log2, pow, round, ceil, floor, fmax;
28 	import std.file : exists;
29 	import std.path : dirName;
30 	
31 	import derelict.opengl3.gl;
32 	import derelict.sdl2.sdl;
33 
34 	import Dgame.Internal.Log;
35 
36 	import Dgame.Math.Vector2;
37 	import Dgame.Math.Rect;
38 	import Dgame.Graphics.Drawable;
39 	import Dgame.Graphics.Transformable;
40 	import Dgame.Graphics.Shape;
41 	import Dgame.Graphics.Surface;
42 	import Dgame.Graphics.Texture;
43 	import Dgame.System.VertexBufferObject;
44 }
45 
46 /**
47  * This structure stores information about the tile map properties
48  */
49 struct TileMapInfo {
50 	/**
51 	 * The map width in pixel
52 	 */
53 	ushort width;
54 	/**
55 	 * The map height in pixel
56 	 */
57 	ushort height;
58 	/**
59 	 * The map size.
60 	 * index 0 is width / tileWidth and index 1 is height / tileHeight
61 	 */
62 	ushort[2] mapSize;
63 	/**
64 	 * The tile width in pixel (for example: 16, 32, 64)
65 	 */
66 	ubyte tileWidth;
67 	/**
68 	 * The tile height in pixel
69 	 */
70 	ubyte tileHeight;
71 	/**
72 	 * The map filename
73 	 */
74 	string source;
75 }
76 
77 /**
78  * The Tile structure contains informations about every tile on the map
79  */
80 struct Tile {
81 	/**
82 	 * The gid is the Tile id.
83 	 * It contains the positions of this tile on the tileset.
84 	 * ----
85 	 * const uint tilesPerRow = mapWidth / tileWidth;
86 	 * 
87 	 * uint y = gid / tilesPerRow;
88 	 * uint x = gid % tilesPerRow;
89 	 * ----
90 	 */
91 	ushort gid;
92 	/**
93 	 * The coordinates in pixel of this tile on the map
94 	 */
95 	ushort[2] pixelCoords;
96 	/**
97 	 * The coordinates of this tile on the map
98 	 */
99 	ushort[2] tileCoords;
100 	/**
101 	 * The layer of the tile, if any
102 	 */
103 	string layer;
104 }
105 
106 ushort roundToNext2Pot(ushort dim) {
107 	float l = log2(dim);
108 	
109 	return cast(ushort) pow(2, round(l));
110 } unittest {
111 	assert(roundToNext2Pot(512) == 512);
112 	assert(roundToNext2Pot(832) == 1024);
113 }
114 
115 ushort calcDim(size_t tileNum, ubyte tileDim) {
116 	if (tileDim == 0)
117 		return 0;
118 	if (tileNum == 0)
119 		return 0;
120 	if (tileNum == 1)
121 		return tileDim;
122 	
123 	enforce(tileNum < ubyte.max, "Too large dimension.");
124 	
125 	ubyte dim1 = cast(ubyte) tileNum;
126 	ubyte dim2 = 1;
127 	
128 	while (dim1 > dim2) {
129 		dim1 = cast(ubyte) ceil(dim1 / 2f);
130 		dim2 *= 2;
131 	}
132 	///debug writeln("TileNum: ", tileNum, " - Dim1: ", dim1, " - Dim2: ", dim2);
133 	version(none) {
134 		return roundToNext2Pot(dim1);
135 	} else {
136 		return cast(ushort)(fmax(dim1, dim2) * tileDim);
137 	}
138 } unittest {
139 	assert(calcDim(14 , 16) == 64);
140 	assert(calcDim(2  , 16) == 32);
141 	assert(calcDim(0  , 16) == 0);
142 	assert(calcDim(1  , 16) == 16);
143 	assert(calcDim(4  , 16) == 32);
144 	assert(calcDim(28 , 16) == 128);
145 	assert(calcDim(100, 16) == 256);
146 	assert(calcDim(96 , 16) == 256);
147 	assert(calcDim(63 , 16) == 128);
148 	assert(calcDim(65 , 16) == 256);
149 	assert(calcDim(46 , 16) == 128);
150 }
151 
152 short[2] calcPos(ushort gid, ushort width, ushort tw, ushort th) pure nothrow {
153 	int tilesPerRow = width / tw;
154 	
155 	int y = gid / tilesPerRow;
156 	int x = gid % tilesPerRow;
157 	
158 	if (x)
159 		x -= 1;
160 	else {
161 		y -= 1;
162 		x = tilesPerRow - 1;
163 	}
164 	
165 	return [cast(short)(x * tw), cast(short)(y * th)];
166 } unittest {
167 	assert(calcPos(109, 832, 16, 16) == [4 * 16, 2 * 16]);
168 }
169 
170 /**
171  * The Tile map consist of tiles which are stored in a XML file (preferably build with tiled)
172  *
173  * Author: rschuett
174  */
175 class TileMap : Transformable, Drawable {
176 protected:
177 	/**
178 	 * The read method must be overriden by any specialized TileMap.
179 	 */
180 	abstract void _readTileMap();
181 
182 	void _loadTileset() in {
183 		assert(this._tmi.tileWidth == this._tmi.tileHeight, "Tile dimensions must be equal.");
184 	} body {
185 		ShortRect[ushort] used;
186 		uint doubly = 0;
187 
188 		if (!.exists(this._tmi.source))
189 			this._tmi.source = dirName(this._filename) ~ '/' ~ this._tmi.source;
190 
191 		Surface tileset = Surface(this._tmi.source);
192 
193 		/// Sammeln der Tiles, die wirklich benötigt werden
194 		foreach (ref const Tile t; this._tiles) {
195 			if (t.gid !in used) {
196 				const short[2] pos = calcPos(t.gid, tileset.width, this._tmi.tileWidth, this._tmi.tileHeight);
197 				used[t.gid] = ShortRect(pos[0], pos[1], this._tmi.tileWidth, this._tmi.tileHeight);
198 			} else
199 				doubly++;
200 		}
201 		
202 		debug Log.info("%d are double used and we need %d tiles.", doubly, used.length);
203 
204 		if (this._doCompress)
205 			this._compress(tileset, used);
206 		else {
207 			//tileset.saveToFile("new_tilset.png");
208 			Texture.Format t_fmt = Texture.Format.None;
209 			if (!tileset.isMask(Surface.Mask.Red, 0x000000ff))
210 				t_fmt = tileset.bits == 24 ? Texture.Format.BGR : Texture.Format.BGRA;
211 			
212 			this._tex.loadFromMemory(tileset.pixels,
213 			                         tileset.width, tileset.height,
214 			                         tileset.bits, t_fmt);
215 		}
216 
217 		this._loadTexCoords(used);
218 	}
219 
220 	private void _compress(ref Surface tileset, scope ShortRect[ushort] used) {
221 		debug Log.info("Start compress");
222 
223 		const ushort dim = calcDim(used.length, this._tmi.tileWidth);
224 		Surface newTileset = Surface.make(dim, dim, 32);
225 		
226 		ShortRect src = ShortRect(0, 0, this._tmi.tileWidth, this._tmi.tileHeight);
227 		ushort row = 0;
228 		ushort col = 0;
229 		
230 //		ushort c = 0;
231 		/// Anpassen der Tile Koordinaten
232 		foreach (ref ShortRect dst; used) {
233 			Surface clip = tileset.subSurface(dst);
234 //			clip.saveToFile("tile_" ~ to!string(c++) ~ ".png");
235 			if (!newTileset.blit(clip, null, &src)) {
236 				Log.error("An error occured by blitting the tile on the new tileset: %s",
237 				          to!string(SDL_GetError()));
238 			}
239 			
240 			dst.setPosition(col, row);
241 			
242 			col += this._tmi.tileWidth;
243 			if (col >= dim) {
244 				col = 0;
245 				row += this._tmi.tileHeight;
246 			}
247 			
248 			src.setPosition(col, row);
249 		}
250 		
251 //		newTileset.saveToFile("new_tilset.png");
252 		
253 		Texture.Format t_fmt = Texture.Format.None;
254 		if (!newTileset.isMask(Surface.Mask.Red, 0x000000ff))
255 			t_fmt = newTileset.bits == 24 ? Texture.Format.BGR : Texture.Format.BGRA;
256 		
257 		this._tex.loadFromMemory(newTileset.pixels,
258 		                         newTileset.width, newTileset.height,
259 		                         newTileset.bits, t_fmt);
260 
261 		debug Log.info("End compress");
262 	}
263 
264 	void _loadTexCoords(in ShortRect[ushort] used) {
265 		/// Sammeln der Textur Koordinaten
266 
267 		scope Vector2f[] texCoords;
268 		const size_t cap = texCoords.reserve(this._tiles.length * 4);
269 		
270 		debug Log.info("TileMap: Reserve %d texCoords (Needed %d).", cap, this._tiles.length * 4);
271 
272 		const float tsw = this._tex.width;
273 		const float tsh = this._tex.height;
274 		const float tw  = this._tmi.tileWidth;
275 		const float th  = this._tmi.tileHeight;
276 		
277 		foreach (ref const Tile t; this._tiles) {
278 			const ShortRect* src = &used[t.gid];
279 
280 			float tx = src.x;
281 			float ty = src.y;
282 			
283 			texCoords ~= Vector2f(tx > 0 ? (tx / tsw) : tx, ty > 0 ? (ty / tsh) : ty); /// #1
284 			texCoords ~= Vector2f((tx + tw) / tsw,  ty > 0 ? (ty / tsh) : ty); /// #2
285 			texCoords ~= Vector2f(tx > 0 ? (tx / tsw) : tx, (ty + th) / tsh); /// #3
286 			texCoords ~= Vector2f((tx + tw) / tsw, (ty + th) / tsh); /// #4
287 		}
288 
289 		this._vbo.bind(Target.TexCoords);
290 		
291 		if (!this._vbo.isCurrentEmpty())
292 			this._vbo.modify(&texCoords[0], texCoords.length * Vector2f.sizeof);
293 		else
294 			this._vbo.cache(&texCoords[0], texCoords.length * Vector2f.sizeof);
295 		
296 		this._vbo.unbind();
297 	}
298 
299 	void _applyViewport() const {
300 		if (!this._view.isEmpty()) {
301 			if (!glIsEnabled(GL_SCISSOR_TEST))
302 				glEnable(GL_SCISSOR_TEST);
303 			
304 			const int vx = this._view.x + cast(int) super.position.x;
305 			const int vy = this._view.y + this._view.height + cast(int) super.position.y;
306 			
307 			SDL_Window* wnd = SDL_GL_GetCurrentWindow();
308 			int w, h;
309 			SDL_GetWindowSize(wnd, &w, &h);
310 			
311 			glScissor(vx, h - vy, this._view.width, this._view.height);
312 		}
313 	}
314 	
315 	void _render() in {
316 		assert(this._transform !is null, "Transform is null.");
317 	} body {
318 		if (!glIsEnabled(GL_TEXTURE_2D))
319 			glEnable(GL_TEXTURE_2D);
320 		
321 		glPushAttrib(GL_ENABLE_BIT);
322 		scope(exit) glPopAttrib();
323 		
324 		glPushMatrix();
325 		scope(exit) glPopMatrix();
326 		
327 		glDisable(GL_BLEND);
328 		scope(exit) glEnable(GL_BLEND);
329 		
330 		this._applyViewport();
331 		super._applyTranslation();
332 		
333 		this._vbo.bindTexture(this._tex);
334 		this._vbo.drawArrays(Shape.Type.TriangleStrip, this._tiles.length * 4);
335 		
336 		this._vbo.disableAllStates();
337 		this._vbo.unbind();
338 	}
339 
340 protected:
341 	ShortRect _view;
342 	TileMapInfo _tmi;
343 	Texture _tex;
344 	
345 	Tile[] _tiles;
346 	
347 	string _filename;
348 	bool _doCompress;
349 	
350 	VertexBufferObject _vbo;
351 	
352 public:
353 final:
354 	/**
355 	 * CTor
356 	 * 
357 	 * If compress is true, only the needed Tiles are stored
358 	 * (which means that are new tileset is created which contains only the needed tiles)
359 	 * otherwise the whole tileset is taken.
360 	 */
361 	this(string filename, bool compress = true) {
362 		this._tex = new Texture();
363 		this._vbo = new VertexBufferObject(Target.Vertex | Target.TexCoords);
364 
365 		this.load(filename, compress);
366 	}
367 
368 	/**
369 	 * Calculate, store and return the center point.
370 	 */
371 	override ref const(Vector2s) calculateCenter() pure nothrow {
372 		super.setCenter(this._tmi.width / 2, this._tmi.height / 2);
373 		
374 		return super.getCenter();
375 	}
376 
377 	/**
378 	 * Fetch the viewport pointer so that it can modified outside.
379 	 */
380 	inout(ShortRect*) fetchView() inout pure nothrow {
381 		return &this._view;
382 	}
383 	
384 	/**
385 	 * Set a new view.
386 	 */
387 	void setView(short x, short y, short w, short h) pure nothrow {
388 		this._view.set(x, y, w, h);
389 	}
390 	
391 	/**
392 	 * Set a new view.
393 	 */
394 	void setView(ref const ShortRect view) {
395 		this._view = view;
396 	}
397 	
398 	/**
399 	 * Rvalue version.
400 	 */
401 	void setView(const ShortRect view) {
402 		this.setView(view);
403 	}
404 	
405 	/**
406 	 * Reset the viewport.
407 	 */
408 	void resetView() pure nothrow {
409 		this._view.collapse();
410 	}
411 	
412 	/**
413 	 * Adjust the viewport.
414 	 * The position is shifted about <code>view.x * -1</code> and <code>view.y - 1</code>
415 	 * so that the left upper corner of the current view is in the left upper corner of the Window.
416 	 */
417 	void adjustView() {
418 		super.setPosition(this._view.x * -1, this._view.y * -1);
419 	}
420 	
421 	/**
422 	 * Load a new TileMap
423 	 */
424 	void load(string filename, bool compress = true) {
425 		if (!exists(filename))
426 			Log.error("Could not find tilemap " ~ filename);
427 		
428 		this._filename = filename;
429 		this._doCompress = compress;
430 		
431 		this._vbo.depleteAll();
432 		
433 		if (this._tiles.length != 0) {
434 			.destroy(this._tmi);
435 			this._tiles = null;
436 		}
437 		
438 		this._readTileMap();
439 	}
440 	
441 	/**
442 	 * If compress is true, only the needed Tiles are stored
443 	 * (which means that are new tileset is created which contains only the needed tiles)
444 	 * otherwise the whole tileset is taken.
445 	 */
446 	@property
447 	bool doCompress() const pure nothrow {
448 		return this._doCompress;
449 	}
450 	
451 	/**
452 	 * Convert from pixel coordinates to tile coordinates.
453 	 */
454 	Vector2s convertCoords(float cx, float cy) const {
455 		short x = cx >= this._tmi.tileWidth  ? cast(short) .round(cx / this._tmi.tileWidth)  : 0;
456 		short y = cy >= this._tmi.tileHeight ? cast(short) .floor(cy / this._tmi.tileHeight) : 0;
457 		
458 		return Vector2s(x, y);
459 	}
460 	
461 	/**
462 	 * Convert from pixel coordinates to tile coordinates.
463 	 */
464 	Vector2s convertCoords(ref const Vector2f vec) const {
465 		return this.convertCoords(vec.x, vec.y);
466 	}
467 	
468 	/**
469 	 * Convert from tile coordinates to pixel coordinates.
470 	 */
471 	Vector2s reconvertCoords(float cx, float cy) const {
472 		short x = cx != 0 ? cast(short) round(cx * this._tmi.tileWidth)  : 0;
473 		short y = cy != 0 ? cast(short) floor(cy * this._tmi.tileHeight) : 0;
474 		
475 		return Vector2s(x, y);
476 	}
477 	
478 	/**
479 	 * Convert from tile coordinates to pixel coordinates.
480 	 */
481 	Vector2s reconvertCoords(ref const Vector2f vec) const {
482 		return this.reconvertCoords(vec.x, vec.y);
483 	}
484 	
485 	/**
486 	 * Adjusted pixel coordinates so that they lie on valid pixel
487 	 * coordinates based on tile coordinates.
488 	 */
489 	Vector2s adjustCoords(float cx, float cy) const {
490 		const Vector2s convCoords = this.convertCoords(cx, cy);
491 		
492 		return this.reconvertCoords(convCoords.x, convCoords.y);
493 	}
494 	
495 	/**
496 	 * Adjusted pixel coordinates so that they lie on valid pixel coordinates
497 	 * based on tile coordinates.
498 	 */
499 	Vector2s adjustCoords(ref const Vector2f vec) const {
500 		return this.adjustCoords(vec.x, vec.y);
501 	}
502 	
503 	/**
504 	 * Reload multiple tiles.
505 	 * The length of coords must be equal to the length of newCoords.
506 	 * 
507 	 * See: reload for one tile
508 	 */
509 	void reload(const Vector2s[] coords, const Vector2s[] newCoords) in {
510 		assert(coords.length == newCoords.length,
511 		       "Koordinaten Arrays must have a equal length.");
512 	} body {
513 		this._vbo.bind(Target.TexCoords);
514 		scope(exit) this._vbo.unbind();
515 		
516 		float* buffer = cast(float*) this._vbo.map(VertexBufferObject.Access.Read);
517 		this._vbo.unmap();
518 		
519 		foreach (uint index, ref const Vector2s coord; coords) {
520 			uint srcGid = coord.x * (coord.y + 1) + coord.y;
521 			srcGid *= 8;
522 			uint dstGid = newCoords[index].x * (newCoords[index].y + 1) + newCoords[index].y;
523 			dstGid *= 8;
524 			
525 			buffer[srcGid .. srcGid + 8] = buffer[dstGid .. dstGid + 8];
526 			
527 			this.replaceTileAt(coord, this.getTileAt(newCoords[index]));
528 		}
529 	}
530 	
531 	/**
532 	 * Replace multiple tiles with another.
533 	 */
534 	void reload(const Vector2s[] coords, ref const Vector2s newCoord) {
535 		Tile tile = this.getTileAt(newCoord);
536 		
537 		foreach (ref const Vector2s coord; coords) {
538 			this.reload(coord, newCoord);
539 			this.replaceTileAt(coord, tile);
540 		}
541 	}
542 
543 	/**
544 	 * Rvalue version
545 	 */
546 	void reload(const Vector2s[] coords, const Vector2s newCoord) {
547 		this.reload(coords, newCoord);
548 	}
549 	
550 	/**
551 	 * Reload one tile, which means that the tile on the coordinates coord 
552 	 * is replaced with the tile (and the tile surface) on the coordinates newCoord
553 	 */
554 	void reload(ref const Vector2s coord, ref const Vector2s newCoord) {
555 		this._vbo.bind(Target.TexCoords);
556 		scope(exit) this._vbo.unbind();
557 		
558 		float* buffer = cast(float*) this._vbo.map(VertexBufferObject.Access.Read);
559 		this._vbo.unmap();
560 		
561 		uint srcGid = coord.x * (coord.y + 1) + coord.y;
562 		srcGid *= 8;
563 		uint dstGid = newCoord.x * (newCoord.y + 1) + newCoord.y;
564 		dstGid *= 8;
565 		
566 		buffer[srcGid .. srcGid + 8] = buffer[dstGid .. dstGid + 8];
567 		
568 		this.replaceTileAt(coord, this.getTileAt(newCoord));
569 	}
570 
571 	/**
572 	 * Rvalue version
573 	 */
574 	void reload(const Vector2s coord, const Vector2s newCoord) {
575 		this.reload(coord, newCoord);
576 	}
577 	
578 	/**
579 	 * Exchange the tileset
580 	 */
581 	void exchangeTileset(Texture tex) {
582 		this._tex = tex;
583 	}
584 	
585 	/**
586 	 * Returns all containing tiles
587 	 */
588 	inout(Tile[]) getTiles() inout pure nothrow {
589 		return this._tiles;
590 	}
591 	
592 	/**
593 	 * Check whether a tile exist on the given Coordinates.
594 	 * If idx isn't null, the calculated index of the Tile at the given position is stored there.
595 	 * 
596 	 * Note: The position must be in tile coordinates, not pixel coordinates.
597 	 */
598 	bool isTileAt(ref const Vector2s vec, uint* idx = null) const pure nothrow {
599 		return this.isTileAt(vec.x, vec.y, idx);
600 	}
601 	
602 	/**
603 	 * Check whether a tile exist on the given Coordinates.
604 	 * If idx isn't null, the calculated index of the Tile at the given position is stored there.
605 	 * 
606 	 * Note: The position must be in tile coordinates, not pixel coordinates.
607 	 */
608 	bool isTileAt(short x, short y, uint* idx = null) const pure nothrow {
609 		uint index = y * this._tmi.mapSize[0] + x;
610 		if (idx)
611 			*idx = index;
612 		
613 		return index < this._tiles.length;
614 	}
615 	
616 	/**
617 	 * Replace the tile at the given position with the given new Tile.
618 	 * If oldtile is not null, the former Tile is stored there.
619 	 * 
620 	 * Note: This method is designated as helper method for reload
621 	 * Note: The position must be in tile coordinates, not pixel coordinates.
622 	 */
623 	void replaceTileAt(ref const Vector2s vec, Tile newTile, Tile* oldTile = null) {
624 		this.replaceTileAt(vec.x, vec.y, newTile, oldTile);
625 	}
626 	
627 	/**
628 	 * Replace the tile at the given position with the given new Tile.
629 	 * If oldtile is not null, the former Tile is stored there.
630 	 * 
631 	 * Note: This method is designated as helper method for reload
632 	 * Note: The position must be in tile coordinates, not pixel coordinates.
633 	 */
634 	void replaceTileAt(short x, short y, ref Tile newTile, Tile* oldTile = null) {
635 		uint index = 0;
636 		
637 		if (this.isTileAt(x, y, &index)) {
638 			if (oldTile)
639 				.memcpy(oldTile, &this._tiles[index], Tile.sizeof);
640 			
641 			this._tiles[index] = newTile;
642 		}
643 	}
644 	
645 	/**
646 	 * Returns the tile at the given position, or throw an Exception
647 	 * Note: The position must be in tile coordinates, not pixel coordinates.
648 	 */
649 	Tile getTileAt(ref const Vector2s vec) const {
650 		return this.getTileAt(vec.x, vec.y);
651 	}
652 	
653 	/**
654 	 * Returns the tile at the given position, or throw an Exception
655 	 * Note: The position must be in tile coordinates, not pixel coordinates.
656 	 */
657 	Tile getTileAt(short x, short y) const {
658 		uint index = 0;
659 		if (!this.isTileAt(x, y, &index))
660 			Log.error("No Tile at position %d:%d", x, y);
661 		
662 		return this._tiles[index];
663 	}
664 	
665 	/**
666 	 * Returns the information structure of this tilemap
667 	 */
668 	ref const(TileMapInfo) getInfo() const pure nothrow {
669 		return this._tmi;
670 	}
671 	
672 	/**
673 	 * Returns the .xml filename of this tilemap
674 	 */
675 	string getFilename() const pure nothrow {
676 		return this._filename;
677 	}
678 }