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 }