1 /**
2  * Retrograde Engine
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2014-2021 Mike Bierlee
7  * License:
8  *  This software is licensed under the terms of the MIT license.
9  *  The full terms of the license can be found in the LICENSE.txt file.
10  */
11 
12 module retrograde.pipeline.tiled;
13 
14 import std.json;
15 import std.exception;
16 import std.math;
17 import std.algorithm;
18 
19 import retrograde.tiles;
20 import retrograde.file;
21 import retrograde.math;
22 
23 class TiledTilemapReadException : Exception {
24     mixin basicExceptionCtors;
25 }
26 
27 enum ReadMode {
28     LENIENT,
29     STRICT
30 }
31 
32 class TiledTilemapReader {
33     private ReadMode readMode;
34 
35     private Tile[ulong] tileCache;
36 
37     this() {
38         this(ReadMode.STRICT);
39     }
40 
41     this(ReadMode readMode) {
42         this.readMode = readMode;
43     }
44 
45     public Tilemap readTilemap(File file) {
46         tileCache.destroy();
47         tileCache[0] = createEmptyTile();
48         auto jsonText = file.readAsText();
49         auto tilemapJson = parseJSON(jsonText);
50         auto tilemap = composeTilemap(tilemapJson);
51         tileCache.destroy();
52         return tilemap;
53     }
54 
55     private Tile createEmptyTile() {
56         auto tile = new Tile();
57         tile.empty = true;
58         return tile;
59     }
60 
61     private TilemapOrientation getOrientation(string orientation) {
62         switch(orientation) {
63             case "orthogonal":
64                 return TilemapOrientation.orthogonal;
65             case "isometric":
66                 return TilemapOrientation.isometric;
67             default:
68                 throw new TiledTilemapReadException("Unsupported tilemap orientation: " ~ orientation);
69         }
70     }
71 
72     private Tilemap composeTilemap(ref const JSONValue tilemapJson) {
73         if (readMode == ReadMode.STRICT) {
74             enforceTilemapOptionSupport(tilemapJson);
75         }
76 
77         auto tilemap = new Tilemap();
78 
79         tilemap.width = tilemapJson["width"].integer;
80         tilemap.height = tilemapJson["height"].integer;
81         tilemap.tileWidth = tilemapJson["tilewidth"].integer;
82         tilemap.tileHeight = tilemapJson["tileheight"].integer;
83         tilemap.orientation = getOrientation(tilemapJson["orientation"].str);
84 
85         tilemap.tilesets = composeTilesets(tilemapJson["tilesets"].array);
86         tilemap.layers = composeLayerData(tilemapJson["layers"].array);
87 
88         if (readMode == ReadMode.STRICT) {
89             enforce!TiledTilemapReadException(tilemap.tilesets.length > 0, "Tilemap has no tilesets.");
90         }
91 
92         return tilemap;
93     }
94 
95     private Tileset[] composeTilesets(ref const JSONValue[] tilesetsJsons) {
96         Tileset[] tilesets;
97 
98         foreach (tilesetJson; tilesetsJsons) {
99             if (readMode == ReadMode.STRICT) {
100                 enforceTilesetOptionSupport(tilesetJson);
101             }
102 
103             auto tileset = new Tileset();
104 
105             tileset.name = tilesetJson["name"].str;
106             tileset.imageName = tilesetJson["image"].str;
107             tileset.imageWidth = tilesetJson["imagewidth"].integer;
108             tileset.imageHeight = tilesetJson["imageheight"].integer;
109             tileset.tileWidth = tilesetJson["tilewidth"].integer;
110             tileset.tileHeight = tilesetJson["tileheight"].integer;
111             tileset.firstGlobalId = tilesetJson["firstgid"].integer;
112 
113             tileset.tiles = composeTileData(tileset);
114 
115             tilesets ~= tileset;
116         }
117 
118         return tilesets;
119     }
120 
121     private Tile[] composeTileData(ref const Tileset tileset) {
122         Tile[] tiles;
123         auto tileCount = (tileset.imageHeight / tileset.tileHeight) * (tileset.imageWidth / tileset.tileWidth);
124 
125         for (ulong id = 1; id <= tileCount; id++) {
126             auto tile = new Tile();
127             tile.parentTileset = cast(Tileset) tileset;
128             tile.tilesetTileId = id;
129             tile.globalTileId = tileset.firstGlobalId + (id - 1);
130             tile.empty = false;
131             tile.positionInTileset = calculateTilePositionInTileset(tileset, id);
132             tiles ~= tile;
133             tileCache[tile.globalTileId] = tile;
134         }
135 
136         return tiles;
137     }
138 
139     private Vector2UL calculateTilePositionInTileset(ref const Tileset tileset, ulong tileNumber) {
140         Vector2UL tilePositionInTileset;
141 
142         ulong columns = tileset.imageWidth / tileset.tileWidth;
143         ulong row = cast(uint) ceil(cast(double) tileNumber / columns);
144         ulong column = tileNumber - ((row - 1) * columns);
145 
146         tilePositionInTileset.x = (column - 1) * tileset.tileWidth;
147         tilePositionInTileset.y = (row - 1) * tileset.tileHeight;
148         return tilePositionInTileset;
149     }
150 
151     private TileLayer[] composeLayerData(ref const JSONValue[] layersJsons) {
152         TileLayer[] layers;
153 
154         foreach(layerJson ; layersJsons) {
155             if (readMode == ReadMode.STRICT) {
156                 enforceLayerOptionSupport(layerJson);
157             }
158 
159             auto layer = new TileLayer();
160             layer.name = layerJson["name"].str;
161 
162             layer.tileData = composeTileLayerData(layerJson);
163 
164             layers ~= layer;
165         }
166 
167         return layers;
168     }
169 
170     private Tile[] composeTileLayerData(ref const JSONValue layerJson) {
171         Tile[] tiles;
172 
173         auto tilesJsonList = layerJson["data"].array;
174         foreach (tilesJsonItem; tilesJsonList) {
175             auto globalTileId = tilesJsonItem.integer;
176             tiles ~= tileCache[globalTileId];
177         }
178 
179         return tiles;
180     }
181 
182     private void enforceLayerOptionSupport(ref const JSONValue layerJson) {
183         auto name = layerJson["name"].str;
184         enforce!TiledTilemapReadException(layerJson["type"].str == "tilelayer", "Only tilelayers layer types are supported in layer " ~ name);
185         enforce!TiledTilemapReadException(layerJson["visible"].type == JSONType.true_, "Only visible layers are supported in layer " ~ name);
186         enforce!TiledTilemapReadException(layerJson["opacity"].integer == 1, "Only layers with opacity of 1 are supported in layer " ~ name);
187     }
188 
189     private void enforceTilemapOptionSupport(ref const JSONValue tilemapJson) {
190         enforce!TiledTilemapReadException(tilemapJson["version"].integer == 1, "Only Tiled tilemap version 1 is supported by this reader.");
191         enforce!TiledTilemapReadException(tilemapJson["renderorder"].str == "right-down", "Only right-down render order is supported.");
192     }
193 
194     private void enforceTilesetOptionSupport(ref const JSONValue tilesetJson) {
195         auto name = tilesetJson["name"].str;
196         enforce!TiledTilemapReadException(tilesetJson["spacing"].integer == 0, "Only tileset spacing of 0 is supported in tileset " ~ name);
197         enforce!TiledTilemapReadException(tilesetJson["margin"].integer == 0, "Only tileset margin of 0 is supported in tileset " ~ name);
198     }
199 
200 }