diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs index 7d513e1..5a54e7c 100644 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs +++ b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs @@ -64,7 +64,24 @@ public partial class TmxSerializerMapTests externalTemplateResolver); // Act - var map = tmxSerializer.DeserializeMap(reader); + + static Template ResolveTemplate(string source) + { + using var xmlTemplateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{source}"); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate); + return templateReader.ReadTemplate(); + } + + static Tileset ResolveTileset(string source) + { + using var xmlTilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{source}"); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate); + return tilesetReader.ReadTileset(); + } + + var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate); + + var map = mapReader.ReadMap(); var raw = tmxSerializer.DeserializeMap(testDataFileText); // Assert diff --git a/DotTiled/Model/Map.cs b/DotTiled/Model/Map.cs index 99868b1..246f21c 100644 --- a/DotTiled/Model/Map.cs +++ b/DotTiled/Model/Map.cs @@ -60,5 +60,5 @@ public class Map // Any number of public List Tilesets { get; set; } = []; public List Layers { get; set; } = []; - // public List Groups { get; set; } = []; + public List Groups { get; set; } = []; } diff --git a/DotTiled/Serialization/IMapReader.cs b/DotTiled/Serialization/IMapReader.cs new file mode 100644 index 0000000..97c1fd7 --- /dev/null +++ b/DotTiled/Serialization/IMapReader.cs @@ -0,0 +1,8 @@ +using System; + +namespace DotTiled; + +public interface IMapReader : IDisposable +{ + Map ReadMap(); +} diff --git a/DotTiled/Serialization/ITemplateReader.cs b/DotTiled/Serialization/ITemplateReader.cs new file mode 100644 index 0000000..8de77bc --- /dev/null +++ b/DotTiled/Serialization/ITemplateReader.cs @@ -0,0 +1,8 @@ +using System; + +namespace DotTiled; + +public interface ITemplateReader : IDisposable +{ + Template ReadTemplate(); +} diff --git a/DotTiled/Serialization/ITilesetReader.cs b/DotTiled/Serialization/ITilesetReader.cs new file mode 100644 index 0000000..37f5257 --- /dev/null +++ b/DotTiled/Serialization/ITilesetReader.cs @@ -0,0 +1,8 @@ +using System; + +namespace DotTiled; + +public interface ITilesetReader : IDisposable +{ + Tileset ReadTileset(); +} diff --git a/DotTiled/TmxSerializer/ExtensionsXmlReader.cs b/DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs similarity index 100% rename from DotTiled/TmxSerializer/ExtensionsXmlReader.cs rename to DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs diff --git a/DotTiled/Serialization/Tmx/Tmx.Chunk.cs b/DotTiled/Serialization/Tmx/Tmx.Chunk.cs new file mode 100644 index 0000000..9d06082 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.Chunk.cs @@ -0,0 +1,28 @@ +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression) + { + var x = reader.GetRequiredAttributeParseable("x"); + var y = reader.GetRequiredAttributeParseable("y"); + var width = reader.GetRequiredAttributeParseable("width"); + var height = reader.GetRequiredAttributeParseable("height"); + + var usesTileChildrenInsteadOfRawData = encoding is null; + if (usesTileChildrenInsteadOfRawData) + { + var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", reader); + var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); + return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; + } + else + { + var globalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); + return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; + } + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.Data.cs b/DotTiled/Serialization/Tmx/Tmx.Data.cs new file mode 100644 index 0000000..85598e0 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.Data.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static Data ReadData(XmlReader reader, bool usesChunks) + { + var encoding = reader.GetOptionalAttributeEnum("encoding", e => e switch + { + "csv" => DataEncoding.Csv, + "base64" => DataEncoding.Base64, + _ => throw new XmlException("Invalid encoding") + }); + var compression = reader.GetOptionalAttributeEnum("compression", c => c switch + { + "gzip" => DataCompression.GZip, + "zlib" => DataCompression.ZLib, + "zstd" => DataCompression.ZStd, + _ => throw new XmlException("Invalid compression") + }); + + if (usesChunks) + { + var chunks = reader + .ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression)) + .ToArray(); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks }; + } + + var usesTileChildrenInsteadOfRawData = encoding is null && compression is null; + if (usesTileChildrenInsteadOfRawData) + { + var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", reader); + var (tileChildrenGlobalTileIDs, tileChildrenFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(tileChildrenGlobalTileIDsWithFlippingFlags); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = tileChildrenGlobalTileIDs, FlippingFlags = tileChildrenFlippingFlags, Chunks = null }; + } + + var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null }; + } + + internal static (uint[] GlobalTileIDs, FlippingFlags[] FlippingFlags) ReadAndClearFlippingFlagsFromGIDs(uint[] globalTileIDs) + { + var clearedGlobalTileIDs = new uint[globalTileIDs.Length]; + var flippingFlags = new FlippingFlags[globalTileIDs.Length]; + for (var i = 0; i < globalTileIDs.Length; i++) + { + var gid = globalTileIDs[i]; + var flags = gid & 0xF0000000u; + flippingFlags[i] = (FlippingFlags)flags; + clearedGlobalTileIDs[i] = gid & 0x0FFFFFFFu; + } + + return (clearedGlobalTileIDs, flippingFlags); + } + + internal static uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader) + { + return reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable("gid") ?? 0).ToArray(); + } + + internal static uint[] ReadRawData(XmlReader reader, DataEncoding encoding, DataCompression? compression) + { + var data = reader.ReadElementContentAsString(); + if (encoding == DataEncoding.Csv) + return ParseCsvData(data); + + using var bytes = new MemoryStream(Convert.FromBase64String(data)); + if (compression is null) + return ReadMemoryStreamAsInt32Array(bytes); + + var decompressed = compression switch + { + DataCompression.GZip => DecompressGZip(bytes), + DataCompression.ZLib => DecompressZLib(bytes), + DataCompression.ZStd => throw new NotSupportedException("ZStd compression is not supported."), + _ => throw new XmlException("Invalid compression") + }; + + return decompressed; + } + + internal static uint[] ParseCsvData(string data) + { + var values = data + .Split((char[])['\n', '\r', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(uint.Parse) + .ToArray(); + return values; + } + + internal static uint[] ReadMemoryStreamAsInt32Array(Stream stream) + { + var finalValues = new List(); + var int32Bytes = new byte[4]; + while (stream.Read(int32Bytes, 0, 4) == 4) + { + var value = BitConverter.ToUInt32(int32Bytes, 0); + finalValues.Add(value); + } + return finalValues.ToArray(); + } + + internal static uint[] DecompressGZip(MemoryStream stream) + { + using var decompressedStream = new GZipStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } + + internal static uint[] DecompressZLib(MemoryStream stream) + { + using var decompressedStream = new ZLibStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.Helpers.cs b/DotTiled/Serialization/Tmx/Tmx.Helpers.cs new file mode 100644 index 0000000..bfc04f4 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.Helpers.cs @@ -0,0 +1,26 @@ +using System; + +namespace DotTiled; + +internal partial class Tmx +{ + private static class Helpers + { + public static void SetAtMostOnce(ref T? field, T value, string fieldName) + { + if (field is not null) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + } + + public static void SetAtMostOnceUsingCounter(ref T? field, T value, string fieldName, ref int counter) + { + if (counter > 0) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + counter++; + } + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.Map.cs b/DotTiled/Serialization/Tmx/Tmx.Map.cs new file mode 100644 index 0000000..d43d0b1 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.Map.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static Map ReadMap(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + { + // Attributes + var version = reader.GetRequiredAttribute("version"); + var tiledVersion = reader.GetRequiredAttribute("tiledversion"); + var @class = reader.GetOptionalAttribute("class") ?? ""; + var orientation = reader.GetRequiredAttributeEnum("orientation", s => s switch + { + "orthogonal" => MapOrientation.Orthogonal, + "isometric" => MapOrientation.Isometric, + "staggered" => MapOrientation.Staggered, + "hexagonal" => MapOrientation.Hexagonal, + _ => throw new Exception($"Unknown orientation '{s}'") + }); + var renderOrder = reader.GetOptionalAttributeEnum("renderorder", s => s switch + { + "right-down" => RenderOrder.RightDown, + "right-up" => RenderOrder.RightUp, + "left-down" => RenderOrder.LeftDown, + "left-up" => RenderOrder.LeftUp, + _ => throw new Exception($"Unknown render order '{s}'") + }) ?? RenderOrder.RightDown; + var compressionLevel = reader.GetOptionalAttributeParseable("compressionlevel") ?? -1; + var width = reader.GetRequiredAttributeParseable("width"); + var height = reader.GetRequiredAttributeParseable("height"); + var tileWidth = reader.GetRequiredAttributeParseable("tilewidth"); + var tileHeight = reader.GetRequiredAttributeParseable("tileheight"); + var hexSideLength = reader.GetOptionalAttributeParseable("hexsidelength"); + var staggerAxis = reader.GetOptionalAttributeEnum("staggeraxis", s => s switch + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new Exception($"Unknown stagger axis '{s}'") + }); + var staggerIndex = reader.GetOptionalAttributeEnum("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new Exception($"Unknown stagger index '{s}'") + }); + var parallaxOriginX = reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; + var parallaxOriginY = reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; + var backgroundColor = reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); + var nextLayerID = reader.GetRequiredAttributeParseable("nextlayerid"); + var nextObjectID = reader.GetRequiredAttributeParseable("nextobjectid"); + var infinite = (reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; + + // At most one of + Dictionary? properties = null; + + // Any number of + List layers = []; + List tilesets = []; + + reader.ProcessChildren("map", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver)), + "layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)), + "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)), + "imagelayer" => () => layers.Add(ReadImageLayer(r)), + "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)), + _ => r.Skip + }); + + return new Map + { + Version = version, + TiledVersion = tiledVersion, + Class = @class, + Orientation = orientation, + RenderOrder = renderOrder, + CompressionLevel = compressionLevel, + Width = width, + Height = height, + TileWidth = tileWidth, + TileHeight = tileHeight, + HexSideLength = hexSideLength, + StaggerAxis = staggerAxis, + StaggerIndex = staggerIndex, + ParallaxOriginX = parallaxOriginX, + ParallaxOriginY = parallaxOriginY, + BackgroundColor = backgroundColor, + NextLayerID = nextLayerID, + NextObjectID = nextObjectID, + Infinite = infinite, + Properties = properties, + Tilesets = tilesets, + Layers = layers + }; + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs new file mode 100644 index 0000000..2c52429 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static ObjectLayer ReadObjectLayer(XmlReader reader, Func externalTemplateResolver) + { + // Attributes + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var @class = reader.GetOptionalAttribute("class") ?? ""; + var x = reader.GetOptionalAttributeParseable("x") ?? 0; + var y = reader.GetOptionalAttributeParseable("y") ?? 0; + var width = reader.GetOptionalAttributeParseable("width"); + var height = reader.GetOptionalAttributeParseable("height"); + var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var color = reader.GetOptionalAttributeClass("color"); + var drawOrder = reader.GetOptionalAttributeEnum("draworder", s => s switch + { + "topdown" => DrawOrder.TopDown, + "index" => DrawOrder.Index, + _ => throw new Exception($"Unknown draw order '{s}'") + }) ?? DrawOrder.TopDown; + + // Elements + Dictionary? properties = null; + List objects = []; + + reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "object" => () => objects.Add(ReadObject(r, externalTemplateResolver)), + _ => r.Skip + }); + + return new ObjectLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Width = width, + Height = height, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Color = color, + Properties = properties, + DrawOrder = drawOrder, + Objects = objects + }; + } + + internal static Object ReadObject(XmlReader reader, Func externalTemplateResolver) + { + // Attributes + var template = reader.GetOptionalAttribute("template"); + + uint? idDefault = null; + string nameDefault = ""; + string typeDefault = ""; + float xDefault = 0f; + float yDefault = 0f; + float widthDefault = 0f; + float heightDefault = 0f; + float rotationDefault = 0f; + uint? gidDefault = null; + bool visibleDefault = true; + Dictionary? propertiesDefault = null; + + // Perform template copy first + if (template is not null) + { + var resolvedTemplate = externalTemplateResolver(template); + var templObj = resolvedTemplate.Object; + + idDefault = templObj.ID; + nameDefault = templObj.Name; + typeDefault = templObj.Type; + xDefault = templObj.X; + yDefault = templObj.Y; + widthDefault = templObj.Width; + heightDefault = templObj.Height; + rotationDefault = templObj.Rotation; + gidDefault = templObj.GID; + visibleDefault = templObj.Visible; + propertiesDefault = templObj.Properties; + } + + var id = reader.GetOptionalAttributeParseable("id") ?? idDefault; + var name = reader.GetOptionalAttribute("name") ?? nameDefault; + var type = reader.GetOptionalAttribute("type") ?? typeDefault; + var x = reader.GetOptionalAttributeParseable("x") ?? xDefault; + var y = reader.GetOptionalAttributeParseable("y") ?? yDefault; + var width = reader.GetOptionalAttributeParseable("width") ?? widthDefault; + var height = reader.GetOptionalAttributeParseable("height") ?? heightDefault; + var rotation = reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; + var gid = reader.GetOptionalAttributeParseable("gid") ?? gidDefault; + var visible = reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; + + // Elements + Object? obj = null; + int propertiesCounter = 0; + Dictionary? properties = propertiesDefault; + + reader.ProcessChildren("object", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, MergeProperties(properties, ReadProperties(r)), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref obj, ReadPolylineObject(r), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref obj, ReadTextObject(r), "Object marker"), + _ => throw new Exception($"Unknown object marker '{elementName}'") + }); + + if (obj is null) + { + obj = new RectangleObject { ID = id }; + reader.Skip(); + } + + obj.Name = name; + obj.Type = type; + obj.X = x; + obj.Y = y; + obj.Width = width; + obj.Height = height; + obj.Rotation = rotation; + obj.GID = gid; + obj.Visible = visible; + obj.Template = template; + obj.Properties = properties; + + return obj; + } + + internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary overrideProperties) + { + if (baseProperties is null) + return overrideProperties ?? new Dictionary(); + + if (overrideProperties is null) + return baseProperties; + + var result = new Dictionary(baseProperties); + foreach (var (key, value) in overrideProperties) + { + if (!result.TryGetValue(key, out var baseProp)) + { + result[key] = value; + continue; + } + else + { + if (value is ClassProperty classProp) + { + ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); + } + else + { + result[key] = value; + } + } + } + + return result; + } + + internal static EllipseObject ReadEllipseObject(XmlReader reader) + { + reader.Skip(); + return new EllipseObject { }; + } + + internal static PointObject ReadPointObject(XmlReader reader) + { + reader.Skip(); + return new PointObject { }; + } + + internal static PolygonObject ReadPolygonObject(XmlReader reader) + { + // Attributes + var points = reader.GetRequiredAttributeParseable>("points", s => + { + // Takes on format "x1,y1 x2,y2 x3,y3 ..." + var coords = s.Split(' '); + return coords.Select(c => + { + var xy = c.Split(','); + return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); + }).ToList(); + }); + + reader.ReadStartElement("polygon"); + return new PolygonObject { Points = points }; + } + + internal static PolylineObject ReadPolylineObject(XmlReader reader) + { + // Attributes + var points = reader.GetRequiredAttributeParseable>("points", s => + { + // Takes on format "x1,y1 x2,y2 x3,y3 ..." + var coords = s.Split(' '); + return coords.Select(c => + { + var xy = c.Split(','); + return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); + }).ToList(); + }); + + reader.ReadStartElement("polyline"); + return new PolylineObject { Points = points }; + } + + internal static TextObject ReadTextObject(XmlReader reader) + { + // Attributes + var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; + var pixelSize = reader.GetOptionalAttributeParseable("pixelsize") ?? 16; + var wrap = reader.GetOptionalAttributeParseable("wrap") ?? false; + var color = reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); + var bold = reader.GetOptionalAttributeParseable("bold") ?? false; + var italic = reader.GetOptionalAttributeParseable("italic") ?? false; + var underline = reader.GetOptionalAttributeParseable("underline") ?? false; + var strikeout = reader.GetOptionalAttributeParseable("strikeout") ?? false; + var kerning = reader.GetOptionalAttributeParseable("kerning") ?? true; + var hAlign = reader.GetOptionalAttributeEnum("halign", s => s switch + { + "left" => TextHorizontalAlignment.Left, + "center" => TextHorizontalAlignment.Center, + "right" => TextHorizontalAlignment.Right, + "justify" => TextHorizontalAlignment.Justify, + _ => throw new Exception($"Unknown horizontal alignment '{s}'") + }) ?? TextHorizontalAlignment.Left; + var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch + { + "top" => TextVerticalAlignment.Top, + "center" => TextVerticalAlignment.Center, + "bottom" => TextVerticalAlignment.Bottom, + _ => throw new Exception($"Unknown vertical alignment '{s}'") + }) ?? TextVerticalAlignment.Top; + + // Elements + var text = reader.ReadElementContentAsString("text", ""); + + return new TextObject + { + FontFamily = fontFamily, + PixelSize = pixelSize, + Wrap = wrap, + Color = color, + Bold = bold, + Italic = italic, + Underline = underline, + Strikeout = strikeout, + Kerning = kerning, + HorizontalAlignment = hAlign, + VerticalAlignment = vAlign, + Text = text + }; + } + + internal static Template ReadTemplate(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + { + // No attributes + + // At most one of + Tileset? tileset = null; + + // Should contain exactly one of + Object? obj = null; + + reader.ProcessChildren("template", (r, elementName) => elementName switch + { + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver), "Object"), + _ => r.Skip + }); + + if (obj is null) + throw new NotSupportedException("Template must contain exactly one object"); + + return new Template + { + Tileset = tileset, + Object = obj + }; + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/DotTiled/Serialization/Tmx/Tmx.Properties.cs new file mode 100644 index 0000000..5609a85 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.Properties.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static Dictionary ReadProperties(XmlReader reader) + { + return reader.ReadList("properties", "property", (r) => + { + var name = r.GetRequiredAttribute("name"); + var type = r.GetOptionalAttributeEnum("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + "float" => PropertyType.Float, + "bool" => PropertyType.Bool, + "color" => PropertyType.Color, + "file" => PropertyType.File, + "object" => PropertyType.Object, + "class" => PropertyType.Class, + _ => throw new XmlException("Invalid property type") + }) ?? PropertyType.String; + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, + PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Class => ReadClassProperty(r), + _ => throw new XmlException("Invalid property type") + }; + return (name, property); + }).ToDictionary(x => x.name, x => x.property); + } + + internal static ClassProperty ReadClassProperty(XmlReader reader) + { + var name = reader.GetRequiredAttribute("name"); + var propertyType = reader.GetRequiredAttribute("propertytype"); + + reader.ReadStartElement("property"); + var properties = ReadProperties(reader); + reader.ReadEndElement(); + + return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties }; + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs new file mode 100644 index 0000000..e162542 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static TileLayer ReadTileLayer(XmlReader reader, bool dataUsesChunks) + { + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var @class = reader.GetOptionalAttribute("class") ?? ""; + var x = reader.GetOptionalAttributeParseable("x") ?? 0; + var y = reader.GetOptionalAttributeParseable("y") ?? 0; + var width = reader.GetRequiredAttributeParseable("width"); + var height = reader.GetRequiredAttributeParseable("height"); + var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + Dictionary? properties = null; + Data? data = null; + + reader.ProcessChildren("layer", (r, elementName) => elementName switch + { + "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + _ => r.Skip + }); + + return new TileLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Width = width, + Height = height, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Data = data, + Properties = properties + }; + } + + internal static ImageLayer ReadImageLayer(XmlReader reader) + { + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var @class = reader.GetOptionalAttribute("class") ?? ""; + var x = reader.GetOptionalAttributeParseable("x") ?? 0; + var y = reader.GetOptionalAttributeParseable("y") ?? 0; + var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var repeatX = reader.GetRequiredAttributeParseable("repeatx"); + var repeatY = reader.GetRequiredAttributeParseable("repeaty"); + + Dictionary? properties = null; + Image? image = null; + + reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + _ => r.Skip + }); + + return new ImageLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Image = image, + RepeatX = repeatX, + RepeatY = repeatY + }; + } + + internal static Group ReadGroup(XmlReader reader, Func externalTemplateResolver) + { + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var @class = reader.GetOptionalAttribute("class") ?? ""; + var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + Dictionary? properties = null; + List layers = []; + + reader.ProcessChildren("group", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: false)), + "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)), + "imagelayer" => () => layers.Add(ReadImageLayer(r)), + "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)), + _ => r.Skip + }); + + return new Group + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Layers = layers + }; + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/DotTiled/Serialization/Tmx/Tmx.Tileset.cs new file mode 100644 index 0000000..1996bc2 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.Tileset.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static Tileset ReadTileset(XmlReader reader, Func? externalTilesetResolver, Func externalTemplateResolver) + { + // Attributes + var version = reader.GetOptionalAttribute("version"); + var tiledVersion = reader.GetOptionalAttribute("tiledversion"); + var firstGID = reader.GetOptionalAttributeParseable("firstgid"); + var source = reader.GetOptionalAttribute("source"); + var name = reader.GetOptionalAttribute("name"); + var @class = reader.GetOptionalAttribute("class") ?? ""; + var tileWidth = reader.GetOptionalAttributeParseable("tilewidth"); + var tileHeight = reader.GetOptionalAttributeParseable("tileheight"); + var spacing = reader.GetOptionalAttributeParseable("spacing"); + var margin = reader.GetOptionalAttributeParseable("margin"); + var tileCount = reader.GetOptionalAttributeParseable("tilecount"); + var columns = reader.GetOptionalAttributeParseable("columns"); + var objectAlignment = reader.GetOptionalAttributeEnum("objectalignment", s => s switch + { + "unspecified" => ObjectAlignment.Unspecified, + "topleft" => ObjectAlignment.TopLeft, + "top" => ObjectAlignment.Top, + "topright" => ObjectAlignment.TopRight, + "left" => ObjectAlignment.Left, + "center" => ObjectAlignment.Center, + "right" => ObjectAlignment.Right, + "bottomleft" => ObjectAlignment.BottomLeft, + "bottom" => ObjectAlignment.Bottom, + "bottomright" => ObjectAlignment.BottomRight, + _ => throw new Exception($"Unknown object alignment '{s}'") + }) ?? ObjectAlignment.Unspecified; + var renderSize = reader.GetOptionalAttributeEnum("rendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new Exception($"Unknown render size '{s}'") + }) ?? TileRenderSize.Tile; + var fillMode = reader.GetOptionalAttributeEnum("fillmode", s => s switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new Exception($"Unknown fill mode '{s}'") + }) ?? FillMode.Stretch; + + // Elements + Image? image = null; + TileOffset? tileOffset = null; + Grid? grid = null; + Dictionary? properties = null; + List? wangsets = null; + Transformations? transformations = null; + List tiles = []; + + reader.ProcessChildren("tileset", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), + "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(r), "TileOffset"), + "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(r), "Grid"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r), "Wangsets"), + "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"), + "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver)), + _ => r.Skip + }); + + // Check if tileset is referring to external file + if (source is not null) + { + if (externalTilesetResolver is null) + throw new InvalidOperationException("External tileset resolver is required to resolve external tilesets."); + + var resolvedTileset = externalTilesetResolver(source); + resolvedTileset.FirstGID = firstGID; + resolvedTileset.Source = null; + return resolvedTileset; + } + + return new Tileset + { + Version = version, + TiledVersion = tiledVersion, + FirstGID = firstGID, + Source = source, + Name = name, + Class = @class, + TileWidth = tileWidth, + TileHeight = tileHeight, + Spacing = spacing, + Margin = margin, + TileCount = tileCount, + Columns = columns, + ObjectAlignment = objectAlignment, + RenderSize = renderSize, + FillMode = fillMode, + Image = image, + TileOffset = tileOffset, + Grid = grid, + Properties = properties, + Wangsets = wangsets, + Transformations = transformations, + Tiles = tiles + }; + } + + internal static Image ReadImage(XmlReader reader) + { + // Attributes + var format = reader.GetOptionalAttributeEnum("format", s => s switch + { + "png" => ImageFormat.Png, + "jpg" => ImageFormat.Jpg, + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, + _ => throw new Exception($"Unknown image format '{s}'") + }); + var source = reader.GetOptionalAttribute("source"); + var transparentColor = reader.GetOptionalAttributeClass("trans"); + var width = reader.GetOptionalAttributeParseable("width"); + var height = reader.GetOptionalAttributeParseable("height"); + + reader.ProcessChildren("image", (r, elementName) => elementName switch + { + "data" => throw new NotSupportedException("Embedded image data is not supported."), + _ => r.Skip + }); + + return new Image + { + Format = format, + Source = source, + TransparentColor = transparentColor, + Width = width, + Height = height, + }; + } + + internal static TileOffset ReadTileOffset(XmlReader reader) + { + // Attributes + var x = reader.GetOptionalAttributeParseable("x") ?? 0f; + var y = reader.GetOptionalAttributeParseable("y") ?? 0f; + + reader.ReadStartElement("tileoffset"); + return new TileOffset { X = x, Y = y }; + } + + internal static Grid ReadGrid(XmlReader reader) + { + // Attributes + var orientation = reader.GetOptionalAttributeEnum("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new Exception($"Unknown orientation '{s}'") + }) ?? GridOrientation.Orthogonal; + var width = reader.GetRequiredAttributeParseable("width"); + var height = reader.GetRequiredAttributeParseable("height"); + + reader.ReadStartElement("grid"); + return new Grid { Orientation = orientation, Width = width, Height = height }; + } + + internal static Transformations ReadTransformations(XmlReader reader) + { + // Attributes + var hFlip = reader.GetOptionalAttributeParseable("hflip") ?? false; + var vFlip = reader.GetOptionalAttributeParseable("vflip") ?? false; + var rotate = reader.GetOptionalAttributeParseable("rotate") ?? false; + var preferUntransformed = reader.GetOptionalAttributeParseable("preferuntransformed") ?? false; + + reader.ReadStartElement("transformations"); + return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; + } + + internal static Tile ReadTile(XmlReader reader, Func externalTemplateResolver) + { + // Attributes + var id = reader.GetRequiredAttributeParseable("id"); + var type = reader.GetOptionalAttribute("type") ?? ""; + var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; + var x = reader.GetOptionalAttributeParseable("x") ?? 0; + var y = reader.GetOptionalAttributeParseable("y") ?? 0; + var width = reader.GetOptionalAttributeParseable("width"); + var height = reader.GetOptionalAttributeParseable("height"); + + // Elements + Dictionary? properties = null; + Image? image = null; + ObjectLayer? objectLayer = null; + List? animation = null; + + reader.ProcessChildren("tile", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), + "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver), "ObjectLayer"), + "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => + { + var tileID = ar.GetRequiredAttributeParseable("tileid"); + var duration = ar.GetRequiredAttributeParseable("duration"); + return new Frame { TileID = tileID, Duration = duration }; + }), "Animation"), + _ => r.Skip + }); + + return new Tile + { + ID = id, + Type = type, + Probability = probability, + X = x, + Y = y, + Width = width ?? image?.Width ?? 0, + Height = height ?? image?.Height ?? 0, + Properties = properties, + Image = image, + ObjectLayer = objectLayer, + Animation = animation + }; + } + + internal static List ReadWangsets(XmlReader reader) + { + return reader.ReadList("wangsets", "wangset", ReadWangset); + } + + internal static Wangset ReadWangset(XmlReader reader) + { + // Attributes + var name = reader.GetRequiredAttribute("name"); + var @class = reader.GetOptionalAttribute("class") ?? ""; + var tile = reader.GetRequiredAttributeParseable("tile"); + + // Elements + Dictionary? properties = null; + List wangColors = []; + List wangTiles = []; + + reader.ProcessChildren("wangset", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "wangcolor" => () => wangColors.Add(ReadWangColor(r)), + "wangtile" => () => wangTiles.Add(ReadWangTile(r)), + _ => r.Skip + }); + + if (wangColors.Count > 254) + throw new ArgumentException("Wangset can have at most 254 Wang colors."); + + return new Wangset + { + Name = name, + Class = @class, + Tile = tile, + Properties = properties, + WangColors = wangColors, + WangTiles = wangTiles + }; + } + + internal static WangColor ReadWangColor(XmlReader reader) + { + // Attributes + var name = reader.GetRequiredAttribute("name"); + var @class = reader.GetOptionalAttribute("class") ?? ""; + var color = reader.GetRequiredAttributeParseable("color"); + var tile = reader.GetRequiredAttributeParseable("tile"); + var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; + + // Elements + Dictionary? properties = null; + + reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + _ => r.Skip + }); + + return new WangColor + { + Name = name, + Class = @class, + Color = color, + Tile = tile, + Probability = probability, + Properties = properties + }; + } + + internal static WangTile ReadWangTile(XmlReader reader) + { + // Attributes + var tileID = reader.GetRequiredAttributeParseable("tileid"); + var wangID = reader.GetRequiredAttributeParseable("wangid", s => + { + // Comma-separated list of indices (0-254) + var indices = s.Split(',').Select(i => byte.Parse(i)).ToArray(); + if (indices.Length > 8) + throw new ArgumentException("Wang ID can have at most 8 indices."); + return indices; + }); + + return new WangTile + { + TileID = tileID, + WangID = wangID + }; + } +} diff --git a/DotTiled/Serialization/Tmx/TmxMapReader.cs b/DotTiled/Serialization/Tmx/TmxMapReader.cs new file mode 100644 index 0000000..d7e631a --- /dev/null +++ b/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace DotTiled; + +public class TmxMapReader : IMapReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + public TmxMapReader(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + + // Prepare reader + _reader.MoveToContent(); + } + + public Map ReadMap() + { + return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _reader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TmxTiledMapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/DotTiled/Serialization/Tmx/TsxTilesetReader.cs new file mode 100644 index 0000000..c089129 --- /dev/null +++ b/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -0,0 +1,50 @@ +using System; +using System.Xml; + +namespace DotTiled; + +public class TsxTilesetReader : ITilesetReader +{ + // External resolvers + private readonly Func _externalTemplateResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + public TsxTilesetReader(XmlReader reader, Func externalTemplateResolver) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + } + + public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver); + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TsxTilesetReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/DotTiled/Serialization/Tmx/TxTemplateReader.cs new file mode 100644 index 0000000..24b95f4 --- /dev/null +++ b/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml; + +namespace DotTiled; + +public class TxTemplateReader : ITemplateReader +{ + // Resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + public TxTemplateReader(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + + // Prepare reader + _reader.MoveToContent(); + } + + public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver); + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TxTemplateReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +}