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 }