From 1c1ba326b283988fbaa0c901db920de55ca71a70 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Sat, 24 Aug 2024 21:38:40 +0200 Subject: [PATCH 1/7] Restructure properties API --- src/DotTiled.Tests/Assert/AssertMap.cs | 2 +- src/DotTiled.Tests/Assert/AssertProperties.cs | 21 ++++++- .../map-with-common-props.cs | 20 +++---- .../map-with-custom-type-props.cs | 26 ++++---- src/DotTiled/Model/Map.cs | 7 ++- src/DotTiled/Model/Properties/BoolProperty.cs | 2 +- .../Model/Properties/ClassProperty.cs | 26 +++++++- .../Model/Properties/ColorProperty.cs | 2 +- src/DotTiled/Model/Properties/FileProperty.cs | 2 +- .../Model/Properties/FloatProperty.cs | 2 +- .../Model/Properties/IHasProperties.cs | 59 +++++++++++++++++++ src/DotTiled/Model/Properties/IProperty.cs | 12 ++++ src/DotTiled/Model/Properties/IntProperty.cs | 2 +- .../Model/Properties/ObjectProperty.cs | 2 +- .../Model/Properties/StringProperty.cs | 2 +- src/DotTiled/Serialization/Helpers.cs | 47 ++++++++++++++- src/DotTiled/Serialization/Tmj/Tmj.Map.cs | 4 +- .../Serialization/Tmj/Tmj.Properties.cs | 52 ++++++++++++---- src/DotTiled/Serialization/Tmx/Tmx.Map.cs | 6 +- .../Serialization/Tmx/Tmx.Properties.cs | 48 ++++++++++++--- 20 files changed, 282 insertions(+), 62 deletions(-) create mode 100644 src/DotTiled/Model/Properties/IHasProperties.cs diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index 6984b79..6f25f45 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -91,7 +91,7 @@ public static partial class DotTiledAssert AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); - AssertProperties(actual.Properties, expected.Properties); + AssertPropertiesList(actual.Properties, expected.Properties); Assert.NotNull(actual.Tilesets); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); diff --git a/src/DotTiled.Tests/Assert/AssertProperties.cs b/src/DotTiled.Tests/Assert/AssertProperties.cs index 646d6eb..ad4dc53 100644 --- a/src/DotTiled.Tests/Assert/AssertProperties.cs +++ b/src/DotTiled.Tests/Assert/AssertProperties.cs @@ -21,6 +21,25 @@ public static partial class DotTiledAssert } } + internal static void AssertPropertiesList(IList? expected, IList? actual) + { + if (expected is null) + { + Assert.Null(actual); + return; + } + + Assert.NotNull(actual); + AssertEqual(expected.Count, actual.Count, "Properties.Count"); + foreach (var prop in expected) + { + Assert.Contains(actual, p => p.Name == prop.Name); + + var actualProp = actual.First(p => p.Name == prop.Name); + AssertProperty((dynamic)prop, (dynamic)actualProp); + } + } + private static void AssertProperty(IProperty expected, IProperty actual) { AssertEqual(expected.Type, actual.Type, "Property.Type"); @@ -45,6 +64,6 @@ public static partial class DotTiledAssert private static void AssertProperty(ClassProperty expected, ClassProperty actual) { AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType"); - AssertProperties(expected.Properties, actual.Properties); + AssertPropertiesList(expected.Value, actual.Value); } } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs index ba5cf4f..d5d8c1e 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs @@ -55,15 +55,15 @@ public partial class TestData } } ], - Properties = new Dictionary - { - ["boolprop"] = new BoolProperty { Name = "boolprop", Value = true }, - ["colorprop"] = new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, - ["fileprop"] = new FileProperty { Name = "fileprop", Value = "file.txt" }, - ["floatprop"] = new FloatProperty { Name = "floatprop", Value = 4.2f }, - ["intprop"] = new IntProperty { Name = "intprop", Value = 8 }, - ["objectprop"] = new ObjectProperty { Name = "objectprop", Value = 5 }, - ["stringprop"] = new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } - } + Properties = + [ + new BoolProperty { Name = "boolprop", Value = true }, + new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileprop", Value = "file.txt" }, + new FloatProperty { Name = "floatprop", Value = 4.2f }, + new IntProperty { Name = "intprop", Value = 8 }, + new ObjectProperty { Name = "objectprop", Value = 5 }, + new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } + ] }; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs index 758c5c3..2c79478 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -55,24 +55,22 @@ public partial class TestData } } ], - Properties = new Dictionary - { - ["customclassprop"] = new ClassProperty + Properties = [ + new ClassProperty { Name = "customclassprop", PropertyType = "CustomClass", - Properties = new Dictionary - { - ["boolinclass"] = new BoolProperty { Name = "boolinclass", Value = true }, - ["colorinclass"] = new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, - ["fileinclass"] = new FileProperty { Name = "fileinclass", Value = "" }, - ["floatinclass"] = new FloatProperty { Name = "floatinclass", Value = 13.37f }, - ["intinclass"] = new IntProperty { Name = "intinclass", Value = 0 }, - ["objectinclass"] = new ObjectProperty { Name = "objectinclass", Value = 0 }, - ["stringinclass"] = new StringProperty { Name = "stringinclass", Value = "This is a set string" } - } + Value = [ + new BoolProperty { Name = "boolinclass", Value = true }, + new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileinclass", Value = "" }, + new FloatProperty { Name = "floatinclass", Value = 13.37f }, + new IntProperty { Name = "intinclass", Value = 0 }, + new ObjectProperty { Name = "objectinclass", Value = 0 }, + new StringProperty { Name = "stringinclass", Value = "This is a set string" } + ] } - } + ] }; // This comes from map-with-custom-type-props/propertytypes.json diff --git a/src/DotTiled/Model/Map.cs b/src/DotTiled/Model/Map.cs index c9c72dc..48ed975 100644 --- a/src/DotTiled/Model/Map.cs +++ b/src/DotTiled/Model/Map.cs @@ -90,7 +90,7 @@ public enum StaggerIndex /// /// Represents a Tiled map. /// -public class Map +public class Map : HasPropertiesBase { /// /// The TMX format version. Is incremented to match minor Tiled releases. @@ -191,7 +191,10 @@ public class Map /// /// Map properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; /// /// List of tilesets used by the map. diff --git a/src/DotTiled/Model/Properties/BoolProperty.cs b/src/DotTiled/Model/Properties/BoolProperty.cs index bc60a87..315e820 100644 --- a/src/DotTiled/Model/Properties/BoolProperty.cs +++ b/src/DotTiled/Model/Properties/BoolProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a boolean property. /// -public class BoolProperty : IProperty +public class BoolProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/ClassProperty.cs b/src/DotTiled/Model/Properties/ClassProperty.cs index 50d65ba..eb7efce 100644 --- a/src/DotTiled/Model/Properties/ClassProperty.cs +++ b/src/DotTiled/Model/Properties/ClassProperty.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace DotTiled.Model; @@ -6,7 +7,7 @@ namespace DotTiled.Model; /// /// Represents a class property. /// -public class ClassProperty : IProperty +public class ClassProperty : IHasProperties, IProperty> { /// public required string Name { get; set; } @@ -23,13 +24,32 @@ public class ClassProperty : IProperty /// /// The properties of the class property. /// - public required Dictionary Properties { get; set; } + public required IList Value { get; set; } /// public IProperty Clone() => new ClassProperty { Name = Name, PropertyType = PropertyType, - Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) + Value = Value.Select(property => property.Clone()).ToList() }; + + /// + public IList GetProperties() => Value; + + /// + public T GetProperty(string name) where T : IProperty => throw new System.NotImplementedException(); + + /// + public bool TryGetProperty(string name, [NotNullWhen(true)] out T? property) where T : IProperty + { + if (Value.FirstOrDefault(_properties => _properties.Name == name) is T prop) + { + property = prop; + return true; + } + + property = default; + return false; + } } diff --git a/src/DotTiled/Model/Properties/ColorProperty.cs b/src/DotTiled/Model/Properties/ColorProperty.cs index aec43f6..7c4a132 100644 --- a/src/DotTiled/Model/Properties/ColorProperty.cs +++ b/src/DotTiled/Model/Properties/ColorProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a color property. /// -public class ColorProperty : IProperty +public class ColorProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/FileProperty.cs b/src/DotTiled/Model/Properties/FileProperty.cs index 4ed8642..7c65121 100644 --- a/src/DotTiled/Model/Properties/FileProperty.cs +++ b/src/DotTiled/Model/Properties/FileProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a file property. /// -public class FileProperty : IProperty +public class FileProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/FloatProperty.cs b/src/DotTiled/Model/Properties/FloatProperty.cs index 4c6b51f..57ba928 100644 --- a/src/DotTiled/Model/Properties/FloatProperty.cs +++ b/src/DotTiled/Model/Properties/FloatProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a float property. /// -public class FloatProperty : IProperty +public class FloatProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/IHasProperties.cs b/src/DotTiled/Model/Properties/IHasProperties.cs new file mode 100644 index 0000000..28b8532 --- /dev/null +++ b/src/DotTiled/Model/Properties/IHasProperties.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace DotTiled.Model; + +/// +/// Interface for objects that have properties attached to them. +/// +public interface IHasProperties +{ + /// + /// The properties attached to the object. + /// + IList GetProperties(); + + /// + /// Tries to get a property of the specified type with the specified name. + /// + /// The type of the property to get. + /// The name of the property to get. + /// The property with the specified name, if found. + /// True if a property with the specified name was found; otherwise, false. + bool TryGetProperty(string name, out T? property) where T : IProperty; + + /// + /// Gets a property of the specified type with the specified name. + /// + /// The type of the property to get. + /// The name of the property to get. + /// The property with the specified name. + T GetProperty(string name) where T : IProperty; +} + +/// +/// Base class for objects that have properties attached to them. +/// +public abstract class HasPropertiesBase : IHasProperties +{ + /// + public abstract IList GetProperties(); + + /// + public T GetProperty(string name) where T : IProperty => throw new System.NotImplementedException(); + + /// + public bool TryGetProperty(string name, [NotNullWhen(true)] out T? property) where T : IProperty + { + var properties = GetProperties(); + if (properties.FirstOrDefault(_properties => _properties.Name == name) is T prop) + { + property = prop; + return true; + } + + property = default; + return false; + } +} diff --git a/src/DotTiled/Model/Properties/IProperty.cs b/src/DotTiled/Model/Properties/IProperty.cs index 262ee09..d2d98e8 100644 --- a/src/DotTiled/Model/Properties/IProperty.cs +++ b/src/DotTiled/Model/Properties/IProperty.cs @@ -22,3 +22,15 @@ public interface IProperty /// An identical, but non-reference-equal, instance of the same property. IProperty Clone(); } + +/// +/// Interface for properties that can be attached to objects, tiles, tilesets, maps etc. +/// +/// The type of the property value. +public interface IProperty : IProperty +{ + /// + /// The value of the property. + /// + public T Value { get; set; } +} diff --git a/src/DotTiled/Model/Properties/IntProperty.cs b/src/DotTiled/Model/Properties/IntProperty.cs index 29f7a1d..27a7afc 100644 --- a/src/DotTiled/Model/Properties/IntProperty.cs +++ b/src/DotTiled/Model/Properties/IntProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents an integer property. /// -public class IntProperty : IProperty +public class IntProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/ObjectProperty.cs b/src/DotTiled/Model/Properties/ObjectProperty.cs index 04b15ba..de8a1e0 100644 --- a/src/DotTiled/Model/Properties/ObjectProperty.cs +++ b/src/DotTiled/Model/Properties/ObjectProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents an object property. /// -public class ObjectProperty : IProperty +public class ObjectProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/StringProperty.cs b/src/DotTiled/Model/Properties/StringProperty.cs index 49d7aec..0c20b29 100644 --- a/src/DotTiled/Model/Properties/StringProperty.cs +++ b/src/DotTiled/Model/Properties/StringProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a string property. /// -public class StringProperty : IProperty +public class StringProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index c6cdefe..0028650 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -73,6 +73,9 @@ internal static partial class Helpers }; } + internal static List CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => + customClassDefinition.Members.Select(x => x.Clone()).ToList(); + internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary? overrideProperties) { if (baseProperties is null) @@ -93,7 +96,7 @@ internal static partial class Helpers { if (value is ClassProperty classProp) { - ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); + ((ClassProperty)baseProp).Value = MergePropertiesList(((ClassProperty)baseProp).Value, classProp.Value); } else { @@ -105,6 +108,48 @@ internal static partial class Helpers return result; } + internal static IList MergePropertiesList(IList? baseProperties, IList? overrideProperties) + { + if (baseProperties is null) + return overrideProperties ?? []; + + if (overrideProperties is null) + return baseProperties; + + var result = baseProperties.Select(x => x.Clone()).ToList(); + foreach (var overrideProp in overrideProperties) + { + if (!result.Any(x => x.Name == overrideProp.Name)) + { + result.Add(overrideProp); + continue; + } + else + { + var existingProp = result.First(x => x.Name == overrideProp.Name); + if (existingProp is ClassProperty classProp) + { + classProp.Value = MergePropertiesList(classProp.Value, ((ClassProperty)overrideProp).Value); + } + else + { + ReplacePropertyInList(result, overrideProp); + } + } + } + + return result; + } + + internal static void ReplacePropertyInList(List properties, IProperty property) + { + var index = properties.FindIndex(p => p.Name == property.Name); + if (index == -1) + properties.Add(property); + else + properties[index] = property; + } + internal static void SetAtMostOnce(ref T? field, T value, string fieldName) { if (field is not null) diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs b/src/DotTiled/Serialization/Tmj/Tmj.Map.cs index 47dde3f..dd1a012 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -58,7 +58,7 @@ internal partial class Tmj var nextObjectID = element.GetRequiredProperty("nextobjectid"); var infinite = element.GetOptionalProperty("infinite", false); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadPropertiesList(el, customTypeDefinitions), null); 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)), []); @@ -84,7 +84,7 @@ internal partial class Tmj NextLayerID = nextLayerID, NextObjectID = nextObjectID, Infinite = infinite, - Properties = properties, + Properties = properties ?? [], Tilesets = tilesets, Layers = layers }; diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs index 0c35bbe..5b25429 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs @@ -43,6 +43,41 @@ internal partial class Tmj return property!; }).ToDictionary(p => p.Name); + internal static List ReadPropertiesList( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) => + element.GetValueAsList(e => + { + var name = e.GetRequiredProperty("name"); + var type = e.GetOptionalPropertyParseable("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 JsonException("Invalid property type") + }, PropertyType.String); + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, + 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, customTypeDefinitions), + _ => throw new JsonException("Invalid property type") + }; + + return property!; + }); + internal static ClassProperty ReadClassProperty( JsonElement element, IReadOnlyCollection customTypeDefinitions) @@ -54,28 +89,28 @@ internal partial class Tmj if (customTypeDef is CustomClassDefinition ccd) { - var propsInType = CreateInstanceOfCustomClass(ccd); - var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); + var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); - var mergedProps = Helpers.MergeProperties(propsInType, props); + var mergedProps = Helpers.MergePropertiesList(propsInType, props); return new ClassProperty { Name = name, PropertyType = propertyType, - Properties = mergedProps + Value = props }; } throw new JsonException($"Unknown custom class '{propertyType}'."); } - internal static Dictionary ReadCustomClassProperties( + internal static List ReadCustomClassProperties( JsonElement element, CustomClassDefinition customClassDefinition, IReadOnlyCollection customTypeDefinitions) { - Dictionary resultingProps = []; + List resultingProps = Helpers.CreateInstanceOfCustomClass(customClassDefinition); foreach (var prop in customClassDefinition.Members) { @@ -95,12 +130,9 @@ internal partial class Tmj _ => throw new JsonException("Invalid property type") }; - resultingProps[prop.Name] = property; + Helpers.ReplacePropertyInList(resultingProps, property); } return resultingProps; } - - internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => - customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs index 1987bb1..6dd5ea5 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs @@ -61,7 +61,7 @@ internal partial class Tmx var infinite = (reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; // At most one of - Dictionary? properties = null; + List? properties = null; // Any number of List layers = []; @@ -69,7 +69,7 @@ internal partial class Tmx reader.ProcessChildren("map", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadPropertiesList(r, customTypeDefinitions), "Properties"), "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), "layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)), "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), @@ -99,7 +99,7 @@ internal partial class Tmx NextLayerID = nextLayerID, NextObjectID = nextObjectID, Infinite = infinite, - Properties = properties, + Properties = properties ?? [], Tilesets = tilesets, Layers = layers }; diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs index 9a748d6..98ee546 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs @@ -43,6 +43,42 @@ internal partial class Tmx }).ToDictionary(x => x.name, x => x.property); } + internal static List ReadPropertiesList( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) + { + 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, customTypeDefinitions), + _ => throw new XmlException("Invalid property type") + }; + return property; + }); + } + internal static ClassProperty ReadClassProperty( XmlReader reader, IReadOnlyCollection customTypeDefinitions) @@ -54,18 +90,14 @@ internal partial class Tmx if (customTypeDef is CustomClassDefinition ccd) { reader.ReadStartElement("property"); - var propsInType = CreateInstanceOfCustomClass(ccd); - var props = ReadProperties(reader, customTypeDefinitions); - - var mergedProps = Helpers.MergeProperties(propsInType, props); + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); + var props = ReadPropertiesList(reader, customTypeDefinitions); + var mergedProps = Helpers.MergePropertiesList(propsInType, props); reader.ReadEndElement(); - return new ClassProperty { Name = name, PropertyType = propertyType, Properties = mergedProps }; + return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; } throw new XmlException($"Unkonwn custom class definition: {propertyType}"); } - - internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => - customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); } From b46eed774a8816f2795c06ee0c2fe08e97c81229 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Sat, 24 Aug 2024 22:06:14 +0200 Subject: [PATCH 2/7] More property changes --- src/DotTiled.Tests/Serialization/TestData.cs | 16 ++++++++-------- .../map-with-custom-type-props.cs | 2 +- .../Serialization/Tmj/TmjMapReaderTests.cs | 2 +- .../Serialization/Tmx/TmxMapReaderTests.cs | 2 +- src/DotTiled/Model/Properties/ClassProperty.cs | 12 +++++++++++- .../CustomTypes/CustomClassDefinition.cs | 11 ++++++++++- .../CustomTypes/CustomEnumDefinition.cs | 8 +++++++- .../CustomTypes/CustomTypeDefinition.cs | 4 ++-- src/DotTiled/Model/Properties/IHasProperties.cs | 17 +++++++++++++++-- .../Serialization/Tmj/TjTemplateReader.cs | 4 ++-- src/DotTiled/Serialization/Tmj/Tmj.Group.cs | 2 +- .../Serialization/Tmj/Tmj.ImageLayer.cs | 2 +- src/DotTiled/Serialization/Tmj/Tmj.Layer.cs | 2 +- src/DotTiled/Serialization/Tmj/Tmj.Map.cs | 2 +- .../Serialization/Tmj/Tmj.ObjectLayer.cs | 4 ++-- .../Serialization/Tmj/Tmj.Properties.cs | 8 ++++---- src/DotTiled/Serialization/Tmj/Tmj.Template.cs | 2 +- src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs | 2 +- src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs | 8 ++++---- src/DotTiled/Serialization/Tmj/TmjMapReader.cs | 4 ++-- .../Serialization/Tmj/TsjTilesetReader.cs | 4 ++-- src/DotTiled/Serialization/Tmx/Tmx.Map.cs | 2 +- .../Serialization/Tmx/Tmx.ObjectLayer.cs | 6 +++--- .../Serialization/Tmx/Tmx.Properties.cs | 6 +++--- src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs | 6 +++--- src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs | 10 +++++----- src/DotTiled/Serialization/Tmx/TmxMapReader.cs | 4 ++-- .../Serialization/Tmx/TsxTilesetReader.cs | 4 ++-- .../Serialization/Tmx/TxTemplateReader.cs | 4 ++-- 29 files changed, 99 insertions(+), 61 deletions(-) diff --git a/src/DotTiled.Tests/Serialization/TestData.cs b/src/DotTiled.Tests/Serialization/TestData.cs index f6e49b5..b007913 100644 --- a/src/DotTiled.Tests/Serialization/TestData.cs +++ b/src/DotTiled.Tests/Serialization/TestData.cs @@ -32,14 +32,14 @@ public static partial class TestData public static IEnumerable MapTests => [ - ["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty()], - ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty()], + ["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty()], + ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty()], ["Serialization/TestData/Map/map_with_custom_type_props/map-with-custom-type-props", (string f) => MapWithCustomTypeProps(), MapWithCustomTypePropsCustomTypeDefinitions()], - ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => MapWithEmbeddedTileset(), Array.Empty()], - ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty()], - ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty()], - ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], - ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty()], - ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => MapWithEmbeddedTileset(), Array.Empty()], + ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty()], ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs index 2c79478..ea0575e 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -74,7 +74,7 @@ public partial class TestData }; // This comes from map-with-custom-type-props/propertytypes.json - public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ + public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ new CustomClassDefinition { Name = "CustomClass", diff --git a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index bdd19e8..3fe6843 100644 --- a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -11,7 +11,7 @@ public partial class TmjMapReaderTests public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( string testDataFile, Func expectedMap, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Arrange testDataFile += ".tmj"; diff --git a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index ee74718..fb825ed 100644 --- a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -11,7 +11,7 @@ public partial class TmxMapReaderTests public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( string testDataFile, Func expectedMap, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Arrange testDataFile += ".tmx"; diff --git a/src/DotTiled/Model/Properties/ClassProperty.cs b/src/DotTiled/Model/Properties/ClassProperty.cs index eb7efce..5c9d6a5 100644 --- a/src/DotTiled/Model/Properties/ClassProperty.cs +++ b/src/DotTiled/Model/Properties/ClassProperty.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -38,7 +39,16 @@ public class ClassProperty : IHasProperties, IProperty> public IList GetProperties() => Value; /// - public T GetProperty(string name) where T : IProperty => throw new System.NotImplementedException(); + public T GetProperty(string name) where T : IProperty + { + var property = Value.FirstOrDefault(_properties => _properties.Name == name) ?? throw new InvalidOperationException($"Property '{name}' not found."); + if (property is T prop) + { + return prop; + } + + throw new InvalidOperationException($"Property '{name}' is not of type '{typeof(T).Name}'."); + } /// public bool TryGetProperty(string name, [NotNullWhen(true)] out T? property) where T : IProperty diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs index b5aa0ec..313081f 100644 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs +++ b/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs @@ -65,8 +65,14 @@ public enum CustomClassUseAs /// Represents a custom class definition in Tiled. Refer to the /// documentation of custom types to understand how they work. /// -public class CustomClassDefinition : CustomTypeDefinition +public class CustomClassDefinition : HasPropertiesBase, ICustomTypeDefinition { + /// + public uint ID { get; set; } + + /// + public required string Name { get; set; } + /// /// The color of the custom class inside the Tiled editor. /// @@ -86,4 +92,7 @@ public class CustomClassDefinition : CustomTypeDefinition /// The members of the custom class, with their names, types and default values. /// public List Members { get; set; } = []; + + /// + public override IList GetProperties() => Members; } diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs index ee40be0..60b7f5b 100644 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs +++ b/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs @@ -22,8 +22,14 @@ public enum CustomEnumStorageType /// Represents a custom enum definition in Tiled. Refer to the /// documentation of custom types to understand how they work. /// -public class CustomEnumDefinition : CustomTypeDefinition +public class CustomEnumDefinition : ICustomTypeDefinition { + /// + public uint ID { get; set; } + + /// + public required string Name { get; set; } + /// /// The storage type of the custom enum. /// diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs index 3a91b0a..1e595cc 100644 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs +++ b/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Base class for custom type definitions. /// -public abstract class CustomTypeDefinition +public interface ICustomTypeDefinition { /// /// The ID of the custom type. @@ -13,5 +13,5 @@ public abstract class CustomTypeDefinition /// /// The name of the custom type. /// - public string Name { get; set; } = ""; + public string Name { get; set; } } diff --git a/src/DotTiled/Model/Properties/IHasProperties.cs b/src/DotTiled/Model/Properties/IHasProperties.cs index 28b8532..1b124a9 100644 --- a/src/DotTiled/Model/Properties/IHasProperties.cs +++ b/src/DotTiled/Model/Properties/IHasProperties.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -33,7 +34,7 @@ public interface IHasProperties } /// -/// Base class for objects that have properties attached to them. +/// Interface for objects that have properties attached to them. /// public abstract class HasPropertiesBase : IHasProperties { @@ -41,7 +42,19 @@ public abstract class HasPropertiesBase : IHasProperties public abstract IList GetProperties(); /// - public T GetProperty(string name) where T : IProperty => throw new System.NotImplementedException(); + /// Thrown when a property with the specified name is not found. + /// Thrown when a property with the specified name is not of the specified type. + public T GetProperty(string name) where T : IProperty + { + var properties = GetProperties(); + var property = properties.FirstOrDefault(_properties => _properties.Name == name) ?? throw new KeyNotFoundException($"Property '{name}' not found."); + if (property is T prop) + { + return prop; + } + + throw new InvalidCastException($"Property '{name}' is not of type '{typeof(T).Name}'."); + } /// public bool TryGetProperty(string name, [NotNullWhen(true)] out T? property) where T : IProperty diff --git a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs index 3a66f1b..c4ada75 100644 --- a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -16,7 +16,7 @@ public class TjTemplateReader : ITemplateReader private readonly string _jsonString; private bool disposedValue; - private readonly IReadOnlyCollection _customTypeDefinitions; + private readonly IReadOnlyCollection _customTypeDefinitions; /// /// Constructs a new . @@ -30,7 +30,7 @@ public class TjTemplateReader : ITemplateReader string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs b/src/DotTiled/Serialization/Tmj/Tmj.Group.cs index 0885167..ae44a75 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Group.cs @@ -11,7 +11,7 @@ internal partial class Tmj internal static Group ReadGroup( JsonElement element, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs index 794f2b5..bbbb151 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs @@ -9,7 +9,7 @@ internal partial class Tmj { internal static ImageLayer ReadImageLayer( JsonElement element, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs index b9f8de0..aeef011 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs @@ -10,7 +10,7 @@ internal partial class Tmj internal static BaseLayer ReadLayer( JsonElement element, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var type = element.GetRequiredProperty("type"); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs b/src/DotTiled/Serialization/Tmj/Tmj.Map.cs index dd1a012..cdde458 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -12,7 +12,7 @@ internal partial class Tmj JsonElement element, Func? externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var version = element.GetRequiredProperty("version"); var tiledVersion = element.GetRequiredProperty("tiledversion"); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs index 6775389..75d5ee0 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs @@ -12,7 +12,7 @@ internal partial class Tmj internal static ObjectLayer ReadObjectLayer( JsonElement element, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -66,7 +66,7 @@ internal partial class Tmj internal static Model.Object ReadObject( JsonElement element, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { uint? idDefault = null; string nameDefault = ""; diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs index 5b25429..caa5fbe 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs @@ -10,7 +10,7 @@ internal partial class Tmj { internal static Dictionary ReadProperties( JsonElement element, - IReadOnlyCollection customTypeDefinitions) => + IReadOnlyCollection customTypeDefinitions) => element.GetValueAsList(e => { var name = e.GetRequiredProperty("name"); @@ -45,7 +45,7 @@ internal partial class Tmj internal static List ReadPropertiesList( JsonElement element, - IReadOnlyCollection customTypeDefinitions) => + IReadOnlyCollection customTypeDefinitions) => element.GetValueAsList(e => { var name = e.GetRequiredProperty("name"); @@ -80,7 +80,7 @@ internal partial class Tmj internal static ClassProperty ReadClassProperty( JsonElement element, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var name = element.GetRequiredProperty("name"); var propertyType = element.GetRequiredProperty("propertytype"); @@ -108,7 +108,7 @@ internal partial class Tmj internal static List ReadCustomClassProperties( JsonElement element, CustomClassDefinition customClassDefinition, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { List resultingProps = Helpers.CreateInstanceOfCustomClass(customClassDefinition); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs b/src/DotTiled/Serialization/Tmj/Tmj.Template.cs index 3746b76..71ba1e7 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Template.cs @@ -11,7 +11,7 @@ internal partial class Tmj JsonElement element, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var type = element.GetRequiredProperty("type"); var tileset = element.GetOptionalPropertyCustom("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs index 353f7fd..cdda654 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs @@ -9,7 +9,7 @@ internal partial class Tmj { internal static TileLayer ReadTileLayer( JsonElement element, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var compression = element.GetOptionalPropertyParseable("compression", s => s switch { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs index e0e2bd2..7f3d6be 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs @@ -12,7 +12,7 @@ internal partial class Tmj JsonElement element, Func? externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var @class = element.GetOptionalProperty("class", ""); @@ -162,7 +162,7 @@ internal partial class Tmj internal static List ReadTiles( JsonElement element, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) => + IReadOnlyCollection customTypeDefinitions) => element.GetValueAsList(e => { var animation = e.GetOptionalPropertyCustom?>("animation", e => e.GetValueAsList(ReadFrame), null); @@ -218,7 +218,7 @@ internal partial class Tmj internal static Wangset ReadWangset( JsonElement element, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var @clalss = element.GetOptionalProperty("class", ""); var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); @@ -241,7 +241,7 @@ internal partial class Tmj internal static WangColor ReadWangColor( JsonElement element, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var @class = element.GetOptionalProperty("class", ""); var color = element.GetRequiredPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); diff --git a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs index c26311b..1d277b1 100644 --- a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs +++ b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -17,7 +17,7 @@ public class TmjMapReader : IMapReader private readonly string _jsonString; private bool disposedValue; - private readonly IReadOnlyCollection _customTypeDefinitions; + private readonly IReadOnlyCollection _customTypeDefinitions; /// /// Constructs a new . @@ -31,7 +31,7 @@ public class TmjMapReader : IMapReader string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); diff --git a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs index aca5556..dde9075 100644 --- a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -15,7 +15,7 @@ public class TsjTilesetReader : ITilesetReader private readonly string _jsonString; private bool disposedValue; - private readonly IReadOnlyCollection _customTypeDefinitions; + private readonly IReadOnlyCollection _customTypeDefinitions; /// /// Constructs a new . @@ -27,7 +27,7 @@ public class TsjTilesetReader : ITilesetReader public TsjTilesetReader( string jsonString, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs index 6dd5ea5..cabfdcc 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs @@ -13,7 +13,7 @@ internal partial class Tmx XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var version = reader.GetRequiredAttribute("version"); diff --git a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs index 7a375d9..4929ede 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs @@ -13,7 +13,7 @@ internal partial class Tmx internal static ObjectLayer ReadObjectLayer( XmlReader reader, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var id = reader.GetRequiredAttributeParseable("id"); @@ -75,7 +75,7 @@ internal partial class Tmx internal static Model.Object ReadObject( XmlReader reader, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var template = reader.GetOptionalAttribute("template"); @@ -308,7 +308,7 @@ internal partial class Tmx XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // No attributes diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs index 98ee546..f641c4f 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs @@ -9,7 +9,7 @@ internal partial class Tmx { internal static Dictionary ReadProperties( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { return reader.ReadList("properties", "property", (r) => { @@ -45,7 +45,7 @@ internal partial class Tmx internal static List ReadPropertiesList( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { return reader.ReadList("properties", "property", (r) => { @@ -81,7 +81,7 @@ internal partial class Tmx internal static ClassProperty ReadClassProperty( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var name = reader.GetRequiredAttribute("name"); var propertyType = reader.GetRequiredAttribute("propertytype"); diff --git a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs index 8b972a3..320019d 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs @@ -11,7 +11,7 @@ internal partial class Tmx internal static TileLayer ReadTileLayer( XmlReader reader, bool dataUsesChunks, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -61,7 +61,7 @@ internal partial class Tmx internal static ImageLayer ReadImageLayer( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -112,7 +112,7 @@ internal partial class Tmx internal static Group ReadGroup( XmlReader reader, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs index 198f32d..6fb1ff5 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs @@ -14,7 +14,7 @@ internal partial class Tmx XmlReader reader, Func? externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var version = reader.GetOptionalAttribute("version"); @@ -207,7 +207,7 @@ internal partial class Tmx internal static Tile ReadTile( XmlReader reader, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var id = reader.GetRequiredAttributeParseable("id"); @@ -256,12 +256,12 @@ internal partial class Tmx internal static List ReadWangsets( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) => + IReadOnlyCollection customTypeDefinitions) => reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); internal static Wangset ReadWangset( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var name = reader.GetRequiredAttribute("name"); @@ -297,7 +297,7 @@ internal partial class Tmx internal static WangColor ReadWangColor( XmlReader reader, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Attributes var name = reader.GetRequiredAttribute("name"); diff --git a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs index 5a99ff6..279c83d 100644 --- a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -17,7 +17,7 @@ public class TmxMapReader : IMapReader private readonly XmlReader _reader; private bool disposedValue; - private readonly IReadOnlyCollection _customTypeDefinitions; + private readonly IReadOnlyCollection _customTypeDefinitions; /// /// Constructs a new . @@ -31,7 +31,7 @@ public class TmxMapReader : IMapReader XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { _reader = reader ?? throw new ArgumentNullException(nameof(reader)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); diff --git a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs index 0034150..0b69d4c 100644 --- a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -16,7 +16,7 @@ public class TsxTilesetReader : ITilesetReader private readonly XmlReader _reader; private bool disposedValue; - private readonly IReadOnlyCollection _customTypeDefinitions; + private readonly IReadOnlyCollection _customTypeDefinitions; /// /// Constructs a new . @@ -28,7 +28,7 @@ public class TsxTilesetReader : ITilesetReader public TsxTilesetReader( XmlReader reader, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { _reader = reader ?? throw new ArgumentNullException(nameof(reader)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); diff --git a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs index fbaf85c..ed7ba8e 100644 --- a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -17,7 +17,7 @@ public class TxTemplateReader : ITemplateReader private readonly XmlReader _reader; private bool disposedValue; - private readonly IReadOnlyCollection _customTypeDefinitions; + private readonly IReadOnlyCollection _customTypeDefinitions; /// /// Constructs a new . @@ -31,7 +31,7 @@ public class TxTemplateReader : ITemplateReader XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { _reader = reader ?? throw new ArgumentNullException(nameof(reader)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); From 4580772cede4fd44d5d4b2efdef85a697b7820a2 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Sat, 24 Aug 2024 23:08:24 +0200 Subject: [PATCH 3/7] Fix rest of model to use new structure and prepare docs --- docs/docs/accessing-properties.md | 1 + docs/docs/toc.yml | 3 +- src/DotTiled.Tests/Assert/AssertMap.cs | 2 +- src/DotTiled.Tests/Assert/AssertProperties.cs | 31 +++----------- .../map-external-tileset-multi.cs | 19 ++++----- .../map-with-many-layers.cs | 7 ++-- src/DotTiled/Model/Layers/BaseLayer.cs | 7 +++- src/DotTiled/Model/Layers/Objects/Object.cs | 7 +++- src/DotTiled/Model/Tilesets/Tile.cs | 7 +++- src/DotTiled/Model/Tilesets/Tileset.cs | 11 +++-- src/DotTiled/Model/Tilesets/WangColor.cs | 7 +++- src/DotTiled/Model/Tilesets/Wangset.cs | 7 +++- src/DotTiled/Serialization/Helpers.cs | 36 +--------------- src/DotTiled/Serialization/Tmj/Tmj.Group.cs | 2 +- .../Serialization/Tmj/Tmj.ImageLayer.cs | 2 +- src/DotTiled/Serialization/Tmj/Tmj.Map.cs | 4 +- .../Serialization/Tmj/Tmj.ObjectLayer.cs | 6 +-- .../Serialization/Tmj/Tmj.Properties.cs | 39 +---------------- .../Serialization/Tmj/Tmj.TileLayer.cs | 2 +- src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs | 8 ++-- src/DotTiled/Serialization/Tmx/Tmx.Map.cs | 2 +- .../Serialization/Tmx/Tmx.ObjectLayer.cs | 14 +++---- .../Serialization/Tmx/Tmx.Properties.cs | 42 ++----------------- .../Serialization/Tmx/Tmx.TileLayer.cs | 12 +++--- src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs | 16 +++---- 25 files changed, 94 insertions(+), 200 deletions(-) create mode 100644 docs/docs/accessing-properties.md diff --git a/docs/docs/accessing-properties.md b/docs/docs/accessing-properties.md new file mode 100644 index 0000000..0269b88 --- /dev/null +++ b/docs/docs/accessing-properties.md @@ -0,0 +1 @@ +# Accessing properties \ No newline at end of file diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml index 13cc1f7..d582202 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -3,4 +3,5 @@ - href: quickstart.md - name: Essentials -- href: loading-a-map.md \ No newline at end of file +- href: loading-a-map.md +- href: accessing-properties.md \ No newline at end of file diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index 6f25f45..6984b79 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -91,7 +91,7 @@ public static partial class DotTiledAssert AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); - AssertPropertiesList(actual.Properties, expected.Properties); + AssertProperties(actual.Properties, expected.Properties); Assert.NotNull(actual.Tilesets); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); diff --git a/src/DotTiled.Tests/Assert/AssertProperties.cs b/src/DotTiled.Tests/Assert/AssertProperties.cs index ad4dc53..21fa639 100644 --- a/src/DotTiled.Tests/Assert/AssertProperties.cs +++ b/src/DotTiled.Tests/Assert/AssertProperties.cs @@ -4,24 +4,7 @@ namespace DotTiled.Tests; public static partial class DotTiledAssert { - internal static void AssertProperties(Dictionary? expected, Dictionary? actual) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - Assert.NotNull(actual); - AssertEqual(expected.Count, actual.Count, "Properties.Count"); - foreach (var kvp in expected) - { - Assert.Contains(kvp.Key, actual.Keys); - AssertProperty((dynamic)kvp.Value, (dynamic)actual[kvp.Key]); - } - } - - internal static void AssertPropertiesList(IList? expected, IList? actual) + internal static void AssertProperties(IList? expected, IList? actual) { if (expected is null) { @@ -36,17 +19,13 @@ public static partial class DotTiledAssert Assert.Contains(actual, p => p.Name == prop.Name); var actualProp = actual.First(p => p.Name == prop.Name); + AssertEqual(prop.Type, actualProp.Type, "Property.Type"); + AssertEqual(prop.Name, actualProp.Name, "Property.Name"); + AssertProperty((dynamic)prop, (dynamic)actualProp); } } - private static void AssertProperty(IProperty expected, IProperty actual) - { - AssertEqual(expected.Type, actual.Type, "Property.Type"); - AssertEqual(expected.Name, actual.Name, "Property.Name"); - AssertProperties((dynamic)actual, (dynamic)expected); - } - private static void AssertProperty(StringProperty expected, StringProperty actual) => AssertEqual(expected.Value, actual.Value, "StringProperty.Value"); private static void AssertProperty(IntProperty expected, IntProperty actual) => AssertEqual(expected.Value, actual.Value, "IntProperty.Value"); @@ -64,6 +43,6 @@ public static partial class DotTiledAssert private static void AssertProperty(ClassProperty expected, ClassProperty actual) { AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType"); - AssertPropertiesList(expected.Value, actual.Value); + AssertProperties(expected.Value, actual.Value); } } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs index 6dac137..a937685 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs @@ -49,16 +49,15 @@ public partial class TestData Width = 1, Height = 1 }, - Properties = new Dictionary - { - ["tilesetbool"] = new BoolProperty { Name = "tilesetbool", Value = true }, - ["tilesetcolor"] = new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, - ["tilesetfile"] = new FileProperty { Name = "tilesetfile", Value = "" }, - ["tilesetfloat"] = new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, - ["tilesetint"] = new IntProperty { Name = "tilesetint", Value = 9 }, - ["tilesetobject"] = new ObjectProperty { Name = "tilesetobject", Value = 0 }, - ["tilesetstring"] = new StringProperty { Name = "tilesetstring", Value = "hello world!" } - }, + Properties = [ + new BoolProperty { Name = "tilesetbool", Value = true }, + new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "tilesetfile", Value = "" }, + new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, + new IntProperty { Name = "tilesetint", Value = 9 }, + new ObjectProperty { Name = "tilesetobject", Value = 0 }, + new StringProperty { Name = "tilesetstring", Value = "hello world!" } + ], Tiles = [ new Tile { diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs index 6e4ae55..ba0013a 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs @@ -95,10 +95,9 @@ public partial class TestData new Vector2(35.6667f, 32.3333f) ], Template = fileExt == "tmx" ? "poly.tx" : "poly.tj", - Properties = new Dictionary - { - ["templateprop"] = new StringProperty { Name = "templateprop", Value = "helo there" } - } + Properties = [ + new StringProperty { Name = "templateprop", Value = "helo there" } + ] }, new TileObject { diff --git a/src/DotTiled/Model/Layers/BaseLayer.cs b/src/DotTiled/Model/Layers/BaseLayer.cs index aa78191..a16b41a 100644 --- a/src/DotTiled/Model/Layers/BaseLayer.cs +++ b/src/DotTiled/Model/Layers/BaseLayer.cs @@ -7,7 +7,7 @@ namespace DotTiled.Model; /// To check the type of a layer, use C# pattern matching, /// or some other mechanism to determine the type of the layer at runtime. /// -public abstract class BaseLayer +public abstract class BaseLayer : HasPropertiesBase { /// /// Unique ID of the layer. Each layer that is added to a map gets a unique ID. Even if a layer is deleted, no layer ever gets the same ID. @@ -62,5 +62,8 @@ public abstract class BaseLayer /// /// Layer properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; } diff --git a/src/DotTiled/Model/Layers/Objects/Object.cs b/src/DotTiled/Model/Layers/Objects/Object.cs index fad05db..f2990dc 100644 --- a/src/DotTiled/Model/Layers/Objects/Object.cs +++ b/src/DotTiled/Model/Layers/Objects/Object.cs @@ -5,7 +5,7 @@ namespace DotTiled.Model; /// /// Base class for objects in object layers. /// -public abstract class Object +public abstract class Object : HasPropertiesBase { /// /// Unique ID of the objects. Each object that is placed on a map gets a unique ID. Even if an object was deleted, no object gets the same ID. @@ -60,5 +60,8 @@ public abstract class Object /// /// Object properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; } diff --git a/src/DotTiled/Model/Tilesets/Tile.cs b/src/DotTiled/Model/Tilesets/Tile.cs index c6b964d..9919cfd 100644 --- a/src/DotTiled/Model/Tilesets/Tile.cs +++ b/src/DotTiled/Model/Tilesets/Tile.cs @@ -6,7 +6,7 @@ namespace DotTiled.Model; /// Represents a single tile in a tileset, when using a collection of images to represent the tileset. /// Tiled documentation for Tileset tiles /// -public class Tile +public class Tile : HasPropertiesBase { /// /// The local tile ID within its tileset. @@ -46,7 +46,10 @@ public class Tile /// /// Tile properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; /// /// The image representing this tile. Only used for tilesets that composed of a collection of images. diff --git a/src/DotTiled/Model/Tilesets/Tileset.cs b/src/DotTiled/Model/Tilesets/Tileset.cs index 147f3d2..47f2021 100644 --- a/src/DotTiled/Model/Tilesets/Tileset.cs +++ b/src/DotTiled/Model/Tilesets/Tileset.cs @@ -8,7 +8,7 @@ namespace DotTiled.Model; public enum ObjectAlignment { /// - /// The alignment is unspecified. Tile objects will use in orthogonal maps, and in isometric maps. + /// The alignment is unspecified. Tile objects will use in orthogonal maps, and in isometric maps. /// Unspecified, @@ -93,7 +93,7 @@ public enum FillMode /// /// A tileset is a collection of tiles that can be used in a tile layer, or by tile objects. /// -public class Tileset +public class Tileset : HasPropertiesBase { /// /// The TMX format version. Is incremented to match minor Tiled releases. @@ -161,7 +161,7 @@ public class Tileset public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; /// - /// The size to use when rendering tiles from thie tileset on a tile layer. When set to , the tile is drawn at the tile grid size of the map. + /// The size to use when rendering tiles from thie tileset on a tile layer. When set to , the tile is drawn at the tile grid size of the map. /// public TileRenderSize RenderSize { get; set; } = TileRenderSize.Tile; @@ -188,7 +188,10 @@ public class Tileset /// /// Tileset properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; // public List? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization diff --git a/src/DotTiled/Model/Tilesets/WangColor.cs b/src/DotTiled/Model/Tilesets/WangColor.cs index 20678cb..f5d1186 100644 --- a/src/DotTiled/Model/Tilesets/WangColor.cs +++ b/src/DotTiled/Model/Tilesets/WangColor.cs @@ -5,7 +5,7 @@ namespace DotTiled.Model; /// /// Represents a Wang color in a Wang set. /// -public class WangColor +public class WangColor : HasPropertiesBase { /// /// The name of this color. @@ -35,5 +35,8 @@ public class WangColor /// /// The Wang color properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; } diff --git a/src/DotTiled/Model/Tilesets/Wangset.cs b/src/DotTiled/Model/Tilesets/Wangset.cs index 1a6f7c3..66952e8 100644 --- a/src/DotTiled/Model/Tilesets/Wangset.cs +++ b/src/DotTiled/Model/Tilesets/Wangset.cs @@ -5,7 +5,7 @@ namespace DotTiled.Model; /// /// Defines a list of colors and any number of Wang tiles using these colors. /// -public class Wangset +public class Wangset : HasPropertiesBase { /// /// The name of the Wang set. @@ -25,7 +25,10 @@ public class Wangset /// /// The Wang set properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; // Up to 254 Wang colors /// diff --git a/src/DotTiled/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index 0028650..073a19f 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -76,39 +76,7 @@ internal static partial class Helpers internal static List CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => customClassDefinition.Members.Select(x => x.Clone()).ToList(); - 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).Value = MergePropertiesList(((ClassProperty)baseProp).Value, classProp.Value); - } - else - { - result[key] = value; - } - } - } - - return result; - } - - internal static IList MergePropertiesList(IList? baseProperties, IList? overrideProperties) + internal static IList MergeProperties(IList? baseProperties, IList? overrideProperties) { if (baseProperties is null) return overrideProperties ?? []; @@ -129,7 +97,7 @@ internal static partial class Helpers var existingProp = result.First(x => x.Name == overrideProp.Name); if (existingProp is ClassProperty classProp) { - classProp.Value = MergePropertiesList(classProp.Value, ((ClassProperty)overrideProp).Value); + classProp.Value = MergeProperties(classProp.Value, ((ClassProperty)overrideProp).Value); } else { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs b/src/DotTiled/Serialization/Tmj/Tmj.Group.cs index ae44a75..a714038 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Group.cs @@ -23,7 +23,7 @@ internal partial class Tmj 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 properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); return new Group diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs index bbbb151..74fb230 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs @@ -21,7 +21,7 @@ internal partial class Tmj 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 properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var image = element.GetRequiredProperty("image"); var repeatX = element.GetOptionalProperty("repeatx", false); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs b/src/DotTiled/Serialization/Tmj/Tmj.Map.cs index cdde458..eeb47b0 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -58,7 +58,7 @@ internal partial class Tmj var nextObjectID = element.GetRequiredProperty("nextobjectid"); var infinite = element.GetOptionalProperty("infinite", false); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadPropertiesList(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); 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)), []); @@ -84,7 +84,7 @@ internal partial class Tmj NextLayerID = nextLayerID, NextObjectID = nextObjectID, Infinite = infinite, - Properties = properties ?? [], + Properties = properties, Tilesets = tilesets, Layers = layers }; diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs index 75d5ee0..589c151 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs @@ -24,7 +24,7 @@ internal partial class Tmj 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 properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var x = element.GetOptionalProperty("x", 0); var y = element.GetOptionalProperty("y", 0); @@ -82,7 +82,7 @@ internal partial class Tmj bool pointDefault = false; List? polygonDefault = null; List? polylineDefault = null; - Dictionary? propertiesDefault = null; + List propertiesDefault = []; var template = element.GetOptionalProperty("template", null); if (template is not null) @@ -114,7 +114,7 @@ internal partial class Tmj var point = element.GetOptionalProperty("point", pointDefault); var polygon = element.GetOptionalPropertyCustom?>("polygon", ReadPoints, polygonDefault); var polyline = element.GetOptionalPropertyCustom?>("polyline", ReadPoints, polylineDefault); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); var rotation = element.GetOptionalProperty("rotation", rotationDefault); var text = element.GetOptionalPropertyCustom("text", ReadText, null); var type = element.GetOptionalProperty("type", typeDefault); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs index caa5fbe..d9777e7 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs @@ -8,42 +8,7 @@ namespace DotTiled.Serialization.Tmj; internal partial class Tmj { - internal static Dictionary ReadProperties( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) => - element.GetValueAsList(e => - { - var name = e.GetRequiredProperty("name"); - var type = e.GetOptionalPropertyParseable("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 JsonException("Invalid property type") - }, PropertyType.String); - - IProperty property = type switch - { - PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, - 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, customTypeDefinitions), - _ => throw new JsonException("Invalid property type") - }; - - return property!; - }).ToDictionary(p => p.Name); - - internal static List ReadPropertiesList( + internal static List ReadProperties( JsonElement element, IReadOnlyCollection customTypeDefinitions) => element.GetValueAsList(e => @@ -92,7 +57,7 @@ internal partial class Tmj var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); - var mergedProps = Helpers.MergePropertiesList(propsInType, props); + var mergedProps = Helpers.MergeProperties(propsInType, props); return new ClassProperty { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs index cdda654..905e447 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs @@ -35,7 +35,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, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var repeatX = element.GetOptionalProperty("repeatx", false); var repeatY = element.GetOptionalProperty("repeaty", false); var startX = element.GetOptionalProperty("startx", 0); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs index 7f3d6be..466b5d3 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs +++ b/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs @@ -44,7 +44,7 @@ internal partial class Tmj "bottomright" => ObjectAlignment.BottomRight, _ => throw new JsonException($"Unknown object alignment '{s}'") }, ObjectAlignment.Unspecified); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); var source = element.GetOptionalProperty("source", null); var spacing = element.GetOptionalProperty("spacing", null); var tileCount = element.GetOptionalProperty("tilecount", null); @@ -176,7 +176,7 @@ internal partial class Tmj var height = e.GetOptionalProperty("height", imageHeight ?? 0); var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null); var probability = e.GetOptionalProperty("probability", 0.0f); - var properties = e.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = e.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); // var terrain, replaced by wangsets var type = e.GetOptionalProperty("type", ""); @@ -223,7 +223,7 @@ internal partial class Tmj var @clalss = element.GetOptionalProperty("class", ""); var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); var name = element.GetRequiredProperty("name"); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var tile = element.GetOptionalProperty("tile", 0); var type = element.GetOptionalProperty("type", ""); var wangTiles = element.GetOptionalPropertyCustom>("wangtiles", e => e.GetValueAsList(ReadWangTile), []); @@ -247,7 +247,7 @@ internal partial class Tmj var color = element.GetRequiredPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); var name = element.GetRequiredProperty("name"); var probability = element.GetOptionalProperty("probability", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var tile = element.GetOptionalProperty("tile", 0); return new WangColor diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs index cabfdcc..4ce03a4 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs @@ -69,7 +69,7 @@ internal partial class Tmx reader.ProcessChildren("map", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadPropertiesList(r, customTypeDefinitions), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), "layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)), "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), diff --git a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs index 4929ede..2ce3ca3 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs @@ -39,7 +39,7 @@ internal partial class Tmx }) ?? DrawOrder.TopDown; // Elements - Dictionary? properties = null; + List? properties = null; List objects = []; reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch @@ -66,7 +66,7 @@ internal partial class Tmx ParallaxX = parallaxX, ParallaxY = parallaxY, Color = color, - Properties = properties, + Properties = properties ?? [], DrawOrder = drawOrder, Objects = objects }; @@ -93,7 +93,7 @@ internal partial class Tmx float rotationDefault = obj?.Rotation ?? 0f; uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null; bool visibleDefault = obj?.Visible ?? true; - Dictionary? propertiesDefault = obj?.Properties ?? null; + List? propertiesDefault = obj?.Properties ?? null; var id = reader.GetOptionalAttributeParseable("id") ?? idDefault; var name = reader.GetOptionalAttribute("name") ?? nameDefault; @@ -109,11 +109,11 @@ internal partial class Tmx // Elements Model.Object? foundObject = null; int propertiesCounter = 0; - Dictionary? properties = propertiesDefault; + List? properties = propertiesDefault; reader.ProcessChildren("object", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)), "Properties", ref propertiesCounter), + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)).ToList(), "Properties", ref propertiesCounter), "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(r), "Object marker"), "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(r), "Object marker"), "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(r), "Object marker"), @@ -139,7 +139,7 @@ internal partial class Tmx foundObject.Height = height; foundObject.Rotation = rotation; foundObject.Visible = visible; - foundObject.Properties = properties; + foundObject.Properties = properties ?? []; foundObject.Template = template; return OverrideObject(obj, foundObject); @@ -161,7 +161,7 @@ internal partial class Tmx obj.Height = foundObject.Height; obj.Rotation = foundObject.Rotation; obj.Visible = foundObject.Visible; - obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties); + obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties).ToList(); obj.Template = foundObject.Template; return obj; } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs index f641c4f..2e288a5 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs @@ -7,43 +7,7 @@ namespace DotTiled.Serialization.Tmx; internal partial class Tmx { - internal static Dictionary ReadProperties( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - 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, customTypeDefinitions), - _ => throw new XmlException("Invalid property type") - }; - return (name, property); - }).ToDictionary(x => x.name, x => x.property); - } - - internal static List ReadPropertiesList( + internal static List ReadProperties( XmlReader reader, IReadOnlyCollection customTypeDefinitions) { @@ -91,8 +55,8 @@ internal partial class Tmx { reader.ReadStartElement("property"); var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); - var props = ReadPropertiesList(reader, customTypeDefinitions); - var mergedProps = Helpers.MergePropertiesList(propsInType, props); + var props = ReadProperties(reader, customTypeDefinitions); + var mergedProps = Helpers.MergeProperties(propsInType, props); reader.ReadEndElement(); return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; diff --git a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs index 320019d..8605ade 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs @@ -28,7 +28,7 @@ internal partial class Tmx var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - Dictionary? properties = null; + List? properties = null; Data? data = null; reader.ProcessChildren("layer", (r, elementName) => elementName switch @@ -55,7 +55,7 @@ internal partial class Tmx ParallaxX = parallaxX, ParallaxY = parallaxY, Data = data, - Properties = properties + Properties = properties ?? [] }; } @@ -78,7 +78,7 @@ internal partial class Tmx var repeatX = (reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; var repeatY = (reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; - Dictionary? properties = null; + List? properties = null; Image? image = null; reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch @@ -102,7 +102,7 @@ internal partial class Tmx OffsetY = offsetY, ParallaxX = parallaxX, ParallaxY = parallaxY, - Properties = properties, + Properties = properties ?? [], Image = image, RepeatX = repeatX, RepeatY = repeatY @@ -125,7 +125,7 @@ internal partial class Tmx var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - Dictionary? properties = null; + List? properties = null; List layers = []; reader.ProcessChildren("group", (r, elementName) => elementName switch @@ -150,7 +150,7 @@ internal partial class Tmx OffsetY = offsetY, ParallaxX = parallaxX, ParallaxY = parallaxY, - Properties = properties, + Properties = properties ?? [], Layers = layers }; } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs index 6fb1ff5..84ccd24 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs @@ -60,7 +60,7 @@ internal partial class Tmx Image? image = null; TileOffset? tileOffset = null; Grid? grid = null; - Dictionary? properties = null; + List? properties = null; List? wangsets = null; Transformations? transformations = null; List tiles = []; @@ -109,7 +109,7 @@ internal partial class Tmx Image = image, TileOffset = tileOffset, Grid = grid, - Properties = properties, + Properties = properties ?? [], Wangsets = wangsets, Transformations = transformations, Tiles = tiles @@ -219,7 +219,7 @@ internal partial class Tmx var height = reader.GetOptionalAttributeParseable("height"); // Elements - Dictionary? properties = null; + List? properties = null; Image? image = null; ObjectLayer? objectLayer = null; List? animation = null; @@ -247,7 +247,7 @@ internal partial class Tmx Y = y, Width = width ?? image?.Width ?? 0, Height = height ?? image?.Height ?? 0, - Properties = properties, + Properties = properties ?? [], Image = image, ObjectLayer = objectLayer, Animation = animation @@ -269,7 +269,7 @@ internal partial class Tmx var tile = reader.GetRequiredAttributeParseable("tile"); // Elements - Dictionary? properties = null; + List? properties = null; List wangColors = []; List wangTiles = []; @@ -289,7 +289,7 @@ internal partial class Tmx Name = name, Class = @class, Tile = tile, - Properties = properties, + Properties = properties ?? [], WangColors = wangColors, WangTiles = wangTiles }; @@ -307,7 +307,7 @@ internal partial class Tmx var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; // Elements - Dictionary? properties = null; + List? properties = null; reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch { @@ -322,7 +322,7 @@ internal partial class Tmx Color = color, Tile = tile, Probability = probability, - Properties = properties + Properties = properties ?? [] }; } From 7ca19713215e62fbdbd9d327d90e728deee37d02 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Sun, 25 Aug 2024 00:16:59 +0200 Subject: [PATCH 4/7] A first attempt at documentation for properties --- docs/docs/accessing-properties.md | 96 +++++++++++++++++++++++++- docs/images/monster-spawner-class.png | Bin 0 -> 17831 bytes 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 docs/images/monster-spawner-class.png diff --git a/docs/docs/accessing-properties.md b/docs/docs/accessing-properties.md index 0269b88..04e1846 100644 --- a/docs/docs/accessing-properties.md +++ b/docs/docs/accessing-properties.md @@ -1 +1,95 @@ -# Accessing properties \ No newline at end of file +# Accessing properties + +[Tiled facilitates a very flexible way to store custom data in your maps using properties](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-properties). Accessing these properties is a common task when working with Tiled maps in your game since it will allow you to fully utilize the strengths of Tiled, such as customizing the behavior of your game objects or setting up the initial state of your game world. + +### All classes that can contain properties + +All classes that can contain custom properties implement the interface in some way. Below is an exhaustive list of all classes that can contain custom properties: + +- + - + - + - + - +- (allows for recursive property objects) +- (used to define custom Tiled property types) +- + - + - + - + - + - + - + - +- +- +- +- + +### How to access properties + +To access the properties on one of the classes listed above, you will make use of the interface. + +In situations where you know that a property must exist, and you simply want to retrieve it, you can use the method like so: + +```csharp +var map = LoadMap(); +var propertyValue = map.GetProperty("boolPropertyInMap").Value; +``` + +If you are unsure whether a property exists, or you want to provide some kind of default behaviour if the property is not present, you can instead use the method like so: + +```csharp +var map = LoadMap(); +if (map.TryGetProperty("boolPropertyInMap", out var property)) +{ + // Do something with existing property + var propertyValue = property.Value; +} +else +{ + // Do something if property does not exist +} +``` + +### All types of properties + +Tiled supports a variety of property types, which are represented in the DotTiled library as classes that implement the interface. Below is a list of all property types that Tiled supports and their corresponding classes in DotTiled: + +- `bool` - +- `color` - +- `float` - +- `file` - +- `int` - +- `object` - +- `string` - + +In addition to these primitive property types, [Tiled also supports more complex property types](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types). These custom property types are defined in Tiled according to the linked documentation, and to work with them in DotTiled, you *must* define their equivalences as a collection of . This collection of definitions shall then be passed to the corresponding reader when loading a map, tileset, or template. + +Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will attempt to find the corresponding definition, and if it does not find one, it will throw an exception. However, if it does find the definition, it will use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file. More information about these `class` properties can be found in [the next section](#class-properties). + +Finally, Tiled also allows you to define custom property types that work as enums. These custom property types are just parsed and retrieved as their corresponding storage type. So for a custom property type that is defined as an enum where the values are stored as strings, DotTiled will just parse those as . Similarly, if the values are stored as integers, DotTiled will parse those as . + +### Class properties + +As mentioned, Tiled supports `class` properties which allow you to create hierarchical structures of properties. DotTiled supports this feature through the class. For all your custom `class` types in Tiled, you must create an equivalent and pass it to the corresponding reader when loading a map, tileset, or template. + +For example, if you have a `class` property in Tiled that looks like this: + +![MonsterSpawner class in Tiled UI](../images/monster-spawner-class.png) + +The equivalent definition in DotTiled would look like the following: + +```csharp +var monsterSpawnerDefinition = new CustomClassDefinition +{ + Name = "MonsterSpawner", + UseAs = CustomClassUseAs.All, // Not really validated by DotTiled + Members = [ // Make sure that the default values match the Tiled UI + new BoolProperty { Name = "enabled", Value = true }, + new IntProperty { Name = "maxSpawnAmount", Value = 10 }, + new IntProperty { Name = "minSpawnAmount", Value = 0 }, + new StringProperty { Name = "monsterNames", Value = "" } + ] +}; +``` \ No newline at end of file diff --git a/docs/images/monster-spawner-class.png b/docs/images/monster-spawner-class.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b5ef410d3649cd504f7f4ab564ac5b2b7b3134 GIT binary patch literal 17831 zcmc(H1yEe!mL^G%1Shx$cXtl~0tD^g?(Xg$f`tY`aEIXTP9wpB2Wg;jcWB(%ChyJE z%xvx3nX0YYTSd{`*Z%*%N4|5;cW#8TqBI&Z5i%Sc9Ga|*KK%JBLvv@H;yt|E^u(@Jg%1M$XG(<3k;io3it7SjJ^+BFy)g@YKuNHZz%XTq!9B zvb2NpTTkm+E#P^dOMM}ix#KpQ5&5+4?rv>NEG(=z1Mtoh_VWjF)YNd;#%wq)3ENCx zgwRsLHWUJG*iJ01&g=--&ViSR3eS7-*e#!T)&z?GJr&{q`Kj|bcz82=X0Ur$1CXk` zZ}k@%+-cWb<*vVP*B}eIDG&*Gu$X^^MsQ3%4<{AXwMN-dSA&=ZF+Q=ZcfKC@NBD(G z)mT^PKgUOsNkvJCOt`fw(3~E#kKa=&(FI$YLt(BlG4ht7X*>muFL3U zedqT}n%@(Gi(-Y5Jcgf_EVK;S76;nNEYQZ|LG50V3M3hJM)ipB?(G9h4H&Z9wVJ-I z;*iDwL}Y(}CvKOD&9SGOA=NV@>nQ1*SaQw+^*Vq@ajV0~KBIi9qPRVub(POz9kQN? zr~hd>BIPjcFj1-2sf)`__yJ0y`}Y}zGCM)D@Yg1vQP*<52*oV1PoMq9du)2@UZUW%R=y)KSM&(D8qkEFjk z&zr+GbV=avv$n=%RMyQ^Ve6Mu*3IGK0<@143sr*&`i1lZ?=uR@6ly1XoNdfnWYOnR zLNEXA6yxzbOQ6YPZ0eH<5xR;?;Q2yse`H+MFYwj--qnUUZ?ZquSB?cn!!D@>RHZ8> zIHu*@6S7y~RK2U2veMOzrEsHw2H1k)`+Eb!!|4wMM8&rrW$PWWZ?`+;|C8XYoRB<= zFv8!J|9>g0@YfKRegNz-^Q!My4^uM_eiHvt@%8(w(n6DIMLmv)lsb4VhMZXwn4lt? z&4d)JBMf-kQhAT*MWr}ZGz70R$Z+f>l6LOx-t4qfqbQJrxVNiedQ?1NsW({dw3Z)Z#I)HX>#@tS_tRl% zP>&|t&L@K1swkmG6ic|kqi2hi)RUChXAGt>Hfkw^_+cqF+s4$!>{bz$D#c7#bvrp? z^^4zS`CERIe2IW#B2T6PkB~-Wlon7S)sjFWpJmzY-MSLaXWB@}k(-^D;j!Ykbj#sE zPErXHa$=WrsM&I4+`GFHAkxMVuG^^G;TPCEwHfxsReAxMgWFSB)5IJ?8aHEzq+(z5 zD8D(I*W`X?cQQo)3OZ1}EYlKfifC0p(0v0H{GxJ^$8VnR;ta{~aSL>V^NCUH78y0{ zcSSvesx7;!%bO>tAL0ZcS9kyRf(Zhk)QH70GE(}(uwwFe?wJTUYMVpt;SY+hQBD4e zmRiA%r?@DP_SfA&Ch4=B0|+CU1@nGJ$}@#d{k?!nZL-xL37t~m;8!=h0%|cIjtNxE zcRKYw{pZCPX;9>7dsm7Ye<-W6o#Qn+BAOY4ZiF62a;%$*uHmz<-P-g~H5xt@zjul_ zO7lWbI!Yt@e%5{b&yo3gvf3A9+H9oW3yUCyYXn6z6@5PRF|+>|A76fP8Dn=49kJ)~ z>v9jana+)DJtuEi-Jd z7JLR1!`vChPPRr{(hYorqxKPDFCCcjEN?zr$qubC?s`1uoiV$Y{LhQ@KuK@;XStL* z?D3n@AnXS$Eu6R>m**pcBHLC4p09=YUz`E+n3bT%5ech7$2Yko)?K{kV@_r|{Cw(c zz~~u59y2T*TF9xUoz!moGYSt`evzYElkHHkDC1aBhPmZ;5WBD69&t* z*k{eHH&-_%RfMO5UGKl=Qw(>9Sd`NFU!;u`*jzwB0U&)!Ya)rSs1tH=g}J#e4g4-} zN7DuJbi(N|^;%rJqWPA>{U2_3@s5Ec0=XRvHyeZTE%{~_I}@+?791tsgH38i?Hltf z#xh&B^x_c|#tgoJ>sMO5FWz=M>@m;lPJV1u{#jIB-dmsH@GC&~YAhV>d6ov;0Vg5k zJ9w1V;W47OG6<3!92_mZdTMG3w_edgM@_Hn=D+k^eC$9xny*LEsL&P967_RsLXay( zO=Zoxd3ortS8ju2#hOvpm&d2^(RTbC+D~qBx-r=DQ8K#Ps0T62;XOK;aCqx+hYpKD zySLf{C|}Pd+T`O%nnOoKR|rz688R7@xXSE^mG$IDSFNvJ%vf5KBA@{Zf!r17m9L=N z-j7?*@ZLZ+15V^Yl<9eNszH=#YQ+>uk4CG07h2IS#+k`Jr64@23FmsY?~R^y{R{^kkF^<2XLKX15i}ue?_}HktOy8sOICit6E<| zTw-v?MzUC^DXDnT=OVcgn$9ffuq141VL`>kg?-$9m8rjebboW^9T6^^64s}kI^E%W zHo_+(EBmHs020eHg2!DPxVz4m@gn|wkS=RQ^TufQ?1}vMjBrKdL;mtA{u9*2R0Ntu zZ*Zm4o*!jm9R;mK$xIf2NAPgtsBeBmBXCDSHeU)3Ye~`3xn2$=n4p`mP&1CT)$b3$ zdj$B2Ua!2m-wt>z^t5Qvolaub%3T6Hon<|#x`m{}`3$i%7Ab9D0`$~hC2Seivz^BK zpYG$_;|k;~=wc)&A$<)of0N2OO(Nae16h1opd|Ky^K=Ua!4}fncJ7pj*TEn6>C$15 z-Q!5t6{eV=*t?xfD(63<B00P(nQsNGl~(MV_=y)hCVn$L)}IlG@BaJVay5ZEu4_Tm$8nWL(gY3AtMPgE8be4o>0%cT(jnwZ;5xm~E63L1b$ zDmO=-T9%l_RqpHoMFKwPxCuZFwG^ZICMWDqpF}@jyum(_vflHvCTXw%SL}($F)T6k zD;ng^fH(bVM4Vgjs1U>91{XxLnZ71vb=$k>$R!<-j<;^TWc=nm!cit*t376%qlp!z zdFMP8k)|zhLu)!$V`l$C#mT8UHO=YvTo&5a67B$cAo9EWXmmJV&*=D_t-G&3`GNRb zSN@pSn{N&4p$SMA&Q*hk9e@Bm&B1Vu8luxR>uJ>G0j zSGCD2YGOy7o9$Qow=PbNZ>RxfUJ}Uwcjz_Q;j_;i}cbWI8Jg_r)YOrQvO!{(5M?+xb9Pzaup( zvWEaf(CDKso$YN^pPf*`?v0o5($wbFD09px30kyUg^#}_UAyAJzW$lrM#BR~TLaZm z(&3<9`+<=9Rd4n3G989{UBZlE%<^(Q1{fOD`aSv~GB<_}vUk2b;5*n+ zbaNqCg-o*E2Gs)V|Z$o&Z5m|&X`4CcN)(tN|J{_yr~Cf75QR!j9#mRVU>SB0)1ZKknU zem7nD(YQ|KO3EeC*kt3=qp%a}2+Bf@H|+=}%10C$YBk<~(>tC)pmIbGsN`^%L;!)< zkfoO(VXYcbr+Xadgf1xNu3R%7oez3S0yFI%$n9b0zw~PS-YYEx+*al3{h-4DuMAFy zRTJY^?W#>&R=ggb>S#4iDl3J6jZfLB&Zb}ZDJDf+a<(zu-fZlJ%;Ivrb{HiIUM7Cv z0gC;^lXG2Wr|z3;^jMz0OX!b|RzEyW%<4XwWd%aecOpgCbfaw{?*aY0Q56r~LTp2R z?&lPBkqB!UvZ*ZibE2mWGy(-bYR~lI6)zG8RI5xfbk*89@&l z1gp<&eNyy7Egl2|8ix~fK_pW2(nl}XdBE$uEJa!&w8eL%k76}ezDV)+zh5lx>B}rH z(gkNz00`(4kBJzA^B($@t(OXPq|dQ}rW8&1%eDx3e?+Zhw^m2L4!#_OlVK)L?C~a@ z&_HAeeWzO_K#Lp(oi+<&UFcBUlh~hFmn3qgob~ zMWZq}r=MjBt<~M#`k5;{$-5SjiZq4jLs0jVx8O4_zpXG7hSZ8PNiN^G@% z^Io4`I1ho4-|u=g%=2Pe!!4@y#Vf>>Ig70NbR%cN(2X}bRU=K^8hC3g-+C+?vyR_( zz2EzU>f1*^los|!V2gqm3!YTbz0Ak_B|4G#>?MAc^9mQx9e@{26e@+3*#(c<*EW)C z7blN9=odxxyQM|sB;BPS2_U$_Fwr4%{&~rrtFJwwa+R zsaNWgeqoMh+pL0Y*laKluo0bJ&J$(5)0ZO!Bzo>Le-*)-Z6AYrYqdM@EvS5gCet{S zh9TgGbtk9G)*>mW^Mjl_iN|b;0E!uX74!DjfG11&qtDS7`T7l3G6HJRSx@=!a@M30 zx)l^0W1rVkS6GIG3fpZ>7>z-4pID7OzmYAuFlCHu(8Vp##XO;;ZQytPc3<~xb?+Tu z<+=7LF7uH5RuqHjchg#(vHaFBOCn#BHq-aaZeY>YA3u*U9(7uXhzly2AwsriMU<7D z+0+xZM^|0~agrAWm&QcU^b1kcXKk+e#ffkS(^_r*PVgRVJq~!f+j|P~-|Q?3lfpb- z5F8kKyu_DuJRjh0>XlExr98~?SNR9I z|HhuFw@JEhut;`_iNb^y%;A@=@!qdM*#w?32ZtNsPZAc@$D-vKCT8Ye@k*j|*Ua(p zUOU6*L=0X&cF_$KGyd`lN%Ra2CZ6+-ggg=6Jf_1N?su5@oZ|7|N)pYxg2FCokUl zX}q;%@T@){O6Yr@k;*pHJ}HN>CDQTs@y(cZmc)TUARzv=>*u&50S4WIkfc{<)2%sX zGWf^dM6|NU5=mMLXFgF1z1P2k0?&z~ojg%OA>h^KKVIx>Up`{L7#>1GUtthFq7gFX z?c(Xu!Al4_l{mO6_6pIPqvT5{igl=%S`(byfR14DIkAs6Itw)<0DE=-HU66Ll1nwV z9q_Xj$b?Cznr@v3rJEBipaLdy%H-$s zU^-#AM?4T+h;n44yFK+9XGxU9GZU>_6!vs1oL3ai15Y*6sM3Dw!>QUB=K6HeUJ@`$ zIIE|CoL6=Wv(ya)948g{5KfNj$qPg{F`2f@epumMF~?7_A-*yq>&EVy##7lUCVMDo z|29Ht%%EmPgyFqhvC2X&`F?v|()kC zr|FyD-V6=xj*8JqrMtu@?uwh&Y7uc;W$Yrj6<=;GXv`kHlq;nJ?o z2U5As+t;E^GItf>vKT*FNjdJ4ZQ9D;NM~>%OfKdYu)VpUf2@O7>4#qb2X11W1Cp-z zeU5I!I7ptt0%ByNTrdb_csmfYT%uY>Z@*hM?$m?Ex3bXevPN36&xG^{ivcJZ8IhCN z4EpN9`ZiD-q&fhG$TmAg0gjW|Gnpsjj(s!#qM?bsWpI7SG$*tehp^4Md# zJX$PCLJ6xH{g(Wxwei$mQ56|$%j`cmMDt0b8A2u_sXUr>mT|W%?VTwF6s8+p4Yne_ zw9^l54IhV!WZ?mQ%IRg0r~@M&Zo2vbjLTc+%~Ccer|qHZwQG*7tn60bTbH`_>JGMb z6h1xZT}r#-#&ig!=7hq=bV4m1)bd9LNCZES$7W{j3iJfpD@FWizgW>9@N^YWx;*t= zEa&8AJFE0^M#{RkUjem`t9R3nKexRsQHJ=0|6)Jg0A4};VG4n;WU@&s1P^qq49$U+8b2!P9`<8%9 zA(|Q_BD^BIVic1?>#Si=Ap5uw9%rT9F#LNA$en?J;$QyA#-(tI)Z}-OMl|}VfZ-sx zpi~3Y)o7tORLM8Pg2Q2L<8+9&2qn)DGlykxqty+@xxLRVEyi&eO~S>)(ryS_7D zz~=U~$7PC9%KJG9-PQk(EY!QdDe3=XNdlJO>P8n6k(OK{5pagz9?j?(jHh!$k;)|# z@h+4~PR;q4(Ui)CaUe1ru@;NtS^FY82 zDE$SibqJFRdt!Pl`!#AOit&7ilY2amKHo}nFg{u7vYtcsSAwOVrM{>XDi%Kt9R#DM zYEm{1^aq*>(*{R8mmKPmmsqoz<8)GAhHcXyNS>bP{%@%Y9%PNS;|EB!2@} zxfN2mVfSO;w1kDcCgUNr=wn!+SU1J*){{p4a0TA{(cPwt&_-Iix;W#N?0MUq&%kb( zt6l&vK$tSCU0xd5*f6sj#n#c8%wbYx+>5-p7IWWf`mRSy`);|&O$_~I>oMB&Y^s$v zs;NCL%V(5_@=wmfw^#%>02i>-*(+fYdy+nphoIsSwG!UZz9y&I>gtlCmOfmsg@Rd! zS3^ijQD-;-NP6ZDWt`<#li-r*WYSODScY*ip#`vVQW5TR(ChSg@AaIMXq=+M+Z~!Q zdAPmMOhn%>=Q}W-_Kd@xWjhp`AUp_{&1e$q%AJt8?es%&i-KMGc1vGU!MsD~X2@ofQ0Ysq$5X^5Pl-g( ziZsMxEh!Di-w2%p>sxtlMO(zq3=Oq-4m=}6%j>~-E6In36h(AbKg=pW)C<691jXIxx+NCpXRM)}1na9jc^hAe+Zhcuzh@jo7QG(i0gQoPI?g!k~sFj$HwuKX@U4_K8x z1oy%X-kL0TpLOl8%RD~l#k^?jSArAfLR}^!P_XO~rfpCP z@uWhTBoa`NGa7b6Cos}@jt(PWW*|56BP~37_2&*6P+BNzh^o@Dp zN^hKEft~te406W0CJJ$xwvy-Deo&FG9Os)j_Or)a?!Z%{7rjpyeOcU*3S1Wy^E>+3 zFkoX~>XaVhq|f9&1Yzms{jpXV6VAK6T(g?*alFJRa|^S6s&PX#BanuzKR7U5H)WXn zrVH`icKoug*WlTuUq#$=!9IO^^FA{P|L|JNyM6XuG5wfR&_xSq3%VLcPRYcCQmRpD z@0uuEvXmN@JG)A-=UZri-fGvtU#S(Y5kMtv{vl((#&tF+Ed<@Edgzt;zT3da%>Mm@ zUG&2fIw(Fs#iYn_MvovX@k_2T7`O3f(`#9)Es8Wt5X7^+lDYQM(^6Kv`PjrRF|3nW z>~bHL8uTXVHA&p%sPgpf`J=$uy3%4P$ca|35w;5CI5q;4E&7`_z8-z+q*GVDG$DGR zaWgQMwCO=`rq8Oc(iOb0#Xhq&vj%q7yW}=JMD_zQmVbM^QX&HuN(~Qn<)G(3c@=Cb+b=CE=JATzbCKGYT38$JuE%& zCeGTn5XCU?69@)xiA3i9vh~xyxt^{vx=s)lL+#{leyIFfY{FZh8}M+^bhYF&eLOfw znPisMXcrB0eE@bz2&PWm0u{D1syo#X#qt9y{;|iQKI`{BYPAcX=2-*CoYVN%>tDZq zWmi>U!mOa_t6KSrGyPbb9^b}h>S_XeN##8U)8F!vG1A)KUwfw)vaq_tMn1BC|7fRh zcZHMnj&-zm?JZM6_G~6lbqu8nWY@N+p|0g8T$i1_aJA{KvQh*s?V zDHI*p&p=(X@zB_*`?I6CG<)^TgR9Fp7PVf@*8v4h`Xe`0BzGUR7n z9XHHvC4c)XvrO1%I%WFYMfldl-h=zKo^krjORJ+F8(LmBeZ~0BZU7Qe?>%FwA^EI{ z-af+lleqt+8B{y)U^B#=6p6vts;l_SFZ@%Ha?T8_ZT&l1Fl)-TaUXrNG*1|66^VrnGpOb zP+v>%tvMIUKB-8uMxoClzn6WLj3%?SCdx% z;jsUg`b4{=jM_%P>w_cI0>xH z<|X~mnnzygRy{=AzQYY(vsu8wzzP3MG~R<-*_oPdKM$^0e>jog+gXa2aildq0N`O> z?gajEl;J0Gh-%vrNHK0(nCMvSf(}Or5(zCdhy-?~XL z!+B`F;v3LNVKEp&V^atc$F3KI*8XZWf<+QUiUXa_K7xsaDKEF#LrnO8JLN?8GxK0r z1i9#C4E8{aEBwqMdUMQ`cQ&G@df-$Zn<~o*%AbW%E|#1?P@JBw>GVyBfY{csDzmM- zv^#~wh!)3;)-{9?DkJkp!nn^{z}1B5LmUg5=u;V|2_-JR-~Ez zpke(OODW8~+%t<5%4Fr;`n-@3d6oR0?CwI8e9wo32Y4q{tFGH*<1|e`8JQLSZ5Ghv z(1!kLcz?5H9%MB7aJBxZrnZA^v!G~?6(y=x>Il;1+jiKgJpo8}R$q-Z1sI6}1!F95 zywW)D?SQlTOCNuP-bg=?Nc<>*HdQK7Am1&7K>>aKYtWq%)Tu{1po`emlUL;uXtzQ1 zj^fjOo~M}+%cJGO8Lo;L^2@_y3LP{cd z6ss`Qa=ezq&~LaH#U7zC)AtoCb72;f1|fF60jp#&{g)jUlf5Qf_%Z0yXwg&S4+yQ~ ztfvc6aj|#wM6LS;`*{wqkfl@TxUFY7lPYo}ySS`O-fuw|lT^rE^6Am}rIdi78g5EAYiZ`&bvEM<5bGQ$w^eet8PlwqQ+xvH{>F%a!R z9yttQHrNq|(B{gZr+!eW3ZcEx6GMsBVWlfZ67!KNk4H`27#n~Wvh+R@htB$YbNJ6w z$MjgY#oZ8ZsQ-G`J_y9h-s6kCNz+w*_vLm9j?>Z`R&#^VR{mmgPD>&YZ`armT4W7Q z%kKt*SO?h=*j*7Y#>b-PdUTWT4JYj8`);4v=T)SWzm+B7f}w-Jq+KAI^~iY&#NV)UIRHc-|W zW-{JBQ=E`_N3CFGXG-L^|B~9YQur6h7jAF3M1_Z!z1h(2swBx)XZRCR5Jx2qi>QWd zmODg5AI>KOLD*qM#fJ;c@#5HkpGXRgi|%O^SoOk)3OFT~u!_Wac7#i6c(=Tr9Se$_ z46Z1v)`#l9P!ksM{0CkYAQ_tY)!fLcxVxWPt|;*t0@B+a_SEj~%6KF`g@8*h7<2Jz zj^|wO$;Xh^IxONI4@C8#)a3oX2cJD*1`)?x_xPtN3!X|M=v$J3KjBaz9NW{b8;MJW z!)Pv>Z+~04NyN{$StnFKATiDLhfI$`{y#8}XSNvkj9AEL$qt#BqnVy91t8hU$B2D8 z&p-9+&r^&OBRK@tOh1!|;kVPo)d9;PexnV3vu9V-3%tMTqAgZU!t&qz(;mNZ=H!fx zQ_0;wDrq#Qq@u#s3E{msR8v=k|S(>T$4LHB1{c}+XZN|d4z zvg_A;_+PN8u}y*3(+62}PR_<)Dt4@Q-t=Gl@p5LHp5!6{KYj#yHYb}112@)x-NSKs zie?G#>61rEj)cMN+=uG$`faZ_rk|<=8g<#iDq+<+^`z1@Qg4hRve>NAeyMwowU6l*7A7jeMOW&efnxQbC$XI@;{ldY4kem&p zMx{Q_v_oI)7Xp&%q5g=DaWKy zr3Q8SUx~&P$+oX6eE44RmFeo-1wmhn9~TyZ_I7L7Cf}JNdkcblS1zwJfp(=aU98=y zMoza4A}Z`0)i0$llrtADgQTCq(bS=Ce{zd65+hJ>tF-CRU>c!amzWPVLg+GAa$2qm z{0Mi%0UY_WCpUHt2@<(e*3*eLxgyvCHdlVMT|LmfR{kuy zQ;c(;Ck=-dZ9gq*nAl{J?iRmkskkBZ=ETq#jDznV&&)gG^`%L<#GV~bw8i!t;%_K87O^otHA9c`?65zzCtW&R>g*HBHq3z!Iq( zBocXUdRSdUFDF`TMP@4T%|v6jQ9Xahr6CQgAIj2eX9n30&p>E$7FgbK*|jp0!oa#j z$m5km7)f93DFlW_6! zI1r1FS;<$CZ3=2 zrOAi=4KXy?7j=_F%Aqo)fc~dENkm59x+@#|=gR6wO=@dFJ7g4;eYAeXXYx)uwQ<}% z%_S1KuQE71ct1Z$|MYBV+IfcwqrohSLsT2x$Ly6=PqmJDFy2sSUmclFTyrOoJLPbq z(VhJ`ucKvK&}$f>s<6Dj%(C>(Beo|1#d#=>d)cEwRs{MFZyNaaq3}Z9zN=2(l zXrOB(Xq&t{tmiQ@t2LL)+Dy>>ApDst0ZVaBvqae`5lXaw$wjN}2ek zqBRe>1g%Oe*2;f&Wt&wtCE~Oxat8l&5kS?j(5Bn(?2Sp~VgZKyo(BmI(r6V{e)wA~mRkBs|Kr)C=VrHVrP?^INAumXWI_f6 z0rcxsKf=RP7x4{zgE&n340|JSTSDp2VaMju{&`ip)o(Pa(6ZOJENDRFOpL5HCic{J zdy6U%6G5;0%s$OEr}_7aK8M$->Eb7x+h@)zAil*Dnv48h^md0zElI1uq2mUT%Y68K zp<;$AF@XP>ymn;27jl2&VF+Se?iDEkU_?jXSJ;DI-B4Ak*!=XNC4fY6=7iZ#%inP= z_eFzEkk#$wD2{tD#3cVweH>xN4&{LX15?W2JS<#}*8B(`I@HI1HhX1jXc~^`G`) z5&k`Pqbpk%EZ%pUS;luBx5-Zqg_HA$Sev7F?QrB6%Y z3*=LRVfl)+hpT0a#E5P@7~(l~e5+Jc7RRdNn!OQ4^5Kr*-0m@`UX%}CT-~ExQ#!E! zXH|S^YDrRH7@2oCY+!PK2R2j*2qG(>5J0it`{GZ>w!&4v@XDh_!D{b|(39S=T)9w$ zouJFc#(r{5dl*_RqCiQ=$II0dU4s3EX0Dkstq`dwJPHvJy~ekp_|MDf{MKc9Ay4PR zF^xVGyH$j=#)QRoAOQkA{({^{J%NCAy^|xvBetv3Rsn9h7A&v~Sf~E@Fg4L2IVB*B zzhn=3a9{8_ZXf0Z%&uWvex6;$xI9JjVW{)G>(Uq4y*uBXY%x~=SLkuTGT?et*69V; zUHZ>@K)}smnLED5Vo7OjrGUi^gNHCS;=hyH9X)~Fia#;N29+paiV^_JB(JCcRg^z} z{tNhw&HIsA>tm#-F57QU-WoAC$By z+yZ;%^LK249n_F3t>ww7l z_QDyZm~kGFTB&!+)KCYja;a+)pU_6q@W(E6y;TSq5X(sfY!$&$u{b+cTzw|Zr|h8i z122+u!jv}#)TVC<^}|%B#uM#5P@M!gQAh1Il!w1vUXSb z{x~IE!^xSKufBJA(eX8;B-^;)XU9(3&sgNOJ-6hNTF zKqm9No)C(UT7nq?&OU0|M593f2_xSFX~YMTXK{VUI%5;aEbyg<8vy}D`kmapiiCBp zKmDDQla`0f@Q)s5U{2zXmjb_NxKN#ErR=F#AD!vV6~2zL(onj95vd*04zM(33QX*i z(UQN~?U70J8RQy-dLzMR%{!1#tvpAy9jbNE=vxQwh(fDI8^q*@Z}O&#$hUJ;^45G1 z3O`FvkA9SM6O(!x!b^Qc6VP^{^qBFh*%;H@0rN10B^^jbN%tIKi0hWsx@J?=O60QE zazvIS<@|a5Iz8Uo%>36o)?2xt&|EY*t_$UF;uw&k&TbO^6yO}+E3VJzT`7Sl?%z79 z2~^dKIVFUUhhl|$25_2dq|uE*DsC9Uw?_o2a(`av84Q@JTt;oljIJ%`lze8+H>`8Y z&3w2T@^}iTKYtdSuTE*R?JsJPcHNmI4dZC+SBCMLi$9p9RPU@4j1G^Z<0R5hF#u+U z0A{0PH}$XB%*nfRY^7)95(jfx1vh?60>-`2`@PV?SfPTj=}fV2MJ#e^VsV6pkR?KB zCKU=|(vCG9h-$tQ=13HvY*B!}z<7um%}_5?X>&`R8KNJ>wsB=iIVZi&DwhKo0u)~3 zwzkS$$pBMjb+}5WbTPlX95MOE_^bgYc1|TfETMsP9_;?@sJbIs;<@Af zu)+%wtrMd;6>+>-0c|PCgBqTUBD_I(&74Xc{i8yL-q+C4h?NIrRyy9&;Zal5D}zWL z!af8S%-bXjftlhPac|ABeGZbT4LevJ5?@b?jfuY26>ABg87S@+%tYvU^V|O>K3ra8 zVQ1%wm)e2-E6p9H$yXE6AKDt*RM#EU#;(6})cLS<%?ZFoIXX92%GgUQzOOMugLp$j zL+icg3bE-FJI!l%wI42zmt+0|Zt0GtkZ@(G8Q50W)=sUc$2*-gw!v%-!XJ0MfK}y9 z4rZ%cKH?Cao}RvJQJ+0@KR=jL0k@eH{MVkETs&G(Qn-YRB+e~=A*^urbIfXQp}F?2 z--ekZl-qRLf4cKEm#7a4{{Eu0is?l*k~tLPHVAaV&pim|g^MQ;A_%gzvX&Un1~Z`UlP>hpIL$bK;ER%CRm&@+Uj-&g0~ zaukJHu?06q!XNmm#+EEFG7+dInU%?u5%?y5Obg{Ub;MoRhzAqRYE3x(>V}J;=@hE& zs3`e-kzzHHzOJrUn0cj~v#GqF*cxMhcQ*z)VXUt@b?St+r6Mg}+RbuO5CU7haL6+I zL0t9d&z?upLa<>7My(o;BdkfVuw`+A|81`7hhF)-yPRc9T%HI#iAB*H(ExEYz@F%y9jWl`mipDS*qkVm zjw2?Mid3tOg;8NmE=~v%e~;;T6k&Ks7!%C!`#xhDjJ4bcl9L}K+Fs@_8-^o#);)i7 z)Y0xCX>`0}JzffjkSbo*EYdRzRuQf*eY$wfOg&l6JN8Pxhd&Q*Fo#qtv+D~B(5MbD zm@5W3M{jVD*frEZlrW1)$G0}{!(v2%;`NFDl3%kH)n;x-xS|f9KEoGRd?sdnyiz#_ zxdH{0Py2PAHkM`6nGj8u%qKdELMNRhRgwN-A?KA8LQ+epYb9aEhZAOea=UE|-`QVP zb&P}OD##1kZQ=2@ItN|L{3lNazJxf^Qm11_m|aRm5e9bU<1%v9`ROq9i5Qzk>dsLi zE&Dm4jzqhYsN8GNrm1%r0ymg51I$7-Q(>&52Iv9E;PFvi5~l=)zW`@gLA@$xh^;kJ z_KX04HFE`*0@*UE6E>2Cx#fRgypR+e#2G-K-M>TloBqUIe5X;Hk_yvJp||E9Tjd{H z70h+)g(-gO%%xia1yc%^pk+%SnX3Y3V$hg)Ml**JnZ}{BxU=ckCGUHIA0^Lb#u$PU zrZ7h(wgJD{bylT?VDd3ed=Aeudi{PEKO~5KQPH2$Au7%*vMaD_t1N}TLjT)qPO2SL zNwaM0(U)&S*`5tt-N)XQs`S}1PN8B=ffK^EgZs?jE&|VMr+z^mG0V7hY>hm!KH)OS z65D`#c0{5ST5*LXJnf|c5>VTG%|Q2rp;F#W6BAF3?Dn-s zpxNy@X_B*?sTR9V_-XwtfJ~+%`vF~WX=R=#VshS*OA2S*$uy3nQMQ7Skx{w+u;8|{FJ4%~FgMk<^+SxEkEX@3cU(Lx&|*{$HO7rlItm6sAgec72Kqm+hJd z0Otb_sPP;hv@MVrzQpY@ZLFT}R_`8|jBAMp;0+ixM@9#&`5B63v!@7}=b6Wag*KfHl{5PDC zXPT}iPMf`Vl`>oa#Ml6)XHgdpU!h@o<+o|PW{EpYO;?;+k(BcpkyI_gm;oU#`n|Zh z&u8?U1;W_y2l;8Hk;Z-EC~*Xo#ajN%sSdX-S#u{myp>p&{({FQtS<1M#{vHdxfYAC zIOummmfH8D({cf2=_pwBM9)UoD`LfZq(YA|Z#_T;)_CHED2>ZxmHw+Z`4C2XLoa3d zvIIsPCi@9_wUsZHI2IbN{pzptfA-eM%9ilRl>AyMhQEcU^Bnq!Y%#IqQ$B5aj^DYX z4Qfr}6KXjpkN*IaF!tw&R|9Q7t6?>eel8CxE)?^Bz7pE=mEK1TsGcRC$0nqnt&5Hg zFl{T(y_O$6iXkNAOqcz&gClaE|A*%vOd~f?)L>W#;Q(i0yy@kexn$<6I5prS+HdZg zofxnSMea<=e^Xgbn-dZf3t&!h^{{cdZ?@XR4Tjw}Ha8Jm%c~%VJ9D*#6<+=SxqtBA z+MKpwA#cRFfWmVt6AvZj=_ZUzC7v}2BZRJr>q9+yB7T{%fuI-@SA&Ei8b^ zEx;KRo0XH4oPobX@=2rqt9;0G>cP+S4%qfT?#0c0eRUbc#ea6u2y5o)_#!#(dbHL! zz*HfADP&-Hc*zF1*yQs@!{R~A(oxOQ@R)uVaAj3C?;IS24G{T2X({Nls-tV`LfW?k zJ>FT!XZjRlFm4lG#Bseq8DQFZz9elieHt;DRNZ`Cr%g+F3_GI))+x0)w49rJi^{6o z_5~Jz?(s-Y5%}82v26v)5@Ali!1#7hVI#Sr5QU)NJi9Vm>|iM7g_V`nL9IVqxw4Uh za=mv?lP>D@l1L5h@Gj*!^xEZiH_uc zjFQtBXPScgea*fdHv|!_+s4c5`gXr^qEv(Jo|{Gr{xd{bUA>@@nyGuY*{s%U&O??X z^1EbJL1}ryFtZ!})g*$G*x=o#L==rchl+--&3T*Y2USQz$o%N?1I}widCBNWV{w&! zcMYiQ96tZm1)p_=RM_RAQ#*YUn_gci%JNF-8UFZDYH3qJbS0fD&(!|yxTPe9 zTIE4*uVkCb=v0q&icZY#$C2e0?_JfW8ItWPy@IpDnr-&L=87aa-TuTpY+%mov4_~V z3xJ4lOvN!49cv=n=Cod}9_8qAFZZ8?v^y>4^A;aJ0+%{ojL|iAq|DsR1tpfX{TL)l%mm|s!!7H@UOi*~Ofu8CGSMEa{J!wVxg^QS0e_%urRGZoI*6Lp2 zdcs4{Q1Z#0z}JOk-oOQc&jTdh#;@Cp#vk=Xw9~FZTmI9xkA>*qJsq#2rLaW;bZ84Y z^fzmwd(TCITH6ZCK&V}ZiKbnHrp8b3wZV2VjEN&`AWt)4ujGgd8BzMs3_$^jt+{)3 z+WNKTZW0dQiM;j|D-R>_7dnXvSQ*cekdc$a#=qWoJecKZ11JZ!9)0@w^!RX*XexCr z-1yUzni@A})^j{9W}o_sK|9*$6h>^}T6wc^V><7yB^aA_=*_im@#LPiu*^I$xpKQZ zp2lvjO!w{a3P|vaq71P0jTjI_*m!vyY*Pt+{N;{HjvBgt!9#S4 z=5%EK@K(zY^>Wtr=blE(4%Di`6$El~OWx*AG2oYKtq!?{ck&Se3a~LghC@ckj(2KX z^vb;XD&gz0X4e1Y%UM7E(Hk_uB*xAQE(-8JedB)}a=bpH^1*HNL+rXS7I zQ;0pmSKDcmf2xnar%6+4|lhh_mXwA{h%KZ+q=e$MI?tY59k_F~{eYz82K) zebJ;*tw${=V0}oF$7~!ebC%9W+EPhHVGjh|BN&&X-Dx6!B5RbMfA!&evJxv3|9KG^piE=&UU4vO!}Wkes^x;TJ1F zANBYl^)`hwLx=mZ@lw4)^JZK4Ef+(vWTh$(6YYjfW1(jw&>3Mcdl1Mi8V^P=@ke4B z^xu=pEx^khE4TcnR7>IERl(3|MtQ|>x%M9u;9uYgi4p9cW(Z*;BNuet$rcsggN<(a zZZP;t^zjoL2W)g-@rbWt*$6TO-MTNp1{gYH_MV2<(Vm8{PiloKdSZ{yJJI+SvbP!r z@X;LD2xxkx!pqNIGKFCo_Yxc5M3Tm6c>_ay3)5MahFZS9xi3F6{RD~n{3L1T^}_K` zC{ME^jK*4Y^=yGNLJ3z~>+PRDA7W(eJ5`rh-IU5(kkIcm&Dn!FAf(hwqtDT&>XMwm zOKgz2w2kc_>)1x_x2SYc9pFV6c7X4=?qodbRaI3>ZO0MVbcXR+X!ew+ zD5#aX&AD4O10G$cy^@JvJ6b6zkwhCQHCDyO)>_c*IBYxJ);hq^k{j`h_R?dSB5otC zv7U^B_)aO2iN_@yPsH&+T1tG_8Q`7c}R7K)k literal 0 HcmV?d00001 From eda3fbe308070d40a32d04c0d0518fdb74441fd3 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Sun, 25 Aug 2024 00:37:28 +0200 Subject: [PATCH 5/7] More docs --- docs/docs/accessing-properties.md | 36 +++++++++++++++++++++--------- docs/images/resolve-types.png | Bin 0 -> 19541 bytes 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 docs/images/resolve-types.png diff --git a/docs/docs/accessing-properties.md b/docs/docs/accessing-properties.md index 04e1846..59fcfea 100644 --- a/docs/docs/accessing-properties.md +++ b/docs/docs/accessing-properties.md @@ -2,7 +2,7 @@ [Tiled facilitates a very flexible way to store custom data in your maps using properties](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-properties). Accessing these properties is a common task when working with Tiled maps in your game since it will allow you to fully utilize the strengths of Tiled, such as customizing the behavior of your game objects or setting up the initial state of your game world. -### All classes that can contain properties +## All classes that can contain properties All classes that can contain custom properties implement the interface in some way. Below is an exhaustive list of all classes that can contain custom properties: @@ -26,7 +26,7 @@ All classes that can contain custom properties implement the interface - -### How to access properties +## How to access properties To access the properties on one of the classes listed above, you will make use of the interface. @@ -52,7 +52,9 @@ else } ``` -### All types of properties +For both methods, you can replace `BoolProperty` with any of the property types that Tiled supports. You can find a list of all property types and their corresponding classes in the [next section](#all-types-of-properties). + +## All types of properties Tiled supports a variety of property types, which are represented in the DotTiled library as classes that implement the interface. Below is a list of all property types that Tiled supports and their corresponding classes in DotTiled: @@ -66,11 +68,11 @@ Tiled supports a variety of property types, which are represented in the DotTile In addition to these primitive property types, [Tiled also supports more complex property types](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types). These custom property types are defined in Tiled according to the linked documentation, and to work with them in DotTiled, you *must* define their equivalences as a collection of . This collection of definitions shall then be passed to the corresponding reader when loading a map, tileset, or template. -Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will attempt to find the corresponding definition, and if it does not find one, it will throw an exception. However, if it does find the definition, it will use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file. More information about these `class` properties can be found in [the next section](#class-properties). +Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will attempt to find the corresponding definition, and if it does not find one, it will throw an exception. However, if it does find the definition, it will use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file when populating a instance. More information about these `class` properties can be found in [the next section](#class-properties). Finally, Tiled also allows you to define custom property types that work as enums. These custom property types are just parsed and retrieved as their corresponding storage type. So for a custom property type that is defined as an enum where the values are stored as strings, DotTiled will just parse those as . Similarly, if the values are stored as integers, DotTiled will parse those as . -### Class properties +## Class properties As mentioned, Tiled supports `class` properties which allow you to create hierarchical structures of properties. DotTiled supports this feature through the class. For all your custom `class` types in Tiled, you must create an equivalent and pass it to the corresponding reader when loading a map, tileset, or template. @@ -86,10 +88,24 @@ var monsterSpawnerDefinition = new CustomClassDefinition Name = "MonsterSpawner", UseAs = CustomClassUseAs.All, // Not really validated by DotTiled Members = [ // Make sure that the default values match the Tiled UI - new BoolProperty { Name = "enabled", Value = true }, - new IntProperty { Name = "maxSpawnAmount", Value = 10 }, - new IntProperty { Name = "minSpawnAmount", Value = 0 }, - new StringProperty { Name = "monsterNames", Value = "" } + new BoolProperty { Name = "enabled", Value = true }, + new IntProperty { Name = "maxSpawnAmount", Value = 10 }, + new IntProperty { Name = "minSpawnAmount", Value = 0 }, + new StringProperty { Name = "monsterNames", Value = "" } ] }; -``` \ No newline at end of file +``` + +### Resolve object types and properties automatically + +If you don't want to have to rely on creating an equivalent definition for every `class` property that you may be using in your Tiled maps, you can check the `Resolve object types and properties` checkbox in `Edit > Preferences > General | Export Options` in Tiled. + +![Resolve object types and properties](../images/resolve-types.png) + +This will make sure that all properties, even those that do not differ from their default values, are included in the exported map, tileset, or template file. This will allow DotTiled to resolve the properties of the `class` property without needing an equivalent definition. However, you *must* enable a similar configuration flag in DotTiled when loading the map, tileset, or template to make sure that DotTiled knows to not throw an exception when it encounters a `class` property without an equivalent definition. + +### [Future] Automatically map custom property `class` types to C# classes + +In the future, DotTiled will support automatically mapping custom property `class` types to C# classes. This will allow you to define a C# class that matches the structure of the `class` property in Tiled, and DotTiled will automatically map the properties of the `class` property to the properties of the C# class. This will make working with `class` properties much easier and more intuitive. + +The idea is to expand on the interface with a method like `GetMappedProperty(string propertyName)`, where `T` is a class that matches the structure of the `class` property in Tiled. \ No newline at end of file diff --git a/docs/images/resolve-types.png b/docs/images/resolve-types.png new file mode 100644 index 0000000000000000000000000000000000000000..ea66d43618f89214acd5428815ca152ec1cf5f2d GIT binary patch literal 19541 zcmd_SXIxX?o-Z8iir^n<0s;a`FVdykXwtiMq<54KA)qJ%(nNaiq4!=w5v3O)p#=yc zolvBN03mR9@SJ<@%$%9e%suzj^SodqvPpKvU#HN1O%##B0hO_8Tk47YXyBb5Qwys z@I}<^QeX)JDXl9$f1>SevN=oQm^uMj!7=K{^Mw`#eb{g63^%x?=j5Er9Hf|F9wdN# z!@=|HD!UZ1<3l1O|-Bz)H3l0#({Pn>M7CoH@3 zYu26U*1wqZo@l$$M~c0spJq3ZjWsvKrrHeM%%Bq?S#l62G8QK+g=ggXvmI;JOc#sNVrp&b1mx++skAIU;$G32~g$Jq=9 zamPB%v-#6HrJbhX`7&pP^jje>YTVOICArg1`)^;FV#LQvpN$QpwM5Y}6lNDWc9Nq- z_s!1&Ciw8Z8HHjsu>sxGt(g*sf-HE#C@n{U|*=2}}QjRpP?aUA520dMK z!7Rqe?C(;wj%4#m+C}kC%Z#nwI+)hwwp|YjI~G0bwuxge`%4DvCX>&IUzI;zm6tL2 zmeF|@$_qPDJD!uSR%`ni;op*3FSFm&dNjxcJK60&?g;ib#WUa!)G!c49mjHFjlP`_PIJ8 z4Z&f@%StLz{I(;^X_C5XDHb%YO_KeXJEZL8AFZMS> zN@MoQ2IJGxednFkm;V57;Z8l|Fm3g{caNG9gMjE2X&iW_JGd)XSVIdpwG!`N!U)ai3yrp81I-2nc%pSv=pQ*DRwV#q$o5W2a| zcDF5a?qT8FVW33U-Tk#{z7n{2v=Np2@t|qgE!dn_GnHzFaGRV=mhRcZ7Ezs7{u&I; znQBe*_SJ@bY%8y+_Lob&Qclh?&wfhq@mGBos5wLSJFj=2x_W3ap<2s1rCGidr`K9< zeP07+zs+cVf zRSaPJUd$}$?rr79zp3XfVx2XFJ0LQbYF-N z-ESfhmv*^GQ4QHF5j%-E^eGx)teYX@TZz2lHiY(Fh&Wj}8F%^cYh`j1Joma|c*1>r z>tJ^?>>K)5-I(CJ&iq-II<=GJ>-SLz!zBOivqhfj?vrfEWvX4Nvt%liU;k#wIGY>Z zQPG>u8xFZ`T}DG_$#+fpJSm7GFaZUdvD<{3X1uvc-rev}LCOr7Yr zuXQ}I$(mS^Fdyg9?9cNiH6SBy&BVtKI~lJ5(@qfe_$z?s;hZooAb$5`BR74ac_^^| zrGpZNK>|{|#pTJPt+A~a9}v~{y12*`Y@&AcueU@YT$?=#nC|_4k&qrS<{dkHZ+)v( zAVodolr$kjiD-p86>)|aZd_>1k|}v}BpNa8kpULit9!~YyzzT~&vgZ&5nOESugX=B zukAmtb$kM;Paa~!lglR|_pd{af2W|xn;_=?dokc@cTJfQLzkw6#*$T&S?(7*=~pmr zuP%J}77{KhKnZfmDyG*tQsQH$Rm9ILgFPa5oX!3k5!`&xw(x zjeq(kZT2_ZF+?IVMm-SAB?h6+;4~0{T{j!liHM4_+4LArlgJ;2Y`Hc01#azO1wu0ZYHj+$BKf|97} zoe_h4aR0i)AHdYSnJG_|yHxb(^zjQkc^7)LfXxUmnSB0Muh z)`w+(HPO%z>9V;zbY13@xdRsoE>~5mMbbMatEN4t*mbll_6G;Kb%f6^4tj44Q#s2%w$dZbd42g? zr}$xC$lBdvk6cWqNs^Jct?w=+y`xn2k|0^Nxb)m#GH3Jelci3x*CKh~95U`k&5V%= z_)L>AE|XK&ezcdw3w2NH!j%l~b(2AvMhXf`XElD0GM#23(93{4<97R}p)6{Nkw4IW z5F$m_SLhg}v>bXxA1i^WP}QNi;${I=G-4I?jT9WYjQP0PLXG<2H#jObCT}p<$}KRq z&GBR^X>xCnh9ndXjNbVB>g1?Kcjy6y&I)ZU9q276_R*a;$}6~^FyWo(TM|y6jV6y{ zmFu+fo43*?dI~SQ^@J?nnu&g;oqMTjobj`(127?oxQrhPK>9@Sm6?0xLq*nJ>>jVs zUwx7`a@QqD70O!+s7+@}$IWFpHSzqL=~89Xv#VhwwgRe(iv=>pPy2_u(p`oWhMp#Y zEEg$7HNTWf6_>&}Woba5lIq*MX86Qq#!q~ylD_*)^mF)%^a>)mS>qbm?y@oJ9qcPlU2+1a7n!U(JkrNOhF1-s*qJrcCKHuk%p?b%Ds zi$h+kfk5x3T70U9QpIxcIBaw3#<=yM0tjj4IsDt7}cnkhuy2*1hnZPUhDy{md zoMxMJnmspN#TqYHy&b#4yxn%z>bW(u<_b*f2R_(YFArcrQ&4=q1pGGg<6j{lkb0w| zOj|?4s%D!MB>VU#VtDmqKs{jpt`MkcD3o zQPI-T$)lI>T$~YcLdJfnTLH$QPqWTJ4&{sHne|yo6ZhOm?C-Ubd%wA9k9B%4=&YxE zkjfD8-N<$91~;DwG=1?+p7S$O4h(8wd?&rs!Fb$v;wmt@j_;#wG9bEoWkw@IX%fb& z(qy$55+Gg82y;O%M1N--@)sXn)AE_Uk$yb)W zkB26Q61#kF`?PxUNQh_|`=9#GqLcCjHs&iU(=DZ>TCY%f;w=g%bOvf4)urdP)Zi@p z`wi;1t*uKY;)F#KOC*-=szu`TUK{VAl^V-vv}}eGr($@!VLh6r%UGu2+Pqku9`Gd) zx|};OdT(VIJtEIICO9lv{YistB6QNUL<>W^?xZP6#ot!a%r^Ep^=q`8pvUvs&Kq8h z7RN}6P1EB%$S4gMB_XZfRAvca!UZKvjCR*FtAyYATHu}iTdAwpvG z0p#ZJ7|lmy#?U%QVX6K)CebZ>dBuE6?D`a{*nIsO`}P(atI& z`&JFqQ|tW)(l4jVoGx#Zfi7hNAMoN;($%_YXZYb^0|R)Yk}~pBgX*_Sx1&P~7x`X) zr=qBBDqNSC>C6_xX=$5Z{~IAaM(Ul`HTwFLv#jtAS1T~q->;7qc1Xh1x*ge2nN0>M zk(|~NE_<9bVT(1Y*zYlgWd_2{8Euo31=%DXb~(&kHLNnhXrFS>z2++YcI3>=hKV9} zF5s+h{L`a@>klO*|6=SS)!}kXXK^Ea(roc12fa*vw)%mNBQ$BHTj3EZ%UiAHAn4U_ z65Y6>SLCL*W`=H9Y<^l6)C7~nz;4gzL)-|})bGVmyYX;5EvkOcbTL$Q2AY#BPE*^- zyd$z$m5zNNZ_KpKth#R5XB!zs(!?xVCpeA!+|P+(0lieSA`d43oM9nJyyPAE2Ekj{Y z%y_4k>L8V9Z!z5`8LEqkaDL>c5A)YYCf!YT(Zjx;B-ylShCW4ee*IL|mgu0kiV?0yDbmcL0ly z=-S3`hiQ|$Vrgk9ug;z33Xg3Rj5nytJfEy7$DYRZ$;=fSnIi$6Hos`+Oz$XA>|8mOiI1P{!*P6FouQ2%Reh8Yro*G89Sx7K2ReGe1&JFHNmT;5-UBkNrN`UZ$& zx*Bsx!~bc;pO7P?(^3ORb(!-*svGpDNkHsLRU~rhkOO_=Mps!0uPt}6BjBTo=4LTf zdkEWS2mJWJ{=MqSpbC=zV=_VlQ4Ia8G{^0n>{-5Vv%q?qbm6UsW_(8oJtYkd%?MDu z-GQC6d4?G=UaX5c=3r*5=cB0ix$>9KBjxXcb4s-q)4Pe5>dVI}>G zO?)I;QCfgn)1+mPLrY!oz5O6ctN8^R+?97%zK@XwFt%lvb4&1T`f81Cd30sm0u>kmWg}H4z$8lg zxFFqUIY}q{Q-kT@LBgRn?4xfAMU?CS-{F>`@csPPxf(cP|K;3|oyI$o)gFlPqRtIR zWo4{8qRB0oa?5q)pq2x<1)ho{Y7$-qHCP>cscmUeYMa6CjOHzrRi465)#J0 zE57V*P-yQSTMJP-X}S3_Ce+XBFE+C!9ZtL`^}wswi>Y`dJ>5$mpMjVS93 z^}#o=Wsm$TEu>a<>;b|8h!`zpVhyZ7j`Gr2(Uh^Nqoel7$EF&MXq_G}wW=+?-=}50 zQl=M!YT&AuG5y9~8OYs$MgPmY=R?7sa=B!1aBwTm{S1#ScPtGdx3|}L#NR?+q&q2D zZ=Ehs*?mwnynla0x!8ENu~(lAP86YfnBIinMO0g=#=y4{Cu>+&KJoEsPux>UNA&hq z7zjXaKCo$ic0=AzZQ2tO0Z#h7mwtx>01N@u@uma#At;5_BeCsg+(nHFj~K4`Zs#Nz zzV*YlwG$@Qy?DnZROgOc z4t`70#p#3baI|8)Z+s~G-3XaC^cDN6)?K%{X7vZ`IiE8kkk++!FJ})L-W6Pb_$!|`;Z!hg;S_uWe+0lqvdN71 zZSHGd3=9p8iynHuLZe_{<8^DHoQ?yHAe{5pwyS|2?Za2!?&@jGsR&9)pdq>ScMR*D zbyQR$lxD+M9X}JxFoOC_fx;U>!nwgN&p$m#4`AoF%6-;@JzeU>ktsWBbrxyA_lV3a znC}4f;oZM}ETUy(Y@0XXb0fGF;_eC)<)&U@Q^1YqxJ%ls?VrGBIzCpc2YjCCIwyF# z6xaxW>?QTKLPZMqe`!uRp<489hEhjP@i=2u&v%zusQF9~^PR!K`GD9k-U`9}IluOr z+=UcqVNua&B)x>IbBc)53&m-`J9=!zF)hsE0>i{_wd|<^_#@Os8#gFJt4!CIZC(J)IQ~b34HgVxZ^;s$ zF`8%|*}4oe%lj`Xur438GX3DN=H1j4o7+>K$M>RTY|~fZR?;p2$}XSnuVJD1gRZB? zV(t|6XtnaNO;MHKoMR7`64m#Nx^fk4L=(E8i8ivAN4obUrttC`RvE{aJ4snswC;`O z8a>N~6?){yro72$Gqw{z<#2CPgEEfi}e3z4K#;vt2&r2vRE$wbyqyeAS z``H-#)vy&355=4{m{r#vQ0%-S#R}h;BiyTM*DJ~?HBvarCsLw%O#Pi8Ijm44H3`8y z+vmxtk&NUi>4?!EUsJQ^Rkc)ujmX0h;=#8glwMs3w1pM7X}EQ=8QZu_i&Nx8o+(}3-Iz&>s5o!! zZ`Xn-lUCK}XkMxd@fVq8wBrV={e*b#^tz|Vy-Zz;09k6-9*taf*;yIT)+XU05uxQ@ zOLXKORZm#$VQHQC{*dU28*^ZhDA7}G;yQupI)#+m%}-T$tWB?=goTQv$!>l~MvU5C z$f@anS?P#npN3ooY3Ej5liQH@-fmoBfc);bDh@u_xFIL|^hSQb;uL!IYf&Afq5kb? zBr01VTD^$2P37}?@qT<@M0`p3{ z7>7}=P#I@&nk|iSEHOwuw9lHz!Og82{9rcV3@;n`>4P@Ch}z^uB}V=a2r1@sKpbW+r<%#RStS2$L_4^;czzk9YHLPERT@+_XK;NcT z!8KIU!jZ3{z3Ck~>YSWkvKg~6($Nh{eqAVI;*YVsSUGZiPNCO+sZpK-OAk`7>|?z% z1=x}4;4Lw!)58UO2M4#tpyJI{Edu%s#tn`W;G6kbn44Z~ z*zeTujxEK@>wF5+->vSFCFNe|6MYKY2qG18+Ae4g`N)NQD|?-}RjAk}TIq*QXnVx- zfEMG6_C1m+z}2irI$+1W4kt=WbmPIXJNOGA?V|S6yC-={2?!)|T{EbX*buuli`HbP zk-#A6b(H!p=z~D<#Rc2Yq_i}Q^r&HYsbPH)6xx-i&UwJng;E$bIIR=`J(evfFuU}m zDxf>Zu(KD#>q*9u!whz8Pi`xpi3bHf#K4pfBDE+;j#(yJE|%( zfdN-NRp*of5TFIXCpUwYey=~cLKSY*;;jK_tDc2lQ>7sWxPwhM4nV&|jWgf5?wAZM zDtaN|xxsE<{YOk-a*(qYnnaa(&WSu)@Vq=!S1ND8p8#Ac2d6EO!*eIdGg?{!L%kf< zb`MDcFBJu{wf6GZ*_%5P%Ak^sDO8Iw2~hKAgS9dtXmub z)^?^*XVEeXV#_5R&(o(`8t)mZwy_H3&u=|9@$4V2H>XAG_c5k&WDZzuh9yL|d`x;$ ziw#dQ;?ww6dz|0!YlVdLAn~Mpl}onP&PU9`0;=L{`U0=GhGj}`K1u%`rDPX)=3Cw3 z1$w)y{iGuVec$P-kWP6PiuJn%8Jx^ht%yl1h$Vtm;;GwE0m5z%hs>QP61^OaPeqa@ z*a*0P6 zUGdepjJui-xt;MTNlR$P?f{~1Iy4QPS-L4di2GGrui<7Q0q1VMwX?B=<<5o1%c&^y z#61)g&g!B9!=~mWK#zTjT7+4kZ*or%uPK45%HVSmQ6Nw;fpxQoI=}_mS1kIbD+1b+CFe0DEWqC7*vWxw5VUo z32fLk5|&7ny_Lt-Ddu@Ncd%z-LI7U_;yQJEDk)$U69R}6g5T_M4KmUaS(`9m6_^b! z!M5lQZb2d=A~=!iD|D9)24O_NUi`^O@b)=0BOxO|Gc_BAq73+AO@EY%gX@sgl#K+N zb$TPqym$Tz{q_b58-^RCYF1>Nn!Dc-OOVp@l>`O;{B=;E!FNTr@r zM0RC-orYmZLklCgR}dn_DxK?fYwrraw&w32%ZUnqc6iA$ebj3g;^}dVO?=$Q%G!SJ z?8i&BwrrUT3cE^2V=K93dfR1%y0!?A+Q&c+InRX?xmo&wc39$ zSMm4nvH(pSFE!NB(pu0gCdiJDD>>Jj?Htu#$%}oOdO=M?MPfHfMaI;V)bj3)`& z)C8RQC&U;lz9$3HImL-)A1;^t-?QrnH|3zhulEZgkL%vq)pOaxFLG@9E+PV_HFATr zd>2<)4A$0wQrLJqDjd-D^Ho>Bg&vOUuT)LMk_5V5x+e0Mk1J}TEZ_a-Sn!RT3Zs2x z|L{aWAl=fh`F2$9_KTGCev^!kwb4EAu`jw!g|dL`(KRIPRTkDe-rq!d%;h;4#H?Il z&;1aukg5>NMCl#qa{-m3Z^LxJ?-ymD-BHqodP2nuw~`h{SAwX9&|_2Y?G+fa3R|CD zwFi6#J>xph$&>vmxZI1~njJ)8-da|kL)cu*5>S#W_~QUifxdoXgfx-2(Lg@k`(-ms z;~M;l42RUoH?o)%vq;8hh8j4V+SJIW^5#g?UEGg1oBoq3rHPO|wc|jL8|q~1>R{`c z{Mi!Vtt2Kg^a=54R~s^61>!hZmQAc3<x9a;TWU~roJf*?1zcB;^qOv zecXnV7^tJQA06bxDYtD@@=q)F7d7ZY@9*9o*tym8Gxs>e;8qjp)jY{y%ENnN+4FNN zg(L4IZWuN2OO!9#aoE>L(BxU#$@5;W*+DhW3^iSG%cTpwzNtINrAF0YeHkDmdU6hO zRQp{mV0})M`v6z6a>k7vsEa4sf@UE}*fuz${@TF@2E+tG{BM65Z1zoMIJKga#dE#O zMYlZNi{;WOhJK6@?tBy&5~M}z^Pm$M)lzhB9w`=zO2e|j+q)5?gexd^w7^8O?IU2q z$x(bpL?E+HgHB-<&ynS`e61+SLZ9-5a=0;e2;IAQLAO}0d1Zm+k~|DU-C9WRLh_Ld zou%jTvv8jy{e!shU3ZK%t#tu9milU=2t5WGrn(v$JG^4;B7aa7W|tGS?UF9(?pJSc zl=={4_P+R^ahE)y(swy5NWAusfchLGBoXNG(&WFfM*3_Afpt6?@+bXu;ilZi+zxwi zVYI8=RS<}doaH|#QV;O9+tV&>r4AEi1P8LJ_s$ADefXN5aDuRv*Et^n_oSyd_eDTN&Pxsp|s?6v}vN=`Vod;N?zF~4XpPn=G z#u?uOG%v@8t~81T9^KQ5u@7rxo*AJhcWf*35zrrrLsEBiOvNES&Fr83il!8iV zl19wH%}B`f&+uuThlX+l^{bKjay&r_+ilQ95QEwWivy`DeYh$dRyc%r~L&n4UeAC+FUT{?KJmrn0vB!(ZY*ExS|AcIER_B_Qyz zg3wb>k^({LG>cJytOznrPq32Q;x0w6rfWYlr!7#T#Q^o-RZ#P9oOD^XPlK{w6L`7-bHsb8{|;vzLIW#KCqK1{HwU?6ec;jDOb&)0y|EN7ervi)^{+{jPgdTZzc2&zBc) z=-!##|DMX4KYQ(|qd|;YL{U8+Tww4@rJ<09F-1nSB&O_rR78YzB9!B4oiAV&lpiCR zKOwVHCA*gO#6r#!b271x*vy2yy5g zpU&|pEMi|m0yiPPIP-GJ->f@l;~TfUiXpGNHv6q%;+w8FNw2*m7^zQXRJ4jCTSa79 zp~?xw08ZCm@~;uq&i~l+0eX8x!BJVN*Dm!5dG2KNJAV~h)y`Sg;fU;ko4hKR+{hAO zc30Q%c}!?d_2&l*OjNf>#nY80>LYD#MMYp57EYGvg4{7?&Bhb8XA{x`ERtn6z$Bwu;z!-&?5Z2i`1OMUYMSE+EC?%7OPB zE%4`RWk|Rvua_!*$bB_UW%mFv zoq91tX!K2p+m;Xkq|z=itCkYwiCA94l&7y`S&t{|o&aT?rD(?N z8}YrbMXFu52+AfF6~9I{r>eEzPY)3B6UH2qpbJQ?*mL-7sxc9prNY{?CR{Ed{PQx0 zGT>joD1G{#Ok(L_$!opf7G7!X(h62TEhhbM*~wua7FbA39xg1gPGl@fwW$Zyv6YLd zs5T4Ctg1kKm18!ON0;oh3JUcgtY1Qx=?{oNnU5$b!VjT$kn)sUd*v=60Zmn$0QsWUVW2G(js;R$z zd11TAgM4q~nAF`N0iDZ0c;)gxT}yOn_~+pyO}?^zfIq@ViJ29uuC6vPGI9sz_}_h) z5rT^{{(mTibnk#YEiV774AQW7WA4T)?MtB7do2H-)(AaoadjOu^~IKs^is58jf=@W z8uGvzb1*p@w3R|7++KiPdP`wN7cPn0sBUsy`g9&p*bOZxby0Zs%$gutoMo;-ITZ>N zVb5UpHyYhFTs6`Q+@effwocVn#{n#;?|$;s6KTm38qJIh;ttrlaitxfD+ z0RqLs09}cSni}?V;cF1F2{Q7NQM0GYEy>?+vDcHc2bJxZM>Ctg#pH`}rzd_@3{NC% zV6P0=mL`?qQagK^yEa$GP=eM6cWBTrV#-P9PSY0`+>vmPao^GM{`86R>w|U;(~Ef` zl1_7HJ#U=KPLq_K1O@8Y`lSw%XpE3;2ob%B#KP&#eqB9+m#>^q4BLUce=o&4S1XJL zSA9VrB%Dz~tfuf@R?B{TdjJnEp!jFb%)vnd_8-p7>EE51Y2pEblg^cHQx}Q+R(Wp1 z;{X#L>p`3TIoW_#n7f&kt2@Cf-qj-O|{T_FWPt6sE%OFJJ+{ z_Dl~EY1~>m*COSD8;Q9fYE`gXy4fdu=zjg}w%T3rRZv{^KX=+q51`c{%F2zA{UJQ; z_7XAAGe=$0HFA~{AncBjXf<$OZMR5~^iG(}xnWayNV+p=muX2anEa`(a4J2fl(((L zBN4soo)%DdTs=mfGQMb(W1w4ohiSUjr>dqj5u7>{*)r+YGICKyOE25}tR|(Ds|o=H zeEO*bl`=4m#xwI($GmODI`qL|->{W1mK4PK9EfWiu(V)j+@hope_l+J0(&kw$a2H= z-RorGa`$P+meL=>2Boc&7kmfNswCcNkKc6uthM*__|_#y=~Upxa#i(B_^QNxLu z3e_f@%|)mYT*zlTuXYrBf7pZCGLJA43R9O@rt4iMfLuX-H)DX2lamv#SKbC3mWHe+ z<^vRk!9B0;;EqjeM!^{P#m#jF)k_pR7J!BG$c1;VjH9RB^8`wsbZr86U{lB?+bYg{ zLw-T6a}y>0Q5j=X%$5s9@sfw|EOi$Xige12o*(H}xDD;+lO~2r$ZoajTHyX|Xib-h zkQ7fd2>zwhkQq&;m4F@D^lrSkY}_NP5_4u^@MpHIxz3$Y&H^jv-aEY5p;7{GVQnpd zuPvzSwq~0r@t0RpzC;IHYa?}4eTs;0Ar5!CTt4WsmFC?fxiVL zP2Ub#{uC*j&RyxU4XDs~avD@Yzu^YZk#U{-PGMo;jvOFAY>3fC8ZX)fqj5P(8LKY^ zw_FS-?LG;Y15~l09cl;Q`N8~b!gmn&{}o~QKWRomdn^0)?oxk~FQy1kPXL1lS#1+_ z?vK||nZ{DT{Li>Tae>Qb=^roW6LLe0z+<#~lB0pKs3*XpJgx6tz6ca5r!p}?UG0|# zZxBO*bKVLOsN#vvj|<}=%p)T=pR|Xok`6ac$o=o?EAr>8xDOLc%6DYwgR%I;U$=dJNT~(73RYeWTv$B{-0E3_R zLE?^Jcf^adt%g6eW5p*6iaI>a;+?8YDvLCTaS+Ej!;7R-T!!Q15|}S^GYFQvPLP0> ztBm*u{QRs~D&FGDY+r2!_Rw$jOmP*GtoRBf=w8xMQiUg$)l9k1@}4ZQb%ubg_qA3b6y1`F^xAJA%LRxBNuAZB}D6_#XNLvU?B5& zEDu5SQO?7Mh1J=xek7QHvmOB9!0g>XvLFQF_W0|w3WR!sjkaa!AiFgCFJh6WV>cX_ zI}zbg64rK2y_a;!L!-ZZ{z^8t`q4NOZDK7_UsEulYB*S0Ll{-L%U4+BoT`z`5%$-Y z_i7bAeXYNql+W=nIBi$LOShJHhp6&2@8Oag1 zuaiINq$NH*5jC=&*?%lKH+==_6v=^Bgk|f~2e3!m9QHl(b#K4BWVMc932n)FaThR> z`#Qy=!V`bKdhYxC+x0XcSiE*PF)I{yotHtpZ1z@)U-kAAnEi!_2OeoVC**7l;~jc( z17aTQHRU=v;0%GIG>>I=!>(d+Gx)(|a2}cA;kmf>%IoyHPw)lM+p5Z;j*#mN43gRQ zxGI90He@etZNk@*b(9SDNFb&timFL|+-c+69^p7S@|oL_CN%mscq69VcASabu zu1p^U47XLzhYkgWW`~*nOMH(fOQU>7-J`Bd^OD}ukptz~A(k!G3O#h&&3I*ZyW2YK&6TgSEQ z0OQQH_wzgX2!;(bx~;gc{$L{XB`z&mP=`t9E`+|#S~iiwd`OAkp-PLK>>!5izBqaN z{uXZh^<-fp+X9oqAPj4d-O;PX6|%YKBK52XpSS_JWf|MOO%C~`G}qtrhwLHrvOYbR z9-DA{9CS%kj;CW3Tid@Z50HHx$A4q|KOmKJ4ZX^;1M0K_Wb9Ac$`7lCh+hI@r}-!C zRci6j{7mGU7BC?9DF4R-+r0prZ~sNcc-KpA!}i)a|LH&21J((zj`#x-LW8H3Vc2Q< zzodcVvRfJ(#ezCtCG`C#J0(?w9A)TpBhKc>t+n-&W{dwkZ&pXupYZLZ&A z=w9@e!G5&A1p#_iY84=P7W4qpjprsZuX($5dWkAWmmcEuNm)6m3{W9stN#mLLkWa9 znFO*p_-J=zO1lBkLMV-#QSuGWW+PVaqa8y=Qb(TK0t?^H?(O#v)Bo6uQvPL;{MCF~ z^eFci*&9#zLaID?;!;F@nMGm8GS~(4=o!v@e2|}GO`uZcrsT=+*Y&+_aolaxNs&qp zUN6K=r|IWlJM~^Zc#|@{)1PKhy-{=WfTza=WVKF?_I&q8q z*HW205KVs5?;>BMiL{lg&@%k?$W*eIh=&H!0v;qk6CpUp%z*K>hO#0iM{}11S)h@& zkdu=#xj~W)_q4pV$LFew_4*e%m8DuttE&x^B0?bsz9aV?%9dAA$b`mN#*~ss_xAz8 zBN*zNc4D#R5v<40NcVEB6b@I_a_ZeG#YVd&bVT0=mH=c=!nOXpTp1>ZS&^)~O_l5h zm%vEItLN?RnQ6Rl2?^j4cUzi3G8aTAn6{n;r0N7)yGgF~^qiZBS@7AeD{NG(uoqo6 zly~fCam~42w{d!XaH`MAFozg*t^ePpCTt8+Fuqm2qgUFV_7Ax8ebP{u54qg~vUbe=*XgWK^>4|Zg=SzL0-0G87 zX0%_|$yn>i?tS&Hagjnn@-q;TKJKk7SZ1F0&YY)TSn_@9;feT_tI8_J{psBLoSrsg z)lo31yci1VCIoryd>3op+8kj@Oojy9X2P_Yf>=&R!V9f(*c+$QmmnMs{-DQ=Tq$Oj zfOGG=)wr^MxDZubS~|Ey)hRkSB3<;_G+y{xZx$XfD}%?roD4Vs0_w?in3UnYf3OGP zw37b%4T`7UPs!)wDgDAlPS+USh&XUkS1QX5C>2ZdXW0|M); z#dKW>mU;6LIj^^a{qe$5t=bg-z5L8N8y{0w7$DiEy!bbGW-_Ss+}G5s%4n}38dyJN z2*$g2YeSgA6o^0#Vt*4}eHqtyy8do#=4l6-eJgu)=d(Ci5c25?;5?`U2B^jR8xnmQ z19c+rdh8q9;$F!lryL>x*Mt4#R0+-Xcs%Y77d_?m!omsvY>W4&jXr=;7i#wZCQMQE z&-N-yQBl$S*N-+dkBtAhj+;GFz#JW-WK|Dz-KPP)WrS{dv8l`dz2rqtqZ6`*IFd=teYAiV3CM#fT-H%)o7>cQ1@l zO-QqCDNu>}zb~{pH-rnl3iMQ%5==_J#Ol3E)6qxS6#oFLfQW$5F*hJSata6tWE(eo zu5C4Mdo}`$qvA^i}n3-tg()u7#O~ zTKw`!irkxUIf5sZ$-KAPhRp09!;H#L^1wGiasRAE3Gd8TqWXs-+5J~q(`i~0$uHW42JUsA?U|O-Jee$Y#%^FcoHL< zfX)k1NI~I)%|}|g`J1s3NZ)F{q99B8=qH8!N&+hnZeTKO(A8;1%!liu!@C;QEEKx; zn|?+(4k*ix{TMaNA&ZIZ?P3Oc8Ho8DlgPEo1T#jo5ysEzcBP{8Tpc0gczrpxo8_r; zV{E<%^%w(M75NlN$$rd|cDvXSGR{ms(Dh2G4~2HrHcYMUv;>3O;s-hY$CeEJ@lO=F z8pfyX{nGKl>xfK6m2O9~kw5(vR4F2jtgw=RVjVX_1RW;iJUK$WEhI(Lr?$ewTg&p~ zqDg+TN3uDLa+FuXW0cP;0%G5P7Janzsr3{fexV5u3K#+9YKq&;$=TVnJQug!*7m_c zR^XZrmn6Y?+N+@7`IH_`{=(Qqx=vcFa}=TYRKdgZq2)cYmy_<=T5Kw}5^vjbU-w~4 zJx>7NX4LOB@VEYxKoLroem>u)ICLqA)Z6{5sAg4HRbBt5a}{Yjlw^FlW#;D78}cVP zG+Ob^_O2u z&u1Za*zv5APVtcN%@@rRwS)HXil)ss7d6OVe^WE|OubIsQY#$lkXU6Qr;0+?6OY5*;6<%j1!9Q^q|QnR zpjg}3z@ql4EYRbnMTLcL3P&p$D>tI*3d<*WgpBeu6RrZA6_9|t|MFn_nY+UyF*O%z zhcau5oby*m|KnzJt{Z}ZYWJ^bLk0g$qWa>X6c+Ea`0H0TkU?1nbc@FiDp;6B{+a@M z)%tr&6J0iW>bc~HRU8@Pc)xF^J2cFEV zNUy+7f#$Q8GiIO#{8#bgR{W+5TF-w;uVtdK!Wzg_+`X+ONaz!%4zF06+kjQwNCWbh zzzakv)AJghmYz2kExwv~TtV4JNx1!QB?Y7~)k||X&a+<(N#X`|Cx%()*Vd^jX1VUD z)P{e_CVp$}2UERA`j05s$@cm%smJ=aD0md#T!bAhU@@|5*%P+ugO-04tb-J?m+nc@P;CJnS59Fj z+xX$*@s=qs=jUge32k?WMW^4JytatS_~Am*u+YX$65xQ+J3u;y0~fCTG)a8=i?gS} z0%JglEfnIsCnvX&9KG6dnjl?@_i#2X(3h9NM;*UGw2~Gt2l&gH-i_c*L(`u{9dD<# z<x=;#(`_IBexYro<|=hRTvqGthwOL3GzUJ0RZMo}3n~2%mxR#6tr#A> z4ZW6x6Qhke9wd;r&ZE4J9l$?#c= z|3P)eTD+li@8oNM=`BqZ*8L zPDr6AXoEhJZx>BZmrPqoj;6@SnkjFG{H+DAga%me|NVXvqA#Zb3w4wv5ql{zfEfW& NlvRCR_S7u!e*rjBFwFn} literal 0 HcmV?d00001 From 11f1ef783e0f3c255b14567cab1ec5cb239e474d Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Sun, 25 Aug 2024 17:43:13 +0200 Subject: [PATCH 6/7] Tmx reader is now a base abstract class --- src/DotTiled.Benchmark/Program.cs | 2 +- src/DotTiled.Tests/Assert/AssertMap.cs | 2 +- src/DotTiled.Tests/Assert/AssertProperties.cs | 10 + src/DotTiled.Tests/Assert/AssertTileset.cs | 6 +- .../map-with-custom-type-props.cs | 68 ++++ .../map-with-custom-type-props.tmj | 24 ++ .../map-with-custom-type-props.tmx | 4 + .../Serialization/Tmx/TmxMapReaderTests.cs | 10 +- src/DotTiled/Model/Properties/EnumProperty.cs | 50 +++ src/DotTiled/Model/Properties/PropertyType.cs | 7 +- src/DotTiled/Serialization/Tmx/Tmx.Map.cs | 107 ------ .../Serialization/Tmx/Tmx.Properties.cs | 67 ---- .../Serialization/Tmx/Tmx.TileLayer.cs | 157 -------- src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs | 350 ------------------ .../Serialization/Tmx/TmxMapReader.cs | 65 +--- .../{Tmx.Chunk.cs => TmxReaderBase.Chunk.cs} | 17 +- .../{Tmx.Data.cs => TmxReaderBase.Data.cs} | 16 +- .../Serialization/Tmx/TmxReaderBase.Map.cs | 104 ++++++ ...tLayer.cs => TmxReaderBase.ObjectLayer.cs} | 147 ++++---- .../Tmx/TmxReaderBase.Properties.cs | 140 +++++++ .../Tmx/TmxReaderBase.TileLayer.cs | 147 ++++++++ .../Tmx/TmxReaderBase.Tileset.cs | 317 ++++++++++++++++ .../Serialization/Tmx/TmxReaderBase.cs | 74 ++++ .../Serialization/Tmx/TsxTilesetReader.cs | 63 +--- .../Serialization/Tmx/TxTemplateReader.cs | 65 +--- 25 files changed, 1059 insertions(+), 960 deletions(-) create mode 100644 src/DotTiled/Model/Properties/EnumProperty.cs delete mode 100644 src/DotTiled/Serialization/Tmx/Tmx.Map.cs delete mode 100644 src/DotTiled/Serialization/Tmx/Tmx.Properties.cs delete mode 100644 src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs delete mode 100644 src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs rename src/DotTiled/Serialization/Tmx/{Tmx.Chunk.cs => TmxReaderBase.Chunk.cs} (59%) rename src/DotTiled/Serialization/Tmx/{Tmx.Data.cs => TmxReaderBase.Data.cs} (88%) create mode 100644 src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs rename src/DotTiled/Serialization/Tmx/{Tmx.ObjectLayer.cs => TmxReaderBase.ObjectLayer.cs} (58%) create mode 100644 src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs create mode 100644 src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs create mode 100644 src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs create mode 100644 src/DotTiled/Serialization/Tmx/TmxReaderBase.cs diff --git a/src/DotTiled.Benchmark/Program.cs b/src/DotTiled.Benchmark/Program.cs index bf67fd0..3cd6e15 100644 --- a/src/DotTiled.Benchmark/Program.cs +++ b/src/DotTiled.Benchmark/Program.cs @@ -39,7 +39,7 @@ namespace DotTiled.Benchmark { using var stringReader = new StringReader(_tmxContents); using var xmlReader = XmlReader.Create(stringReader); - using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), []); + using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException()); return mapReader.ReadMap(); } diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index 6984b79..358029b 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -91,7 +91,7 @@ public static partial class DotTiledAssert AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); Assert.NotNull(actual.Tilesets); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); diff --git a/src/DotTiled.Tests/Assert/AssertProperties.cs b/src/DotTiled.Tests/Assert/AssertProperties.cs index 21fa639..84af365 100644 --- a/src/DotTiled.Tests/Assert/AssertProperties.cs +++ b/src/DotTiled.Tests/Assert/AssertProperties.cs @@ -45,4 +45,14 @@ public static partial class DotTiledAssert AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType"); AssertProperties(expected.Value, actual.Value); } + + private static void AssertProperty(EnumProperty expected, EnumProperty actual) + { + AssertEqual(expected.PropertyType, actual.PropertyType, "EnumProperty.PropertyType"); + AssertEqual(expected.Value.Count, actual.Value.Count, "EnumProperty.Value.Count"); + foreach (var value in expected.Value) + { + Assert.Contains(actual.Value, v => v == value); + } + } } diff --git a/src/DotTiled.Tests/Assert/AssertTileset.cs b/src/DotTiled.Tests/Assert/AssertTileset.cs index 134cc30..befc79a 100644 --- a/src/DotTiled.Tests/Assert/AssertTileset.cs +++ b/src/DotTiled.Tests/Assert/AssertTileset.cs @@ -141,9 +141,9 @@ public static partial class DotTiledAssert AssertEqual(expected.Height, actual.Height, nameof(Tile.Height)); // Elements - AssertProperties(actual.Properties, expected.Properties); - AssertImage(actual.Image, expected.Image); - AssertLayer((BaseLayer?)actual.ObjectLayer, (BaseLayer?)expected.ObjectLayer); + AssertProperties(expected.Properties, actual.Properties); + AssertImage(expected.Image, actual.Image); + AssertLayer((BaseLayer?)expected.ObjectLayer, (BaseLayer?)actual.ObjectLayer); if (expected.Animation is not null) { Assert.NotNull(actual.Animation); diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs index ea0575e..9a965c2 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -69,6 +69,30 @@ public partial class TestData new ObjectProperty { Name = "objectinclass", Value = 0 }, new StringProperty { Name = "stringinclass", Value = "This is a set string" } ] + }, + new EnumProperty + { + Name = "customenumstringprop", + PropertyType = "CustomEnumString", + Value = new HashSet { "CustomEnumString_2" } + }, + new EnumProperty + { + Name = "customenumstringflagsprop", + PropertyType = "CustomEnumStringFlags", + Value = new HashSet { "CustomEnumStringFlags_1", "CustomEnumStringFlags_2" } + }, + new EnumProperty + { + Name = "customenumintprop", + PropertyType = "CustomEnumInt", + Value = new HashSet { "CustomEnumInt_4" } + }, + new EnumProperty + { + Name = "customenumintflagsprop", + PropertyType = "CustomEnumIntFlags", + Value = new HashSet { "CustomEnumIntFlags_2", "CustomEnumIntFlags_3" } } ] }; @@ -116,6 +140,50 @@ public partial class TestData Value = "" } ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumString", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = false, + Values = [ + "CustomEnumString_1", + "CustomEnumString_2", + "CustomEnumString_3" + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumStringFlags", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = true, + Values = [ + "CustomEnumStringFlags_1", + "CustomEnumStringFlags_2" + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumInt", + StorageType = CustomEnumStorageType.Int, + ValueAsFlags = false, + Values = [ + "CustomEnumInt_1", + "CustomEnumInt_2", + "CustomEnumInt_3", + "CustomEnumInt_4", + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumIntFlags", + StorageType = CustomEnumStorageType.Int, + ValueAsFlags = true, + Values = [ + "CustomEnumIntFlags_1", + "CustomEnumIntFlags_2", + "CustomEnumIntFlags_3" + ] } ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj index a8c7f43..74f892b 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj @@ -32,6 +32,30 @@ "floatinclass":13.37, "stringinclass":"This is a set string" } + }, + { + "name":"customenumintflagsprop", + "propertytype":"CustomEnumIntFlags", + "type":"int", + "value":6 + }, + { + "name":"customenumintprop", + "propertytype":"CustomEnumInt", + "type":"int", + "value":3 + }, + { + "name":"customenumstringflagsprop", + "propertytype":"CustomEnumStringFlags", + "type":"string", + "value":"CustomEnumStringFlags_1,CustomEnumStringFlags_2" + }, + { + "name":"customenumstringprop", + "propertytype":"CustomEnumString", + "type":"string", + "value":"CustomEnumString_2" }], "renderorder":"right-down", "tiledversion":"1.11.0", diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx index c364577..cadc2fa 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx @@ -8,6 +8,10 @@ + + + + diff --git a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index fb825ed..748d4e3 100644 --- a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -20,16 +20,20 @@ public partial class TmxMapReaderTests Template ResolveTemplate(string source) { using var xmlTemplateReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); - using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return templateReader.ReadTemplate(); } Tileset ResolveTileset(string source) { using var xmlTilesetReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); - using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType); // Act var map = mapReader.ReadMap(); diff --git a/src/DotTiled/Model/Properties/EnumProperty.cs b/src/DotTiled/Model/Properties/EnumProperty.cs new file mode 100644 index 0000000..19e1b1f --- /dev/null +++ b/src/DotTiled/Model/Properties/EnumProperty.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotTiled.Model; + +/// +/// Represents an enum property. +/// +public class EnumProperty : IProperty> +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => Model.PropertyType.Enum; + + /// + /// The type of the class property. This will be the name of a custom defined + /// type in Tiled. + /// + public required string PropertyType { get; set; } + + /// + /// The value of the enum property. + /// + public required ISet Value { get; set; } + + /// + public IProperty Clone() => new EnumProperty + { + Name = Name, + PropertyType = PropertyType, + Value = Value.ToHashSet() + }; + + /// + /// Determines whether the enum property is equal to the specified value. + /// For enums which have multiple values (e.g. flag enums), this method will only return true if it is the only value. + /// + /// The value to check. + /// True if the enum property is equal to the specified value; otherwise, false. + public bool IsValue(string value) => Value.Contains(value) && Value.Count == 1; + + /// + /// Determines whether the enum property has the specified value. This method is very similar to the common method. + /// + /// The value to check. + /// True if the enum property has the specified value as one of its values; otherwise, false. + public bool HasValue(string value) => Value.Contains(value); +} diff --git a/src/DotTiled/Model/Properties/PropertyType.cs b/src/DotTiled/Model/Properties/PropertyType.cs index d6057cc..451ca5e 100644 --- a/src/DotTiled/Model/Properties/PropertyType.cs +++ b/src/DotTiled/Model/Properties/PropertyType.cs @@ -43,5 +43,10 @@ public enum PropertyType /// /// A class property. /// - Class + Class, + + /// + /// An enum property. + /// + Enum } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs deleted file mode 100644 index 4ce03a4..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Map ReadMap( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // 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 InvalidOperationException($"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 InvalidOperationException($"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 InvalidOperationException($"Unknown stagger axis '{s}'") - }); - var staggerIndex = reader.GetOptionalAttributeEnum("staggerindex", s => s switch - { - "odd" => StaggerIndex.Odd, - "even" => StaggerIndex.Even, - _ => throw new InvalidOperationException($"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 - List? properties = null; - - // Any number of - List layers = []; - List tilesets = []; - - reader.ProcessChildren("map", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), - "layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), - "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)), - "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)), - _ => 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/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs deleted file mode 100644 index 2e288a5..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static List ReadProperties( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - 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, customTypeDefinitions), - _ => throw new XmlException("Invalid property type") - }; - return property; - }); - } - - internal static ClassProperty ReadClassProperty( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - var name = reader.GetRequiredAttribute("name"); - var propertyType = reader.GetRequiredAttribute("propertytype"); - - var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - if (customTypeDef is CustomClassDefinition ccd) - { - reader.ReadStartElement("property"); - var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); - var props = ReadProperties(reader, customTypeDefinitions); - var mergedProps = Helpers.MergeProperties(propsInType, props); - - reader.ReadEndElement(); - return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; - } - - throw new XmlException($"Unkonwn custom class definition: {propertyType}"); - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs deleted file mode 100644 index 8605ade..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static TileLayer ReadTileLayer( - XmlReader reader, - bool dataUsesChunks, - IReadOnlyCollection customTypeDefinitions) - { - 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; - - List? 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, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new TileLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Width = width, - Height = height, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Data = data, - Properties = properties ?? [] - }; - } - - internal static ImageLayer ReadImageLayer( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - 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.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; - var repeatY = (reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; - - List? 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, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new ImageLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Properties = properties ?? [], - Image = image, - RepeatX = repeatX, - RepeatY = repeatY - }; - } - - internal static Group ReadGroup( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - - List? properties = null; - List layers = []; - - reader.ProcessChildren("group", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "layer" => () => layers.Add(ReadTileLayer(r, false, customTypeDefinitions)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), - "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)), - "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - return new Group - { - ID = id, - Name = name, - Class = @class, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Properties = properties ?? [], - Layers = layers - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs deleted file mode 100644 index 84ccd24..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Tileset ReadTileset( - XmlReader reader, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // 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") ?? 0; - var margin = reader.GetOptionalAttributeParseable("margin") ?? 0; - 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 InvalidOperationException($"Unknown object alignment '{s}'") - }) ?? ObjectAlignment.Unspecified; - var renderSize = reader.GetOptionalAttributeEnum("rendersize", s => s switch - { - "tile" => TileRenderSize.Tile, - "grid" => TileRenderSize.Grid, - _ => throw new InvalidOperationException($"Unknown render size '{s}'") - }) ?? TileRenderSize.Tile; - var fillMode = reader.GetOptionalAttributeEnum("fillmode", s => s switch - { - "stretch" => FillMode.Stretch, - "preserve-aspect-fit" => FillMode.PreserveAspectFit, - _ => throw new InvalidOperationException($"Unknown fill mode '{s}'") - }) ?? FillMode.Stretch; - - // Elements - Image? image = null; - TileOffset? tileOffset = null; - Grid? grid = null; - List? 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, customTypeDefinitions), "Properties"), - "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r, customTypeDefinitions), "Wangsets"), - "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"), - "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - // Check if tileset is referring to external file - if (source is not null) - { - if (externalTilesetResolver is null) - throw new InvalidOperationException("External tileset resolver is required to resolve external tilesets."); - - var resolvedTileset = externalTilesetResolver(source); - resolvedTileset.FirstGID = firstGID; - resolvedTileset.Source = source; - return resolvedTileset; - } - - return new Tileset - { - Version = version, - TiledVersion = tiledVersion, - FirstGID = firstGID, - Source = source, - Name = name, - Class = @class, - TileWidth = tileWidth, - TileHeight = tileHeight, - Spacing = spacing, - Margin = margin, - TileCount = tileCount, - Columns = columns, - ObjectAlignment = objectAlignment, - RenderSize = renderSize, - FillMode = fillMode, - Image = image, - TileOffset = tileOffset, - Grid = grid, - Properties = properties ?? [], - Wangsets = wangsets, - Transformations = transformations, - Tiles = tiles - }; - } - - internal static Image ReadImage(XmlReader reader) - { - // Attributes - var format = reader.GetOptionalAttributeEnum("format", s => s switch - { - "png" => ImageFormat.Png, - "jpg" => ImageFormat.Jpg, - "bmp" => ImageFormat.Bmp, - "gif" => ImageFormat.Gif, - _ => throw new InvalidOperationException($"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 - }); - - if (format is null && source is not null) - format = ParseImageFormatFromSource(source); - - return new Image - { - Format = format, - Source = source, - TransparentColor = transparentColor, - Width = width, - Height = height, - }; - } - - - private static ImageFormat ParseImageFormatFromSource(string source) - { - var extension = Path.GetExtension(source).ToLowerInvariant(); - return extension switch - { - ".png" => ImageFormat.Png, - ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, - ".jpeg" => ImageFormat.Jpg, - ".bmp" => ImageFormat.Bmp, - _ => throw new XmlException($"Unsupported image format '{extension}'") - }; - } - - internal static TileOffset ReadTileOffset(XmlReader reader) - { - // Attributes - var x = reader.GetOptionalAttributeParseable("x") ?? 0f; - var y = reader.GetOptionalAttributeParseable("y") ?? 0f; - - reader.ReadStartElement("tileoffset"); - return new TileOffset { X = x, Y = y }; - } - - internal static Grid ReadGrid(XmlReader reader) - { - // Attributes - var orientation = reader.GetOptionalAttributeEnum("orientation", s => s switch - { - "orthogonal" => GridOrientation.Orthogonal, - "isometric" => GridOrientation.Isometric, - _ => throw new InvalidOperationException($"Unknown orientation '{s}'") - }) ?? GridOrientation.Orthogonal; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - - reader.ReadStartElement("grid"); - return new Grid { Orientation = orientation, Width = width, Height = height }; - } - - internal static Transformations ReadTransformations(XmlReader reader) - { - // Attributes - var hFlip = (reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; - var vFlip = (reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; - var rotate = (reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; - var preferUntransformed = (reader.GetOptionalAttributeParseable("preferuntransformed") ?? 0) == 1; - - reader.ReadStartElement("transformations"); - return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; - } - - internal static Tile ReadTile( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // 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 - List? 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, customTypeDefinitions), "Properties"), - "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions), "ObjectLayer"), - "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => - { - var tileID = ar.GetRequiredAttributeParseable("tileid"); - var duration = ar.GetRequiredAttributeParseable("duration"); - return new Frame { TileID = tileID, Duration = duration }; - }), "Animation"), - _ => r.Skip - }); - - return new Tile - { - ID = id, - Type = type, - Probability = probability, - X = x, - Y = y, - Width = width ?? image?.Width ?? 0, - Height = height ?? image?.Height ?? 0, - Properties = properties ?? [], - Image = image, - ObjectLayer = objectLayer, - Animation = animation - }; - } - - internal static List ReadWangsets( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) => - reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); - - internal static Wangset ReadWangset( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var name = reader.GetRequiredAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tile = reader.GetRequiredAttributeParseable("tile"); - - // Elements - List? properties = null; - List wangColors = []; - List wangTiles = []; - - reader.ProcessChildren("wangset", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)), - "wangtile" => () => wangTiles.Add(ReadWangTile(r)), - _ => r.Skip - }); - - if (wangColors.Count > 254) - throw new ArgumentException("Wangset can have at most 254 Wang colors."); - - return new Wangset - { - Name = name, - Class = @class, - Tile = tile, - Properties = properties ?? [], - WangColors = wangColors, - WangTiles = wangTiles - }; - } - - internal static WangColor ReadWangColor( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - // 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 - List? properties = null; - - reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new WangColor - { - Name = name, - Class = @class, - Color = color, - Tile = tile, - Probability = probability, - Properties = properties ?? [] - }; - } - - internal static WangTile ReadWangTile(XmlReader reader) - { - // Attributes - var tileID = reader.GetRequiredAttributeParseable("tileid"); - var wangID = reader.GetRequiredAttributeParseable("wangid", s => - { - // Comma-separated list of indices (0-254) - var indices = s.Split(',').Select(i => byte.Parse(i, CultureInfo.InvariantCulture)).ToArray(); - if (indices.Length > 8) - throw new ArgumentException("Wang ID can have at most 8 indices."); - return indices; - }); - - reader.ReadStartElement("wangtile"); - - return new WangTile - { - TileID = tileID, - WangID = wangID - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs index 279c83d..e90caa1 100644 --- a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A map reader for the Tiled XML format. /// -public class TmxMapReader : IMapReader +public class TmxMapReader : TmxReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled map in the Tiled XML format. - /// A function that resolves external tilesets given their source. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TmxMapReader( XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _reader = reader ?? throw new ArgumentNullException(nameof(reader)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - - // Prepare reader - _ = _reader.MoveToContent(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Map ReadMap() => Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - - /// - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - _reader.Dispose(); - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TmxTiledMapReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public new Map ReadMap() => base.ReadMap(); } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs similarity index 59% rename from src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs index b42ebb0..73c8052 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs @@ -1,27 +1,26 @@ -using System.Xml; using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression) + internal Chunk ReadChunk(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 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 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 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/src/DotTiled/Serialization/Tmx/Tmx.Data.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs similarity index 88% rename from src/DotTiled/Serialization/Tmx/Tmx.Data.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs index e1b6111..254c1d3 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs @@ -8,17 +8,17 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static Data ReadData(XmlReader reader, bool usesChunks) + internal Data ReadData(bool usesChunks) { - var encoding = reader.GetOptionalAttributeEnum("encoding", e => e switch + 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 + var compression = _reader.GetOptionalAttributeEnum("compression", c => c switch { "gzip" => DataCompression.GZip, "zlib" => DataCompression.ZLib, @@ -28,8 +28,8 @@ internal partial class Tmx if (usesChunks) { - var chunks = reader - .ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression)) + var chunks = _reader + .ReadList("data", "chunk", (r) => ReadChunk(encoding, compression)) .ToArray(); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks }; } @@ -37,12 +37,12 @@ internal partial class Tmx var usesTileChildrenInsteadOfRawData = encoding is null && compression is null; if (usesTileChildrenInsteadOfRawData) { - var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", reader); + 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 rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression); var (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null }; } diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs new file mode 100644 index 0000000..89f0c9b --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase +{ + internal Map ReadMap() + { + // 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 InvalidOperationException($"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 InvalidOperationException($"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 InvalidOperationException($"Unknown stagger axis '{s}'") + }); + var staggerIndex = _reader.GetOptionalAttributeEnum("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new InvalidOperationException($"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 + List? properties = null; + + // Any number of + List layers = []; + List tilesets = []; + + _reader.ProcessChildren("map", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "tileset" => () => tilesets.Add(ReadTileset()), + "layer" => () => layers.Add(ReadTileLayer(infinite)), + "objectgroup" => () => layers.Add(ReadObjectLayer()), + "imagelayer" => () => layers.Add(ReadImageLayer()), + "group" => () => layers.Add(ReadGroup()), + _ => 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/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs similarity index 58% rename from src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs index 2ce3ca3..a680544 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs @@ -3,35 +3,31 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; -using System.Xml; using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static ObjectLayer ReadObjectLayer( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal ObjectLayer ReadObjectLayer() { // 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 + 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, @@ -42,10 +38,10 @@ internal partial class Tmx List? properties = null; List objects = []; - reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch + _reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "object" => () => objects.Add(ReadObject()), _ => r.Skip }); @@ -72,16 +68,13 @@ internal partial class Tmx }; } - internal static Model.Object ReadObject( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Model.Object ReadObject() { // Attributes - var template = reader.GetOptionalAttribute("template"); + var template = _reader.GetOptionalAttribute("template"); Model.Object? obj = null; if (template is not null) - obj = externalTemplateResolver(template).Object; + obj = _externalTemplateResolver(template).Object; uint? idDefault = obj?.ID ?? null; string nameDefault = obj?.Name ?? ""; @@ -95,30 +88,30 @@ internal partial class Tmx bool visibleDefault = obj?.Visible ?? true; List? propertiesDefault = obj?.Properties ?? null; - 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; + 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 Model.Object? foundObject = null; int propertiesCounter = 0; List? properties = propertiesDefault; - reader.ProcessChildren("object", (r, elementName) => elementName switch + _reader.ProcessChildren("object", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)).ToList(), "Properties", ref propertiesCounter), - "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(r), "Object marker"), - "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(r), "Object marker"), - "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(r), "Object marker"), - "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(r), "Object marker"), - "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(r), "Object marker"), + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties()).ToList(), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(), "Object marker"), _ => throw new InvalidOperationException($"Unknown object marker '{elementName}'") }); @@ -169,26 +162,26 @@ internal partial class Tmx return OverrideObject((dynamic)obj, (dynamic)foundObject); } - internal static EllipseObject ReadEllipseObject(XmlReader reader) + internal EllipseObject ReadEllipseObject() { - reader.Skip(); + _reader.Skip(); return new EllipseObject { }; } internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject _) => obj; - internal static PointObject ReadPointObject(XmlReader reader) + internal PointObject ReadPointObject() { - reader.Skip(); + _reader.Skip(); return new PointObject { }; } internal static PointObject OverrideObject(PointObject obj, PointObject _) => obj; - internal static PolygonObject ReadPolygonObject(XmlReader reader) + internal PolygonObject ReadPolygonObject() { // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => + var points = _reader.GetRequiredAttributeParseable>("points", s => { // Takes on format "x1,y1 x2,y2 x3,y3 ..." var coords = s.Split(' '); @@ -199,7 +192,7 @@ internal partial class Tmx }).ToList(); }); - reader.ReadStartElement("polygon"); + _reader.ReadStartElement("polygon"); return new PolygonObject { Points = points }; } @@ -209,10 +202,10 @@ internal partial class Tmx return obj; } - internal static PolylineObject ReadPolylineObject(XmlReader reader) + internal PolylineObject ReadPolylineObject() { // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => + var points = _reader.GetRequiredAttributeParseable>("points", s => { // Takes on format "x1,y1 x2,y2 x3,y3 ..." var coords = s.Split(' '); @@ -223,7 +216,7 @@ internal partial class Tmx }).ToList(); }); - reader.ReadStartElement("polyline"); + _reader.ReadStartElement("polyline"); return new PolylineObject { Points = points }; } @@ -233,19 +226,19 @@ internal partial class Tmx return obj; } - internal static TextObject ReadTextObject(XmlReader reader) + internal TextObject ReadTextObject() { // 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 + 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, @@ -253,7 +246,7 @@ internal partial class Tmx "justify" => TextHorizontalAlignment.Justify, _ => throw new InvalidOperationException($"Unknown horizontal alignment '{s}'") }) ?? TextHorizontalAlignment.Left; - var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch + var vAlign = _reader.GetOptionalAttributeEnum("valign", s => s switch { "top" => TextVerticalAlignment.Top, "center" => TextVerticalAlignment.Center, @@ -262,7 +255,7 @@ internal partial class Tmx }) ?? TextVerticalAlignment.Top; // Elements - var text = reader.ReadElementContentAsString("text", ""); + var text = _reader.ReadElementContentAsString("text", ""); return new TextObject { @@ -304,11 +297,7 @@ internal partial class Tmx return obj; } - internal static Template ReadTemplate( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Template ReadTemplate() { // No attributes @@ -318,10 +307,10 @@ internal partial class Tmx // Should contain exactly one of Model.Object? obj = null; - reader.ProcessChildren("template", (r, elementName) => elementName switch + _reader.ProcessChildren("template", (r, elementName) => elementName switch { - "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), "Tileset"), - "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"), + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(), "Object"), _ => r.Skip }); diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs new file mode 100644 index 0000000..1335d75 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal List ReadProperties() + { + 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; + var propertyType = r.GetOptionalAttribute("propertytype"); + if (propertyType is not null) + { + return ReadPropertyWithCustomType(); + } + + 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(), + _ => throw new XmlException("Invalid property type") + }; + return property; + }); + } + + internal IProperty ReadPropertyWithCustomType() + { + var isClass = _reader.GetOptionalAttribute("type") == "class"; + + if (isClass) + { + return ReadClassProperty(); + } + + return ReadEnumProperty(); + } + + internal ClassProperty ReadClassProperty() + { + var name = _reader.GetRequiredAttribute("name"); + var propertyType = _reader.GetRequiredAttribute("propertytype"); + + var customTypeDef = _customTypeResolver(propertyType); + if (customTypeDef is CustomClassDefinition ccd) + { + _reader.ReadStartElement("property"); + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); + var props = ReadProperties(); + var mergedProps = Helpers.MergeProperties(propsInType, props); + + _reader.ReadEndElement(); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + } + + throw new XmlException($"Unkonwn custom class definition: {propertyType}"); + } + + internal EnumProperty ReadEnumProperty() + { + var name = _reader.GetRequiredAttribute("name"); + var propertyType = _reader.GetRequiredAttribute("propertytype"); + var typeInXml = _reader.GetOptionalAttributeEnum("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + _ => throw new XmlException("Invalid property type") + }) ?? PropertyType.String; + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is not CustomEnumDefinition ced) + throw new XmlException($"Unknown custom enum definition: {propertyType}. Enums must be defined"); + + if (ced.StorageType == CustomEnumStorageType.String) + { + var value = _reader.GetRequiredAttribute("value"); + if (value.Contains(',') && !ced.ValueAsFlags) + throw new XmlException("Enum value must not contain ',' if not ValueAsFlags is set to true."); + + if (ced.ValueAsFlags) + { + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = _reader.GetRequiredAttributeParseable("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + for (var i = 0; i < allValues.Count; i++) + { + var mask = 1 << i; + if ((value & mask) == mask) + { + var enumValue = allValues[i]; + _ = enumValues.Add(enumValue); + } + } + return new EnumProperty { Name = name, PropertyType = propertyType, Value = enumValues }; + } + else + { + var allValues = ced.Values; + var enumValue = allValues[value]; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { enumValue } }; + } + } + + throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}"); + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs new file mode 100644 index 0000000..f69a739 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.Linq; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal TileLayer ReadTileLayer(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; + + List? properties = null; + Data? data = null; + + _reader.ProcessChildren("layer", (r, elementName) => elementName switch + { + "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(dataUsesChunks), "Data"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new TileLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Width = width, + Height = height, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Data = data, + Properties = properties ?? [] + }; + } + + internal ImageLayer ReadImageLayer() + { + 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.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; + var repeatY = (_reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; + + List? properties = null; + Image? image = null; + + _reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new ImageLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties ?? [], + Image = image, + RepeatX = repeatX, + RepeatY = repeatY + }; + } + + internal Group ReadGroup() + { + var id = _reader.GetRequiredAttributeParseable("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + List? properties = null; + List layers = []; + + _reader.ProcessChildren("group", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "layer" => () => layers.Add(ReadTileLayer(false)), + "objectgroup" => () => layers.Add(ReadObjectLayer()), + "imagelayer" => () => layers.Add(ReadImageLayer()), + "group" => () => layers.Add(ReadGroup()), + _ => r.Skip + }); + + return new Group + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties ?? [], + Layers = layers + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs new file mode 100644 index 0000000..72e66e9 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal Tileset ReadTileset() + { + // 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") ?? 0; + var margin = _reader.GetOptionalAttributeParseable("margin") ?? 0; + 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 InvalidOperationException($"Unknown object alignment '{s}'") + }) ?? ObjectAlignment.Unspecified; + var renderSize = _reader.GetOptionalAttributeEnum("rendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new InvalidOperationException($"Unknown render size '{s}'") + }) ?? TileRenderSize.Tile; + var fillMode = _reader.GetOptionalAttributeEnum("fillmode", s => s switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new InvalidOperationException($"Unknown fill mode '{s}'") + }) ?? FillMode.Stretch; + + // Elements + Image? image = null; + TileOffset? tileOffset = null; + Grid? grid = null; + List? properties = null; + List? wangsets = null; + Transformations? transformations = null; + List tiles = []; + + _reader.ProcessChildren("tileset", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(), "TileOffset"), + "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(), "Grid"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(), "Wangsets"), + "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(), "Transformations"), + "tile" => () => tiles.Add(ReadTile()), + _ => r.Skip + }); + + // Check if tileset is referring to external file + if (source is not null) + { + var resolvedTileset = _externalTilesetResolver(source); + resolvedTileset.FirstGID = firstGID; + resolvedTileset.Source = source; + return resolvedTileset; + } + + return new Tileset + { + Version = version, + TiledVersion = tiledVersion, + FirstGID = firstGID, + Source = source, + Name = name, + Class = @class, + TileWidth = tileWidth, + TileHeight = tileHeight, + Spacing = spacing, + Margin = margin, + TileCount = tileCount, + Columns = columns, + ObjectAlignment = objectAlignment, + RenderSize = renderSize, + FillMode = fillMode, + Image = image, + TileOffset = tileOffset, + Grid = grid, + Properties = properties ?? [], + Wangsets = wangsets, + Transformations = transformations, + Tiles = tiles + }; + } + + internal Image ReadImage() + { + // Attributes + var format = _reader.GetOptionalAttributeEnum("format", s => s switch + { + "png" => ImageFormat.Png, + "jpg" => ImageFormat.Jpg, + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, + _ => throw new InvalidOperationException($"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 + }); + + if (format is null && source is not null) + format = Helpers.ParseImageFormatFromSource(source); + + return new Image + { + Format = format, + Source = source, + TransparentColor = transparentColor, + Width = width, + Height = height, + }; + } + + internal TileOffset ReadTileOffset() + { + // Attributes + var x = _reader.GetOptionalAttributeParseable("x") ?? 0f; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0f; + + _reader.ReadStartElement("tileoffset"); + return new TileOffset { X = x, Y = y }; + } + + internal Grid ReadGrid() + { + // Attributes + var orientation = _reader.GetOptionalAttributeEnum("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new InvalidOperationException($"Unknown orientation '{s}'") + }) ?? GridOrientation.Orthogonal; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + + _reader.ReadStartElement("grid"); + return new Grid { Orientation = orientation, Width = width, Height = height }; + } + + internal Transformations ReadTransformations() + { + // Attributes + var hFlip = (_reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; + var vFlip = (_reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; + var rotate = (_reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; + var preferUntransformed = (_reader.GetOptionalAttributeParseable("preferuntransformed") ?? 0) == 1; + + _reader.ReadStartElement("transformations"); + return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; + } + + internal Tile ReadTile() + { + // 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 + List? properties = null; + Image? image = null; + ObjectLayer? objectLayer = null; + List? animation = null; + + _reader.ProcessChildren("tile", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(), "ObjectLayer"), + "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => + { + var tileID = ar.GetRequiredAttributeParseable("tileid"); + var duration = ar.GetRequiredAttributeParseable("duration"); + return new Frame { TileID = tileID, Duration = duration }; + }), "Animation"), + _ => r.Skip + }); + + return new Tile + { + ID = id, + Type = type, + Probability = probability, + X = x, + Y = y, + Width = width ?? image?.Width ?? 0, + Height = height ?? image?.Height ?? 0, + Properties = properties ?? [], + Image = image, + ObjectLayer = objectLayer, + Animation = animation + }; + } + + internal List ReadWangsets() => + _reader.ReadList("wangsets", "wangset", r => ReadWangset()); + + internal Wangset ReadWangset() + { + // Attributes + var name = _reader.GetRequiredAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tile = _reader.GetRequiredAttributeParseable("tile"); + + // Elements + List? properties = null; + List wangColors = []; + List wangTiles = []; + + _reader.ProcessChildren("wangset", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "wangcolor" => () => wangColors.Add(ReadWangColor()), + "wangtile" => () => wangTiles.Add(ReadWangTile()), + _ => r.Skip + }); + + if (wangColors.Count > 254) + throw new ArgumentException("Wangset can have at most 254 Wang colors."); + + return new Wangset + { + Name = name, + Class = @class, + Tile = tile, + Properties = properties ?? [], + WangColors = wangColors, + WangTiles = wangTiles + }; + } + + internal WangColor ReadWangColor() + { + // 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 + List? properties = null; + + _reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new WangColor + { + Name = name, + Class = @class, + Color = color, + Tile = tile, + Probability = probability, + Properties = properties ?? [] + }; + } + + internal WangTile ReadWangTile() + { + // 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, CultureInfo.InvariantCulture)).ToArray(); + if (indices.Length > 8) + throw new ArgumentException("Wang ID can have at most 8 indices."); + return indices; + }); + + _reader.ReadStartElement("wangtile"); + + return new WangTile + { + TileID = tileID, + WangID = wangID + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs new file mode 100644 index 0000000..5851d76 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + /// + /// Constructs a new , which is the base class for all Tiled XML format readers. + /// + /// An XML reader for reading a Tiled map in the Tiled XML format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + protected TmxReaderBase( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + + // Prepare reader + _ = _reader.MoveToContent(); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _reader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TmxReaderBase() + // { + // // 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/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs index 0b69d4c..176872b 100644 --- a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,68 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A tileset reader for the Tiled XML format. /// -public class TsxTilesetReader : ITilesetReader +public class TsxTilesetReader : TmxReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled tileset in the Tiled XML format. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TsxTilesetReader( XmlReader reader, + Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _reader = reader ?? throw new ArgumentNullException(nameof(reader)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - - // Prepare reader - _ = _reader.MoveToContent(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions); - - /// - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - _reader.Dispose(); - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TsxTilesetReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public new Tileset ReadTileset() => base.ReadTileset(); } diff --git a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs index ed7ba8e..1ff8445 100644 --- a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A template reader for the Tiled XML format. /// -public class TxTemplateReader : ITemplateReader +public class TxTemplateReader : TmxReaderBase, ITemplateReader { - // Resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled template in the Tiled XML format. - /// A function that resolves external tilesets given their source. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TxTemplateReader( XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _reader = reader ?? throw new ArgumentNullException(nameof(reader)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - - // Prepare reader - _ = _reader.MoveToContent(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - - /// - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - _reader.Dispose(); - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TxTemplateReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public new Template ReadTemplate() => base.ReadTemplate(); } From ab8173bb06f4230a4e09aa672ea18419e04dd76c Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Mon, 26 Aug 2024 21:36:44 +0200 Subject: [PATCH 7/7] Tmj reader is now base class and new properties docs --- Makefile | 6 +- ...ing-properties.md => custom-properties.md} | 72 ++++++-- docs/docs/toc.yml | 2 +- docs/images/entity-type-enum.png | Bin 0 -> 11906 bytes src/DotTiled.Benchmark/Program.cs | 2 +- src/DotTiled.Tests/Serialization/TestData.cs | 1 + .../map-with-deep-props.cs | 161 ++++++++++++++++ .../map-with-deep-props.tmj | 55 ++++++ .../map-with-deep-props.tmx | 25 +++ .../Serialization/Tmj/TmjMapReaderTests.cs | 10 +- src/DotTiled/Serialization/Helpers.cs | 21 ++- .../Serialization/Tmj/TjTemplateReader.cs | 64 +------ src/DotTiled/Serialization/Tmj/Tmj.Layer.cs | 26 --- .../Serialization/Tmj/Tmj.Properties.cs | 103 ----------- .../Serialization/Tmj/Tmj.Template.cs | 26 --- .../Serialization/Tmj/TmjMapReader.cs | 63 +------ .../{Tmj.Data.cs => TmjReaderBase.Data.cs} | 2 +- .../{Tmj.Group.cs => TmjReaderBase.Group.cs} | 12 +- ...geLayer.cs => TmjReaderBase.ImageLayer.cs} | 9 +- .../Serialization/Tmj/TmjReaderBase.Layer.cs | 21 +++ .../Tmj/{Tmj.Map.cs => TmjReaderBase.Map.cs} | 15 +- ...tLayer.cs => TmjReaderBase.ObjectLayer.cs} | 21 +-- .../Tmj/TmjReaderBase.Properties.cs | 174 ++++++++++++++++++ .../Tmj/TmjReaderBase.Template.cs | 20 ++ ...ileLayer.cs => TmjReaderBase.TileLayer.cs} | 9 +- ...mj.Tileset.cs => TmjReaderBase.Tileset.cs} | 43 ++--- .../Serialization/Tmj/TmjReaderBase.cs | 74 ++++++++ .../Serialization/Tmj/TsjTilesetReader.cs | 68 +------ .../Tmx/TmxReaderBase.Properties.cs | 30 ++- 29 files changed, 702 insertions(+), 433 deletions(-) rename docs/docs/{accessing-properties.md => custom-properties.md} (59%) create mode 100644 docs/images/entity-type-enum.png create mode 100644 src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs create mode 100644 src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj create mode 100644 src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx delete mode 100644 src/DotTiled/Serialization/Tmj/Tmj.Layer.cs delete mode 100644 src/DotTiled/Serialization/Tmj/Tmj.Properties.cs delete mode 100644 src/DotTiled/Serialization/Tmj/Tmj.Template.cs rename src/DotTiled/Serialization/Tmj/{Tmj.Data.cs => TmjReaderBase.Data.cs} (98%) rename src/DotTiled/Serialization/Tmj/{Tmj.Group.cs => TmjReaderBase.Group.cs} (77%) rename src/DotTiled/Serialization/Tmj/{Tmj.ImageLayer.cs => TmjReaderBase.ImageLayer.cs} (88%) create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs rename src/DotTiled/Serialization/Tmj/{Tmj.Map.cs => TmjReaderBase.Map.cs} (88%) rename src/DotTiled/Serialization/Tmj/{Tmj.ObjectLayer.cs => TmjReaderBase.ObjectLayer.cs} (93%) create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs rename src/DotTiled/Serialization/Tmj/{Tmj.TileLayer.cs => TmjReaderBase.TileLayer.cs} (91%) rename src/DotTiled/Serialization/Tmj/{Tmj.Tileset.cs => TmjReaderBase.Tileset.cs} (86%) create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.cs diff --git a/Makefile b/Makefile index 9b64573..14a40ab 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,10 @@ lint: dotnet format style --verify-no-changes src/DotTiled.sln dotnet format analyzers --verify-no-changes src/DotTiled.sln -BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj -BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts +BENCHMARK_SOURCES = src/DotTiled.Benchmark/Program.cs src/DotTiled.Benchmark/DotTiled.Benchmark.csproj +BENCHMARK_OUTPUTDIR = src/DotTiled.Benchmark/BenchmarkDotNet.Artifacts .PHONY: benchmark benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES) - dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file + dotnet run --project src/DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file diff --git a/docs/docs/accessing-properties.md b/docs/docs/custom-properties.md similarity index 59% rename from docs/docs/accessing-properties.md rename to docs/docs/custom-properties.md index 59fcfea..78cbd00 100644 --- a/docs/docs/accessing-properties.md +++ b/docs/docs/custom-properties.md @@ -1,4 +1,4 @@ -# Accessing properties +# Custom properties [Tiled facilitates a very flexible way to store custom data in your maps using properties](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-properties). Accessing these properties is a common task when working with Tiled maps in your game since it will allow you to fully utilize the strengths of Tiled, such as customizing the behavior of your game objects or setting up the initial state of your game world. @@ -66,15 +66,15 @@ Tiled supports a variety of property types, which are represented in the DotTile - `object` - - `string` - -In addition to these primitive property types, [Tiled also supports more complex property types](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types). These custom property types are defined in Tiled according to the linked documentation, and to work with them in DotTiled, you *must* define their equivalences as a collection of . This collection of definitions shall then be passed to the corresponding reader when loading a map, tileset, or template. +In addition to these primitive property types, [Tiled also supports more complex property types](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types). These custom property types are defined in Tiled according to the linked documentation, and to work with them in DotTiled, you *must* define their equivalences as a . You must then provide a resolving function to a defined type given a custom type name, as it is defined in Tiled. -Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will attempt to find the corresponding definition, and if it does not find one, it will throw an exception. However, if it does find the definition, it will use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file when populating a instance. More information about these `class` properties can be found in [the next section](#class-properties). +## Custom types -Finally, Tiled also allows you to define custom property types that work as enums. These custom property types are just parsed and retrieved as their corresponding storage type. So for a custom property type that is defined as an enum where the values are stored as strings, DotTiled will just parse those as . Similarly, if the values are stored as integers, DotTiled will parse those as . +Tiled allows you to define custom property types that can be used in your maps. These custom property types can be of type `class` or `enum`. DotTiled supports custom property types by allowing you to define the equivalent in C# and then providing a custom type resolver function that will return the equivalent definition given a custom type name. -## Class properties +### Class properties -As mentioned, Tiled supports `class` properties which allow you to create hierarchical structures of properties. DotTiled supports this feature through the class. For all your custom `class` types in Tiled, you must create an equivalent and pass it to the corresponding reader when loading a map, tileset, or template. +Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will use the supplied custom type resolver function to retrieve the custom type definition. It will then use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file when populating a instance. `class` properties allow you to create hierarchical structures of properties. For example, if you have a `class` property in Tiled that looks like this: @@ -96,16 +96,66 @@ var monsterSpawnerDefinition = new CustomClassDefinition }; ``` -### Resolve object types and properties automatically +### Enum properties -If you don't want to have to rely on creating an equivalent definition for every `class` property that you may be using in your Tiled maps, you can check the `Resolve object types and properties` checkbox in `Edit > Preferences > General | Export Options` in Tiled. +Tiled also allows you to define custom property types that work as enums. Similarly to `class` properties, you must define the equivalent in DotTiled as a . You can then return the corresponding definition in the resolving function. -![Resolve object types and properties](../images/resolve-types.png) +For example, if you have a custom property type in Tiled that looks like this: -This will make sure that all properties, even those that do not differ from their default values, are included in the exported map, tileset, or template file. This will allow DotTiled to resolve the properties of the `class` property without needing an equivalent definition. However, you *must* enable a similar configuration flag in DotTiled when loading the map, tileset, or template to make sure that DotTiled knows to not throw an exception when it encounters a `class` property without an equivalent definition. +![EntityType enum in Tiled UI](../images/entity-type-enum.png) + +The equivalent definition in DotTiled would look like the following: + +```csharp +var entityTypeDefinition = new CustomEnumDefinition +{ + Name = "EntityType", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = false, + Values = [ + "Bomb", + "Chest", + "Flower", + "Chair" + ] +}; +``` ### [Future] Automatically map custom property `class` types to C# classes In the future, DotTiled will support automatically mapping custom property `class` types to C# classes. This will allow you to define a C# class that matches the structure of the `class` property in Tiled, and DotTiled will automatically map the properties of the `class` property to the properties of the C# class. This will make working with `class` properties much easier and more intuitive. -The idea is to expand on the interface with a method like `GetMappedProperty(string propertyName)`, where `T` is a class that matches the structure of the `class` property in Tiled. \ No newline at end of file +The idea is to expand on the interface with a method like `GetMappedProperty(string propertyName)`, where `T` is a class that matches the structure of the `class` property in Tiled. + +This functionality would be accompanied by a way to automatically create a matching given a C# class or enum. Something like this would then be possible: + +```csharp +class MonsterSpawner +{ + public bool Enabled { get; set; } = true; + public int MaxSpawnAmount { get; set; } = 10; + public int MinSpawnAmount { get; set; } = 0; + public string MonsterNames { get; set; } = ""; +} + +enum EntityType +{ + Bomb, + Chest, + Flower, + Chair +} + +var monsterSpawnerDefinition = CustomClassDefinition.FromClass(); +var entityTypeDefinition = CustomEnumDefinition.FromEnum(); + +// ... + +var map = LoadMap(); +var monsterSpawner = map.GetMappedProperty("monsterSpawnerPropertyInMap"); +var entityType = map.GetMappedProperty("entityTypePropertyInMap"); +``` + +Finally, it might be possible to also make some kind of exporting functionality for . Given a collection of custom type definitions, DotTiled could generate a corresponding `propertytypes.json` file that you then can import into Tiled. This would make it so that you only have to define your custom property types once (in C#) and then import them into Tiled to use them in your maps. + +Depending on implementation this might become something that can inhibit native AOT compilation due to potential reflection usage. Source generators could be used to mitigate this, but it is not yet clear how this will be implemented. \ No newline at end of file diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml index d582202..62d2c9d 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -4,4 +4,4 @@ - name: Essentials - href: loading-a-map.md -- href: accessing-properties.md \ No newline at end of file +- href: custom-properties.md \ No newline at end of file diff --git a/docs/images/entity-type-enum.png b/docs/images/entity-type-enum.png new file mode 100644 index 0000000000000000000000000000000000000000..459689b3e63e007155e0d2fd55a9ce1597106305 GIT binary patch literal 11906 zcmcI~c{rP2yKYqR>wvb}YH86y2a2|YqNZwTYbr`wW0e|$n1w`Wb z<-o_i-sgSyu^#{+)VTZMY4OT)0s!Pi@7>mU7-UBs6ZZd1c)?nhl-b#ecyImJL1~^v zt8eaq0i&9(JRB)0KWt-Vdj{fA5nv?jBIxc`{N_C$h&yIIAc5J1> z(_EbSJ;!TnYyp5DiD>UI9>8(x8ho-4fWix?ypuNS)huH|erqxn| ztq!O}5#rkEgt>1a2UB)uwe}#=?&?Vk=ZM@y!~QPd0gIfHo(JXm71c|h-W7k(3r-7p z0Ly3-R6wRH^4u8Qn^y5PDbUqut;a7?D5{*YCC=&Gl)fN5<{f0Ur)Lz^A_|_kWu2{$lrabV~KNA(4IhYjKJKNeQy7Y(gQMxbrwQ`fA!a{p<%ORauUI6Tu zL%D>n3PP4G!OOLEYx;4Z5>9?$LJwG0DX}`k^@{LS}1Q9kWx{i;7()EyHD)j zaaOeP@jhNadgMU(){>!J{@qopbIB_W2J$v|MJpEBD*>T#eKwT{1&?qh z{I*O1#rJ_8%B^cn#9Z9(+jqngX4r$gi>H7`nm>wb6j+ z5xq5L4SPmU*n(A&+22hEIkI2WvK6 zqdKD&bm+mh#c5k&2)eJv#uKzq6GbgXBCFW>T{JIWDcMLJuYNIXUQhJseL;o!9R52n zM(XaaU>`>qJ;!^IyZTgwhqCY~cK?)|wz2u=B7nvg*9Pp*#PO!)md5x!)q^nxuX^VMsTF%(z7lK?Jcg z*v>SMmZ>G0-)IXTK$}jf7j0M*X0|`It5e+E1B4UQ(trYhb#t)B%65Yg35shK1OURs zrF3YTEYApPBM8L{=n+0xnGpa0Y{tC+a(9ikmM(Xr|91QvArtykSIkky^IIFMYEar& zcOC#B>AF+vAwh|okBD~gBKLb&++s03h8f`%}HmBL9CLCLZ-Eyi)a@e!J(z^FrwgI12(jpncsu*U+b8 z3zL&`hhED@8-KkqVFh>iABqJ3CRbBrY2%1q=SKM*`{2^6VtPG_51%7^Bc`iME` zzNn%D(cQvP(EZekahet2=jI=Uuz7?v7y?1N-;0(KD0vjNe?^5Hq${kk7F)=3cY0$s zjo!=SC8N`&>I+DWn(&tbCIuB8`WL=1MpRd_Fd^0#typ*~mfnY?^pB;rlVI(gfwZoV zJ8LK{LRO6C0vK>4cp)F<&l-c`t$ZH`6qB~YHmo_Ewm;~>8n1^}h_AHR0w4pY?7`1r zxBG27N0wLItyaWA_nJDV%G~Pja-13^iV&o7X`k^$eA(o2&^zDsZBbmBS{AVif;W2^Wtb+Tp79bn+LdhI4&T~BPM?t*IFCU}FQt?uC%gv@ESZV}!@FYbo z6xXnoR%g}Au7nk1E0ck6szlLSO`OTh5B|=y)%VVxp~eTywu0!<&o-=1)tI`hI3gJ~ z1+w+eGQ8B!YK%e|eHd)=IKB^q-7oY{7&k5@%c~#^-T&xC|^}Fjp z6#PqPTg@-$+Z{!DF9q(q)vvjdm;10b_p*W{VLkJCQs6@anK1Y0s%}>fe}C;CS654u z#WLlhBCUdX$zlgd-?XZltnGAMYPeR4lbxUa`|K7fV)&2}y+iVvfDm@&|do8bqSp_M~Ft60Vtlnum3qw%LhF%LW)i)2QX)boq5nU3W5bByvkGqQ3; z*9~0WXy`*Ch~}3A$p*8qLGgskT3X|>t2L0opi7|`%kFE`_Q2aS8i&-WcRatODR?D( zKkbZIRmpRY;ApF?;u!Ry^oCoilPH`s$jcQAop<5RE;Nz_-EPbH%{aysqk6E&)vd7W`V4n!PlE40Y(S? zjchkL5s9&cti5QR)dz32P6W!Q$Ay>p@_`30+a-v8wkX7nB={a9dVWYoJqC*B+zGS_ zE?$r^C^wG0wVDj`>vR?!y0DfwSm+vWZBkWstzM_`Pl+gQ*l#-j^FxLA<~M@ydo$d| zRTP!>)&^pk=+I_EB>Gry<3vjHgd<8LoiC@4itBKvwVU^e zFl#+R4XroV#z8uA(feN~SuczI}nMZ<&jmM4_3=Kr) z;tHp#VGoLwF(j)_Igw2R+nDMSixA2Dw?k4C!floT4Jx$VK-lhf$Diyuw! zr2T#pfdy*hF3I5M0=XWO>Ybq(`a&Y52E?}Npz_Mi*xfn(_6j+(hP*f3WphGxWo@G~ z4Edx+I=#d?J8cWfl9V&JN%ux9u*PCHI?plkKkAov=5CQtgr_k2LDV^EK0hS|W`m0Q zdArN;aE%p<#^S#n6?Yo%Nb2|}rJI?q2^mTn~vAKIZabNFv)wAlzvCO6<=ZJ=wsDU*{ zfw-`rS0<`m2Cnzn231IrI}hIMR&=VLYIXJOWrs zd`&5^e}=JcW7K#2O~23Zx^u<(MbRG}Tb^KhrTzfBzeA&b$S*B@GGld;8D5?P?7NAn zF)BZ1VByGn6Eh?(FMOqhTHJmipOjkj*saA!JFy#mF>ztGHEcyB&iC^A0Lt06Z?;u8 z?GNM{)x_;-i%TOlk#(x_oZXA`xs7kPkxuwd%%bDKY!bRS@t`I7T_aCGr9MrqW)T` zGi(Y+G5q5>eVC-`!Y}xxLs2nl zJdeL=@l^>fpV56Y$DKo8y3R`**%1_#MQ{gu33 z_1x>@(hnE6zq6Y{O@(4F?^S)?9=JGkjZS}kK(u-(^VRz?Nz0%eK$3PW@7>5C9ld?W zq~f@_g_}dSfcd28KiY>e7w`Y`{$Yh5mooM^9nleTDr}aCPe{O-<>(Q=7A+p~O*{mr zN&?*nuYk5qF-^uXAaaOKq_bS#0k7tynZ|}5wd+w_R$^*hBqNivvH-RqmB^$IUNKpb zKNGlghx5nRZMa>5^2~aBqY<_WRHp(hLTL(&$@(uFjn6u4iV(p?Gtuu9J5hYC=fh96 z>Urt_XZb!)6|`M8z4rp2l9Q=GTe-jVJo!RY|B(0C-nG$Na8if3*Y*{Ki^gAjD(X0% z2#WRif|&S z6q~-+u+QtR*%IAe4~j$RW!pD!YqzyR6KQUnqMUKMxd!;TnNZHhcVff5%qtm1w*1db+!hKAV-5U=UWt_B1+S#HfaZv@H?WVs*pAvw#oxvV?=F6i; z`m?+q68d1rihSEY8;DPxI^}PRa`PDdmaJXnjmaQ)Mav0!JS;;fEh(JD;Q)WfwqMIGeW63y5S6rG&-hZEz! z9+68<6kYIv;{Jlf8F~pPMCGoT_r|N*F>1WCkQt(Mq0?WnBk0 zoOCG@88emAF3}R~zP+66?ErK7qaQ|ev;;zZyDZw%KS{-}NhoV)S#qbCr9xgC`p1uD zHIMWxVNpi-1!TPF)!tKx!!{)Oq!}Sq^T_PY&Ji2Qp3x(sQ>uf7|k@Y0#Mzw!F6b;J}oDEk^Mv2LlQz_y5{nuKfzUIqZ&#)D} zq{LbMz9+sY`6A&}S2z?A8Vw&b?ZzU4o9jl!PkZtWnMoRYZ3J;Y$8W(2~VVOVq zgJ0}N(TS|ZvM|`#fDwkU1ub^GdlN?tkxHr zeQBXFTPQ6~zUFFD_Z){c4wXZsQ>8gnV)*8*P7sg)eD*R!H)26wYx5(mmJK29$&MaB zC(z?Q$+!(K-I}e_K+RMai=B3{vi~PS(a&N!5-bU!jVbb1b4t*U?AhaK#JS2xqDVRFD+3qx=1<3Z8N-ICOnB7Pd z#fLpNJi~peEdH>LL-h)nXy6k(dHvNS7cm3t4o$b7=Snr3cf)YtF|eCk@2|LvHQ|TE ze`BX-W^l*TUCgS$DN?)75BLJ+cTpKIH~ZhwY}TDs(qq<8UV-jla9@(5i;WzxF+!M_ zeJ{>NQ2S=c9og8XP4n2D1yEg|B&S!h$_l*A8Q-z37z;!e^1O|}xbpSL@(YR$Ec2ep zKaM~6yqz0+TCJiia%76Y{^j5GEaENmuyV{m<%OB;azeq`9Cqq_%aA(zdlB~Rir4U? zoIbNvFKg36>_LxlO)r`2oaZHf=?F=wC#924T(R`=c5K$oh5p>jW%HJnn1}bG3t28R zlrLplNfdqp)eoCmj1>-<( z7>Hv!H|`65=sJ5jwwL$IsrU{-F5_HuHQu?+o$@Wu{WYxFOD+Y8)49@oWbXhs*P+4B zt#={Qfr+WbwikMr5ROiWz~f#)Lxgj>6~(%Z#R2VDivr`$g2&(l7FJZlo=BBGB}Q92 zQP}aRcjTeo^e-jeRaf^sA1Vn5b^A@hkvPE#a|7JycatM5>#eHg0J8%DGOEsSV4vtrEv78dBFc*U4Y3!+7>}Zw->4ny78G<4{ap2D z5s^=szZ|)j1o<2Bt-Z=|nq|s-NM5li>SPz&;n~;K{mXR6*)YQdY7XvptLUC%?V47<5jdcXZO3kE$O0kp8XsOkQTygk7k=*R_Ow z$UC%C5S=8NvIwo30KtzOccMVn-xH6|GU`8}epUDfGC zYe?Z(-jns`twKhiHaYzL)s^Zb_@ypvO(fGpAvlHQbqxAb(<0GVQUsy}JD(nQu9e3r zv6-KcL?#w^Kk~26Cte2stdv#%4BqrUz53opO7jJ$gTg>6Y|AFF5O99Av6E4k2w7t8Kr%7pzsQ#OF*9nzdd&?IsVm5sWcf^efX^!fp7mg%B3$^!3Di$_zqn9J`j<7 z4;mv~k5zxuKyAG@@IqGrdMLi_qVN&2eyJxkC48()ZFNqK2Fj$8UWWhp@@3X^=O>5@ z@7Y?2I}~yaobiKKxY7!27qWcEDD~zrfnN;{Wa;r)GWxsYNoQ6A`ZE0ykWsXk+XKJC zsnd&(nD+y{Ue=DMg%H*L#xVMLoxMwUYC!qGL2+e6kqd_2m!#`>oBQ5 z0}os}dVlm&ez6QU8A$#aJe&F8(lu!gHz3USL&$mDhg+Y{aFM0#yO;S#-RElRpdnDY@pCc)vOA zf^^g((&DS{R+$j^i)M)9SGS{W&o3u&D;;;!g6Otb!%EXTY->h}BRW(!x|S2ahtm^Z zhdh+Z;^&NkE>OBJ-p(bt#pN2)zOYE<5&DDf?NJ|t@rGFsO7eF{cgkokFtZrIRwegy zgG7NI=p7-ipl^SZd>{Vlc=?-!7YfF`!jjkiqy}2l!bZzudd3kxWkTCQiJGjk%i~^z zO1Nf$7JaS=`6suGa!;Mw((=&1`=|z*lM!r$Sp00d>A$Aju$s(py$>0HUHa^CWu6KM#um4U{KU7Z`(e#QTn9F(NAVv%|R7Z}6v{*5= zwllJ7fLgD@hD(vYjwM1gSH0l0a&L_xT)z~QCJ#B^V zS2>H{^?(D%WRZTOC3JJ2_GM_<>iSha$a_UO&u5j*V}~#)w|+%`;^tA@H*yOuVT=VQ z&U<-0u=UCCh$cC(`piI+Lgg(&r7Z&_u*G4!+OoFOSIY9SXG_Sh&aIEVXAS;oi#oUV zOT2sT_ccEfeu>I&gglX|Y$g0BNn%qIL^nNIU}AjgIF9N$Wk%G^ShKvR#m#o^t0r50 zMm=%Agvg2@9B3=Bu=O;kdQVYiE>&0VW8^Soto{s|%6pc{y8bf`w8@vxj=P=k$&*-b zILcX)%?+90=PWka&#No9l42-Ga!YR}ib^a@NiZt^^77^3Du_l_fHx@z9dT>^^{y|* z=J@6wzsJBE+E*DB)!*dv9o-A>MH2nc4^!OlKmI3cXQRIzNQq>aT><}$>l;eTbM)vR2JLMC>8vbk~hDIIgQeg!v^6>8G`jM=9H5x_*k3QSB&(2qF zrG5{}c}Qb?1-aSTxelgY4s6Fi#gD5>Te(3L`fvu4Ru!kCgK7@#e?9q6la((n;U1bl zK6lIr2+ZnSL#^(iVfU>&!_Qep?+NZ)sGJ*1wTG(2E9&u>ufFN$#+=9b7Bh2FH?zkT znYQn5Wt_i473~~fo7$zMpZe$@NfMP%V|dQ}Tk}OD?{f-@c~U_hn}a%h$A{gR;n=AC zuVzad>GATlk)0s{&b3QQUMawZ8~m5#K>-fBPNA3Rw8XKPD!gf%BY9iD0v>0_RTz^uHr0bEJ{`j54r zb+uM;gS6iF^XOkH=>q7e87*{1S<$K$>0ltY4nrr#}5}nj{}~-<&GO zb{@bO5aGPy=mr+0I z<6Yc~cFS8#Y>PsV-wab^6DMv3E#1Sr8#{TM-RrYT%b)oe{+1X@7yPv+C43x&TZwY*Z?_}{#CY@~GK8Zhaquzh;MPJo+UpZRcqdBYVH!S8JQaMp{{ z?P?2tmL`2!2Nu(}{5!vPHbG7dtuqwUW0oi`KYsijU7j2Nmsx!*5A&klSVc+IkDT=i8B7N_~a~`t|gdTEkx2S3+{xpkN)m?(^~ei}Q*u?gr~E z9Z(PRey90(YQ}l3@0??9Pthq)62k73HZ-!_k1_gDaDvkEzW&-<_rg2`msGhC>W#mq ze(hOhQ{l3_d!<0d(f=UMj=oDC9XKoAu8sw|4Q7>kd_r#W44+B^I#ye2ENzU&D&p4a|r|L*J0Q}94{ZnU)2h*$;CMB z@e^r<_eL_8{=#XDa|RN;g%Q=xL&odGG_(G1Cv%-CMkCA)P10!> z?eIUycK_REYdY`9)o}zRS#DJ&NbA*+?fhN|1ipY3m!o0Be*&<(1t$3Dr>JnH1%0E` zSgid*Mc&^vp_?eat-O;x`*z*f&}55}F)l`5d+BuHgXj|PGi@vG-|g$(9-SlSjfUq%^lDykz&#G^)sL6Cf0kX&)R2@!3jxXSG~FYuRE^X{)0m4_9KA$DpeBfvf?Q^;>EsgDkA zdM?i^0KUf%QNGsduy#PAc7;c2sj>pn#ff~kN&mSMy_fM52Z{* zNCpA{?{2aeOOvlZdtbOAj)8O88)8i0rB0L>u#GU8rT_o^0MGx^GeZA74dioL)%W%b z?H`0sq#Aeyo=+<2vUzT$_22$!Yhw2=|Ll|C)^MdX%=Y=d8RP*hT?;=207!EA?|IWwn~aDCRh-)af0MCr zv&brx?%O4n1Z)-f;=A18z*1V?Cx6Aqt&(iDZ(N1elwF-USoY{wLc0(*d*F&XEDg>k zv5vW*{MBwFW~V2{ZY)<=bNz?Ac~L3eyK_sprzGH*>pE=`(k0 ziY&!nHpvJ0U4$KZVcEQ zy+i+N1zHdFUn+tIOs~78Z2aDDdqU|E=%54|>o}9gxiy(;NJ7K%U884?ayeefbnK1x zbw_(->N-x0ugy^?!T0{d$*h+b#YZ=wqZ!s-H5OV!m8K|@w81ae9M?ABbl^m-5t~Ja z`Ux4<2FcIt{R|>L*&KF=0_VvlnD~xTUW_|2mrs_)P(!hQI{4-}kuUvgIrqIJE9`*- zNDLlvFl;_fN>X_}Gqijs%zt5xP2MQZ(B-Er6h_R8nRlhBV-#J;L_9sE_qVM4!h4;U zqh`e;(>1B68Lkw>dLP*MJ(}2fK(Nv803Qo zDSEJ+VEOa0$>#b~`wFTV!f(MVf&9I|+Qq50boF~ko~oU-q{GpE$?hhu2HI9=uDf2< zHyfut+-kP;7quK4~4KNK13OfeY!~-qxc6#7S#R#;oI=UX!`;Fjg60vByL`9^(QWm;&1kPDqEeG z3R|L`5|o7HSj)UYtb#svP*m3@70X-etAcst1Q|<FL#@6!6Rt$D8WgGZ z4LwU$Wwi@jNr_lBL1^wQ z*!LM9NGy#ZCg=ptg>)-~W{hvms3faH(fjfX2_qu~i4_n6N=w6hH~HpTben5YSFmYn zcu_{Yo^W9cf}GO<86RC;%E$S-Ir0mQYH2Z?$NVVXo$DNj<+4G!Iz+}0Y6HFFluW0yEz&__)R)o9r zcIT#HeT09@tHZdJ_v`vm858CCw9fI`^=z&>#!s?3)`3K+(xTK+PU!c#D)_X)J8P;d z*XemTf0jjn9z1YO5N@lX#a(Teli4@s;#GAM<1)c5rkc$n5o4zoM@tu?Yt~!UD+@kU zpM>jX#;525C(EDpRQb(TuTG?;2FI-U`qezy!Ps-|hk3JG{98^#gLLVuZW#qu<@fHw6y*YOk7En6Vi&!}|1UOqtK zo0%LiAV95xZ+ww5G|@pDn)r1;e^kBFDknG?t%15NTa!AdM%RR$OIsef?mG0{VY4r7 zJ706NHwG3UK3b~De%vp*WYvNh3op-5F4YS1rsC1|9?Oyf}%>bX~ z80lHjFG6=*H^m5SaO;TGD&>V}T$`*e=96*FDmJtjj)~qYywL29ULpeoge4Y@DpsKm z-PTE26pLPk{q&|k4N@*2_H=Typ9h$+69hks^T@MyQz^{j(*h&RP#h9buf| ziK)FV3SRvdg|_z0wHJ|i!|E!67oGOKZwZbMP81Z5vRWSL!EZ5ePz_F7@?E;tuT}YM zB(ZL+Hviev8yZ)tW2l;^)co^Um){RPCE>?fmU1*tQc6v*6O9p+WOuablYywsxJ(M; zqItGf4Fzme^(=e0Kuh1sL^L$PONLi1gHJcQJ7oA-2asiP+RtdG$~3_CF3|<&8ePYX zB99@WZ^lRVo_#Sb*Z(A@o}J*u4A4m`0Ck#Qt)_cCzGxIhEj!xLdz0XrucF~fzF`Y* z`HrII@F`XN9GwdCoKxHm9z&KVpYNvlFUZ!fqT{%L;mn@d@FYJ_OO5-bB{1$6C`tNw z1}{ksjh4fXZHu2DX<0&P?w|wMcTS4R+bnfm969I9zFqeE;^WO?%tyH(`91l|P0%8e zDCRV+SpOYUL02=W@!!H|_mA2S#{qz}|6^$XzwBTjHm&>Spicz=pzU>E`ehvd#_#%< z0bA=nAYSe<#EGF|M?3zDimq%Xl}Zb|g)&a*+gP0qppB{$xu?FbH!**DzO{!_%hA%s zpLZ#U(?$6}-hbw`7fu{p(3RYbg(7#)th(&qzkgJ2x6^nqrKqC%>E9Ba_L{evE`@vK j+HlPqHjQ1|;ql#s8|v;2B5==*1Mb~1zKy>1B>aB>$gvI^ literal 0 HcmV?d00001 diff --git a/src/DotTiled.Benchmark/Program.cs b/src/DotTiled.Benchmark/Program.cs index 3cd6e15..523a7f1 100644 --- a/src/DotTiled.Benchmark/Program.cs +++ b/src/DotTiled.Benchmark/Program.cs @@ -47,7 +47,7 @@ namespace DotTiled.Benchmark [Benchmark(Baseline = true, Description = "DotTiled")] public DotTiled.Model.Map LoadWithDotTiledFromInMemoryTmjString() { - using var mapReader = new DotTiled.Serialization.Tmj.TmjMapReader(_tmjContents, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), []); + using var mapReader = new DotTiled.Serialization.Tmj.TmjMapReader(_tmjContents, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException()); return mapReader.ReadMap(); } diff --git a/src/DotTiled.Tests/Serialization/TestData.cs b/src/DotTiled.Tests/Serialization/TestData.cs index b007913..1c0b885 100644 --- a/src/DotTiled.Tests/Serialization/TestData.cs +++ b/src/DotTiled.Tests/Serialization/TestData.cs @@ -41,5 +41,6 @@ public static partial class TestData ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty()], ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_deep_props/map-with-deep-props", (string f) => MapWithDeepProps(), MapWithDeepPropsCustomTypeDefinitions()], ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs new file mode 100644 index 0000000..1b36b4e --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs @@ -0,0 +1,161 @@ +using System.Globalization; +using DotTiled.Model; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapWithDeepProps() => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 2, + NextObjectID = 1, + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + 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 + ] + } + } + ], + Properties = [ + new ClassProperty + { + Name = "customouterclassprop", + PropertyType = "CustomOuterClass", + Value = [ + new ClassProperty + { + Name = "customclasspropinclass", + PropertyType = "CustomClass", + Value = [ + new BoolProperty { Name = "boolinclass", Value = false }, + new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileinclass", Value = "" }, + new FloatProperty { Name = "floatinclass", Value = 0f }, + new IntProperty { Name = "intinclass", Value = 0 }, + new ObjectProperty { Name = "objectinclass", Value = 0 }, + new StringProperty { Name = "stringinclass", Value = "" } + ] + } + ] + }, + new ClassProperty + { + Name = "customouterclasspropset", + PropertyType = "CustomOuterClass", + Value = [ + new ClassProperty + { + Name = "customclasspropinclass", + PropertyType = "CustomClass", + Value = [ + new BoolProperty { Name = "boolinclass", Value = true }, + new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileinclass", Value = "" }, + new FloatProperty { Name = "floatinclass", Value = 13.37f }, + new IntProperty { Name = "intinclass", Value = 0 }, + new ObjectProperty { Name = "objectinclass", Value = 0 }, + new StringProperty { Name = "stringinclass", Value = "" } + ] + } + ] + } + ] + }; + + public static IReadOnlyCollection MapWithDeepPropsCustomTypeDefinitions() => [ + new CustomClassDefinition + { + Name = "CustomClass", + UseAs = CustomClassUseAs.Property, + Members = [ + new BoolProperty + { + Name = "boolinclass", + Value = false + }, + new ColorProperty + { + Name = "colorinclass", + Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) + }, + new FileProperty + { + Name = "fileinclass", + Value = "" + }, + new FloatProperty + { + Name = "floatinclass", + Value = 0f + }, + new IntProperty + { + Name = "intinclass", + Value = 0 + }, + new ObjectProperty + { + Name = "objectinclass", + Value = 0 + }, + new StringProperty + { + Name = "stringinclass", + Value = "" + } + ] + }, + new CustomClassDefinition + { + Name = "CustomOuterClass", + UseAs = CustomClassUseAs.Property, + Members = [ + new ClassProperty + { + Name = "customclasspropinclass", + PropertyType = "CustomClass", + Value = [] // So no overrides of defaults in CustomClass + } + ] + } + ]; +} diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj new file mode 100644 index 0000000..9fa2bba --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj @@ -0,0 +1,55 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[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], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"customouterclassprop", + "propertytype":"CustomOuterClass", + "type":"class", + "value": + { + + } + }, + { + "name":"customouterclasspropset", + "propertytype":"CustomOuterClass", + "type":"class", + "value": + { + "customclasspropinclass": + { + "boolinclass":true, + "floatinclass":13.37 + } + } + }], + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx new file mode 100644 index 0000000..56e8f2e --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + +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/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index 3fe6843..48cc13f 100644 --- a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -20,16 +20,20 @@ public partial class TmjMapReaderTests Template ResolveTemplate(string source) { var templateJson = TestData.GetRawStringFor($"{fileDir}/{source}"); - using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions); + using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return templateReader.ReadTemplate(); } Tileset ResolveTileset(string source) { var tilesetJson = TestData.GetRawStringFor($"{fileDir}/{source}"); - using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions); + using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions); + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType); // Act var map = mapReader.ReadMap(); diff --git a/src/DotTiled/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index 073a19f..1e95695 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -73,8 +73,25 @@ internal static partial class Helpers }; } - internal static List CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => - customClassDefinition.Members.Select(x => x.Clone()).ToList(); + internal static List CreateInstanceOfCustomClass( + CustomClassDefinition customClassDefinition, + Func customTypeResolver) + { + return customClassDefinition.Members.Select(x => + { + if (x is ClassProperty cp) + { + return new ClassProperty + { + Name = cp.Name, + PropertyType = cp.PropertyType, + Value = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver) + }; + } + + return x.Clone(); + }).ToList(); + } internal static IList MergeProperties(IList? baseProperties, IList? overrideProperties) { diff --git a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs index c4ada75..6a71eb7 100644 --- a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; @@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj; /// /// A template reader for reading Tiled JSON templates. /// -public class TjTemplateReader : ITemplateReader +public class TjTemplateReader : TmjReaderBase, ITemplateReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// A string containing a Tiled template in the Tiled JSON format. + /// A string containing a Tiled map in the Tiled JSON format. /// A function that resolves external tilesets given their source. /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// A function that resolves custom types given their name. /// Thrown when any of the arguments are null. 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)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - 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); - } + public Template ReadTemplate() => ReadTemplate(RootElement); } diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs deleted file mode 100644 index aeef011..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - internal static BaseLayer ReadLayer( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var type = element.GetRequiredProperty("type"); - - return type switch - { - "tilelayer" => ReadTileLayer(element, customTypeDefinitions), - "objectgroup" => ReadObjectLayer(element, externalTemplateResolver, customTypeDefinitions), - "imagelayer" => ReadImageLayer(element, customTypeDefinitions), - "group" => ReadGroup(element, externalTemplateResolver, customTypeDefinitions), - _ => throw new JsonException($"Unsupported layer type '{type}'.") - }; - } -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs deleted file mode 100644 index d9777e7..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.Json; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - internal static List ReadProperties( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) => - element.GetValueAsList(e => - { - var name = e.GetRequiredProperty("name"); - var type = e.GetOptionalPropertyParseable("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 JsonException("Invalid property type") - }, PropertyType.String); - - IProperty property = type switch - { - PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, - 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, customTypeDefinitions), - _ => throw new JsonException("Invalid property type") - }; - - return property!; - }); - - internal static ClassProperty ReadClassProperty( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) - { - var name = element.GetRequiredProperty("name"); - var propertyType = element.GetRequiredProperty("propertytype"); - - var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - - if (customTypeDef is CustomClassDefinition ccd) - { - var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); - var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); - - var mergedProps = Helpers.MergeProperties(propsInType, props); - - return new ClassProperty - { - Name = name, - PropertyType = propertyType, - Value = props - }; - } - - throw new JsonException($"Unknown custom class '{propertyType}'."); - } - - internal static List ReadCustomClassProperties( - JsonElement element, - CustomClassDefinition customClassDefinition, - IReadOnlyCollection customTypeDefinitions) - { - List resultingProps = Helpers.CreateInstanceOfCustomClass(customClassDefinition); - - 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") - }; - - Helpers.ReplacePropertyInList(resultingProps, property); - } - - return resultingProps; - } -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs b/src/DotTiled/Serialization/Tmj/Tmj.Template.cs deleted file mode 100644 index 71ba1e7..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - 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/src/DotTiled/Serialization/Tmj/TmjMapReader.cs b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs index 1d277b1..7ec12e1 100644 --- a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs +++ b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text.Json; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; @@ -8,73 +6,24 @@ namespace DotTiled.Serialization.Tmj; /// /// A map reader for reading Tiled JSON maps. /// -public class TmjMapReader : IMapReader +public class TmjMapReader : TmjReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// /// A string containing a Tiled map in the Tiled JSON format. /// A function that resolves external tilesets given their source. /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// A function that resolves custom types given their name. /// Thrown when any of the arguments are null. 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)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Map ReadMap() - { - var jsonDoc = JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadMap(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 - // ~TmjMapReader() - // { - // // 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); - } + public Map ReadMap() => ReadMap(RootElement); } diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Data.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs similarity index 98% rename from src/DotTiled/Serialization/Tmj/Tmj.Data.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs index c021a6c..22a35e2 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Data.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs @@ -5,7 +5,7 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs similarity index 77% rename from src/DotTiled/Serialization/Tmj/Tmj.Group.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs index a714038..e132d0c 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; @@ -6,12 +5,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Group ReadGroup( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Group ReadGroup(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -23,8 +19,8 @@ internal partial class Tmj 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), []); - var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); + var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(ReadLayer), []); return new Group { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs similarity index 88% rename from src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs index 74fb230..576fa52 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; using System.Globalization; using System.Text.Json; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static ImageLayer ReadImageLayer( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal ImageLayer ReadImageLayer(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -21,7 +18,7 @@ internal partial class Tmj 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), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var image = element.GetRequiredProperty("image"); var repeatX = element.GetOptionalProperty("repeatx", false); diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs new file mode 100644 index 0000000..9e01d84 --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmj; + +public abstract partial class TmjReaderBase +{ + internal BaseLayer ReadLayer(JsonElement element) + { + var type = element.GetRequiredProperty("type"); + + return type switch + { + "tilelayer" => ReadTileLayer(element), + "objectgroup" => ReadObjectLayer(element), + "imagelayer" => ReadImageLayer(element), + "group" => ReadGroup(element), + _ => throw new JsonException($"Unsupported layer type '{type}'.") + }; + } +} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs similarity index 88% rename from src/DotTiled/Serialization/Tmj/Tmj.Map.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs index eeb47b0..47abc66 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; @@ -6,13 +5,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Map ReadMap( - JsonElement element, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Map ReadMap(JsonElement element) { var version = element.GetRequiredProperty("version"); var tiledVersion = element.GetRequiredProperty("tiledversion"); @@ -58,10 +53,10 @@ internal partial class Tmj var nextObjectID = element.GetRequiredProperty("nextobjectid"); var infinite = element.GetOptionalProperty("infinite", false); - var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); - 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)), []); + List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el)), []); + List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el)), []); return new Map { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs similarity index 93% rename from src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs index 589c151..cae4790 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Numerics; @@ -7,12 +6,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static ObjectLayer ReadObjectLayer( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal ObjectLayer ReadObjectLayer(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -24,7 +20,7 @@ internal partial class Tmj 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), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var x = element.GetOptionalProperty("x", 0); var y = element.GetOptionalProperty("y", 0); @@ -38,7 +34,7 @@ internal partial class Tmj _ => throw new JsonException($"Unknown draw order '{s}'.") }, DrawOrder.TopDown); - var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); + var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el)), []); return new ObjectLayer { @@ -63,10 +59,7 @@ internal partial class Tmj }; } - internal static Model.Object ReadObject( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Model.Object ReadObject(JsonElement element) { uint? idDefault = null; string nameDefault = ""; @@ -87,7 +80,7 @@ internal partial class Tmj var template = element.GetOptionalProperty("template", null); if (template is not null) { - var resolvedTemplate = externalTemplateResolver(template); + var resolvedTemplate = _externalTemplateResolver(template); var templObj = resolvedTemplate.Object; idDefault = templObj.ID; @@ -114,7 +107,7 @@ internal partial class Tmj var point = element.GetOptionalProperty("point", pointDefault); var polygon = element.GetOptionalPropertyCustom?>("polygon", ReadPoints, polygonDefault); var polyline = element.GetOptionalPropertyCustom?>("polyline", ReadPoints, polylineDefault); - var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, propertiesDefault); var rotation = element.GetOptionalProperty("rotation", rotationDefault); var text = element.GetOptionalPropertyCustom("text", ReadText, null); var type = element.GetOptionalProperty("type", typeDefault); diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs new file mode 100644 index 0000000..c5b6a7d --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmj; + +public abstract partial class TmjReaderBase +{ + internal List ReadProperties(JsonElement element) => + element.GetValueAsList(e => + { + var name = e.GetRequiredProperty("name"); + var type = e.GetOptionalPropertyParseable("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 JsonException("Invalid property type") + }, PropertyType.String); + var propertyType = e.GetOptionalProperty("propertytype", null); + if (propertyType is not null) + { + return ReadPropertyWithCustomType(e); + } + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, + 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 => throw new JsonException("Class property must have a property type"), + PropertyType.Enum => throw new JsonException("Enum property must have a property type"), + _ => throw new JsonException("Invalid property type") + }; + + return property!; + }); + + internal IProperty ReadPropertyWithCustomType(JsonElement element) + { + var isClass = element.GetOptionalProperty("type", null) == "class"; + if (isClass) + { + return ReadClassProperty(element); + } + + return ReadEnumProperty(element); + } + + internal ClassProperty ReadClassProperty(JsonElement element) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is CustomClassDefinition ccd) + { + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + var props = element.GetOptionalPropertyCustom>("value", e => ReadPropertiesInsideClass(e, ccd), []); + var mergedProps = Helpers.MergeProperties(propsInType, props); + + return new ClassProperty + { + Name = name, + PropertyType = propertyType, + Value = mergedProps + }; + } + + throw new JsonException($"Unknown custom class '{propertyType}'."); + } + + internal List ReadPropertiesInsideClass( + JsonElement element, + CustomClassDefinition customClassDefinition) + { + List resultingProps = []; + + foreach (var prop in customClassDefinition.Members) + { + if (!element.TryGetProperty(prop.Name, out var propElement)) + continue; + + 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 => new ClassProperty { Name = prop.Name, PropertyType = ((ClassProperty)prop).PropertyType, Value = ReadPropertiesInsideClass(propElement, (CustomClassDefinition)_customTypeResolver(((ClassProperty)prop).PropertyType)) }, + PropertyType.Enum => ReadEnumProperty(propElement), + _ => throw new JsonException("Invalid property type") + }; + + resultingProps.Add(property); + } + + return resultingProps; + } + + internal EnumProperty ReadEnumProperty(JsonElement element) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + var typeInXml = element.GetOptionalPropertyParseable("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + _ => throw new JsonException("Invalid property type") + }, PropertyType.String); + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is not CustomEnumDefinition ced) + throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); + + if (ced.StorageType == CustomEnumStorageType.String) + { + var value = element.GetRequiredProperty("value"); + if (value.Contains(',') && !ced.ValueAsFlags) + throw new JsonException("Enum value must not contain ',' if not ValueAsFlags is set to true."); + + if (ced.ValueAsFlags) + { + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = element.GetRequiredProperty("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + for (var i = 0; i < allValues.Count; i++) + { + var mask = 1 << i; + if ((value & mask) == mask) + { + var enumValue = allValues[i]; + _ = enumValues.Add(enumValue); + } + } + return new EnumProperty { Name = name, PropertyType = propertyType, Value = enumValues }; + } + else + { + var allValues = ced.Values; + var enumValue = allValues[value]; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { enumValue } }; + } + } + + throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); + } +} diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs new file mode 100644 index 0000000..45031db --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmj; + +public abstract partial class TmjReaderBase +{ + internal Template ReadTemplate(JsonElement element) + { + var type = element.GetRequiredProperty("type"); + var tileset = element.GetOptionalPropertyCustom("tileset", ReadTileset, null); + var @object = element.GetRequiredPropertyCustom("object", ReadObject); + + return new Template + { + Tileset = tileset, + Object = @object + }; + } +} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs similarity index 91% rename from src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs index 905e447..ecc74d8 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; using System.Globalization; using System.Text.Json; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static TileLayer ReadTileLayer( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal TileLayer ReadTileLayer(JsonElement element) { var compression = element.GetOptionalPropertyParseable("compression", s => s switch { @@ -35,7 +32,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, customTypeDefinitions), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var repeatX = element.GetOptionalProperty("repeatx", false); var repeatY = element.GetOptionalProperty("repeaty", false); var startX = element.GetOptionalProperty("startx", 0); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs similarity index 86% rename from src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs index 466b5d3..5fef5f3 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; @@ -6,13 +5,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Tileset ReadTileset( - JsonElement element, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Tileset ReadTileset(JsonElement element) { var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var @class = element.GetOptionalProperty("class", ""); @@ -44,7 +39,7 @@ internal partial class Tmj "bottomright" => ObjectAlignment.BottomRight, _ => throw new JsonException($"Unknown object alignment '{s}'") }, ObjectAlignment.Unspecified); - var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var source = element.GetOptionalProperty("source", null); var spacing = element.GetOptionalProperty("spacing", null); var tileCount = element.GetOptionalProperty("tilecount", null); @@ -57,20 +52,17 @@ internal partial class Tmj "grid" => TileRenderSize.Grid, _ => throw new JsonException($"Unknown tile render size '{s}'") }, TileRenderSize.Tile); - var tiles = element.GetOptionalPropertyCustom>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []); + var tiles = element.GetOptionalPropertyCustom>("tiles", ReadTiles, []); var tileWidth = element.GetOptionalProperty("tilewidth", null); var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var type = element.GetOptionalProperty("type", null); var version = element.GetOptionalProperty("version", null); var transformations = element.GetOptionalPropertyCustom("transformations", ReadTransformations, null); - var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e, customTypeDefinitions)), null); + var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e)), null); if (source is not null) { - if (externalTilesetResolver is null) - throw new JsonException("External tileset resolver is required to resolve external tilesets."); - - var resolvedTileset = externalTilesetResolver(source); + var resolvedTileset = _externalTilesetResolver(source); resolvedTileset.FirstGID = firstGID; resolvedTileset.Source = source; return resolvedTileset; @@ -159,10 +151,7 @@ internal partial class Tmj }; } - internal static List ReadTiles( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) => + internal List ReadTiles(JsonElement element) => element.GetValueAsList(e => { var animation = e.GetOptionalPropertyCustom?>("animation", e => e.GetValueAsList(ReadFrame), null); @@ -174,9 +163,9 @@ internal partial class Tmj var y = e.GetOptionalProperty("y", 0); var width = e.GetOptionalProperty("width", imageWidth ?? 0); var height = e.GetOptionalProperty("height", imageHeight ?? 0); - var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null); + var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e), null); var probability = e.GetOptionalProperty("probability", 0.0f); - var properties = e.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); + var properties = e.GetOptionalPropertyCustom("properties", ReadProperties, []); // var terrain, replaced by wangsets var type = e.GetOptionalProperty("type", ""); @@ -216,14 +205,12 @@ internal partial class Tmj }; } - internal static Wangset ReadWangset( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal Wangset ReadWangset(JsonElement element) { var @clalss = element.GetOptionalProperty("class", ""); - var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); + var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el)), []); var name = element.GetRequiredProperty("name"); - var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var tile = element.GetOptionalProperty("tile", 0); var type = element.GetOptionalProperty("type", ""); var wangTiles = element.GetOptionalPropertyCustom>("wangtiles", e => e.GetValueAsList(ReadWangTile), []); @@ -239,15 +226,13 @@ internal partial class Tmj }; } - internal static WangColor ReadWangColor( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal WangColor ReadWangColor(JsonElement element) { var @class = element.GetOptionalProperty("class", ""); var color = element.GetRequiredPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); var name = element.GetRequiredProperty("name"); var probability = element.GetOptionalProperty("probability", 1.0f); - var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var tile = element.GetOptionalProperty("tile", 0); return new WangColor diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs new file mode 100644 index 0000000..4ae338f --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Text.Json; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmj; + +/// +/// Base class for Tiled JSON format readers. +/// +public abstract partial class TmjReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + /// + /// The root element of the JSON document being read. + /// + protected JsonElement RootElement { get; private set; } + + private bool disposedValue; + + /// + /// Constructs a new . + /// + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// Thrown when any of the arguments are null. + protected TmjReaderBase( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement; + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + } + + /// + 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 + // ~TmjMapReader() + // { + // // 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/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs index dde9075..8c918b5 100644 --- a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; @@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj; /// /// A tileset reader for the Tiled JSON format. /// -public class TsjTilesetReader : ITilesetReader +public class TsjTilesetReader : TmjReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// A string containing a Tiled tileset in the Tiled JSON format. + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// A function that resolves custom types given their name. /// Thrown when any of the arguments are null. public TsjTilesetReader( string jsonString, + Func externalTilesetResolver, 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)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - 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); - GC.SuppressFinalize(this); - } + public Tileset ReadTileset() => ReadTileset(RootElement); } diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs index 1335d75..863e125 100644 --- a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -9,6 +9,9 @@ public abstract partial class TmxReaderBase { internal List ReadProperties() { + if (!_reader.IsStartElement("properties")) + return []; + return _reader.ReadList("properties", "property", (r) => { var name = r.GetRequiredAttribute("name"); @@ -39,7 +42,8 @@ public abstract partial class TmxReaderBase 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(), + PropertyType.Class => throw new XmlException("Class property must have a property type"), + PropertyType.Enum => throw new XmlException("Enum property must have a property type"), _ => throw new XmlException("Invalid property type") }; return property; @@ -49,7 +53,6 @@ public abstract partial class TmxReaderBase internal IProperty ReadPropertyWithCustomType() { var isClass = _reader.GetOptionalAttribute("type") == "class"; - if (isClass) { return ReadClassProperty(); @@ -62,17 +65,24 @@ public abstract partial class TmxReaderBase { var name = _reader.GetRequiredAttribute("name"); var propertyType = _reader.GetRequiredAttribute("propertytype"); - var customTypeDef = _customTypeResolver(propertyType); + if (customTypeDef is CustomClassDefinition ccd) { - _reader.ReadStartElement("property"); - var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); - var props = ReadProperties(); - var mergedProps = Helpers.MergeProperties(propsInType, props); - - _reader.ReadEndElement(); - return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + if (!_reader.IsEmptyElement) + { + _reader.ReadStartElement("property"); + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + var props = ReadProperties(); + var mergedProps = Helpers.MergeProperties(propsInType, props); + _reader.ReadEndElement(); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + } + else + { + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType }; + } } throw new XmlException($"Unkonwn custom class definition: {propertyType}");