From 70fc74f43b6bd1190e0154921707f574f02f9c43 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Fri, 26 Jul 2024 00:37:47 +0200 Subject: [PATCH] Modelling soon done, starting tests structuring --- DotTiled.Tests/DotTiled.Tests.csproj | 5 + DotTiled.Tests/MapTests.cs | 259 ------ .../TmxSerializer/TestData/Map/empty-map.cs | 46 + .../TmxSerializer/TestData/Map/empty-map.tmx | 12 + .../TmxSerializer/TestData/TestData.cs | 18 + .../TmxSerializer/TmxSerializer.MapTests.cs | 50 ++ .../TmxSerializer/TmxSerializerTests.cs | 30 + DotTiled/DotTiled.csproj | 1 - DotTiled/Map.cs | 809 ------------------ DotTiled/Model/Color.cs | 69 ++ DotTiled/Model/IProperty.cs | 78 ++ DotTiled/Model/Layers/BaseLayer.cs | 23 + DotTiled/Model/Layers/Data.cs | 51 ++ DotTiled/Model/Layers/ImageLayer.cs | 11 + DotTiled/Model/Layers/ObjectLayer.cs | 21 + .../Model/Layers/Objects/EllipseObject.cs | 3 + DotTiled/Model/Layers/Objects/Object.cs | 22 + DotTiled/Model/Layers/Objects/PointObject.cs | 3 + .../Model/Layers/Objects/PolygonObject.cs | 10 + .../Model/Layers/Objects/PolylineObject.cs | 10 + .../Model/Layers/Objects/RectangleObject.cs | 3 + DotTiled/Model/Layers/Objects/TextObject.cs | 38 + DotTiled/Model/Layers/TileLayer.cs | 11 + DotTiled/Model/Map.cs | 64 ++ DotTiled/Model/Tileset/Frame.cs | 8 + DotTiled/Model/Tileset/Grid.cs | 15 + DotTiled/Model/Tileset/Image.cs | 19 + DotTiled/Model/Tileset/Tile.cs | 21 + DotTiled/Model/Tileset/TileOffset.cs | 8 + DotTiled/Model/Tileset/Tileset.cs | 61 ++ DotTiled/Model/Tileset/Transformations.cs | 10 + DotTiled/Model/Tileset/WangColor.cs | 16 + DotTiled/Model/Tileset/WangTile.cs | 8 + DotTiled/Model/Tileset/Wangset.cs | 21 + DotTiled/TmxSerializer/ExtensionsXmlReader.cs | 140 +++ DotTiled/TmxSerializer/TmxSerializer.Chunk.cs | 28 + DotTiled/TmxSerializer/TmxSerializer.Data.cs | 122 +++ .../TmxSerializer/TmxSerializer.Helpers.cs | 17 + DotTiled/TmxSerializer/TmxSerializer.Map.cs | 101 +++ .../TmxSerializer.ObjectLayer.cs | 218 +++++ .../TmxSerializer/TmxSerializer.Properties.cs | 54 ++ .../TmxSerializer/TmxSerializer.TileLayer.cs | 105 +++ .../TmxSerializer/TmxSerializer.Tileset.cs | 313 +++++++ DotTiled/TmxSerializer/TmxSerializer.cs | 20 + DotTiled/XML/ExtensionsXmlReader.cs | 109 --- DotTiled/XML/XmlHelpers.cs | 52 -- 46 files changed, 1883 insertions(+), 1230 deletions(-) delete mode 100644 DotTiled.Tests/MapTests.cs create mode 100644 DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs create mode 100644 DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.tmx create mode 100644 DotTiled.Tests/TmxSerializer/TestData/TestData.cs create mode 100644 DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs create mode 100644 DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs delete mode 100644 DotTiled/Map.cs create mode 100644 DotTiled/Model/Color.cs create mode 100644 DotTiled/Model/IProperty.cs create mode 100644 DotTiled/Model/Layers/BaseLayer.cs create mode 100644 DotTiled/Model/Layers/Data.cs create mode 100644 DotTiled/Model/Layers/ImageLayer.cs create mode 100644 DotTiled/Model/Layers/ObjectLayer.cs create mode 100644 DotTiled/Model/Layers/Objects/EllipseObject.cs create mode 100644 DotTiled/Model/Layers/Objects/Object.cs create mode 100644 DotTiled/Model/Layers/Objects/PointObject.cs create mode 100644 DotTiled/Model/Layers/Objects/PolygonObject.cs create mode 100644 DotTiled/Model/Layers/Objects/PolylineObject.cs create mode 100644 DotTiled/Model/Layers/Objects/RectangleObject.cs create mode 100644 DotTiled/Model/Layers/Objects/TextObject.cs create mode 100644 DotTiled/Model/Layers/TileLayer.cs create mode 100644 DotTiled/Model/Map.cs create mode 100644 DotTiled/Model/Tileset/Frame.cs create mode 100644 DotTiled/Model/Tileset/Grid.cs create mode 100644 DotTiled/Model/Tileset/Image.cs create mode 100644 DotTiled/Model/Tileset/Tile.cs create mode 100644 DotTiled/Model/Tileset/TileOffset.cs create mode 100644 DotTiled/Model/Tileset/Tileset.cs create mode 100644 DotTiled/Model/Tileset/Transformations.cs create mode 100644 DotTiled/Model/Tileset/WangColor.cs create mode 100644 DotTiled/Model/Tileset/WangTile.cs create mode 100644 DotTiled/Model/Tileset/Wangset.cs create mode 100644 DotTiled/TmxSerializer/ExtensionsXmlReader.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.Chunk.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.Data.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.Helpers.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.Map.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.Properties.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.TileLayer.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.Tileset.cs create mode 100644 DotTiled/TmxSerializer/TmxSerializer.cs delete mode 100644 DotTiled/XML/ExtensionsXmlReader.cs delete mode 100644 DotTiled/XML/XmlHelpers.cs diff --git a/DotTiled.Tests/DotTiled.Tests.csproj b/DotTiled.Tests/DotTiled.Tests.csproj index fe5627e..c110013 100644 --- a/DotTiled.Tests/DotTiled.Tests.csproj +++ b/DotTiled.Tests/DotTiled.Tests.csproj @@ -24,4 +24,9 @@ + + + + + diff --git a/DotTiled.Tests/MapTests.cs b/DotTiled.Tests/MapTests.cs deleted file mode 100644 index 450e170..0000000 --- a/DotTiled.Tests/MapTests.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Text; -using System.Xml.Serialization; -using DotTiled; - -namespace DotTiled.Tests; - -public class MapTests -{ - [Fact] - public void ReadXml_Always_SetsRequiredAttributes() - { - // Arrange - var xml = - """ - - - - """; - var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml)); - - // Act - var map = Map.LoadFromStream(xmlStream); - - // Assert - // Assert all required properties are set - Assert.Equal("1.2", map.Version); - Assert.Equal("class", map.Class); - Assert.Equal(Orientation.Orthogonal, map.Orientation); - Assert.Equal(RenderOrder.RightDown, map.RenderOrder); - Assert.Equal(5, map.CompressionLevel); - Assert.Equal(10u, map.Width); - Assert.Equal(10u, map.Height); - Assert.Equal(32u, map.TileWidth); - Assert.Equal(32u, map.TileHeight); - Assert.Equal(0.5f, map.ParallaxOriginX); - Assert.Equal(0.5f, map.ParallaxOriginY); - Assert.Equal(1u, map.NextLayerId); - Assert.Equal(1u, map.NextObjectId); - Assert.True(map.Infinite); - - // Assert all optional properties are set to their default values - Assert.Null(map.TiledVersion); - Assert.Null(map.HexSideLength); - Assert.Null(map.StaggerAxis); - Assert.Null(map.StaggerIndex); - Assert.Null(map.BackgroundColor); - } - - public static IEnumerable ColorData => - new List - { - new object[] { "#ff0000", new TiledColor { R = 255, G = 0, B = 0, A = 255 } }, - new object[] { "#00ff00", new TiledColor { R = 0, G = 255, B = 0, A = 255 } }, - new object[] { "#0000ff", new TiledColor { R = 0, G = 0, B = 255, A = 255 } }, - new object[] { "#ffffff", new TiledColor { R = 255, G = 255, B = 255, A = 255 } }, - new object[] { "#000000", new TiledColor { R = 0, G = 0, B = 0, A = 255 } }, - new object[] { "#ff000000", new TiledColor { R = 0, G = 0, B = 0, A = 255 } }, - new object[] { "#fe000000", new TiledColor { R = 0, G = 0, B = 0, A = 254 } }, - new object[] { "#fe00ff00", new TiledColor { R = 0, G = 255, B = 0, A = 254 } }, - }; - - [Theory] - [MemberData(nameof(ColorData))] - public void ReadXml_WhenPresent_SetsOptionalAttributes(string color, TiledColor expectedColor) - { - // Arrange - var xml = - $""" - - - - """; - var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml)); - - // Act - var map = Map.LoadFromStream(xmlStream); - - // Assert - // Assert all required properties are set - Assert.Equal("1.2", map.Version); - Assert.Equal("class", map.Class); - Assert.Equal(Orientation.Orthogonal, map.Orientation); - Assert.Equal(RenderOrder.RightDown, map.RenderOrder); - Assert.Equal(5, map.CompressionLevel); - Assert.Equal(10u, map.Width); - Assert.Equal(10u, map.Height); - Assert.Equal(32u, map.TileWidth); - Assert.Equal(32u, map.TileHeight); - Assert.Equal(10u, map.HexSideLength); - Assert.Equal(StaggerAxis.Y, map.StaggerAxis); - Assert.Equal(StaggerIndex.Odd, map.StaggerIndex); - Assert.Equal(0.5f, map.ParallaxOriginX); - Assert.Equal(0.5f, map.ParallaxOriginY); - Assert.Equal(expectedColor, map.BackgroundColor); - Assert.Equal(1u, map.NextLayerId); - Assert.Equal(1u, map.NextObjectId); - Assert.True(map.Infinite); - } - - [Fact] - public void ReadXml_Always_ReadsPropertiesCorrectly() - { - // Arrange - var xml = - """ - - - - - - - - - - - - - - - - - - - """; - var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml)); - - // Act - var map = Map.LoadFromStream(xmlStream); - - // Assert - Assert.NotNull(map.Properties); - Assert.Equal(8, map.Properties.Count); - - Assert.Equal(PropertyType.String, map.Properties["string"].Type); - Assert.Equal("string", map.GetProperty("string").Value); - - Assert.Equal(PropertyType.Int, map.Properties["int"].Type); - Assert.Equal(42, map.GetProperty("int").Value); - - Assert.Equal(PropertyType.Float, map.Properties["float"].Type); - Assert.Equal(42.42f, map.GetProperty("float").Value); - - Assert.Equal(PropertyType.Bool, map.Properties["bool"].Type); - Assert.True(map.GetProperty("bool").Value); - - Assert.Equal(PropertyType.Color, map.Properties["color"].Type); - Assert.Equal(new TiledColor { R = 255, G = 0, B = 0, A = 255 }, map.GetProperty("color").Value); - - Assert.Equal(PropertyType.File, map.Properties["file"].Type); - Assert.Equal("file", map.GetProperty("file").Value); - - Assert.Equal(PropertyType.Object, map.Properties["object"].Type); - Assert.Equal(5, map.GetProperty("object").Value); - - Assert.Equal(PropertyType.Class, map.Properties["class"].Type); - var classProperty = map.GetProperty("class"); - Assert.Equal("TestClass", classProperty.PropertyType); - Assert.Equal(2, classProperty.Value.Count); - Assert.Equal("string", classProperty.GetProperty("TestClassString").Value); - Assert.Equal(43, classProperty.GetProperty("TestClassInt").Value); - } - - [Fact] - public void ReadXml_Always_1() - { - // Arrange - var xml = - """ - - - - - - - - - - - - - - - - - - - - """; - var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml)); - - // Act - var map = Map.LoadFromStream(xmlStream); - } -} - diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs b/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs new file mode 100644 index 0000000..077c573 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs @@ -0,0 +1,46 @@ +namespace DotTiled.Tests; + +public partial class TmxSerializerMapTests +{ + private readonly static Map EmptyMap = new Map + { + Version = "1.10", + TiledVersion = "1.11.0", + Orientation = MapOrientation.Orthogonal, + RenderOrder = RenderOrder.RightDown, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + NextLayerID = 2, + NextObjectID = 1, + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + GlobalTileIDs = [ + 0,0,0,0,0, + 0,0,0,0,0, + 0,0,0,0,0, + 0,0,0,0,0, + 0,0,0,0,0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ] + }; +} diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.tmx b/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.tmx new file mode 100644 index 0000000..9b103c0 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.tmx @@ -0,0 +1,12 @@ + + + + +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0 + + + diff --git a/DotTiled.Tests/TmxSerializer/TestData/TestData.cs b/DotTiled.Tests/TmxSerializer/TestData/TestData.cs new file mode 100644 index 0000000..e2e36c9 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/TestData.cs @@ -0,0 +1,18 @@ +using System.Xml; + +namespace DotTiled.Tests; + +public static class TmxSerializerTestData +{ + public static XmlReader GetReaderFor(string testDataFile) + { + var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}"; + using var stream = typeof(TmxSerializerTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) + ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found"); + + using var stringReader = new StreamReader(stream); + var xml = stringReader.ReadToEnd(); + var xmlStringReader = new StringReader(xml); + return XmlReader.Create(xmlStringReader); + } +} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs new file mode 100644 index 0000000..e875438 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs @@ -0,0 +1,50 @@ +namespace DotTiled.Tests; + +public partial class TmxSerializerMapTests +{ + private static void AssertMap(Map actual, Map expected) + { + // Attributes + Assert.Equal(expected.Version, actual.Version); + Assert.Equal(expected.TiledVersion, actual.TiledVersion); + Assert.Equal(expected.Class, actual.Class); + Assert.Equal(expected.Orientation, actual.Orientation); + Assert.Equal(expected.RenderOrder, actual.RenderOrder); + Assert.Equal(expected.CompressionLevel, actual.CompressionLevel); + Assert.Equal(expected.Width, actual.Width); + Assert.Equal(expected.Height, actual.Height); + Assert.Equal(expected.TileWidth, actual.TileWidth); + Assert.Equal(expected.TileHeight, actual.TileHeight); + Assert.Equal(expected.HexSideLength, actual.HexSideLength); + Assert.Equal(expected.StaggerAxis, actual.StaggerAxis); + Assert.Equal(expected.StaggerIndex, actual.StaggerIndex); + Assert.Equal(expected.ParallaxOriginX, actual.ParallaxOriginX); + Assert.Equal(expected.ParallaxOriginY, actual.ParallaxOriginY); + Assert.Equal(expected.BackgroundColor, actual.BackgroundColor); + Assert.Equal(expected.NextLayerID, actual.NextLayerID); + Assert.Equal(expected.NextObjectID, actual.NextObjectID); + Assert.Equal(expected.Infinite, actual.Infinite); + } + + public static IEnumerable DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data => + [ + ["TmxSerializer.TestData.Map.empty-map.tmx", EmptyMap] + ]; + + [Theory] + [MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))] + public void DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing(string testDataFile, Map expectedMap) + { + // Arrange + using var reader = TmxSerializerTestData.GetReaderFor(testDataFile); + Func externalTilesetResolver = (string s) => throw new NotSupportedException("External tilesets are not supported in this test"); + var tmxSerializer = new TmxSerializer(externalTilesetResolver); + + // Act + var map = tmxSerializer.DeserializeMap(reader); + + // Assert + Assert.NotNull(map); + AssertMap(map, expectedMap); + } +} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs new file mode 100644 index 0000000..3d1730e --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs @@ -0,0 +1,30 @@ +namespace DotTiled.Tests; + +public class TmxSerializerTests +{ + [Fact] + public void TmxSerializerConstructor_ExternalTilesetResolverIsNull_ThrowsArgumentNullException() + { + // Arrange + Func externalTilesetResolver = null!; + + // Act + Action act = () => _ = new TmxSerializer(externalTilesetResolver); + + // Assert + Assert.Throws(act); + } + + [Fact] + public void TmxSerializerConstructor_ExternalTilesetResolverIsNotNull_DoesNotThrow() + { + // Arrange + Func externalTilesetResolver = _ => new Tileset(); + + // Act + var tmxSerializer = new TmxSerializer(externalTilesetResolver); + + // Assert + Assert.NotNull(tmxSerializer); + } +} diff --git a/DotTiled/DotTiled.csproj b/DotTiled/DotTiled.csproj index bb23fb7..0102e6e 100644 --- a/DotTiled/DotTiled.csproj +++ b/DotTiled/DotTiled.csproj @@ -2,7 +2,6 @@ net8.0 - enable enable diff --git a/DotTiled/Map.cs b/DotTiled/Map.cs deleted file mode 100644 index 5630f6d..0000000 --- a/DotTiled/Map.cs +++ /dev/null @@ -1,809 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace DotTiled; - -public static class Helpers -{ - public static void SetAtMostOnce(ref T? field, T value, string fieldName) - { - if (field is not null) - throw new XmlException($"{fieldName} already set"); - - field = value; - } -} - -public enum Orientation -{ - [XmlEnum(Name = "orthogonal")] - Orthogonal, - - [XmlEnum(Name = "isometric")] - Isometric, - - [XmlEnum(Name = "staggered")] - Staggered, - - [XmlEnum(Name = "hexagonal")] - Hexagonal -} - -public enum RenderOrder -{ - [XmlEnum(Name = "right-down")] - RightDown, - - [XmlEnum(Name = "right-up")] - RightUp, - - [XmlEnum(Name = "left-down")] - LeftDown, - - [XmlEnum(Name = "left-up")] - LeftUp -} - -public enum StaggerAxis -{ - [XmlEnum(Name = "x")] - X, - - [XmlEnum(Name = "y")] - Y -} - -public enum StaggerIndex -{ - [XmlEnum(Name = "even")] - Even, - - [XmlEnum(Name = "odd")] - Odd -} - -public class TiledColor : IParsable, IEquatable -{ - public required byte R { get; set; } - public required byte G { get; set; } - public required byte B { get; set; } - public byte A { get; set; } = 255; - - public static TiledColor Parse(string s, IFormatProvider? provider) - { - TryParse(s, provider, out var result); - return result ?? throw new FormatException($"Invalid format for TiledColor: {s}"); - } - - public static bool TryParse( - [NotNullWhen(true)] string? s, - IFormatProvider? provider, - [MaybeNullWhen(false)] out TiledColor result) - { - // Format: #RRGGBB or #AARRGGBB - if (s is null || s.Length != 7 && s.Length != 9 || s[0] != '#') - { - result = default; - return false; - } - - if (s.Length == 7) - { - result = new TiledColor - { - R = byte.Parse(s[1..3], NumberStyles.HexNumber, provider), - G = byte.Parse(s[3..5], NumberStyles.HexNumber, provider), - B = byte.Parse(s[5..7], NumberStyles.HexNumber, provider) - }; - } - else - { - result = new TiledColor - { - A = byte.Parse(s[1..3], NumberStyles.HexNumber, provider), - R = byte.Parse(s[3..5], NumberStyles.HexNumber, provider), - G = byte.Parse(s[5..7], NumberStyles.HexNumber, provider), - B = byte.Parse(s[7..9], NumberStyles.HexNumber, provider) - }; - } - - return true; - } - - public bool Equals(TiledColor? other) - { - if (other is null) - return false; - - return R == other.R && G == other.G && B == other.B && A == other.A; - } - - public override bool Equals(object? obj) => obj is TiledColor other && Equals(other); - - public override int GetHashCode() => HashCode.Combine(R, G, B, A); -} - -public enum PropertyType -{ - [XmlEnum(Name = "string")] - String, - - [XmlEnum(Name = "int")] - Int, - - [XmlEnum(Name = "float")] - Float, - - [XmlEnum(Name = "bool")] - Bool, - - [XmlEnum(Name = "color")] - Color, - - [XmlEnum(Name = "file")] - File, - - [XmlEnum(Name = "object")] - Object, - - [XmlEnum(Name = "class")] - Class -} - -[XmlRoot(ElementName = "property")] -public interface IProperty : IXmlSerializable -{ - public string Name { get; set; } - public PropertyType Type { get; set; } -} - -[XmlRoot(ElementName = "property")] -public class BooleanProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required bool Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class ColorProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required TiledColor Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class FileProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required string Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class FloatProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required float Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class IntProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required int Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class ObjectProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required int Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class StringProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required string Value { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - Value = reader.GetRequiredAttribute("value"); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "property")] -public class ClassProperty : IProperty -{ - public required string Name { get; set; } - public required PropertyType Type { get; set; } - public required string PropertyType { get; set; } - public required Dictionary Value { get; set; } - - public T GetProperty(string propertyName) where T : IProperty => - (T)Value[propertyName]; - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - Name = reader.GetRequiredAttribute("name"); - Type = reader.GetRequiredAttributeEnum("type"); - PropertyType = reader.GetRequiredAttribute("propertytype"); - - // First read the start element - reader.ReadStartElement("property"); - // Then read the properties - Value = XmlHelpers.ReadProperties(reader); - // Finally read the end element - reader.ReadEndElement(); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -public enum ObjectAlignment -{ - [XmlEnum(Name = "unspecified")] - Unspecified, - - [XmlEnum(Name = "topleft")] - TopLeft, - - [XmlEnum(Name = "top")] - Top, - - [XmlEnum(Name = "topright")] - TopRight, - - [XmlEnum(Name = "left")] - Left, - - [XmlEnum(Name = "center")] - Center, - - [XmlEnum(Name = "right")] - Right, - - [XmlEnum(Name = "bottomleft")] - BottomLeft, - - [XmlEnum(Name = "bottom")] - Bottom, - - [XmlEnum(Name = "bottomright")] - BottomRight -} - -public enum TileRenderSize -{ - [XmlEnum(Name = "tile")] - Tile, - - [XmlEnum(Name = "grid")] - Grid -} - -public enum FillMode -{ - [XmlEnum(Name = "stretch")] - Stretch, - - [XmlEnum(Name = "preserve-aspect-fit")] - PreserveAspectFit -} - -public enum ImageFormat -{ - [XmlEnum(Name = "png")] - Png, - - [XmlEnum(Name = "gif")] - Gif, - - [XmlEnum(Name = "jpg")] - Jpg, - - [XmlEnum(Name = "bmp")] - Bmp -} - -public enum TiledDataEncoding -{ - [XmlEnum(Name = "csv")] - Csv, - - [XmlEnum(Name = "base64")] - Base64 -} - -public enum TiledDataCompression -{ - [XmlEnum(Name = "gzip")] - GZip, - - [XmlEnum(Name = "zlib")] - ZLib, - - [XmlEnum(Name = "zstd")] - ZStd -} - -[XmlRoot(ElementName = "data")] -public class TiledData : IXmlSerializable -{ - public TiledDataEncoding? Encoding { get; set; } - public TiledDataCompression? Compression { get; set; } - public required int[] Data { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - ReadXmlAttributes(reader); - ReadXmlElements(reader); - } - - private void ReadXmlAttributes(XmlReader reader) - { - Encoding = reader.GetOptionalAttributeEnum("encoding"); - Compression = reader.GetOptionalAttributeEnum("compression"); - } - - private void ReadXmlElements(XmlReader reader) - { - if (Encoding is null && Compression is null) - { - // Plain csv - reader.ReadStartElement("data"); - var dataAsCsvStringFromFile = reader.ReadContentAsString(); - var data = dataAsCsvStringFromFile - .Split((char[])['\n', '\r', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Select(int.Parse) - .ToArray(); - Data = data; - reader.ReadEndElement(); - } - - throw new NotImplementedException(); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "image")] -public class Image : IXmlSerializable -{ - public ImageFormat? Format { get; set; } - public string? ID { get; set; } = null; // Deprecated and unsupported - public string? Source { get; set; } - public TiledColor? TransparentColor { get; set; } - public uint? Width { get; set; } - public uint? Height { get; set; } - - private TiledData? _data = null; - public TiledData? Data - { - get => _data; - set => _data = value; - } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - ReadXmlAttributes(reader); - ReadXmlElements(reader); - } - - private void ReadXmlAttributes(XmlReader reader) - { - Format = reader.GetOptionalAttributeEnum("format"); - ID = reader.GetOptionalAttribute("id"); - Source = reader.GetOptionalAttribute("source"); - TransparentColor = reader.GetOptionalAttributeClass("trans"); - Width = reader.GetOptionalAttribute("width"); - Height = reader.GetOptionalAttribute("height"); - } - - private void ReadXmlElements(XmlReader reader) - { - reader.ReadStartElement("image"); - - while (reader.IsStartElement()) - { - var name = reader.Name; - Action action = name switch - { - "data" => () => Helpers.SetAtMostOnce(ref _data, reader.ReadElementAs(), "Data"), - _ => reader.Skip - }; - - action(); - - if (reader.NodeType == XmlNodeType.EndElement) - return; - } - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -public abstract class BaseTileset : IXmlSerializable -{ - public required string? FirstGID { get; set; } // Not set in tsx - public required string? Source { get; set; } // Not set in tsx - public required string Name { get; set; } - public required string Class { get; set; } - public required uint TileWidth { get; set; } - public required uint TileHeight { get; set; } - public required uint? Spacing { get; set; } - public required uint? Margin { get; set; } - public required uint TileCount { get; set; } - public required uint Columns { get; set; } - public required ObjectAlignment ObjectAlignment { get; set; } - public required TileRenderSize TileRenderSize { get; set; } - public required FillMode FillMode { get; set; } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - ReadXmlAttributes(reader); - ReadXmlElements(reader); - } - - private void ReadXmlAttributes(XmlReader reader) - { - FirstGID = reader.GetOptionalAttribute("firstgid"); - Source = reader.GetOptionalAttribute("source"); - Name = reader.GetRequiredAttribute("name"); - Class = reader.GetOptionalAttribute("class") ?? ""; // default value - TileWidth = reader.GetRequiredAttribute("tilewidth"); - TileHeight = reader.GetRequiredAttribute("tileheight"); - Spacing = reader.GetOptionalAttribute("spacing"); - Margin = reader.GetOptionalAttribute("margin"); - TileCount = reader.GetRequiredAttribute("tilecount"); - Columns = reader.GetRequiredAttribute("columns"); - ObjectAlignment = reader.GetOptionalAttributeEnum("objectalignment") ?? ObjectAlignment.Unspecified; - TileRenderSize = reader.GetOptionalAttributeEnum("tilerendersize") ?? TileRenderSize.Tile; - FillMode = reader.GetOptionalAttributeEnum("fillmode") ?? FillMode.Stretch; - } - - protected abstract void ReadXmlElements(XmlReader reader); - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "tileset")] -public class ImageTileset : BaseTileset -{ - private Image? _image = null; - public required Image Image - { - get => _image ?? throw new InvalidOperationException("Image not set"); // Should not be able to happen - set => _image = value; - } - - protected override void ReadXmlElements(XmlReader reader) - { - // Different types of tilesets - reader.ReadStartElement("tileset"); - - while (reader.IsStartElement()) - { - var name = reader.Name; - Action action = name switch - { - "image" => () => Helpers.SetAtMostOnce(ref _image, reader.ReadElementAs(), "Image"), - "tileoffset" => reader.Skip, - "tile" => reader.Skip, - "terraintypes" => reader.Skip, - "wangsets" => reader.Skip, - _ => reader.Skip - }; - - action(); - - if (reader.NodeType == XmlNodeType.EndElement) - return; - } - } -} - -[XmlRoot(ElementName = "layer")] -public class Layer : IXmlSerializable -{ - public required string ID { get; set; } - public required string Name { get; set; } - public required string Class { get; set; } - public required uint X { get; set; } - public required uint Y { get; set; } - public required uint Width { get; set; } - public required uint Height { get; set; } - public required float Opacity { get; set; } - public required bool Visible { get; set; } - public required TiledColor? TintColor { get; set; } - public required float OffsetX { get; set; } - public required float OffsetY { get; set; } - public required float ParallaxX { get; set; } - public required float ParallaxY { get; set; } - - private Dictionary? _properties = null; - public required Dictionary? Properties - { - get => _properties; - set => _properties = value; - } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - ReadXmlAttributes(reader); - ReadXmlElements(reader); - } - - private void ReadXmlAttributes(XmlReader reader) - { - ID = reader.GetRequiredAttribute("id"); - Name = reader.GetRequiredAttribute("name"); - Class = reader.GetOptionalAttribute("class") ?? ""; // default value - X = reader.GetRequiredAttribute("x"); - Y = reader.GetRequiredAttribute("y"); - Width = reader.GetRequiredAttribute("width"); - Height = reader.GetRequiredAttribute("height"); - Opacity = reader.GetRequiredAttribute("opacity"); - Visible = reader.GetRequiredAttribute("visible") == 1; - TintColor = reader.GetOptionalAttributeClass("tintcolor"); - OffsetX = reader.GetRequiredAttribute("offsetx"); - OffsetY = reader.GetRequiredAttribute("offsety"); - ParallaxX = reader.GetRequiredAttribute("parallaxx"); - ParallaxY = reader.GetRequiredAttribute("parallaxy"); - } - - private void ReadXmlElements(XmlReader reader) - { - reader.ReadStartElement("layer"); - - while (reader.IsStartElement()) - { - var name = reader.Name; - Action action = name switch - { - "properties" => () => Helpers.SetAtMostOnce(ref _properties, XmlHelpers.ReadProperties(reader), "Properties"), - "data" => reader.Skip, - _ => reader.Skip - }; - - action(); - } - - reader.ReadEndElement(); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} - -[XmlRoot(ElementName = "map")] -public class Map : IXmlSerializable -{ - public required string Version { get; set; } - public string? TiledVersion { get; set; } - public required string Class { get; set; } - public required Orientation Orientation { get; set; } - public required RenderOrder RenderOrder { get; set; } - public required int CompressionLevel { get; set; } - public required uint Width { get; set; } - public required uint Height { get; set; } - public required uint TileWidth { get; set; } - public required uint TileHeight { get; set; } - public uint? HexSideLength { get; set; } - public StaggerAxis? StaggerAxis { get; set; } - public StaggerIndex? StaggerIndex { get; set; } - public required float ParallaxOriginX { get; set; } - public required float ParallaxOriginY { get; set; } - public TiledColor? BackgroundColor { get; set; } - public required uint NextLayerId { get; set; } - public required uint NextObjectId { get; set; } - public required bool Infinite { get; set; } - - private Dictionary? _properties = null; - public required Dictionary? Properties - { - get => _properties; - set => _properties = value; - } - - public required List Tilesets { get; set; } = []; - - public T GetProperty(string propertyName) where T : IProperty - { - if (Properties is null) - throw new InvalidOperationException("Properties not set"); - - return (T)Properties[propertyName]; - } - - public static Map LoadFromStream(Stream stream) - { - using var reader = new StreamReader(stream, Encoding.UTF8); - var serializer = new XmlSerializer(typeof(Map)); - return (Map)serializer.Deserialize(reader)!; - } - - public XmlSchema? GetSchema() => null; - - public void ReadXml(XmlReader reader) - { - ReadXmlAttributes(reader); - ReadXmlElements(reader, (s) => null); - } - - private void ReadXmlAttributes(XmlReader reader) - { - Version = reader.GetRequiredAttribute("version"); - TiledVersion = reader.GetOptionalAttribute("tiledversion"); - Class = reader.GetOptionalAttribute("class") ?? ""; // default value - Orientation = reader.GetRequiredAttributeEnum("orientation"); - RenderOrder = reader.GetRequiredAttributeEnum("renderorder"); - CompressionLevel = reader.GetRequiredAttribute("compressionlevel"); - Width = reader.GetRequiredAttribute("width"); - Height = reader.GetRequiredAttribute("height"); - TileWidth = reader.GetRequiredAttribute("tilewidth"); - TileHeight = reader.GetRequiredAttribute("tileheight"); - HexSideLength = reader.GetOptionalAttribute("hexsidelength"); - StaggerAxis = reader.GetOptionalAttributeEnum("staggeraxis"); - StaggerIndex = reader.GetOptionalAttributeEnum("staggerindex"); - ParallaxOriginX = reader.GetRequiredAttribute("parallaxoriginx"); - ParallaxOriginY = reader.GetRequiredAttribute("parallaxoriginy"); - BackgroundColor = reader.GetOptionalAttributeClass("backgroundcolor"); - NextLayerId = reader.GetRequiredAttribute("nextlayerid"); - NextObjectId = reader.GetRequiredAttribute("nextobjectid"); - Infinite = reader.GetRequiredAttribute("infinite") == 1; - } - - private void ReadXmlElements(XmlReader reader, Func tilesetResolver) - { - reader.ReadStartElement("map"); - - while (reader.IsStartElement()) - { - var name = reader.Name; - Action action = name switch - { - "properties" => () => Helpers.SetAtMostOnce(ref _properties, XmlHelpers.ReadProperties(reader), "Properties"), - "editorsettings" => reader.Skip, - "tileset" => () => Tilesets.Add(XmlHelpers.ReadTileset(reader, tilesetResolver)), - _ => reader.Skip - }; - - action(); - } - - reader.ReadEndElement(); - } - - public void WriteXml(XmlWriter writer) - { - throw new NotImplementedException(); - } -} diff --git a/DotTiled/Model/Color.cs b/DotTiled/Model/Color.cs new file mode 100644 index 0000000..29bafe9 --- /dev/null +++ b/DotTiled/Model/Color.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace DotTiled; + +public class Color : IParsable, IEquatable +{ + public required byte R { get; set; } + public required byte G { get; set; } + public required byte B { get; set; } + public byte A { get; set; } = 255; + + public static Color Parse(string s, IFormatProvider? provider) + { + TryParse(s, provider, out var result); + return result ?? throw new FormatException($"Invalid format for TiledColor: {s}"); + } + + public static bool TryParse( + [NotNullWhen(true)] string? s, + IFormatProvider? provider, + [MaybeNullWhen(false)] out Color result) + { + if (s is not null && !s.StartsWith('#')) + return TryParse($"#{s}", provider, out result); + + // Format: #RRGGBB or #AARRGGBB + if (s is null || s.Length != 7 && s.Length != 9 || s[0] != '#') + { + result = default; + return false; + } + + if (s.Length == 7) + { + result = new Color + { + R = byte.Parse(s[1..3], NumberStyles.HexNumber, provider), + G = byte.Parse(s[3..5], NumberStyles.HexNumber, provider), + B = byte.Parse(s[5..7], NumberStyles.HexNumber, provider) + }; + } + else + { + result = new Color + { + A = byte.Parse(s[1..3], NumberStyles.HexNumber, provider), + R = byte.Parse(s[3..5], NumberStyles.HexNumber, provider), + G = byte.Parse(s[5..7], NumberStyles.HexNumber, provider), + B = byte.Parse(s[7..9], NumberStyles.HexNumber, provider) + }; + } + + return true; + } + + public bool Equals(Color? other) + { + if (other is null) + return false; + + return R == other.R && G == other.G && B == other.B && A == other.A; + } + + public override bool Equals(object? obj) => obj is Color other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(R, G, B, A); +} diff --git a/DotTiled/Model/IProperty.cs b/DotTiled/Model/IProperty.cs new file mode 100644 index 0000000..9558ee2 --- /dev/null +++ b/DotTiled/Model/IProperty.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public enum PropertyType +{ + String, + Int, + Float, + Bool, + Color, + File, + Object, + Class +} + +public interface IProperty +{ + public string Name { get; set; } + public PropertyType Type { get; } +} + +public class StringProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.String; + public required string Value { get; set; } +} + +public class IntProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Int; + public required int Value { get; set; } +} + +public class FloatProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Float; + public required float Value { get; set; } +} + +public class BoolProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Bool; + public required bool Value { get; set; } +} + +public class ColorProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Color; + public required Color Value { get; set; } +} + +public class FileProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.File; + public required string Value { get; set; } +} + +public class ObjectProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Object; + public required uint Value { get; set; } +} + +public class ClassProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => DotTiled.PropertyType.Class; + public required string PropertyType { get; set; } + public required Dictionary Properties { get; set; } +} diff --git a/DotTiled/Model/Layers/BaseLayer.cs b/DotTiled/Model/Layers/BaseLayer.cs new file mode 100644 index 0000000..cf9c383 --- /dev/null +++ b/DotTiled/Model/Layers/BaseLayer.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public abstract class BaseLayer +{ + // Attributes + public required uint ID { get; set; } + public string Name { get; set; } = ""; + public string Class { get; set; } = ""; + public uint X { get; set; } = 0; + public uint Y { get; set; } = 0; + public float Opacity { get; set; } = 1.0f; + public bool Visible { get; set; } = true; + public Color? TintColor { get; set; } + public float OffsetX { get; set; } = 0.0f; + public float OffsetY { get; set; } = 0.0f; + public float ParallaxX { get; set; } = 1.0f; + public float ParallaxY { get; set; } = 1.0f; + + // Elements + public Dictionary? Properties { get; set; } +} diff --git a/DotTiled/Model/Layers/Data.cs b/DotTiled/Model/Layers/Data.cs new file mode 100644 index 0000000..7fcb5d6 --- /dev/null +++ b/DotTiled/Model/Layers/Data.cs @@ -0,0 +1,51 @@ +using System; + +namespace DotTiled; + +public enum DataEncoding +{ + Csv, + Base64 +} + +public enum DataCompression +{ + GZip, + ZLib, + ZStd +} + +[Flags] +public enum FlippingFlags : uint +{ + None = 0, + FlippedHorizontally = 0x80000000u, + FlippedVertically = 0x40000000u, + FlippedDiagonally = 0x20000000u, + RotatedHexagonal120 = 0x10000000u +} + +public class Chunk +{ + // Attributes + public required int X { get; set; } + public required int Y { get; set; } + public required uint Width { get; set; } + public required uint Height { get; set; } + + // Data + public required uint[] GlobalTileIDs { get; set; } + public required FlippingFlags[] FlippingFlags { get; set; } +} + +public class Data +{ + // Attributes + public DataEncoding? Encoding { get; set; } + public DataCompression? Compression { get; set; } + + // Elements + public uint[]? GlobalTileIDs { get; set; } + public FlippingFlags[]? FlippingFlags { get; set; } + public Chunk[]? Chunks { get; set; } +} diff --git a/DotTiled/Model/Layers/ImageLayer.cs b/DotTiled/Model/Layers/ImageLayer.cs new file mode 100644 index 0000000..7d0b141 --- /dev/null +++ b/DotTiled/Model/Layers/ImageLayer.cs @@ -0,0 +1,11 @@ +namespace DotTiled; + +public class ImageLayer : BaseLayer +{ + // Attributes + public required bool RepeatX { get; set; } + public required bool RepeatY { get; set; } + + // At most one of + public Image? Image { get; set; } +} diff --git a/DotTiled/Model/Layers/ObjectLayer.cs b/DotTiled/Model/Layers/ObjectLayer.cs new file mode 100644 index 0000000..acf96a5 --- /dev/null +++ b/DotTiled/Model/Layers/ObjectLayer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public enum DrawOrder +{ + TopDown, + Index +} + +public class ObjectLayer : BaseLayer +{ + // Attributes + public uint? Width { get; set; } + public uint? Height { get; set; } + public required Color? Color { get; set; } + public required DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; + + // Elements + public required List Objects { get; set; } +} diff --git a/DotTiled/Model/Layers/Objects/EllipseObject.cs b/DotTiled/Model/Layers/Objects/EllipseObject.cs new file mode 100644 index 0000000..a4c2297 --- /dev/null +++ b/DotTiled/Model/Layers/Objects/EllipseObject.cs @@ -0,0 +1,3 @@ +namespace DotTiled; + +public class EllipseObject : Object { } diff --git a/DotTiled/Model/Layers/Objects/Object.cs b/DotTiled/Model/Layers/Objects/Object.cs new file mode 100644 index 0000000..2684188 --- /dev/null +++ b/DotTiled/Model/Layers/Objects/Object.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public abstract class Object +{ + // Attributes + public required uint ID { get; set; } + public string Name { get; set; } = ""; + public string Type { get; set; } = ""; + public float X { get; set; } = 0f; + public float Y { get; set; } = 0f; + public float Width { get; set; } = 0f; + public float Height { get; set; } = 0f; + public float Rotation { get; set; } = 0f; + public uint? GID { get; set; } + public bool Visible { get; set; } = true; + public string? Template { get; set; } + + // Elements + public Dictionary? Properties { get; set; } +} diff --git a/DotTiled/Model/Layers/Objects/PointObject.cs b/DotTiled/Model/Layers/Objects/PointObject.cs new file mode 100644 index 0000000..7dc8d14 --- /dev/null +++ b/DotTiled/Model/Layers/Objects/PointObject.cs @@ -0,0 +1,3 @@ +namespace DotTiled; + +public class PointObject : Object { } diff --git a/DotTiled/Model/Layers/Objects/PolygonObject.cs b/DotTiled/Model/Layers/Objects/PolygonObject.cs new file mode 100644 index 0000000..073f317 --- /dev/null +++ b/DotTiled/Model/Layers/Objects/PolygonObject.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace DotTiled; + +public class PolygonObject : Object +{ + // Attributes + public required List Points { get; set; } +} diff --git a/DotTiled/Model/Layers/Objects/PolylineObject.cs b/DotTiled/Model/Layers/Objects/PolylineObject.cs new file mode 100644 index 0000000..9b1a90d --- /dev/null +++ b/DotTiled/Model/Layers/Objects/PolylineObject.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace DotTiled; + +public class PolylineObject : Object +{ + // Attributes + public required List Points { get; set; } +} diff --git a/DotTiled/Model/Layers/Objects/RectangleObject.cs b/DotTiled/Model/Layers/Objects/RectangleObject.cs new file mode 100644 index 0000000..27a3753 --- /dev/null +++ b/DotTiled/Model/Layers/Objects/RectangleObject.cs @@ -0,0 +1,3 @@ +namespace DotTiled; + +public class RectangleObject : Object { } diff --git a/DotTiled/Model/Layers/Objects/TextObject.cs b/DotTiled/Model/Layers/Objects/TextObject.cs new file mode 100644 index 0000000..b41906a --- /dev/null +++ b/DotTiled/Model/Layers/Objects/TextObject.cs @@ -0,0 +1,38 @@ +using System.Globalization; + +namespace DotTiled; + + +public enum TextHorizontalAlignment +{ + Left, + Center, + Right, + Justify +} + +public enum TextVerticalAlignment +{ + Top, + Center, + Bottom +} + +public class TextObject : Object +{ + // Attributes + public string FontFamily { get; set; } = "sans-serif"; + public int PixelSize { get; set; } = 16; + public bool Wrap { get; set; } = false; + public Color Color { get; set; } = Color.Parse("#000000", CultureInfo.InvariantCulture); + public bool Bold { get; set; } = false; + public bool Italic { get; set; } = false; + public bool Underline { get; set; } = false; + public bool Strikeout { get; set; } = false; + public bool Kerning { get; set; } = true; + public TextHorizontalAlignment HorizontalAlignment { get; set; } = TextHorizontalAlignment.Left; + public TextVerticalAlignment VerticalAlignment { get; set; } = TextVerticalAlignment.Top; + + // Elements + public string Text { get; set; } = ""; +} diff --git a/DotTiled/Model/Layers/TileLayer.cs b/DotTiled/Model/Layers/TileLayer.cs new file mode 100644 index 0000000..fc771f4 --- /dev/null +++ b/DotTiled/Model/Layers/TileLayer.cs @@ -0,0 +1,11 @@ +namespace DotTiled; + +public class TileLayer : BaseLayer +{ + // Attributes + public required uint Width { get; set; } + public required uint Height { get; set; } + + // Elements + public Data? Data { get; set; } +} diff --git a/DotTiled/Model/Map.cs b/DotTiled/Model/Map.cs new file mode 100644 index 0000000..99868b1 --- /dev/null +++ b/DotTiled/Model/Map.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Globalization; + +namespace DotTiled; + +public enum MapOrientation +{ + Orthogonal, + Isometric, + Staggered, + Hexagonal +} + +public enum RenderOrder +{ + RightDown, + RightUp, + LeftDown, + LeftUp +} + +public enum StaggerAxis +{ + X, + Y +} + +public enum StaggerIndex +{ + Odd, + Even +} + +public class Map +{ + // Attributes + public required string Version { get; set; } + public required string TiledVersion { get; set; } + public string Class { get; set; } = ""; + public required MapOrientation Orientation { get; set; } + public RenderOrder RenderOrder { get; set; } = RenderOrder.RightDown; + public int CompressionLevel { get; set; } = -1; + public required uint Width { get; set; } + public required uint Height { get; set; } + public required uint TileWidth { get; set; } + public required uint TileHeight { get; set; } + public uint? HexSideLength { get; set; } + public StaggerAxis? StaggerAxis { get; set; } + public StaggerIndex? StaggerIndex { get; set; } + public float ParallaxOriginX { get; set; } = 0.0f; + public float ParallaxOriginY { get; set; } = 0.0f; + public Color BackgroundColor { get; set; } = Color.Parse("#00000000", CultureInfo.InvariantCulture); + public required uint NextLayerID { get; set; } + public required uint NextObjectID { get; set; } + public bool Infinite { get; set; } = false; + + // At most one of + public Dictionary? Properties { get; set; } + + // Any number of + public List Tilesets { get; set; } = []; + public List Layers { get; set; } = []; + // public List Groups { get; set; } = []; +} diff --git a/DotTiled/Model/Tileset/Frame.cs b/DotTiled/Model/Tileset/Frame.cs new file mode 100644 index 0000000..9c308fe --- /dev/null +++ b/DotTiled/Model/Tileset/Frame.cs @@ -0,0 +1,8 @@ +namespace DotTiled; + +public class Frame +{ + // Attributes + public required uint TileID { get; set; } + public required uint Duration { get; set; } +} diff --git a/DotTiled/Model/Tileset/Grid.cs b/DotTiled/Model/Tileset/Grid.cs new file mode 100644 index 0000000..027e36b --- /dev/null +++ b/DotTiled/Model/Tileset/Grid.cs @@ -0,0 +1,15 @@ +namespace DotTiled; + +public enum GridOrientation +{ + Orthogonal, + Isometric +} + +public class Grid +{ + // Attributes + public GridOrientation Orientation { get; set; } = GridOrientation.Orthogonal; + public required uint Width { get; set; } + public required uint Height { get; set; } +} diff --git a/DotTiled/Model/Tileset/Image.cs b/DotTiled/Model/Tileset/Image.cs new file mode 100644 index 0000000..bd64838 --- /dev/null +++ b/DotTiled/Model/Tileset/Image.cs @@ -0,0 +1,19 @@ +namespace DotTiled; + +public enum ImageFormat +{ + Png, + Gif, + Jpg, + Bmp +} + +public class Image +{ + // Attributes + public ImageFormat? Format { get; set; } + public string? Source { get; set; } + public Color? TransparentColor { get; set; } + public uint? Width { get; set; } + public uint? Height { get; set; } +} diff --git a/DotTiled/Model/Tileset/Tile.cs b/DotTiled/Model/Tileset/Tile.cs new file mode 100644 index 0000000..f6b40ff --- /dev/null +++ b/DotTiled/Model/Tileset/Tile.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public class Tile +{ + // Attributes + public required uint ID { get; set; } + public string Type { get; set; } = ""; + public float Probability { get; set; } = 0f; + public uint X { get; set; } = 0; + public uint Y { get; set; } = 0; + public required uint Width { get; set; } + public required uint Height { get; set; } + + // Elements + public Dictionary? Properties { get; set; } + public Image? Image { get; set; } + public ObjectLayer? ObjectLayer { get; set; } + public List? Animation { get; set; } +} diff --git a/DotTiled/Model/Tileset/TileOffset.cs b/DotTiled/Model/Tileset/TileOffset.cs new file mode 100644 index 0000000..fdd0506 --- /dev/null +++ b/DotTiled/Model/Tileset/TileOffset.cs @@ -0,0 +1,8 @@ +namespace DotTiled; + +public class TileOffset +{ + // Attributes + public float X { get; set; } = 0f; + public float Y { get; set; } = 0f; +} diff --git a/DotTiled/Model/Tileset/Tileset.cs b/DotTiled/Model/Tileset/Tileset.cs new file mode 100644 index 0000000..a0196e9 --- /dev/null +++ b/DotTiled/Model/Tileset/Tileset.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public enum ObjectAlignment +{ + Unspecified, + TopLeft, + Top, + TopRight, + Left, + Center, + Right, + BottomLeft, + Bottom, + BottomRight +} + +public enum TileRenderSize +{ + Tile, + Grid +} + +public enum FillMode +{ + Stretch, + PreserveAspectFit +} + +public class Tileset +{ + // Attributes + public string? Version { get; set; } + public string? TiledVersion { get; set; } + public uint? FirstGID { get; set; } + public string? Source { get; set; } + public string? Name { get; set; } + public string? Class { get; set; } = ""; + public uint? TileWidth { get; set; } + public uint? TileHeight { get; set; } + public float? Spacing { get; set; } + public float? Margin { get; set; } + public uint? TileCount { get; set; } + public uint? Columns { get; set; } + public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; + public TileRenderSize RenderSize { get; set; } = TileRenderSize.Tile; + public FillMode FillMode { get; set; } = FillMode.Stretch; + + // At most one of + public Image? Image { get; set; } + public TileOffset? TileOffset { get; set; } + public Grid? Grid { get; set; } + public Dictionary? Properties { get; set; } + // public List? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization + public List? Wangsets { get; set; } + public Transformations? Transformations { get; set; } + + // Any number of + public List Tiles { get; set; } = []; +} diff --git a/DotTiled/Model/Tileset/Transformations.cs b/DotTiled/Model/Tileset/Transformations.cs new file mode 100644 index 0000000..86ef359 --- /dev/null +++ b/DotTiled/Model/Tileset/Transformations.cs @@ -0,0 +1,10 @@ +namespace DotTiled; + +public class Transformations +{ + // Attributes + public bool HFlip { get; set; } = false; + public bool VFlip { get; set; } = false; + public bool Rotate { get; set; } = false; + public bool PreferUntransformed { get; set; } = false; +} diff --git a/DotTiled/Model/Tileset/WangColor.cs b/DotTiled/Model/Tileset/WangColor.cs new file mode 100644 index 0000000..e278bb9 --- /dev/null +++ b/DotTiled/Model/Tileset/WangColor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public class WangColor +{ + // Attributes + public required string Name { get; set; } + public string Class { get; set; } = ""; + public required Color Color { get; set; } + public required uint Tile { get; set; } + public float Probability { get; set; } = 0f; + + // Elements + public Dictionary? Properties { get; set; } +} diff --git a/DotTiled/Model/Tileset/WangTile.cs b/DotTiled/Model/Tileset/WangTile.cs new file mode 100644 index 0000000..7fad06f --- /dev/null +++ b/DotTiled/Model/Tileset/WangTile.cs @@ -0,0 +1,8 @@ +namespace DotTiled; + +public class WangTile +{ + // Attributes + public required uint TileID { get; set; } + public required byte[] WangID { get; set; } +} diff --git a/DotTiled/Model/Tileset/Wangset.cs b/DotTiled/Model/Tileset/Wangset.cs new file mode 100644 index 0000000..8d4d1a5 --- /dev/null +++ b/DotTiled/Model/Tileset/Wangset.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public class Wangset +{ + // Attributes + public required string Name { get; set; } + public string Class { get; set; } = ""; + public required uint Tile { get; set; } + + // Elements + // At most one of + public Dictionary? Properties { get; set; } + + // Up to 254 Wang colors + public List? WangColors { get; set; } = []; + + // Any number of + public List WangTiles { get; set; } = []; +} diff --git a/DotTiled/TmxSerializer/ExtensionsXmlReader.cs b/DotTiled/TmxSerializer/ExtensionsXmlReader.cs new file mode 100644 index 0000000..79ddb31 --- /dev/null +++ b/DotTiled/TmxSerializer/ExtensionsXmlReader.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Xml; + +namespace DotTiled; + +internal static class ExtensionsXmlReader +{ + internal static string GetRequiredAttribute(this XmlReader reader, string attribute) + { + return reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); ; + } + + internal static T GetRequiredAttributeParseable(this XmlReader reader, string attribute) where T : IParsable + { + var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); + return T.Parse(value, CultureInfo.InvariantCulture); + } + + internal static T GetRequiredAttributeParseable(this XmlReader reader, string attribute, Func parser) + { + var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); + return parser(value); + } + + internal static T GetRequiredAttributeEnum(this XmlReader reader, string attribute, Func enumParser) where T : Enum + { + var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); + return enumParser(value); + } + + internal static string? GetOptionalAttribute(this XmlReader reader, string attribute, string? defaultValue = default) + { + return reader.GetAttribute(attribute) ?? defaultValue; + } + + internal static T? GetOptionalAttributeParseable(this XmlReader reader, string attribute) where T : struct, IParsable + { + var value = reader.GetAttribute(attribute); + if (value is null) + return null; + + return T.Parse(value, CultureInfo.InvariantCulture); + } + + internal static T? GetOptionalAttributeParseable(this XmlReader reader, string attribute, Func parser) where T : struct + { + var value = reader.GetAttribute(attribute); + if (value is null) + return null; + + return parser(value); + } + + internal static T? GetOptionalAttributeClass(this XmlReader reader, string attribute) where T : class, IParsable + { + var value = reader.GetAttribute(attribute); + if (value is null) + return null; + + return T.Parse(value, CultureInfo.InvariantCulture); + } + + internal static T? GetOptionalAttributeEnum(this XmlReader reader, string attribute, Func enumParser) where T : struct, Enum + { + var value = reader.GetAttribute(attribute); + return value != null ? enumParser(value) : null; + } + + internal static List ReadList(this XmlReader reader, string wrapper, string elementName, Func readElement) + { + var list = new List(); + + if (reader.IsEmptyElement) + return list; + + reader.ReadStartElement(wrapper); + while (reader.IsStartElement(elementName)) + { + list.Add(readElement(reader)); + + if (reader.NodeType == XmlNodeType.EndElement) + continue; // At end of list, no need to read again + + reader.Read(); + } + reader.ReadEndElement(); + + return list; + } + + internal static void ProcessChildren(this XmlReader reader, string wrapper, Func getProcessAction) + { + if (reader.IsEmptyElement) + { + reader.ReadStartElement(wrapper); + return; + } + + reader.ReadStartElement(wrapper); + while (reader.IsStartElement()) + { + var elementName = reader.Name; + var action = getProcessAction(reader, elementName); + action(); + } + reader.ReadEndElement(); + } + + [return: NotNull] + internal static List ProcessChildren(this XmlReader reader, string wrapper, Func getProcessAction) + { + var list = new List(); + + if (reader.IsEmptyElement) + { + reader.ReadStartElement(wrapper); + return list; + } + + reader.ReadStartElement(wrapper); + while (reader.IsStartElement()) + { + var elementName = reader.Name; + var item = getProcessAction(reader, elementName); + list.Add(item); + } + reader.ReadEndElement(); + + return list; + } + + internal static void SkipXmlDeclaration(this XmlReader reader) + { + if (reader.NodeType == XmlNodeType.XmlDeclaration) + reader.Read(); + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Chunk.cs b/DotTiled/TmxSerializer/TmxSerializer.Chunk.cs new file mode 100644 index 0000000..47a2cb2 --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.Chunk.cs @@ -0,0 +1,28 @@ +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private 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/TmxSerializer/TmxSerializer.Data.cs b/DotTiled/TmxSerializer/TmxSerializer.Data.cs new file mode 100644 index 0000000..4725614 --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.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; + +public partial class TmxSerializer +{ + private 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 }; + } + + private (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); + } + + private uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader) + { + return reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable("gid") ?? 0).ToArray(); + } + + private 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; + } + + private uint[] ParseCsvData(string data) + { + var values = data + .Split((char[])['\n', '\r', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(uint.Parse) + .ToArray(); + return values; + } + + private 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(); + } + + private uint[] DecompressGZip(MemoryStream stream) + { + using var decompressedStream = new GZipStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } + + private uint[] DecompressZLib(MemoryStream stream) + { + using var decompressedStream = new ZLibStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs b/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs new file mode 100644 index 0000000..9676f4b --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs @@ -0,0 +1,17 @@ +using System; + +namespace DotTiled; + +public partial class TmxSerializer +{ + 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; + } + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Map.cs b/DotTiled/TmxSerializer/TmxSerializer.Map.cs new file mode 100644 index 0000000..bf3432b --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.Map.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private Map ReadMap(XmlReader reader) + { + // 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)), + "layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)), + "objectgroup" => () => layers.Add(ReadObjectLayer(r)), + "imagelayer" => () => layers.Add(ReadImageLayer(r)), + _ => 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/TmxSerializer/TmxSerializer.ObjectLayer.cs b/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs new file mode 100644 index 0000000..8772c04 --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private ObjectLayer ReadObjectLayer(XmlReader reader) + { + // 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)), + _ => 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 + }; + } + + private Object ReadObject(XmlReader reader) + { + // Attributes + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var type = reader.GetOptionalAttribute("type") ?? ""; + var x = reader.GetOptionalAttributeParseable("x") ?? 0f; + var y = reader.GetOptionalAttributeParseable("y") ?? 0f; + var width = reader.GetOptionalAttributeParseable("width") ?? 0f; + var height = reader.GetOptionalAttributeParseable("height") ?? 0f; + var rotation = reader.GetOptionalAttributeParseable("rotation") ?? 0f; + var gid = reader.GetOptionalAttributeParseable("gid"); + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var template = reader.GetOptionalAttribute("template"); + + // Elements + Object? obj = null; + Dictionary? properties = null; + + reader.ProcessChildren("object", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r, id), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r, id), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r, id), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref obj, ReadPolylineObject(r, id), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref obj, ReadTextObject(r, id), "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; + } + + private EllipseObject ReadEllipseObject(XmlReader reader, uint id) + { + reader.Skip(); + return new EllipseObject { ID = id }; + } + + private PointObject ReadPointObject(XmlReader reader, uint id) + { + reader.Skip(); + return new PointObject { ID = id }; + } + + private PolygonObject ReadPolygonObject(XmlReader reader, uint id) + { + // 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 { ID = id, Points = points }; + } + + private PolylineObject ReadPolylineObject(XmlReader reader, uint id) + { + // 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 { ID = id, Points = points }; + } + + private TextObject ReadTextObject(XmlReader reader, uint id) + { + // 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 + { + ID = id, + FontFamily = fontFamily, + PixelSize = pixelSize, + Wrap = wrap, + Color = color, + Bold = bold, + Italic = italic, + Underline = underline, + Strikeout = strikeout, + Kerning = kerning, + HorizontalAlignment = hAlign, + VerticalAlignment = vAlign, + Text = text + }; + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Properties.cs b/DotTiled/TmxSerializer/TmxSerializer.Properties.cs new file mode 100644 index 0000000..1e7cd7e --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.Properties.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private 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); + } + + private 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/TmxSerializer/TmxSerializer.TileLayer.cs b/DotTiled/TmxSerializer/TmxSerializer.TileLayer.cs new file mode 100644 index 0000000..b6853c8 --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.TileLayer.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private 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 + }; + } + + private 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 + }; + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs b/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs new file mode 100644 index 0000000..314b9e9 --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private Tileset ReadTileset(XmlReader reader) + { + // 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)), + _ => r.Skip + }); + + // Check if tileset is referring to external file + if (source is not null) + { + 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 + }; + } + + private 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, + }; + } + + private 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 }; + } + + private 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 }; + } + + private 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 }; + } + + private Tile ReadTile(XmlReader reader) + { + // 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), "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 + }; + } + + private List ReadWangsets(XmlReader reader) + { + return reader.ReadList("wangsets", "wangset", ReadWangset); + } + + private 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 + }; + } + + private 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 + }; + } + + private 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/TmxSerializer/TmxSerializer.cs b/DotTiled/TmxSerializer/TmxSerializer.cs new file mode 100644 index 0000000..9ced612 --- /dev/null +++ b/DotTiled/TmxSerializer/TmxSerializer.cs @@ -0,0 +1,20 @@ +using System; +using System.Xml; + +namespace DotTiled; + +public partial class TmxSerializer +{ + private readonly Func _externalTilesetResolver; + + public TmxSerializer(Func externalTilesetResolver) + { + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + } + + public Map DeserializeMap(XmlReader reader) + { + reader.ReadToFollowing("map"); + return ReadMap(reader); + } +} diff --git a/DotTiled/XML/ExtensionsXmlReader.cs b/DotTiled/XML/ExtensionsXmlReader.cs deleted file mode 100644 index 700325e..0000000 --- a/DotTiled/XML/ExtensionsXmlReader.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Globalization; -using System.Xml; -using System.Xml.Serialization; - -namespace DotTiled; - -internal static class ExtensionsXmlReader -{ - internal static string GetRequiredAttribute(this XmlReader reader, string attribute) - { - return reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); ; - } - - internal static T GetRequiredAttribute(this XmlReader reader, string attribute) where T : IParsable - { - var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); - return T.Parse(value, CultureInfo.InvariantCulture); - } - - internal static T GetRequiredAttributeEnum(this XmlReader reader, string attribute) where T : Enum - { - var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); - return ParseEnumUsingXmlEnumAttribute(value); - } - - internal static string? GetOptionalAttribute(this XmlReader reader, string attribute, string? defaultValue = default) - { - return reader.GetAttribute(attribute) ?? defaultValue; - } - - internal static T? GetOptionalAttribute(this XmlReader reader, string attribute) where T : struct, IParsable - { - var value = reader.GetAttribute(attribute); - if (value is null) - return null; - - return T.Parse(value, CultureInfo.InvariantCulture); - } - - internal static T? GetOptionalAttributeClass(this XmlReader reader, string attribute) where T : class, IParsable - { - var value = reader.GetAttribute(attribute); - if (value is null) - return null; - - return T.Parse(value, CultureInfo.InvariantCulture); - } - - internal static T? GetOptionalAttributeEnum(this XmlReader reader, string attribute) where T : struct, Enum - { - var value = reader.GetAttribute(attribute); - return value != null ? ParseEnumUsingXmlEnumAttribute(value) : null; - } - - internal static T ParseEnumUsingXmlEnumAttribute(string value) where T : Enum - { - var enumType = typeof(T); - var enumValues = Enum.GetValues(enumType); - foreach (var enumValue in enumValues) - { - var enumMember = enumType.GetMember(enumValue.ToString()!)[0]; - var xmlEnumAttribute = enumMember.GetCustomAttributes(typeof(XmlEnumAttribute), false).FirstOrDefault() as XmlEnumAttribute; - if (xmlEnumAttribute?.Name == value) - return (T)enumValue; - } - - throw new XmlException($"Failed to parse enum value {value}"); - } - - internal static List ReadList(this XmlReader reader, string wrapper, string elementName, Func readElement) - { - var list = new List(); - - if (reader.IsEmptyElement) - return list; - - reader.ReadStartElement(wrapper); - while (reader.IsStartElement(elementName)) - { - list.Add(readElement(reader)); - - if (reader.NodeType == XmlNodeType.EndElement) - continue; // At end of list, no need to read again - - reader.Read(); - } - reader.ReadEndElement(); - - return list; - } - - public static T ReadElementAs(this XmlReader reader) where T : IXmlSerializable - { - var serializer = new XmlSerializer(typeof(T)); - return (T)serializer.Deserialize(reader)!; - } - - public static int CountDirectChildrenWithName(this XmlReader reader, string name) - { - var subTree = reader.ReadSubtree(); - int count = 0; - while (subTree.Read()) - { - if (subTree.NodeType == XmlNodeType.Element && subTree.Name == name) - count++; - } - return count; - } -} diff --git a/DotTiled/XML/XmlHelpers.cs b/DotTiled/XML/XmlHelpers.cs deleted file mode 100644 index f8aa799..0000000 --- a/DotTiled/XML/XmlHelpers.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Xml; -using System.Xml.Serialization; - -namespace DotTiled; - -public static class XmlHelpers -{ - public static Dictionary ReadProperties(XmlReader reader) - { - return reader.ReadList<(string PropName, IProperty Prop)>("properties", "property", - reader => - { - var type = reader.GetRequiredAttributeEnum("type"); - var propertyRuntimeType = type switch - { - PropertyType.String => typeof(StringProperty), - PropertyType.Int => typeof(IntProperty), - PropertyType.Float => typeof(FloatProperty), - PropertyType.Bool => typeof(BooleanProperty), - PropertyType.Color => typeof(ColorProperty), - PropertyType.File => typeof(FileProperty), - PropertyType.Object => typeof(ObjectProperty), - PropertyType.Class => typeof(ClassProperty), - _ => throw new XmlException("Invalid property type") - }; - - var serializer = new XmlSerializer(propertyRuntimeType); - var deserializedProperty = (IProperty)serializer.Deserialize(reader)!; - return (deserializedProperty.Name, deserializedProperty); - } - ).ToDictionary(x => x.PropName, x => x.Prop); - } - - public static BaseTileset ReadTileset(XmlReader reader, Func tilesetResolver) - { - var imageChildren = reader.CountDirectChildrenWithName("image"); - var tileChildren = reader.CountDirectChildrenWithName("tile"); - if (imageChildren == 0 && tileChildren == 0) - { - // This is a tileset that must have "source" set - var source = reader.GetRequiredAttribute("source"); - return tilesetResolver(source); - } - if (imageChildren == 1) - { - // This is a single image tileset - return reader.ReadElementAs(); - } - - throw new XmlException("Invalid tileset"); - } -}