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 }