diff --git a/DotTiled.Tests/Assert/AssertObject.cs b/DotTiled.Tests/Assert/AssertObject.cs index 68d74eb..3b08744 100644 --- a/DotTiled.Tests/Assert/AssertObject.cs +++ b/DotTiled.Tests/Assert/AssertObject.cs @@ -17,7 +17,7 @@ public static partial class DotTiledAssert Assert.Equal(expected.Visible, actual.Visible); Assert.Equal(expected.Template, actual.Template); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); AssertObject((dynamic)expected, (dynamic)actual); } diff --git a/DotTiled.Tests/Assert/AssertProperties.cs b/DotTiled.Tests/Assert/AssertProperties.cs index d0a8269..afd28c2 100644 --- a/DotTiled.Tests/Assert/AssertProperties.cs +++ b/DotTiled.Tests/Assert/AssertProperties.cs @@ -19,51 +19,51 @@ public static partial class DotTiledAssert } } - private static void AssertProperty(IProperty actual, IProperty expected) + private static void AssertProperty(IProperty expected, IProperty actual) { Assert.Equal(expected.Type, actual.Type); Assert.Equal(expected.Name, actual.Name); AssertProperties((dynamic)actual, (dynamic)expected); } - private static void AssertProperty(StringProperty actual, StringProperty expected) + private static void AssertProperty(StringProperty expected, StringProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(IntProperty actual, IntProperty expected) + private static void AssertProperty(IntProperty expected, IntProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(FloatProperty actual, FloatProperty expected) + private static void AssertProperty(FloatProperty expected, FloatProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(BoolProperty actual, BoolProperty expected) + private static void AssertProperty(BoolProperty expected, BoolProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(ColorProperty actual, ColorProperty expected) + private static void AssertProperty(ColorProperty expected, ColorProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(FileProperty actual, FileProperty expected) + private static void AssertProperty(FileProperty expected, FileProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(ObjectProperty actual, ObjectProperty expected) + private static void AssertProperty(ObjectProperty expected, ObjectProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(ClassProperty actual, ClassProperty expected) + private static void AssertProperty(ClassProperty expected, ClassProperty actual) { Assert.Equal(expected.PropertyType, actual.PropertyType); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); } } diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs index 26ddde8..1f9cb1c 100644 --- a/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs @@ -2,7 +2,7 @@ namespace DotTiled.Tests; public partial class TestData { - public static Map MapWithObjectTemplate() => new Map + public static Map MapWithObjectTemplate(string templateExtension) => new Map { Version = "1.10", TiledVersion = "1.11.0", @@ -49,7 +49,7 @@ public partial class TestData new RectangleObject { ID = 1, - Template = "map-with-object-template.tx", + Template = $"map-with-object-template.{templateExtension}", Name = "Thingy 2", X = 94.5749f, Y = 33.6842f, @@ -73,7 +73,7 @@ public partial class TestData new RectangleObject { ID = 2, - Template = "map-with-object-template.tx", + Template = $"map-with-object-template.{templateExtension}", Name = "Thingy", X = 29.7976f, Y = 33.8693f, @@ -97,7 +97,7 @@ public partial class TestData new RectangleObject { ID = 3, - Template = "map-with-object-template.tx", + Template = $"map-with-object-template.{templateExtension}", Name = "Thingy 3", X = 5, Y = 5, @@ -112,7 +112,7 @@ public partial class TestData PropertyType = "TestClass", Properties = new Dictionary { - ["Amount"] = new FloatProperty { Name = "Amount", Value = 4.2f }, + ["Amount"] = new FloatProperty { Name = "Amount", Value = 0.0f }, ["Name"] = new StringProperty { Name = "Name", Value = "I am here 3" } } } diff --git a/DotTiled.Tests/Serialization/TestData/map-with-object-template-propertytypes.json b/DotTiled.Tests/Serialization/TestData/map-with-object-template-propertytypes.json new file mode 100644 index 0000000..3505dce --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/map-with-object-template-propertytypes.json @@ -0,0 +1,24 @@ +[ + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 1, + "members": [ + { + "name": "Amount", + "type": "float", + "value": 0 + }, + { + "name": "Name", + "type": "string", + "value": "" + } + ], + "name": "TestClass", + "type": "class", + "useAs": [ + "property" + ] + } +] diff --git a/DotTiled.Tests/Serialization/TestData/propertytypes-2.json b/DotTiled.Tests/Serialization/TestData/propertytypes-2.json new file mode 100644 index 0000000..e21cf83 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/propertytypes-2.json @@ -0,0 +1,103 @@ +[ + { + "id": 4, + "name": "Enum0String", + "storageType": "string", + "type": "enum", + "values": [ + "Enum0_1", + "Enum0_2", + "Enum0_3" + ], + "valuesAsFlags": false + }, + { + "id": 5, + "name": "Enum1Num", + "storageType": "int", + "type": "enum", + "values": [ + "Enum1Num_1", + "Enum1Num_2", + "Enum1Num_3", + "Enum1Num_4" + ], + "valuesAsFlags": false + }, + { + "id": 6, + "name": "Enum2StringFlags", + "storageType": "string", + "type": "enum", + "values": [ + "Enum2StringFlags_1", + "Enum2StringFlags_2", + "Enum2StringFlags_3", + "Enum2StringFlags_4" + ], + "valuesAsFlags": true + }, + { + "id": 7, + "name": "Enum3NumFlags", + "storageType": "int", + "type": "enum", + "values": [ + "Enum3NumFlags_1", + "Enum3NumFlags_2", + "Enum3NumFlags_3", + "Enum3NumFlags_4", + "Enum3NumFlags_5" + ], + "valuesAsFlags": true + }, + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 2, + "members": [ + { + "name": "Yep", + "propertyType": "TestClass", + "type": "class", + "value": { + } + } + ], + "name": "Test", + "type": "class", + "useAs": [ + "property", + "map", + "layer", + "object", + "tile", + "tileset", + "wangcolor", + "wangset", + "project" + ] + }, + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 1, + "members": [ + { + "name": "Amount", + "type": "float", + "value": 0 + }, + { + "name": "Name", + "type": "string", + "value": "" + } + ], + "name": "TestClass", + "type": "class", + "useAs": [ + "property" + ] + } +] diff --git a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index 605cda9..7e220a9 100644 --- a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -20,17 +20,13 @@ public partial class TmjMapReaderTests var json = TestData.GetRawStringFor(testDataFile); static Template ResolveTemplate(string source) { - var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}"); - //var templateReader = new TmjTemplateReader(templateJson, ResolveTemplate); - return null; + throw new NotSupportedException("External templates are not supported in this test."); } static Tileset ResolveTileset(string source) { - var tilesetJson = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}"); - //var tilesetReader = new TmjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate); - return null; + throw new NotSupportedException("External tilesets are not supported in this test."); } - using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate); + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, []); // Act var map = mapReader.ReadMap(); @@ -42,7 +38,7 @@ public partial class TmjMapReaderTests public static IEnumerable DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => [ - ["Serialization.TestData.Map.map-with-object-template.tmj", TestData.MapWithObjectTemplate()], + ["Serialization.TestData.Map.map-with-object-template.tmj", TestData.MapWithObjectTemplate("tj")], ["Serialization.TestData.Map.map-with-group.tmj", TestData.MapWithGroup()], ]; @@ -51,20 +47,55 @@ public partial class TmjMapReaderTests public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) { // Arrange + CustomTypeDefinition[] customTypeDefinitions = [ + new CustomClassDefinition + { + Name = "TestClass", + ID = 1, + UseAs = CustomClassUseAs.Property, + Members = [ + new StringProperty + { + Name = "Name", + Value = "" + }, + new FloatProperty + { + Name = "Amount", + Value = 0f + } + ] + }, + new CustomClassDefinition + { + Name = "Test", + ID = 2, + UseAs = CustomClassUseAs.All, + Members = [ + new ClassProperty + { + Name = "Yep", + PropertyType = "TestClass", + Properties = [] + } + ] + } + ]; + var json = TestData.GetRawStringFor(testDataFile); - static Template ResolveTemplate(string source) + Template ResolveTemplate(string source) { var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}"); - //var templateReader = new TmjTemplateReader(templateJson, ResolveTemplate); - return null; + using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions); + return templateReader.ReadTemplate(); } - static Tileset ResolveTileset(string source) + Tileset ResolveTileset(string source) { - var tilesetJson = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}"); - //var tilesetReader = new TmjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate); - return null; + var tilesetJson = TestData.GetRawStringFor($"Serialization.TestData.Tileset.{source}"); + using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions); + return tilesetReader.ReadTileset(); } - using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate); + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions); // Act var map = mapReader.ReadMap(); diff --git a/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index 7a1fc66..7c25d1b 100644 --- a/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -116,7 +116,7 @@ public partial class TmxMapReaderTests public static IEnumerable DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => [ - ["Serialization.TestData.Map.map-with-object-template.tmx", TestData.MapWithObjectTemplate()], + ["Serialization.TestData.Map.map-with-object-template.tmx", TestData.MapWithObjectTemplate("tx")], ["Serialization.TestData.Map.map-with-group.tmx", TestData.MapWithGroup()], ]; diff --git a/DotTiled/Model/IProperty.cs b/DotTiled/Model/IProperty.cs index 9558ee2..ae522f2 100644 --- a/DotTiled/Model/IProperty.cs +++ b/DotTiled/Model/IProperty.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; namespace DotTiled; @@ -18,6 +20,8 @@ public interface IProperty { public string Name { get; set; } public PropertyType Type { get; } + + IProperty Clone(); } public class StringProperty : IProperty @@ -25,6 +29,12 @@ public class StringProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.String; public required string Value { get; set; } + + public IProperty Clone() => new StringProperty + { + Name = Name, + Value = Value + }; } public class IntProperty : IProperty @@ -32,6 +42,12 @@ public class IntProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Int; public required int Value { get; set; } + + public IProperty Clone() => new IntProperty + { + Name = Name, + Value = Value + }; } public class FloatProperty : IProperty @@ -39,6 +55,12 @@ public class FloatProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Float; public required float Value { get; set; } + + public IProperty Clone() => new FloatProperty + { + Name = Name, + Value = Value + }; } public class BoolProperty : IProperty @@ -46,6 +68,12 @@ public class BoolProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Bool; public required bool Value { get; set; } + + public IProperty Clone() => new BoolProperty + { + Name = Name, + Value = Value + }; } public class ColorProperty : IProperty @@ -53,6 +81,12 @@ public class ColorProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Color; public required Color Value { get; set; } + + public IProperty Clone() => new ColorProperty + { + Name = Name, + Value = Value + }; } public class FileProperty : IProperty @@ -60,6 +94,12 @@ public class FileProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.File; public required string Value { get; set; } + + public IProperty Clone() => new FileProperty + { + Name = Name, + Value = Value + }; } public class ObjectProperty : IProperty @@ -67,6 +107,12 @@ public class ObjectProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Object; public required uint Value { get; set; } + + public IProperty Clone() => new ObjectProperty + { + Name = Name, + Value = Value + }; } public class ClassProperty : IProperty @@ -75,4 +121,53 @@ public class ClassProperty : IProperty public PropertyType Type => DotTiled.PropertyType.Class; public required string PropertyType { get; set; } public required Dictionary Properties { get; set; } + + public IProperty Clone() => new ClassProperty + { + Name = Name, + PropertyType = PropertyType, + Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) + }; +} + +public abstract class CustomTypeDefinition +{ + public uint ID { get; set; } + public string Name { get; set; } = ""; +} + +[Flags] +public enum CustomClassUseAs +{ + Property, + Map, + Layer, + Object, + Tile, + Tileset, + WangColor, + Wangset, + Project, + All = Property | Map | Layer | Object | Tile | Tileset | WangColor | Wangset | Project +} + +public class CustomClassDefinition : CustomTypeDefinition +{ + public Color Color { get; set; } + public bool DrawFill { get; set; } + public CustomClassUseAs UseAs { get; set; } + public List Members { get; set; } +} + +public enum CustomEnumStorageType +{ + Int, + String +} + +public class CustomEnumDefinition : CustomTypeDefinition +{ + public CustomEnumStorageType StorageType { get; set; } + public List Values { get; set; } = []; + public bool ValueAsFlags { get; set; } } diff --git a/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/DotTiled/Serialization/Tmj/TjTemplateReader.cs new file mode 100644 index 0000000..0dc1dd8 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +public class TjTemplateReader : ITemplateReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TjTemplateReader( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + } + + public Template ReadTemplate() + { + var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); + var rootElement = jsonDoc.RootElement; + return Tmj.ReadTemplate(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TjTemplateReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/DotTiled/Serialization/Tmj/Tmj.Layer.cs index 7e8685e..33d87e7 100644 --- a/DotTiled/Serialization/Tmj/Tmj.Layer.cs +++ b/DotTiled/Serialization/Tmj/Tmj.Layer.cs @@ -4,27 +4,33 @@ using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; +using System.Numerics; using System.Text.Json; namespace DotTiled; internal partial class Tmj { - internal static BaseLayer ReadLayer(JsonElement element) + internal static BaseLayer ReadLayer( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { var type = element.GetRequiredProperty("type"); return type switch { - "tilelayer" => ReadTileLayer(element), - // "objectgroup" => ReadObjectGroup(element), + "tilelayer" => ReadTileLayer(element, customTypeDefinitions), + "objectgroup" => ReadObjectLayer(element, externalTemplateResolver, customTypeDefinitions), // "imagelayer" => ReadImageLayer(element), - // "group" => ReadGroup(element), + "group" => ReadGroup(element, externalTemplateResolver, customTypeDefinitions), _ => throw new JsonException($"Unsupported layer type '{type}'.") }; } - internal static TileLayer ReadTileLayer(JsonElement element) + internal static TileLayer ReadTileLayer( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) { var compression = element.GetOptionalPropertyParseable("compression", s => s switch { @@ -50,7 +56,7 @@ internal partial class Tmj var opacity = element.GetOptionalProperty("opacity", 1.0f); var parallaxx = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxy = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e), null); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); var repeatX = element.GetOptionalProperty("repeatx", false); var repeatY = element.GetOptionalProperty("repeaty", false); var startX = element.GetOptionalProperty("startx", 0); @@ -85,4 +91,276 @@ internal partial class Tmj Data = data ?? chunks }; } + + internal static Group ReadGroup( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); + var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + + return new Group + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Layers = layers + }; + } + + internal static ObjectLayer ReadObjectLayer( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); + var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + + var x = element.GetOptionalProperty("x", 0); + var y = element.GetOptionalProperty("y", 0); + var width = element.GetOptionalProperty("width", null); + var height = element.GetOptionalProperty("height", null); + var color = element.GetOptionalPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var drawOrder = element.GetOptionalPropertyParseable("draworder", s => s switch + { + "topdown" => DrawOrder.TopDown, + "index" => DrawOrder.Index, + _ => throw new JsonException($"Unknown draw order '{s}'.") + }, DrawOrder.TopDown); + + var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); + + return new ObjectLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + X = x, + Y = y, + Width = width, + Height = height, + Color = color, + DrawOrder = drawOrder, + Objects = objects + }; + } + + internal static Object ReadObject( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + 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; + bool ellipseDefault = false; + bool pointDefault = false; + List? polygonDefault = null; + List? polylineDefault = null; + Dictionary? propertiesDefault = null; + + var template = element.GetOptionalProperty("template", null); + if (template is not null) + { + var resolvedTemplate = externalTemplateResolver(template); + var templObj = resolvedTemplate.Object; + + idDefault = templObj.ID; + nameDefault = templObj.Name; + typeDefault = templObj.Type; + xDefault = templObj.X; + yDefault = templObj.Y; + widthDefault = templObj.Width; + heightDefault = templObj.Height; + rotationDefault = templObj.Rotation; + gidDefault = templObj.GID; + visibleDefault = templObj.Visible; + propertiesDefault = templObj.Properties; + ellipseDefault = templObj is EllipseObject; + pointDefault = templObj is PointObject; + polygonDefault = (templObj is PolygonObject polygonObj) ? polygonObj.Points : null; + polylineDefault = (templObj is PolylineObject polylineObj) ? polylineObj.Points : null; + } + + var ellipse = element.GetOptionalProperty("ellipse", ellipseDefault); + var gid = element.GetOptionalProperty("gid", gidDefault); + var height = element.GetOptionalProperty("height", heightDefault); + var id = element.GetOptionalProperty("id", idDefault); + var name = element.GetOptionalProperty("name", nameDefault); + var point = element.GetOptionalProperty("point", pointDefault); + var polygon = element.GetOptionalPropertyCustom?>("polygon", e => ReadPoints(e), polygonDefault); + var polyline = element.GetOptionalPropertyCustom?>("polyline", e => ReadPoints(e), polylineDefault); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var rotation = element.GetOptionalProperty("rotation", rotationDefault); + // var text + var type = element.GetOptionalProperty("type", typeDefault); + var visible = element.GetOptionalProperty("visible", visibleDefault); + var width = element.GetOptionalProperty("width", widthDefault); + var x = element.GetOptionalProperty("x", xDefault); + var y = element.GetOptionalProperty("y", yDefault); + + if (ellipse) + { + return new EllipseObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties + }; + } + + if (point) + { + return new PointObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties + }; + } + + if (polygon is not null) + { + return new PolygonObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties, + Points = polygon + }; + } + + if (polyline is not null) + { + return new PolylineObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties, + Points = polyline + }; + } + + // Text + + return new RectangleObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties + }; + } + + internal static List ReadPoints(JsonElement element) => + element.GetValueAsList(e => + { + var x = e.GetRequiredProperty("x"); + var y = e.GetRequiredProperty("y"); + return new Vector2(x, y); + }); + + internal static Template ReadTemplate( + JsonElement element, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var type = element.GetRequiredProperty("type"); + var tileset = element.GetOptionalPropertyCustom("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null); + var @object = element.GetRequiredPropertyCustom("object", el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)); + + return new Template + { + Tileset = tileset, + Object = @object + }; + } } diff --git a/DotTiled/Serialization/Tmj/Tmj.Map.cs b/DotTiled/Serialization/Tmj/Tmj.Map.cs index 0906b70..ea7313f 100644 --- a/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -9,7 +9,11 @@ namespace DotTiled; internal partial class Tmj { - internal static Map ReadMap(JsonElement element, Func? externalTilesetResolver, Func externalTemplateResolver) + internal static Map ReadMap( + JsonElement element, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { var version = element.GetRequiredProperty("version"); var tiledVersion = element.GetRequiredProperty("tiledversion"); @@ -55,10 +59,10 @@ internal partial class Tmj var nextObjectID = element.GetRequiredProperty("nextobjectid"); var infinite = element.GetOptionalProperty("infinite", false); - var properties = element.GetOptionalPropertyCustom?>("properties", ReadProperties, null); + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); - List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(ReadLayer), []); - List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver)), []); + List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []); return new Map { diff --git a/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/DotTiled/Serialization/Tmj/Tmj.Properties.cs index 37b7dd1..2e01749 100644 --- a/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ b/DotTiled/Serialization/Tmj/Tmj.Properties.cs @@ -8,7 +8,9 @@ namespace DotTiled; internal partial class Tmj { - internal static Dictionary ReadProperties(JsonElement element) => + internal static Dictionary ReadProperties( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) => element.GetValueAsList(e => { var name = e.GetRequiredProperty("name"); @@ -34,20 +36,105 @@ internal partial class Tmj PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable("value") }, PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty("value") }, PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Class => ReadClassProperty(e), + PropertyType.Class => ReadClassProperty(e, customTypeDefinitions), _ => throw new JsonException("Invalid property type") }; return property!; }).ToDictionary(p => p.Name); - internal static ClassProperty ReadClassProperty(JsonElement element) + internal static ClassProperty ReadClassProperty( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) { var name = element.GetRequiredProperty("name"); var propertyType = element.GetRequiredProperty("propertytype"); - var properties = element.GetRequiredPropertyCustom>("properties", ReadProperties); + var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties }; + if (customTypeDef is CustomClassDefinition ccd) + { + var propsInType = CreateInstanceOfCustomClass(ccd); + var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); + + var mergedProps = MergeProperties(propsInType, props); + + return new ClassProperty + { + Name = name, + PropertyType = propertyType, + Properties = mergedProps + }; + } + + throw new JsonException($"Unknown custom class '{propertyType}'."); + } + + internal static Dictionary ReadCustomClassProperties( + JsonElement element, + CustomClassDefinition customClassDefinition, + IReadOnlyCollection customTypeDefinitions) + { + Dictionary resultingProps = []; + + foreach (var prop in customClassDefinition.Members) + { + if (!element.TryGetProperty(prop.Name, out var propElement)) + continue; // Property not present in element, therefore will use default value + + IProperty property = prop.Type switch + { + PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs(), CultureInfo.InvariantCulture) }, + PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Class => ReadClassProperty(propElement, customTypeDefinitions), + _ => throw new JsonException("Invalid property type") + }; + + resultingProps[prop.Name] = property; + } + + return resultingProps; + } + + internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) + { + return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); + } + + internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary overrideProperties) + { + if (baseProperties is null) + return overrideProperties ?? new Dictionary(); + + if (overrideProperties is null) + return baseProperties; + + var result = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); + 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; } } diff --git a/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/DotTiled/Serialization/Tmj/Tmj.Tileset.cs index 66d42b1..1d0aa6e 100644 --- a/DotTiled/Serialization/Tmj/Tmj.Tileset.cs +++ b/DotTiled/Serialization/Tmj/Tmj.Tileset.cs @@ -10,7 +10,11 @@ namespace DotTiled; internal partial class Tmj { - internal static Tileset ReadTileset(JsonElement element, Func? externalTilesetResolver, Func externalTemplateResolver) + internal static Tileset ReadTileset( + JsonElement element, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var @class = element.GetOptionalProperty("class", ""); @@ -42,7 +46,7 @@ internal partial class Tmj "bottomright" => ObjectAlignment.BottomRight, _ => throw new JsonException($"Unknown object alignment '{s}'") }, ObjectAlignment.Unspecified); - var properties = element.GetOptionalPropertyCustom?>("properties", ReadProperties, null); + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); var source = element.GetOptionalProperty("source", null); var spacing = element.GetOptionalProperty("spacing", null); var tileCount = element.GetOptionalProperty("tilecount", null); @@ -55,7 +59,7 @@ internal partial class Tmj "grid" => TileRenderSize.Grid, _ => throw new JsonException($"Unknown tile render size '{s}'") }, TileRenderSize.Tile); - var tiles = element.GetOptionalPropertyCustom>("tiles", ReadTiles, []); + var tiles = element.GetOptionalPropertyCustom>("tiles", el => ReadTiles(el, customTypeDefinitions), []); var tileWidth = element.GetOptionalProperty("tilewidth", null); var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var type = element.GetOptionalProperty("type", null); @@ -153,7 +157,9 @@ internal partial class Tmj }; } - internal static List ReadTiles(JsonElement element) => + internal static List ReadTiles( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) => element.GetValueAsList(e => { //var animation = e.GetOptionalPropertyCustom>("animation", ReadFrames, null); @@ -167,7 +173,7 @@ internal partial class Tmj var height = e.GetOptionalProperty("height", imageHeight ?? 0); //var objectGroup = e.GetOptionalPropertyCustom("objectgroup", ReadObjectLayer, null); var probability = e.GetOptionalProperty("probability", 1.0f); - var properties = e.GetOptionalPropertyCustom?>("properties", ReadProperties, null); + var properties = e.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); // var terrain, replaced by wangsets var type = e.GetOptionalProperty("type", ""); diff --git a/DotTiled/Serialization/Tmj/TmjMapReader.cs b/DotTiled/Serialization/Tmj/TmjMapReader.cs index 9aa9675..260cd21 100644 --- a/DotTiled/Serialization/Tmj/TmjMapReader.cs +++ b/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Json; @@ -14,18 +15,25 @@ public class TmjMapReader : IMapReader private string _jsonString; private bool disposedValue; - public TmjMapReader(string jsonString, Func externalTilesetResolver, Func externalTemplateResolver) + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TmjMapReader( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); } public Map ReadMap() { var jsonDoc = JsonDocument.Parse(_jsonString); var rootElement = jsonDoc.RootElement; - return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver); + return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); } protected virtual void Dispose(bool disposing) diff --git a/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/DotTiled/Serialization/Tmj/TsjTilesetReader.cs new file mode 100644 index 0000000..14e5323 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +public class TsjTilesetReader : ITilesetReader +{ + // External resolvers + private readonly Func _externalTemplateResolver; + + private readonly string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TsjTilesetReader( + string jsonString, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + } + + public Tileset ReadTileset() + { + var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); + var rootElement = jsonDoc.RootElement; + return Tmj.ReadTileset( + rootElement, + _ => throw new NotSupportedException("External tilesets cannot refer to other external tilesets."), + _externalTemplateResolver, + _customTypeDefinitions); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TsjTilesetReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +}