diff --git a/docs/docs/essentials/custom-properties.md b/docs/docs/essentials/custom-properties.md index 6ac6b38..0ea066f 100644 --- a/docs/docs/essentials/custom-properties.md +++ b/docs/docs/essentials/custom-properties.md @@ -73,7 +73,10 @@ In addition to these primitive property types, [Tiled also supports more complex 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#. This section will guide you through how to define custom property types in DotTiled and how to map properties in loaded maps to C# classes or enums. > [!NOTE] -> In the future, DotTiled could provide a way to configure the use of custom property types such that they aren't necessary to be defined, given that you have set the `Resolve object types and properties` setting in Tiled. +> While custom types are powerful, they will incur a bit of overhead as you attempt to sync them between Tiled and DotTiled. Defining custom types is recommended, but not necessary for simple use cases as Tiled supports arbitrary strings as classes. + +> [!IMPORTANT] +> If you choose to use custom types in your maps, but don't define them properly in DotTiled, you may get inconsistencies between the map in Tiled and the loaded map with DotTiled. If you still want to use custom types in Tiled without having to define them in DotTiled, it is recommended to set the `Resolve object types and properties` setting in Tiled to `true`. This will make Tiled resolve the custom types for you, but it will still require you to define the custom types in DotTiled if you want to access the properties in a type-safe manner. ### Class properties diff --git a/src/DotTiled.Examples/DotTiled.Example.Console/Program.cs b/src/DotTiled.Examples/DotTiled.Example.Console/Program.cs index 9dba3df..baf36ea 100644 --- a/src/DotTiled.Examples/DotTiled.Example.Console/Program.cs +++ b/src/DotTiled.Examples/DotTiled.Example.Console/Program.cs @@ -73,12 +73,12 @@ public class Program return templateReader.ReadTemplate(); } - private static ICustomTypeDefinition ResolveCustomType(string name) + private static Optional ResolveCustomType(string name) { ICustomTypeDefinition[] allDefinedTypes = [ new CustomClassDefinition() { Name = "a" }, ]; - return allDefinedTypes.FirstOrDefault(type => type.Name == name) ?? throw new InvalidOperationException(); + return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional(ctd) : Optional.Empty; } } diff --git a/src/DotTiled.Examples/DotTiled.Example.Godot/MapParser.cs b/src/DotTiled.Examples/DotTiled.Example.Godot/MapParser.cs index 5ad960b..8319d00 100644 --- a/src/DotTiled.Examples/DotTiled.Example.Godot/MapParser.cs +++ b/src/DotTiled.Examples/DotTiled.Example.Godot/MapParser.cs @@ -1,4 +1,3 @@ -using System; using System.Globalization; using System.Linq; using DotTiled.Serialization; @@ -57,12 +56,12 @@ public partial class MapParser : Node2D return templateReader.ReadTemplate(); } - private static ICustomTypeDefinition ResolveCustomType(string name) + private static Optional ResolveCustomType(string name) { ICustomTypeDefinition[] allDefinedTypes = [ new CustomClassDefinition() { Name = "a" }, ]; - return allDefinedTypes.FirstOrDefault(type => type.Name == name) ?? throw new InvalidOperationException(); + return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional(ctd) : Optional.Empty; } } diff --git a/src/DotTiled.Tests/UnitTests/Serialization/LoaderTests.cs b/src/DotTiled.Tests/UnitTests/Serialization/LoaderTests.cs index d54ab9f..7edff12 100644 --- a/src/DotTiled.Tests/UnitTests/Serialization/LoaderTests.cs +++ b/src/DotTiled.Tests/UnitTests/Serialization/LoaderTests.cs @@ -246,7 +246,7 @@ public class LoaderTests } [Fact] - public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ThrowsException() + public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ReturnsMapWithEmptyProperties() { // Arrange var resourceReader = Substitute.For(); @@ -270,8 +270,11 @@ public class LoaderTests var customTypeDefinitions = Enumerable.Empty(); var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions); - // Act & Assert - Assert.Throws(() => loader.LoadMap("map.tmx")); + // Act + var result = loader.LoadMap("map.tmx"); + + // Assert + DotTiledAssert.AssertProperties([], result.Properties); } [Fact] diff --git a/src/DotTiled.Tests/UnitTests/Serialization/MapReaderTests.cs b/src/DotTiled.Tests/UnitTests/Serialization/MapReaderTests.cs index 885f57e..dd6bcca 100644 --- a/src/DotTiled.Tests/UnitTests/Serialization/MapReaderTests.cs +++ b/src/DotTiled.Tests/UnitTests/Serialization/MapReaderTests.cs @@ -32,9 +32,14 @@ public partial class MapReaderTests using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - ICustomTypeDefinition ResolveCustomType(string name) + Optional ResolveCustomType(string name) { - return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd) + { + return new Optional(ctd); + } + + return Optional.Empty; } using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType); diff --git a/src/DotTiled.Tests/UnitTests/Serialization/Tmj/TmjMapReaderTests.cs b/src/DotTiled.Tests/UnitTests/Serialization/Tmj/TmjMapReaderTests.cs index a896a48..aa289f6 100644 --- a/src/DotTiled.Tests/UnitTests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/src/DotTiled.Tests/UnitTests/Serialization/Tmj/TmjMapReaderTests.cs @@ -28,9 +28,14 @@ public partial class TmjMapReaderTests using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - ICustomTypeDefinition ResolveCustomType(string name) + Optional ResolveCustomType(string name) { - return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd) + { + return new Optional(ctd); + } + + return Optional.Empty; } using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType); diff --git a/src/DotTiled.Tests/UnitTests/Serialization/Tmx/TmxMapReaderTests.cs b/src/DotTiled.Tests/UnitTests/Serialization/Tmx/TmxMapReaderTests.cs index b6e5813..82dba07 100644 --- a/src/DotTiled.Tests/UnitTests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/src/DotTiled.Tests/UnitTests/Serialization/Tmx/TmxMapReaderTests.cs @@ -28,9 +28,14 @@ public partial class TmxMapReaderTests using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - ICustomTypeDefinition ResolveCustomType(string name) + Optional ResolveCustomType(string name) { - return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd) + { + return new Optional(ctd); + } + + return Optional.Empty; } using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType); diff --git a/src/DotTiled/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index e784b80..d716293 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -86,13 +86,16 @@ internal static partial class Helpers }; } - internal static List ResolveClassProperties(string className, Func customTypeResolver) + internal static List ResolveClassProperties(string className, Func> customTypeResolver) { if (string.IsNullOrWhiteSpace(className)) return null; var customType = customTypeResolver(className) ?? throw new InvalidOperationException($"Could not resolve custom type '{className}'."); - if (customType is not CustomClassDefinition ccd) + if (!customType.HasValue) + return null; + + if (customType.Value is not CustomClassDefinition ccd) throw new InvalidOperationException($"Custom type '{className}' is not a class."); return CreateInstanceOfCustomClass(ccd, customTypeResolver); @@ -100,17 +103,31 @@ internal static partial class Helpers internal static List CreateInstanceOfCustomClass( CustomClassDefinition customClassDefinition, - Func customTypeResolver) + Func> customTypeResolver) { return customClassDefinition.Members.Select(x => { if (x is ClassProperty cp) { + var resolvedType = customTypeResolver(cp.PropertyType); + if (!resolvedType.HasValue) + { + return new ClassProperty + { + Name = cp.Name, + PropertyType = cp.PropertyType, + Value = [] + }; + } + + if (resolvedType.Value is not CustomClassDefinition ccd) + throw new InvalidOperationException($"Custom type '{cp.PropertyType}' is not a class."); + return new ClassProperty { Name = cp.Name, PropertyType = cp.PropertyType, - Value = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver) + Value = CreateInstanceOfCustomClass(ccd, customTypeResolver) }; } diff --git a/src/DotTiled/Serialization/Loader.cs b/src/DotTiled/Serialization/Loader.cs index 02e258c..dd4fc2a 100644 --- a/src/DotTiled/Serialization/Loader.cs +++ b/src/DotTiled/Serialization/Loader.cs @@ -12,7 +12,7 @@ public class Loader { private readonly IResourceReader _resourceReader; private readonly IResourceCache _resourceCache; - private readonly IDictionary _customTypeDefinitions; + private readonly Dictionary _customTypeDefinitions; /// /// Initializes a new instance of the class with the given , , and . @@ -114,5 +114,11 @@ public class Loader return templateReader.ReadTemplate(); }); - private ICustomTypeDefinition CustomTypeResolver(string name) => _customTypeDefinitions[name]; + private Optional CustomTypeResolver(string name) + { + if (_customTypeDefinitions.TryGetValue(name, out var customTypeDefinition)) + return new Optional(customTypeDefinition); + + return Optional.Empty; + } } diff --git a/src/DotTiled/Serialization/MapReader.cs b/src/DotTiled/Serialization/MapReader.cs index d202e8f..5a6f6a7 100644 --- a/src/DotTiled/Serialization/MapReader.cs +++ b/src/DotTiled/Serialization/MapReader.cs @@ -14,7 +14,7 @@ public class MapReader : IMapReader // External resolvers private readonly Func _externalTilesetResolver; private readonly Func _externalTemplateResolver; - private readonly Func _customTypeResolver; + private readonly Func> _customTypeResolver; private readonly StringReader _mapStringReader; private readonly XmlReader _xmlReader; @@ -33,7 +33,7 @@ public class MapReader : IMapReader string map, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) + Func> customTypeResolver) { _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); diff --git a/src/DotTiled/Serialization/TemplateReader.cs b/src/DotTiled/Serialization/TemplateReader.cs index bf210c0..d44da3e 100644 --- a/src/DotTiled/Serialization/TemplateReader.cs +++ b/src/DotTiled/Serialization/TemplateReader.cs @@ -14,7 +14,7 @@ public class TemplateReader : ITemplateReader // External resolvers private readonly Func _externalTilesetResolver; private readonly Func _externalTemplateResolver; - private readonly Func _customTypeResolver; + private readonly Func> _customTypeResolver; private readonly StringReader _templateStringReader; private readonly XmlReader _xmlReader; @@ -33,7 +33,7 @@ public class TemplateReader : ITemplateReader string template, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) + Func> customTypeResolver) { _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); diff --git a/src/DotTiled/Serialization/TilesetReader.cs b/src/DotTiled/Serialization/TilesetReader.cs index 180d269..3d7c83a 100644 --- a/src/DotTiled/Serialization/TilesetReader.cs +++ b/src/DotTiled/Serialization/TilesetReader.cs @@ -14,7 +14,7 @@ public class TilesetReader : ITilesetReader // External resolvers private readonly Func _externalTilesetResolver; private readonly Func _externalTemplateResolver; - private readonly Func _customTypeResolver; + private readonly Func> _customTypeResolver; private readonly StringReader _tilesetStringReader; private readonly XmlReader _xmlReader; @@ -33,7 +33,7 @@ public class TilesetReader : ITilesetReader string tileset, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) + Func> customTypeResolver) { _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); diff --git a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs index 792bb73..ef80970 100644 --- a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -19,7 +19,7 @@ public class TjTemplateReader : TmjReaderBase, ITemplateReader string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) : base( + Func> customTypeResolver) : base( jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) { } diff --git a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs index 8ceb211..cffa036 100644 --- a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs +++ b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -19,7 +19,7 @@ public class TmjMapReader : TmjReaderBase, IMapReader string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) : base( + Func> customTypeResolver) : base( jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) { } diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs index b877382..1ffff1c 100644 --- a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -63,21 +64,38 @@ public abstract partial class TmjReaderBase var propertyType = element.GetRequiredProperty("propertytype"); var customTypeDef = _customTypeResolver(propertyType); - if (customTypeDef is CustomClassDefinition ccd) + // If the custom class definition is not found, + // we assume an empty class definition. + if (!customTypeDef.HasValue) { - var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); - var props = element.GetOptionalPropertyCustom>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]); - var mergedProps = Helpers.MergeProperties(propsInType, props); + if (!element.TryGetProperty("value", out var valueElement)) + return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] }; return new ClassProperty { Name = name, PropertyType = propertyType, - Value = mergedProps + Value = ReadPropertiesInsideClass(valueElement, new CustomClassDefinition + { + Name = propertyType, + Members = [] + }) }; } - throw new JsonException($"Unknown custom class '{propertyType}'."); + if (customTypeDef.Value is not CustomClassDefinition ccd) + throw new JsonException($"Custom type {propertyType} is not a class."); + + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + var props = element.GetOptionalPropertyCustom>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]); + var mergedProps = Helpers.MergeProperties(propsInType, props); + + return new ClassProperty + { + Name = name, + PropertyType = propertyType, + Value = mergedProps + }; } internal List ReadPropertiesInsideClass( @@ -91,6 +109,33 @@ public abstract partial class TmjReaderBase if (!element.TryGetProperty(prop.Name, out var propElement)) continue; + if (prop is ClassProperty classProp) + { + var resolvedCustomType = _customTypeResolver(classProp.PropertyType); + if (!resolvedCustomType.HasValue) + { + resultingProps.Add(new ClassProperty + { + Name = classProp.Name, + PropertyType = classProp.PropertyType, + Value = [] + }); + continue; + } + + if (resolvedCustomType.Value is not CustomClassDefinition ccd) + throw new JsonException($"Custom type '{classProp.PropertyType}' is not a class."); + + var readProps = ReadPropertiesInsideClass(propElement, ccd); + resultingProps.Add(new ClassProperty + { + Name = classProp.Name, + PropertyType = classProp.PropertyType, + Value = readProps + }); + continue; + } + IProperty property = prop.Type switch { PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs() }, @@ -100,8 +145,8 @@ public abstract partial class TmjReaderBase 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), + PropertyType.Class => throw new NotImplementedException("Class properties should be handled elsewhere"), _ => throw new JsonException("Invalid property type") }; @@ -115,7 +160,7 @@ public abstract partial class TmjReaderBase { var name = element.GetRequiredProperty("name"); var propertyType = element.GetRequiredProperty("propertytype"); - var typeInXml = element.GetOptionalPropertyParseable("type", (s) => s switch + var typeInJson = element.GetOptionalPropertyParseable("type", (s) => s switch { "string" => PropertyType.String, "int" => PropertyType.Int, @@ -123,8 +168,24 @@ public abstract partial class TmjReaderBase }).GetValueOr(PropertyType.String); var customTypeDef = _customTypeResolver(propertyType); - if (customTypeDef is not CustomEnumDefinition ced) - throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); + if (!customTypeDef.HasValue) + { + if (typeInJson == PropertyType.String) + { + var value = element.GetRequiredProperty("value"); + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + var value = element.GetRequiredProperty("value"); + var values = new HashSet { value.ToString(CultureInfo.InvariantCulture) }; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + } + + if (customTypeDef.Value is not CustomEnumDefinition ced) + throw new JsonException($"Custom type '{propertyType}' is not an enum."); if (ced.StorageType == CustomEnumStorageType.String) { diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs index f32100a..c63b913 100644 --- a/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs @@ -13,7 +13,7 @@ public abstract partial class TmjReaderBase : IDisposable // External resolvers private readonly Func _externalTilesetResolver; private readonly Func _externalTemplateResolver; - private readonly Func _customTypeResolver; + private readonly Func> _customTypeResolver; /// /// The root element of the JSON document being read. @@ -34,7 +34,7 @@ public abstract partial class TmjReaderBase : IDisposable string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) + Func> customTypeResolver) { RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement; _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); diff --git a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs index 5816c8a..87f4b59 100644 --- a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -19,7 +19,7 @@ public class TsjTilesetReader : TmjReaderBase, ITilesetReader string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) : base( + Func> customTypeResolver) : base( jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) { } diff --git a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs index 6029481..e993a4c 100644 --- a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -16,7 +16,7 @@ public class TmxMapReader : TmxReaderBase, IMapReader XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) : base( + Func> customTypeResolver) : base( reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) { } diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs index 32b6a5c..03d3a74 100644 --- a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml; @@ -91,25 +92,35 @@ public abstract partial class TmxReaderBase var propertyType = _reader.GetRequiredAttribute("propertytype"); var customTypeDef = _customTypeResolver(propertyType); - if (customTypeDef is CustomClassDefinition ccd) + // If the custom class definition is not found, + // we assume an empty class definition. + if (!customTypeDef.HasValue) { 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 }; + return new ClassProperty { Name = name, PropertyType = propertyType, Value = props }; } + + return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] }; } - throw new XmlException($"Unkonwn custom class definition: {propertyType}"); + if (customTypeDef.Value is not CustomClassDefinition ccd) + throw new XmlException($"Custom type {propertyType} is not a class."); + + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + if (!_reader.IsEmptyElement) + { + _reader.ReadStartElement("property"); + var props = ReadProperties(); + var mergedProps = Helpers.MergeProperties(propsInType, props); + _reader.ReadEndElement(); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + } + + return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType }; } internal EnumProperty ReadEnumProperty() @@ -124,8 +135,26 @@ public abstract partial class TmxReaderBase }) ?? 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 the custom enum definition is not found, + // we assume an empty enum definition. + if (!customTypeDef.HasValue) + { + if (typeInXml == PropertyType.String) + { + var value = _reader.GetRequiredAttribute("value"); + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + var value = _reader.GetRequiredAttributeParseable("value"); + var values = new HashSet { value.ToString(CultureInfo.InvariantCulture) }; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + } + + if (customTypeDef.Value is not CustomEnumDefinition ced) + throw new XmlException($"Custom defined type {propertyType} is not an enum."); if (ced.StorageType == CustomEnumStorageType.String) { @@ -169,6 +198,6 @@ public abstract partial class TmxReaderBase } } - throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}"); + throw new XmlException($"Unable to read enum property {name} with type {propertyType}"); } } diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs index e30af82..e3fe2d0 100644 --- a/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs @@ -11,7 +11,7 @@ public abstract partial class TmxReaderBase : IDisposable // External resolvers private readonly Func _externalTilesetResolver; private readonly Func _externalTemplateResolver; - private readonly Func _customTypeResolver; + private readonly Func> _customTypeResolver; private readonly XmlReader _reader; private bool disposedValue; @@ -28,7 +28,7 @@ public abstract partial class TmxReaderBase : IDisposable XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) + Func> customTypeResolver) { _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 f0dbcc9..195f767 100644 --- a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -16,7 +16,7 @@ public class TsxTilesetReader : TmxReaderBase, ITilesetReader XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) : base( + Func> customTypeResolver) : base( reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) { } diff --git a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs index 72da9dc..8a9d3b1 100644 --- a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -16,7 +16,7 @@ public class TxTemplateReader : TmxReaderBase, ITemplateReader XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - Func customTypeResolver) : base( + Func> customTypeResolver) : base( reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) { }