1 module PixelPerfectEngine.map.mapformat;
2 /*
3  * Copyright (C) 2015-2019, by Laszlo Szeremi under the Boost license.
4  *
5  * Pixel Perfect Engine, map.mapformat module
6  */
7 import sdlang;
8 
9 import PixelPerfectEngine.graphics.layers;
10 import std.stdio;
11 public import PixelPerfectEngine.map.mapdata;
12 
13 /**
14  * Serializes/deserializes PPE map data in SDLang format.
15  * Each layer can contain objects (eg. for marking events, clipping, or sprites if applicable), tilemapping (not for SpriteLayers), embedded
16  * data such as tilemapping or scripts, and so on.
17  * <br/>
18  * Note on layer tags:
19  * As of this version, additional tags within layers must have individual names. Subtags within a parent also need to have individual names.
20  * Namespaces are reserved for internal use (eg. file sources, objects).
21  */
22 public class MapFormat {
23 	public Tag[int] 	layerData;	///Layerdata stored as SDLang tags.
24 	protected Layer[int] layeroutput;	///Used to fast map and object data pullback in editors
25 	protected Tag 		metadata;	///Stores metadata.
26 	protected Tag		root;		///Root tag for common information.
27 	public TileInfo[][int]	tileDataFromExt;///Stores basic TileData that are loaded through extensions
28 	/**
29 	 * Creates new instance from scratch.
30 	 */
31 	public this (string name, int resX, int resY) @trusted {
32 		root = new Tag("", "", null);
33 		metadata = new Tag(root, null, "Metadata");
34 		new Tag(metadata, null, "Name", [Value(name)]);
35 		new Tag(metadata, null, "resX", [Value(resX)]);
36 		new Tag(metadata, null, "resY", [Value(resY)]);
37 
38 	}
39 	/**
40 	 * Serializes itself from string.
41 	 */
42 	public this (string source) @trusted {
43 		root = parseSource(source);
44 		//Just quickly go through the tags and sort them out
45 		foreach (Tag t0 ; root.all.tags) {
46 			switch (t0.namespace) {
47 				case "Layer":
48 					const int priority = t0.expectTagValue!int("priority");
49 					layerData[priority] = t0;
50 					break;
51 				/*case "Metadata":
52 					metadata = t0;
53 					break;*/
54 				default:
55 					if(t0.name == "Metadata"){
56 						metadata = t0;
57 					}
58 					break;
59 			}
60 		}
61 	}
62 	/**
63 	 * Returns the requested layer
64 	 */
65 	public Layer opIndex(int index) @safe pure {
66 		return layeroutput.get(index, null);
67 	}
68 	/**
69 	 * Returns all layer's basic information.
70 	 */
71 	public LayerInfo[] getLayerInfo() @trusted {
72 		import std.algorithm.sorting : sort;
73 		LayerInfo[] result;
74 		foreach (Tag t ; layerData) {
75 			result ~= LayerInfo(LayerInfo.parseLayerTypeString(t.name), t.values[1].get!int(), t.values[0].get!string());
76 		}
77 		result.sort;
78 		return result;
79 	}
80 	/**
81 	 * Returns a selected tile layer's all tile's basic information.
82 	 * Mainly used to display information in editors.
83 	 */
84 	public TileInfo[] getTileInfo(int pri) @trusted {
85 		import std.algorithm.sorting : sort;
86 		TileInfo[] result;
87 		foreach (Tag t0 ; layerData[pri].namespaces["File"].tags) {
88 			//writeln(t0.toSDLString);
89 			if (t0.name == "TileSource") {
90 				Tag t1 = t0.getTag("Embed:TileInfo");
91 				if (t1 !is null) {
92 					foreach (Tag t2 ; t1.tags) {
93 						result ~= TileInfo(cast(wchar)t2.values[0].get!int(), t2.values[1].get!int(), t2.values[2].get!string());
94 					}
95 				}
96 
97 			}
98 		}
99 		//writeln(result.length);
100 		result ~= tileDataFromExt.get(pri, []);
101 		result.sort;
102 		return result;
103 	}
104 	/**
105 	 * Adds TileInfo to a TileLayer.
106 	 * Joins together multiple chunks with the same source identifier. (should be a path)
107 	 */
108 	public void addTileInfo(int pri, TileInfo[] list, string source, string dpkSource = null) @trusted {
109 		if(list.length == 0) throw new Exception("Empty list!");
110 		Tag t;
111 		foreach (Tag t0 ; layerData[pri].namespaces["File"].tags) {
112 			if (t0.name == "TileSource" && t0.values[0] == source && t0.getAttribute!string("dataPakSrc", null) == dpkSource) {
113 				t = t0.getTag("Embed:TileInfo", null);
114 				if (t is null) t = new Tag(t0, "Embed", "TileInfo");
115 				break;
116 			}
117 		}
118 		//if (t is null) return;
119 		foreach (item ; list) {
120 			new Tag(t, null, null, [Value(cast(int)item.id), Value(item.num), Value(item.name)]);
121 		}
122 		//writeln(t.tags.length);
123 		assert(t.tags.length == list.length);
124 	}
125 	///Ditto, but from preexisting Tag.
126 	public void addTileInfo(int pri, Tag t, string source, string dpkSource = null) @trusted {
127 		foreach (Tag t0 ; layerData[pri].namespaces["File"].tags) {
128 			if (t0.name == "TileSource" && t0.values[0] == source && t0.getAttribute!string("dataPakSrc", null) == dpkSource) {
129 				t0.add(t);
130 				return;
131 			}
132 		}
133 
134 	}
135 	/**
136 	 * Adds a single TileInfo to a preexisting chunk on the layer.
137 	 */
138 	public void addSingleTileInfo(int pri, TileInfo item, string source, string dpkSource = null) {
139 		foreach (Tag t0 ; layerData[pri].namespaces["File"].tags) {
140 			if (t0.name == "TileSource" && t0.values[0] == source && t0.getAttribute!string("dataPakSrc", null) == dpkSource) {
141 				Tag t1 = t0.getTag("Embed:TileInfo");
142 				if (t1 !is null) {
143 					new Tag (t1, null, null, [Value(cast(int)item.id), Value(item.num), Value(item.name)]);
144 				}
145 			}
146 		}
147 	}
148 	///Ditto, but from preexiting Tag.
149 	public void addSingleTileInfo(int pri, Tag t, string source) @trusted {
150 		foreach (Tag t0 ; layerData[pri].namespaces["Embed"].tags) {
151 			if (t0.name == "TileInfo" && t0.values.length >= 1 && t0.values[0].get!string() == source) {
152 				t0.add(t);
153 				return;
154 			}
155 		}
156 	}
157 	/**
158 	 * Removes a single tile from a TileInfo chunk.
159 	 * Returns a tag as a backup.
160 	 * Returns null if source is not found.
161 	 */
162 	public Tag removeTileInfo(int pri, string source) @trusted {
163 		foreach (Tag t0 ; layerData[pri].namespaces["Embed"].tags) {
164 			if (t0.name == "TileInfo" && t0.values.length >= 1 && t0.values[0].get!string() == source) {
165 				return t0.remove;
166 			}
167 		}
168 		return null;
169 	}
170 	/**
171 	 * Removes a given layer of any kind.
172 	 * Returns the Tag of the layer as a backup.
173 	 */
174 	public Tag removeLayer(int pri) @trusted {
175 		Tag backup = layerData[pri];
176 		layeroutput.remove(pri);
177 		layerData.remove(pri);
178 		return backup;
179 	}
180 	/**
181 	 * Adds a layer from external tag.
182 	 */
183 	public void addNewLayer(int pri, Tag t, Layer l) @trusted {
184 		layeroutput[pri] = l;
185 		layerData[pri] = t;
186 	}
187 	/**
188 	 * Adds a new TileLayer to the document.
189 	 */
190 	public void addNewTileLayer(int pri, int tX, int tY, int mX, int mY, string name, TileLayer l) @trusted {
191 		layeroutput[pri] = l;
192 		layerData[pri] = new Tag(root, "Layer", "Tile", [Value(name), Value(pri), Value(tX), Value(tY), Value(mX), Value(mY)]);
193 		//new Tag(null, null, "priority", [Value(pri)]);
194 	}
195 	/**
196 	 * Adds a new tag to a layer.
197 	 */
198 	public void addTagToLayer(T...)(int pri, string name, T args) @trusted {
199 		Value[] vals;
200 		foreach (arg; args) {
201 			vals ~= Value(arg);
202 		}
203 		new Tag(layerData[pri], null, name, vals);
204 	}
205 	/**
206 	 * Adds a new subtag to a layer's property tag.
207 	 */
208 	public void addSubTagToLayersProperty(T...)(int pri, string name, string parent, T args) @trusted {
209 		Value[] vals;
210 		foreach (arg; args) {
211 			vals ~= Value(arg);
212 		}
213 		new Tag(layerData[pri].expectTag(parent), null, name, vals);
214 	}
215 	/**
216 	 * Gets the values of a layer's root tag.
217 	 */
218 	public Value[] getLayerRootTagValues(int pri) @trusted {
219 		return layerData[pri].values;
220 	}
221 	/**
222 	 * Gets the values of a layer's tag.
223 	 */
224 	public Value[] getLayerTagValues(int pri, string name) @trusted {
225 		return layerData[pri].expectTag(name).values;
226 	}
227 	/**
228 	 * Gets the values of a layer's tag.
229 	 */
230 	public Value[] getLayerPropertyTagValues(int pri, string name, string parent) @trusted {
231 		return layerData[pri].expectTag(parent).expectTag(name).values;
232 	}
233 	/**
234 	 * Edits the values of a layer's tag. Returns the original values in an array.
235 	 */
236 	public Value[] editLayerTagValues(T...)(int pri, string name, T args) @trusted {
237 		Value[] backup = layerData[pri].expectTag(name).values;
238 		Value[] vals;
239 		foreach (arg; args) {
240 			vals ~= Value(arg);
241 		}
242 		//new Tag(layerData[pri], null, name, vals);
243 		layerData[pri].expectTag(name).values = vals;
244 		return backup;
245 	}
246 	/**
247 	 * Edits the values of a layer's subtag. Returns the original values in an array.
248 	 */
249 	public Value[] editLayerSubtagValues(T...)(int pri, string name, string parent, T args) @trusted {
250 		Value[] backup = layerData[pri].expectTag(parent).expectTag(name).values;
251 		Value[] vals;
252 		foreach (arg; args) {
253 			vals ~= Value(arg);
254 		}
255 		layerData[pri].expectTag(parent).expectTag(name).values = vals;
256 		return backup;
257 	}
258 	/**
259 	 * Removes a layer's tag.
260 	 * Returns a backup for undoing.
261 	 */
262 	public Tag removeLayerTagValues(int pri, string name) @trusted {
263 		return layerData[pri].expectTag(name).remove;
264 	}
265 	/**
266 	 * Adds an embedded MapData to a TileLayer.
267 	 */
268 	public void addEmbeddedMapData(int pri, ubyte[] base64Code) @trusted {
269 		new Tag(layerData[pri], "Embed", "MapData", [Value(base64Code)]);
270 	}
271 	///Ditto
272 	public void addEmbeddedMapData(int pri, MappingElement[] me) @safe {
273 		import PixelPerfectEngine.system.etc : reinterpretCast;
274 		addEmbeddedMapData(pri, reinterpretCast!ubyte(me));
275 	}
276 	/**
277 	 * Adds a TileData file to a TileLayer.
278 	 * Filename must contain relative path.
279 	 */
280 	public void addMapDataFile(int pri, string filename, string dataPakSrc = null) @trusted {
281 		Attribute[] a;
282 		if (dataPakSrc !is null) a ~= new Attribute("dataPakSrc", Value(dataPakSrc));
283 		new Tag(layerData[pri], "File", "MapData", [Value(filename)], a);
284 	}
285 	/+///Ditto, but for files found in DataPak archives
286 	public void addMapDataFile(int pri, string dataPakPath, string filename) @trusted {
287 		new Tag(layerData[pri], "File", "MapData", [Value(dataPakPath), Value(filename)]);
288 	}+/
289 	/**
290 	 * Removes embedded TileData from a TileLayer.
291 	 * Returns a backup for undoing.
292 	 */
293 	public Tag removeEmbeddedMapData(int pri) @trusted {
294 		return layerData[pri].expectTag("Embed:MapData").remove;
295 	}
296 	/**
297 	 * Removes a TileData file from a TileLayer.
298 	 * Returns a backup for undoing.
299 	 */
300 	public Tag removeMapDataFile(int pri) @trusted {
301 		return layerData[pri].expectTag("File:MapData").remove;
302 	}
303 	/**
304 	 * Pulls TileLayer data from the layer, and stores it in the preconfigured location.
305 	 * Only works with uncompressed data due to the need of recompression.
306 	 */
307 	public void pullMapDataFromLayer(int pri) @trusted {
308 		import PixelPerfectEngine.system.etc : reinterpretCast;
309 		ITileLayer t = cast(ITileLayer)layeroutput[pri];
310 		MappingElement[] mapping = t.getMapping;
311 		if (layerData[pri].getTag("Embed:MapData") !is null) {
312 			layerData[pri].getTag("Embed:MapData").values[0] = Value(reinterpretCast!ubyte(mapping));
313 		} else if (layerData[pri].getTag("File:MapData") !is null) {
314 			string filename = layerData[pri].getTag("File:MapData").getValue!string();
315 			MapDataHeader mdh = MapDataHeader(layerData[pri].values[3].get!int, layerData[pri].values[4].get!int);
316 			saveMapFile(mdh, mapping, File(filename, "wb"));
317 		}
318 
319 	}
320 	/**
321 	 * Adds a tile source file to a TileLayer.
322 	 */
323 	public void addTileSourceFile(int pri, string filename, string dataPakSrc = null, int offset = 0) @trusted {
324 		Attribute[] a;
325 		if (dataPakSrc !is null) a ~= new Attribute("dataPakSrc", Value(dataPakSrc));
326 		if (offset) a ~= new Attribute("offset", Value(offset));
327 		new Tag(layerData[pri], "File", "TileSource", [Value(filename)], a);
328 	}
329 	/**
330 	 * Removes a tile source.
331 	 * Returns a backup copy.
332 	 */
333 	public Tag removeTileSourceFile(int pri, string filename, string dataPakSrc = null) @trusted {
334 		try {
335 			auto namespace = layerData[pri].namespaces["File"];
336 			foreach (t ; namespace.tags) {
337 				if (t.name == "TileSource" && t.values[0] == filename && t.getAttribute!string("dataPakSrc", null) == dataPakSrc) {
338 					return t.remove;
339 				}
340 			}
341 		} catch (DOMRangeException e) {
342 			debug writeln(e);
343 		} catch (Exception e) {
344 			debug writeln(e);
345 		}
346 		return null;
347 	}
348 	/**
349 	 * Accesses tile source tags in documents for adding extra data (eg. tile names).
350 	 */
351 	public Tag getTileSourceTag(int pri, string filename, string dataPakSrc = null) @trusted {
352 		try {
353 			auto namespace = layerData[pri].namespaces["File"];
354 			foreach (t ; namespace.tags) {
355 				if (t.name == "TileSource" && t.values[0] == filename && t.getAttribute!string("dataPakSrc", null) == dataPakSrc) {
356 					return t;
357 				}
358 			}
359 		} catch (DOMRangeException e) {
360 			debug writeln(e);
361 		} catch (Exception e) {
362 			debug writeln(e);
363 		}
364 		return null;
365 	}
366 	/**
367 	 * Returns all tile sources for a given layer.
368 	 * Intended to use with a loader.
369 	 */
370 	public Tag[] getAllTileSources (int pri) @trusted {
371 		Tag[] result;
372 		try {
373 			auto namespace = layerData[pri].namespaces["file"];
374 			foreach (t ; namespace.tags) {
375 				if (t.name == "tileSource") {
376 					result ~= t;
377 				}
378 			}
379 		} catch (DOMRangeException e) {
380 			debug writeln(e);
381 		} catch (Exception e) {
382 			debug writeln(e);
383 		}
384 		return result;
385 	}
386 	/**
387 	 * Adds a palette file source to the document.
388 	 */
389 	public void addPaletteFile (string filename, string dataPakSrc, int offset) @trusted {
390 		Attribute[] a;
391 		if (offset) a ~= new Attribute("offset", Value(offset));
392 		if (dataPakSrc.length) a ~= new Attribute("dataPakSrc", Value(dataPakSrc));
393 		new Tag(root, "File", "Palette", [Value(filename)], a);
394 	}
395 	/**
396 	 * Adds an embedded palette to the document.
397 	 */
398 	public void addEmbeddedPalette (Color[] c, string name, int offset) @trusted {
399 		import PixelPerfectEngine.system.etc : reinterpretCast;
400 		Attribute[] a;
401 		if (offset) a ~= new Attribute("offset", Value(offset));
402 		new Tag(root, "Embed", "Palette", [Value(name), Value(reinterpretCast!ubyte(c))], a);
403 	}
404 }
405 /**
406  * Represents a single object within a layer, that can represent many different things.
407  * All objects have a priority identifier (int), a group identifier (int), and a name.
408  */
409 abstract class MapObject {
410 	/**
411 	 * Enumerator used for differentiating between multiple kinds of objects.
412 	 * The value serialized as a string as the name of a tag.
413 	 */
414 	public enum MapObjectType {
415 		box,			///Can be used for collision detection, event marking for scripts, and masking. Has two coordinates.
416 		/**
417 		 * Only applicable for SpriteLayer.
418 		 * Has one coordinate, a horizontal and vertical scaling indicator (int), and a source indicator (int).
419 		 */
420 		sprite,
421 	}
422 	public int 			pID;		///priority identifier
423 	public int			gID;		///group identifier (equals with layer number)
424 	public string		name;		///name of object
425 	protected MapObjectType	_type;	///type of the object
426 	public Tag[]		ancillaryTags;	///Tags that hold extra information
427 	///Returns the type of this object
428 	public @property MapObjectType type () const @nogc nothrow @safe pure {
429 		return type;
430 	}
431 	///Serializes the object into an SDL tag
432 	public abstract Tag serialize () @trusted;
433 	/**
434 	 * Checks if two objects have the same identifier.
435 	 */
436 	public bool opEquals (MapObject rhs) @nogc @safe nothrow pure const {
437 		return pID == rhs.pID && gID == rhs.gID;
438 	}
439 }
440 /**
441  * Implements a Box object. Adds a single Coordinate property to the default MapObject
442  */
443 public class BoxObject : MapObject {
444 	public Coordinate	position;	///position of object on the layer
445 	/**
446 	 * Creates a new instance from scratch.
447 	 */
448 	public this (int pID, int gID, string name, Coordinate position) @nogc nothrow @safe pure {
449 		this.pID = pID;
450 		this.gID = gID;
451 		this.name = name;
452 		this.position = position;
453 		_type = MapObjectType.box;
454 	}
455 	/**
456 	 * Deserializes itself from a Tag.
457 	 */
458 	public this (Tag t, int gID) @trusted {
459 		name = t.values[0].get!string();
460 		pID = t.values[1].get!int();
461 		position = getCoordinate(t);
462 		this.gID = gID;
463 		_type = MapObjectType.box;
464 		//ancillaryTags = t.tags;
465 		foreach (tag ; t.tags)
466 			ancillaryTags ~= tag;
467 	}
468 	/**
469 	 * Serializes the object into an SDL tag
470 	 */
471 	public override Tag serialize () @trusted {
472 		return new Tag("Object", "Box", [Value(name), Value(pID)], [new Attribute("position","left",Value(position.left)),
473 				new Attribute("position","top",Value(position.top)), new Attribute("position","right",Value(position.right)),
474 				new Attribute("position","bottom",Value(position.bottom))], ancillaryTags);
475 	}
476 }
477 /**
478  * Implements a sprite object. Adds a sprite source identifier, X and Y coordinates, and two 1024 based scaling indicator.
479  */
480 public class SpriteObject : MapObject {
481 	protected int 		_ssID;	///Sprite source identifier
482 	public int			x;		///X position
483 	public int			y;		///Y position
484 	public int			scaleHoriz;	///Horizontal scaling value
485 	public int			scaleVert;	///Vertical scaling value
486 	/**
487 	 * Creates a new instance from scratch.
488 	 */
489 	public this (int pID, int gID, string name, int ssID, int x, int y, int scaleHoriz, int scaleVert) {
490 		this.pID = pID;
491 		this.gID = gID;
492 		this.name = name;
493 		this._ssID = ssID;
494 		this.x = x;
495 		this.y = y;
496 		this.scaleHoriz = scaleHoriz;
497 		this.scaleVert = scaleVert;
498 		_type = MapObjectType.sprite;
499 	}
500 	/**
501 	 * Deserializes itself from a Tag.
502 	 */
503 	public this (Tag t, int gID) @trusted {
504 		name = t.values[0].get!string();
505 		pID = t.values[1].get!int();
506 		_ssID = t.values[2].get!int();
507 		x = t.expectAttribute!int("x");
508 		y = t.expectAttribute!int("y");
509 		scaleHoriz = t.expectAttribute!int("scaleHoriz");
510 		scaleVert = t.expectAttribute!int("scaleVert");
511 		foreach (tag ; t.tags)
512 			ancillaryTags ~= tag;
513 	}
514 	/**
515 	 * Serializes the object into an SDL tag
516 	 */
517 	public override Tag serialize () @trusted {
518 		return new Tag("Object", "Sprite", [Value(name), Value(pID), Value(_ssID)], [new Attribute("x",Value(x)),
519 				new Attribute("y",Value(y)), new Attribute("scaleHoriz",Value(scaleHoriz)),
520 				new Attribute("scaleVert",Value(scaleVert))], ancillaryTags);
521 	}
522 }
523 /**
524  * Gets a coordinate out from a Tag's Attributes with standard attribute namings.
525  */
526 public Coordinate getCoordinate(Tag t) @trusted {
527 	return Coordinate(t.expectAttribute!int("position:left"), t.expectAttribute!int("position:top"),
528 			t.expectAttribute!int("position:right"), t.expectAttribute!int("position:bottom"));
529 }
530 /**
531  * Simple LayerInfo struct, mostly for internal communications.
532  */
533 public struct LayerInfo {
534 	LayerType	type;	///Type of layer
535 	int			pri;	///Priority of layer
536 	string		name;	///Name of layer
537 	int opCmp (LayerInfo rhs) const pure @safe @nogc {
538 		if (pri > rhs.pri)
539 			return 1;
540 		else if (pri < rhs.pri)
541 			return -1;
542 		else
543 			return 0;
544 	}
545 	/+static int opCmp (LayerInfo lhs, LayerInfo rhs) pure @safe @nogc {
546 		if (lhs.pri > rhs.pri)
547 			return 1;
548 		else if (lhs.pri < rhs.pri)
549 			return -1;
550 		else
551 			return 0;
552 	}+/
553 	/**
554 	 * Parses a string as a layer type
555 	 */
556 	static LayerType parseLayerTypeString (string s) pure @safe @nogc {
557 		switch (s) {
558 			case "tile", "Tile", "TILE":
559 				return LayerType.tile;
560 			case "sprite", "Sprite", "SPRITE":
561 				return LayerType.sprite;
562 			case "transformableTile", "TransformableTile", "TRANSFORMABLETILE":
563 				return LayerType.transformableTile;
564 			default:
565 				return LayerType.NULL;
566 		}
567 	}
568 }
569 /**
570  * Simple TileInfo struct, mostly for internal communication and loading.
571  */
572 public struct TileInfo {
573 	wchar		id;		///ID of the tile in wchar format
574 	int			num;	///Number of tile in the file
575 	string		name;	///Name of the tile
576 	int opCmp (TileInfo rhs) const pure @safe @nogc {
577 		if (id > rhs.id)
578 			return 1;
579 		else if (id < rhs.id)
580 			return -1;
581 		else
582 			return 0;
583 	}
584 	public string toString() const pure {
585 		import std.conv : to;
586 		return to!string(id) ~ ";" ~ to!string(num) ~ ";" ~ name;
587 	}
588 }