1 /// Module for saving and loading tilemap data. 2 module isodi.tilemap; 3 4 import std.array; 5 import std.bitmanip; 6 import std.algorithm; 7 import std.exception; 8 9 import rcdata.bin; 10 11 import isodi.cell; 12 import isodi.tests; 13 import isodi.display; 14 import isodi.position; 15 import isodi.exceptions; 16 17 /// A start leet code, just for fun (and identification) 18 private immutable int leet = 0x150D1; 19 20 private struct EntryCell { 21 22 ulong cellID; 23 Height height; 24 25 } 26 27 private struct Entry { 28 29 // Each entry starts with its starting position 30 int x; 31 int y; 32 int layer; 33 34 // Then followed by a list of cells (expanding towards positive X) 35 // Cell under index 0 will be placed under the same position as the entry 36 EntryCell[] cells; 37 38 } 39 40 41 @safe: 42 43 44 /// Save the display contents into a tilemap. 45 /// Params: 46 /// display = Isodi display to use. 47 /// range = An output range the tilemap should be written to. 48 void saveTilemap(T)(Display display, T range) { 49 50 saveTilemap(display.cells.array, range); 51 52 } 53 54 /// Save a tilemap containing the given cells. 55 /// Params: 56 /// cells = Cells to store in the tilemap. 57 /// range = An output range the tilemap should be written to. 58 void saveTilemap(T)(Cell[] cells, T range) @trusted { 59 60 /// Serializer for the tilemap 61 auto bin = rcbinSerializer(range); 62 63 /// IDs assigned to strings. 64 string[] declarations; 65 66 /// Get the entries 67 auto entries = saveTilemapImpl(cells, declarations); 68 69 // Place the contents 70 bin.get(leet.nativeToBigEndian) 71 .get(declarations) 72 .get(entries); 73 74 } 75 76 /// Implementation of 77 private auto saveTilemapImpl(Cell[] allCells, ref string[] declarations) { 78 79 /// Tile names assigned to IDs. 80 ulong[string] ids; 81 82 // Sort the cells by position 83 auto cells = allCells.positionSort; 84 85 // Resulting entries 86 auto entries = [Entry()]; 87 88 // Check each cell 89 foreach (cell; cells) { 90 91 auto entry = &entries[$-1]; 92 93 // Check if this cell matches the entry 94 if (entry.layer != cell.position.layer 95 || entry.y != cell.position.y 96 || entry.x + entry.cells.length != cell.position.x 97 ) { 98 99 // It doesn't, make a new entry 100 entries ~= Entry(cell.position.toUnique.expand); 101 entry = &entries[$-1]; 102 103 } 104 105 // Get tile ID 106 auto id = ids.require(cell.type, declarations.length); 107 108 // Declare the tile if it's new 109 if (id >= declarations.length) { 110 111 declarations ~= cell.type; 112 113 } 114 115 // Add the cell to the entry 116 entry.cells ~= EntryCell(id, cell.position.height); 117 118 } 119 120 return entries; 121 122 } 123 124 /// Load tilemaps 125 /// Params: 126 /// display = Display to place the tilemap in. 127 /// range = Range of bytes containing map data. 128 /// offset = Optional position offset to apply to each cell. Ignores depth. 129 /// postprocess = A callback ran after adding each cell to the tilemap. 130 void loadTilemap(T)(Display display, T range, Position offset = Position.init, 131 void delegate(Cell) @trusted postprocess = null) 132 do { 133 134 LoadTilemap loader; 135 136 string[] declarations; 137 Position cellPosition; 138 139 loader.onDeclarations = (decl) @safe { 140 141 declarations = decl.dup; 142 143 }; 144 145 loader.onEntry = (x, y, layer) @safe { 146 147 cellPosition = position( 148 x + offset.x, 149 y + offset.y, 150 layer 151 ); 152 153 }; 154 155 loader.onCell = (id, height) @safe { 156 157 cellPosition.height = height; 158 cellPosition.height.top += offset.height.top; 159 160 // Add it to the display 161 auto isodiCell = display.addCell(cellPosition, declarations[cast(size_t) id]); 162 163 // Postprocess the cell 164 if (postprocess) postprocess(isodiCell); 165 166 // Increment position 167 cellPosition.x += 1; 168 169 }; 170 171 loader.parse(range); 172 173 } 174 175 /// Struct for advanced tilemap loading. 176 struct LoadTilemap { 177 178 /// This delegate is called with tile declarations at the start of the file. Indexes within the array are later to 179 /// be used to find the tile name for a cell ID. 180 void delegate(scope string[]) onDeclarations; 181 182 /// This delegate is called when matched an entry, that is, a row of cells in an arbitrary position. 183 /// 184 /// The parameters are the position of the entry. Following cell calls will start from this position, each new 185 /// cell should increment x. 186 void delegate(int x, int y, int layer) onEntry; 187 188 /// This delegate is called when found a cell. It is called with an ID corresponding to the tile type declaration 189 /// (from [onDeclarations]) and a definition of the cell's height. 190 void delegate(ulong cellID, Height height) onCell; 191 192 /// Begin parsing. A callback member will be ran for each matched element of the input. 193 void parse(T)(T range) @trusted { 194 195 /// Create the parser 196 auto bin = rcbinParser(range); 197 198 // Check for the magic number 199 enforce!MapException(bin.read!(ubyte[4]).bigEndianToNative!int == leet, "Given file is not a tilemap"); 200 201 /// Load declarations 202 onDeclarations(bin.read!(string[])); 203 204 /// Load entries 205 // rcdata.bin could get support for reading as ranges probably 206 auto size = bin.read!ulong; 207 208 foreach (index; 0..size) { 209 210 auto entry = bin.read!Entry; 211 onEntry(entry.x, entry.y, entry.layer); 212 213 // Load each cell 214 foreach (cell; entry.cells) { 215 216 onCell(cell.cellID, cell.height); 217 218 } 219 220 } 221 222 } 223 224 } 225 226 mixin DisplayTest!((display) { 227 228 // TODO: add tests for layers and objects spaced apart 229 230 foreach (y; 0..3) 231 foreach (x; 0..3) { 232 233 display.addCell(position(x, y), "grass"); 234 235 } 236 display.addCell(position(10, 10), "wood"); 237 238 auto data = appender!(ubyte[]); 239 saveTilemap(display, data); 240 241 auto result = data[]; 242 243 display.addModel(position(4, 1), "wraith-white"); 244 loadTilemap(display, result, position(3, 0)); 245 loadTilemap(display, result, position(0, 3, Height(0.5))); 246 247 }); 248 249 /// Write a hexdump for debugging 250 debug private void hexDump(ubyte[] bytes) { 251 252 import std.stdio; 253 254 writefln("%s bytes:", bytes.length); 255 foreach (i, value; bytes) { 256 257 writef("%0.2x ", value); 258 259 if (i % 16 == 15) writeln(); 260 261 } 262 263 }