diff --git a/DotTiled.Tests/DotTiled.Tests.csproj b/DotTiled.Tests/DotTiled.Tests.csproj index c110013..44236af 100644 --- a/DotTiled.Tests/DotTiled.Tests.csproj +++ b/DotTiled.Tests/DotTiled.Tests.csproj @@ -26,7 +26,7 @@ - + diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-group.cs b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-group.cs new file mode 100644 index 0000000..0bac69e --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-group.cs @@ -0,0 +1,94 @@ +namespace DotTiled.Tests; + +public partial class TmxSerializerMapTests +{ + private static Map MapWithGroup() => 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 = 5, + NextObjectID = 2, + Layers = [ + new TileLayer + { + ID = 4, + Name = "Tile Layer 2", + 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 + ] + } + }, + new Group + { + ID = 3, + Name = "Group 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 + ] + } + }, + new ObjectLayer + { + ID = 2, + Name = "Object Layer 1", + Objects = [ + new RectangleObject + { + ID = 1, + Name = "Name", + X = 35.5f, + Y = 26, + Width = 64.5f, + Height = 64.5f, + } + ] + } + ] + } + ] + }; +} diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-group.tmx b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-group.tmx new file mode 100644 index 0000000..61191c4 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-group.tmx @@ -0,0 +1,26 @@ + + + + +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 + + + + + +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/Map/map-with-object-template.cs b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-object-template.cs new file mode 100644 index 0000000..31a658e --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-object-template.cs @@ -0,0 +1,125 @@ +namespace DotTiled.Tests; + +public partial class TmxSerializerMapTests +{ + private static Map MapWithObjectTemplate() => 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 = 3, + NextObjectID = 3, + 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 + ] + } + }, + new ObjectLayer + { + ID = 2, + Name = "Object Layer 1", + Objects = [ + new RectangleObject + { + ID = 1, + Template = "map-with-object-template.tx", + Name = "Thingy 2", + X = 94.5749f, + Y = 33.6842f, + Width = 37.0156f, + Height = 37.0156f, + Properties = new Dictionary + { + ["Bool"] = new BoolProperty { Name = "Bool", Value = true }, + ["TestClassInTemplate"] = new ClassProperty + { + Name = "TestClassInTemplate", + PropertyType = "TestClass", + Properties = new Dictionary + { + ["Amount"] = new FloatProperty { Name = "Amount", Value = 37 }, + ["Name"] = new StringProperty { Name = "Name", Value = "I am here" } + } + } + } + }, + new RectangleObject + { + ID = 2, + Template = "map-with-object-template.tx", + Name = "Thingy", + X = 29.7976f, + Y = 33.8693f, + Width = 37.0156f, + Height = 37.0156f, + Properties = new Dictionary + { + ["Bool"] = new BoolProperty { Name = "Bool", Value = true }, + ["TestClassInTemplate"] = new ClassProperty + { + Name = "TestClassInTemplate", + PropertyType = "TestClass", + Properties = new Dictionary + { + ["Amount"] = new FloatProperty { Name = "Amount", Value = 4.2f }, + ["Name"] = new StringProperty { Name = "Name", Value = "Hello there" } + } + } + } + }, + new RectangleObject + { + ID = 3, + Template = "map-with-object-template.tx", + Name = "Thingy 3", + X = 5, + Y = 5, + Width = 37.0156f, + Height = 37.0156f, + Properties = new Dictionary + { + ["Bool"] = new BoolProperty { Name = "Bool", Value = true }, + ["TestClassInTemplate"] = new ClassProperty + { + Name = "TestClassInTemplate", + PropertyType = "TestClass", + Properties = new Dictionary + { + ["Amount"] = new FloatProperty { Name = "Amount", Value = 4.2f }, + ["Name"] = new StringProperty { Name = "Name", Value = "I am here 3" } + } + } + } + } + ] + } + ] + }; +} diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-object-template.tmx b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-object-template.tmx new file mode 100644 index 0000000..83716a0 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Map/map-with-object-template.tmx @@ -0,0 +1,35 @@ + + + + +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/Template/map-with-object-template.tx b/DotTiled.Tests/TmxSerializer/TestData/Template/map-with-object-template.tx new file mode 100644 index 0000000..3039be2 --- /dev/null +++ b/DotTiled.Tests/TmxSerializer/TestData/Template/map-with-object-template.tx @@ -0,0 +1,14 @@ + + diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs index 5e36b95..c3289a4 100644 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs +++ b/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs @@ -15,8 +15,6 @@ public partial class TmxSerializerLayerTests Assert.Equal(expected.ID, actual.ID); Assert.Equal(expected.Name, actual.Name); Assert.Equal(expected.Class, actual.Class); - Assert.Equal(expected.X, actual.X); - Assert.Equal(expected.Y, actual.Y); Assert.Equal(expected.Opacity, actual.Opacity); Assert.Equal(expected.Visible, actual.Visible); Assert.Equal(expected.TintColor, actual.TintColor); @@ -34,6 +32,8 @@ public partial class TmxSerializerLayerTests // Attributes Assert.Equal(expected.Width, actual.Width); Assert.Equal(expected.Height, actual.Height); + Assert.Equal(expected.X, actual.X); + Assert.Equal(expected.Y, actual.Y); Assert.NotNull(actual.Data); TmxSerializerDataTests.AssertData(actual.Data, expected.Data); @@ -43,6 +43,8 @@ public partial class TmxSerializerLayerTests { // Attributes Assert.Equal(expected.DrawOrder, actual.DrawOrder); + Assert.Equal(expected.X, actual.X); + Assert.Equal(expected.Y, actual.Y); Assert.NotNull(actual.Objects); Assert.Equal(expected.Objects.Count, actual.Objects.Count); @@ -55,8 +57,19 @@ public partial class TmxSerializerLayerTests // Attributes Assert.Equal(expected.RepeatX, actual.RepeatX); Assert.Equal(expected.RepeatY, actual.RepeatY); + Assert.Equal(expected.X, actual.X); + Assert.Equal(expected.Y, actual.Y); Assert.NotNull(actual.Image); TmxSerializerImageTests.AssertImage(actual.Image, expected.Image); } + + private static void AssertLayer(Group actual, Group expected) + { + // Attributes + Assert.NotNull(actual.Layers); + Assert.Equal(expected.Layers.Count, actual.Layers.Count); + for (var i = 0; i < expected.Layers.Count; i++) + AssertLayer(actual.Layers[i], expected.Layers[i]); + } } diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs index ceeefe4..7d513e1 100644 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs +++ b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs @@ -50,13 +50,18 @@ public partial class TmxSerializerMapTests [Theory] [MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))] - public void DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) + public void DeserializeMapFromXmlReader_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) { // Arrange using var reader = TmxSerializerTestData.GetReaderFor(testDataFile); var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile); - Func externalTilesetResolver = (string s) => throw new NotSupportedException("External tilesets are not supported in this test"); - var tmxSerializer = new TmxSerializer(externalTilesetResolver); + Func externalTilesetResolver = (TmxSerializer serializer, string s) => + throw new NotSupportedException("External tilesets are not supported in this test"); + Func externalTemplateResolver = (TmxSerializer serializer, string s) => + throw new NotSupportedException("External templates are not supported in this test"); + var tmxSerializer = new TmxSerializer( + externalTilesetResolver, + externalTemplateResolver); // Act var map = tmxSerializer.DeserializeMap(reader); @@ -68,5 +73,153 @@ public partial class TmxSerializerMapTests Assert.NotNull(raw); AssertMap(raw, expectedMap); + + AssertMap(map, raw); + } + + [Theory] + [MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))] + public void DeserializeMapFromString_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) + { + // Arrange + var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile); + Func externalTilesetResolver = (TmxSerializer serializer, string s) => + throw new NotSupportedException("External tilesets are not supported in this test"); + Func externalTemplateResolver = (TmxSerializer serializer, string s) => + throw new NotSupportedException("External templates are not supported in this test"); + var tmxSerializer = new TmxSerializer( + externalTilesetResolver, + externalTemplateResolver); + + // Act + var raw = tmxSerializer.DeserializeMap(testDataFileText); + + // Assert + Assert.NotNull(raw); + AssertMap(raw, expectedMap); + } + + [Theory] + [MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))] + public void DeserializeMapFromStringFromXmlReader_ValidXmlNoExternalTilesets_Equal(string testDataFile, Map expectedMap) + { + // Arrange + using var reader = TmxSerializerTestData.GetReaderFor(testDataFile); + var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile); + Func externalTilesetResolver = (TmxSerializer serializer, string s) => + throw new NotSupportedException("External tilesets are not supported in this test"); + Func externalTemplateResolver = (TmxSerializer serializer, string s) => + throw new NotSupportedException("External templates are not supported in this test"); + var tmxSerializer = new TmxSerializer( + externalTilesetResolver, + externalTemplateResolver); + + // Act + var map = tmxSerializer.DeserializeMap(reader); + var raw = tmxSerializer.DeserializeMap(testDataFileText); + + // Assert + Assert.NotNull(map); + Assert.NotNull(raw); + + AssertMap(map, raw); + AssertMap(map, expectedMap); + AssertMap(raw, expectedMap); + } + + public static IEnumerable DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => + [ + ["TmxSerializer.TestData.Map.map-with-object-template.tmx", MapWithObjectTemplate()], + ["TmxSerializer.TestData.Map.map-with-group.tmx", MapWithGroup()], + ]; + + [Theory] + [MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))] + public void DeserializeMapFromXmlReader_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) + { + // Arrange + using var reader = TmxSerializerTestData.GetReaderFor(testDataFile); + Func externalTilesetResolver = (TmxSerializer serializer, string s) => + { + using var tilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{s}"); + return serializer.DeserializeTileset(tilesetReader); + }; + Func externalTemplateResolver = (TmxSerializer serializer, string s) => + { + using var templateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{s}"); + return serializer.DeserializeTemplate(templateReader); + }; + var tmxSerializer = new TmxSerializer( + externalTilesetResolver, + externalTemplateResolver); + + // Act + var map = tmxSerializer.DeserializeMap(reader); + + // Assert + Assert.NotNull(map); + AssertMap(map, expectedMap); + } + + [Theory] + [MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))] + public void DeserializeMapFromString_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) + { + // Arrange + var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile); + Func externalTilesetResolver = (TmxSerializer serializer, string s) => + { + using var tilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{s}"); + return serializer.DeserializeTileset(tilesetReader); + }; + Func externalTemplateResolver = (TmxSerializer serializer, string s) => + { + using var templateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{s}"); + return serializer.DeserializeTemplate(templateReader); + }; + var tmxSerializer = new TmxSerializer( + externalTilesetResolver, + externalTemplateResolver); + + // Act + var map = tmxSerializer.DeserializeMap(testDataFileText); + + // Assert + Assert.NotNull(map); + AssertMap(map, expectedMap); + } + + [Theory] + [MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))] + public void DeserializeMapFromStringFromXmlReader_ValidXmlExternalTilesetsAndTemplates_Equal(string testDataFile, Map expectedMap) + { + // Arrange + using var reader = TmxSerializerTestData.GetReaderFor(testDataFile); + var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile); + Func externalTilesetResolver = (TmxSerializer serializer, string s) => + { + using var tilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{s}"); + return serializer.DeserializeTileset(tilesetReader); + }; + Func externalTemplateResolver = (TmxSerializer serializer, string s) => + { + using var templateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{s}"); + return serializer.DeserializeTemplate(templateReader); + }; + var tmxSerializer = new TmxSerializer( + externalTilesetResolver, + externalTemplateResolver); + + // Act + var map = tmxSerializer.DeserializeMap(reader); + var raw = tmxSerializer.DeserializeMap(testDataFileText); + + // Assert + Assert.NotNull(map); + Assert.NotNull(raw); + + AssertMap(map, raw); + AssertMap(map, expectedMap); + AssertMap(raw, expectedMap); } } diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs index 3d1730e..0c27cf1 100644 --- a/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs +++ b/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs @@ -6,10 +6,11 @@ public class TmxSerializerTests public void TmxSerializerConstructor_ExternalTilesetResolverIsNull_ThrowsArgumentNullException() { // Arrange - Func externalTilesetResolver = null!; + Func externalTilesetResolver = null!; + Func externalTemplateResolver = null!; // Act - Action act = () => _ = new TmxSerializer(externalTilesetResolver); + Action act = () => _ = new TmxSerializer(externalTilesetResolver, externalTemplateResolver); // Assert Assert.Throws(act); @@ -19,10 +20,11 @@ public class TmxSerializerTests public void TmxSerializerConstructor_ExternalTilesetResolverIsNotNull_DoesNotThrow() { // Arrange - Func externalTilesetResolver = _ => new Tileset(); + Func externalTilesetResolver = (_, _) => new Tileset(); + Func externalTemplateResolver = (_, _) => new Template { Object = new RectangleObject { } }; // Act - var tmxSerializer = new TmxSerializer(externalTilesetResolver); + var tmxSerializer = new TmxSerializer(externalTilesetResolver, externalTemplateResolver); // Assert Assert.NotNull(tmxSerializer); diff --git a/DotTiled/Model/Layers/ObjectLayer.cs b/DotTiled/Model/Layers/ObjectLayer.cs index 253b616..ca3be60 100644 --- a/DotTiled/Model/Layers/ObjectLayer.cs +++ b/DotTiled/Model/Layers/ObjectLayer.cs @@ -15,8 +15,8 @@ public class ObjectLayer : BaseLayer public uint Y { get; set; } = 0; public uint? Width { get; set; } public uint? Height { get; set; } - public required Color? Color { get; set; } - public required DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; + public Color? Color { get; set; } + public DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; // Elements public required List Objects { get; set; } diff --git a/DotTiled/Model/Layers/Objects/Object.cs b/DotTiled/Model/Layers/Objects/Object.cs index 2684188..b3313d7 100644 --- a/DotTiled/Model/Layers/Objects/Object.cs +++ b/DotTiled/Model/Layers/Objects/Object.cs @@ -5,7 +5,7 @@ namespace DotTiled; public abstract class Object { // Attributes - public required uint ID { get; set; } + public uint? ID { get; set; } public string Name { get; set; } = ""; public string Type { get; set; } = ""; public float X { get; set; } = 0f; diff --git a/DotTiled/Model/Template.cs b/DotTiled/Model/Template.cs new file mode 100644 index 0000000..11ae128 --- /dev/null +++ b/DotTiled/Model/Template.cs @@ -0,0 +1,8 @@ +namespace DotTiled; + +public class Template +{ + // At most one of (if the template is a tile object) + public Tileset? Tileset { get; set; } + public required Object Object { get; set; } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs b/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs index 9676f4b..14fcfaa 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs +++ b/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs @@ -13,5 +13,14 @@ public partial class TmxSerializer field = value; } + + public static void SetAtMostOnceUsingCounter(ref T? field, T value, string fieldName, ref int counter) + { + if (counter > 0) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + counter++; + } } } diff --git a/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs b/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs index 8772c04..df97300 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs +++ b/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs @@ -71,30 +71,63 @@ public partial class TmxSerializer 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"); + uint? idDefault = null; + string nameDefault = ""; + string typeDefault = ""; + float xDefault = 0f; + float yDefault = 0f; + float widthDefault = 0f; + float heightDefault = 0f; + float rotationDefault = 0f; + uint? gidDefault = null; + bool visibleDefault = true; + Dictionary? propertiesDefault = null; + + // Perform template copy first + if (template is not null) + { + var resolvedTemplate = _externalTemplateResolver(this, template); + var templObj = resolvedTemplate.Object; + + idDefault = templObj.ID; + nameDefault = templObj.Name; + typeDefault = templObj.Type; + xDefault = templObj.X; + yDefault = templObj.Y; + widthDefault = templObj.Width; + heightDefault = templObj.Height; + rotationDefault = templObj.Rotation; + gidDefault = templObj.GID; + visibleDefault = templObj.Visible; + propertiesDefault = templObj.Properties; + } + + var id = reader.GetOptionalAttributeParseable("id") ?? idDefault; + var name = reader.GetOptionalAttribute("name") ?? nameDefault; + var type = reader.GetOptionalAttribute("type") ?? typeDefault; + var x = reader.GetOptionalAttributeParseable("x") ?? xDefault; + var y = reader.GetOptionalAttributeParseable("y") ?? yDefault; + var width = reader.GetOptionalAttributeParseable("width") ?? widthDefault; + var height = reader.GetOptionalAttributeParseable("height") ?? heightDefault; + var rotation = reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; + var gid = reader.GetOptionalAttributeParseable("gid") ?? gidDefault; + var visible = reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; + // Elements Object? obj = null; - Dictionary? properties = null; + int propertiesCounter = 0; + Dictionary? properties = propertiesDefault; 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"), + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, MergeProperties(properties, ReadProperties(r)), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref obj, ReadPolylineObject(r), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref obj, ReadTextObject(r), "Object marker"), _ => throw new Exception($"Unknown object marker '{elementName}'") }); @@ -119,19 +152,51 @@ public partial class TmxSerializer return obj; } - private EllipseObject ReadEllipseObject(XmlReader reader, uint id) + private Dictionary MergeProperties(Dictionary? baseProperties, Dictionary overrideProperties) { - reader.Skip(); - return new EllipseObject { ID = id }; + if (baseProperties is null) + return overrideProperties ?? new Dictionary(); + + if (overrideProperties is null) + return baseProperties; + + var result = new Dictionary(baseProperties); + foreach (var (key, value) in overrideProperties) + { + if (!result.TryGetValue(key, out var baseProp)) + { + result[key] = value; + continue; + } + else + { + if (value is ClassProperty classProp) + { + ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); + } + else + { + result[key] = value; + } + } + } + + return result; } - private PointObject ReadPointObject(XmlReader reader, uint id) + private EllipseObject ReadEllipseObject(XmlReader reader) { reader.Skip(); - return new PointObject { ID = id }; + return new EllipseObject { }; } - private PolygonObject ReadPolygonObject(XmlReader reader, uint id) + private PointObject ReadPointObject(XmlReader reader) + { + reader.Skip(); + return new PointObject { }; + } + + private PolygonObject ReadPolygonObject(XmlReader reader) { // Attributes var points = reader.GetRequiredAttributeParseable>("points", s => @@ -146,10 +211,10 @@ public partial class TmxSerializer }); reader.ReadStartElement("polygon"); - return new PolygonObject { ID = id, Points = points }; + return new PolygonObject { Points = points }; } - private PolylineObject ReadPolylineObject(XmlReader reader, uint id) + private PolylineObject ReadPolylineObject(XmlReader reader) { // Attributes var points = reader.GetRequiredAttributeParseable>("points", s => @@ -164,10 +229,10 @@ public partial class TmxSerializer }); reader.ReadStartElement("polyline"); - return new PolylineObject { ID = id, Points = points }; + return new PolylineObject { Points = points }; } - private TextObject ReadTextObject(XmlReader reader, uint id) + private TextObject ReadTextObject(XmlReader reader) { // Attributes var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; @@ -200,7 +265,6 @@ public partial class TmxSerializer return new TextObject { - ID = id, FontFamily = fontFamily, PixelSize = pixelSize, Wrap = wrap, @@ -215,4 +279,31 @@ public partial class TmxSerializer Text = text }; } + + private Template ReadTemplate(XmlReader reader) + { + // No attributes + + // At most one of + Tileset? tileset = null; + + // Should contain exactly one of + Object? obj = null; + + reader.ProcessChildren("template", (r, elementName) => elementName switch + { + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r), "Object"), + _ => r.Skip + }); + + if (obj is null) + throw new NotSupportedException("Template must contain exactly one object"); + + return new Template + { + Tileset = tileset, + Object = obj + }; + } } diff --git a/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs b/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs index 314b9e9..c16e037 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs +++ b/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs @@ -73,7 +73,7 @@ public partial class TmxSerializer // Check if tileset is referring to external file if (source is not null) { - var resolvedTileset = _externalTilesetResolver(source); + var resolvedTileset = _externalTilesetResolver(this, source); resolvedTileset.FirstGID = firstGID; resolvedTileset.Source = null; return resolvedTileset; diff --git a/DotTiled/TmxSerializer/TmxSerializer.cs b/DotTiled/TmxSerializer/TmxSerializer.cs index a2ae79e..5f5c604 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.cs +++ b/DotTiled/TmxSerializer/TmxSerializer.cs @@ -6,11 +6,16 @@ namespace DotTiled; public partial class TmxSerializer { - private readonly Func _externalTilesetResolver; + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; - public TmxSerializer(Func externalTilesetResolver) + public TmxSerializer( + Func externalTilesetResolver, + Func externalTemplateResolver + ) { _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); } public Map DeserializeMap(XmlReader reader) @@ -25,4 +30,16 @@ public partial class TmxSerializer using var reader = XmlReader.Create(stringReader); return DeserializeMap(reader); } + + public Tileset DeserializeTileset(XmlReader reader) + { + reader.ReadToFollowing("tileset"); + return ReadTileset(reader); + } + + public Template DeserializeTemplate(XmlReader reader) + { + reader.ReadToFollowing("template"); + return ReadTemplate(reader); + } }