Merge pull request #57 from dcronqvist/custom-types-not-required

Make custom types optional
This commit is contained in:
dcronqvist 2024-11-21 18:24:47 +01:00 committed by GitHub
commit 6deb28c1ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 193 additions and 60 deletions

View file

@ -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. 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] > [!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 ### Class properties

View file

@ -73,12 +73,12 @@ public class Program
return templateReader.ReadTemplate(); return templateReader.ReadTemplate();
} }
private static ICustomTypeDefinition ResolveCustomType(string name) private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{ {
ICustomTypeDefinition[] allDefinedTypes = ICustomTypeDefinition[] allDefinedTypes =
[ [
new CustomClassDefinition() { Name = "a" }, 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<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
} }
} }

View file

@ -1,4 +1,3 @@
using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using DotTiled.Serialization; using DotTiled.Serialization;
@ -57,12 +56,12 @@ public partial class MapParser : Node2D
return templateReader.ReadTemplate(); return templateReader.ReadTemplate();
} }
private static ICustomTypeDefinition ResolveCustomType(string name) private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{ {
ICustomTypeDefinition[] allDefinedTypes = ICustomTypeDefinition[] allDefinedTypes =
[ [
new CustomClassDefinition() { Name = "a" }, 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<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
} }
} }

View file

@ -246,7 +246,7 @@ public class LoaderTests
} }
[Fact] [Fact]
public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ThrowsException() public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ReturnsMapWithEmptyProperties()
{ {
// Arrange // Arrange
var resourceReader = Substitute.For<IResourceReader>(); var resourceReader = Substitute.For<IResourceReader>();
@ -270,8 +270,11 @@ public class LoaderTests
var customTypeDefinitions = Enumerable.Empty<ICustomTypeDefinition>(); var customTypeDefinitions = Enumerable.Empty<ICustomTypeDefinition>();
var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions); var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions);
// Act & Assert // Act
Assert.Throws<KeyNotFoundException>(() => loader.LoadMap("map.tmx")); var result = loader.LoadMap("map.tmx");
// Assert
DotTiledAssert.AssertProperties([], result.Properties);
} }
[Fact] [Fact]

View file

@ -32,9 +32,14 @@ public partial class MapReaderTests
using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset(); return tilesetReader.ReadTileset();
} }
ICustomTypeDefinition ResolveCustomType(string name) Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{ {
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
} }
using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType); using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -28,9 +28,14 @@ public partial class TmjMapReaderTests
using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType); using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset(); return tilesetReader.ReadTileset();
} }
ICustomTypeDefinition ResolveCustomType(string name) Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{ {
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
} }
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType); using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -28,9 +28,14 @@ public partial class TmxMapReaderTests
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType); using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset(); return tilesetReader.ReadTileset();
} }
ICustomTypeDefinition ResolveCustomType(string name) Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{ {
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
} }
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType); using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -86,13 +86,16 @@ internal static partial class Helpers
}; };
} }
internal static List<IProperty> ResolveClassProperties(string className, Func<string, ICustomTypeDefinition> customTypeResolver) internal static List<IProperty> ResolveClassProperties(string className, Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
if (string.IsNullOrWhiteSpace(className)) if (string.IsNullOrWhiteSpace(className))
return null; return null;
var customType = customTypeResolver(className) ?? throw new InvalidOperationException($"Could not resolve custom type '{className}'."); 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."); throw new InvalidOperationException($"Custom type '{className}' is not a class.");
return CreateInstanceOfCustomClass(ccd, customTypeResolver); return CreateInstanceOfCustomClass(ccd, customTypeResolver);
@ -100,17 +103,31 @@ internal static partial class Helpers
internal static List<IProperty> CreateInstanceOfCustomClass( internal static List<IProperty> CreateInstanceOfCustomClass(
CustomClassDefinition customClassDefinition, CustomClassDefinition customClassDefinition,
Func<string, ICustomTypeDefinition> customTypeResolver) Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
return customClassDefinition.Members.Select(x => return customClassDefinition.Members.Select(x =>
{ {
if (x is ClassProperty cp) if (x is ClassProperty cp)
{
var resolvedType = customTypeResolver(cp.PropertyType);
if (!resolvedType.HasValue)
{ {
return new ClassProperty return new ClassProperty
{ {
Name = cp.Name, Name = cp.Name,
PropertyType = cp.PropertyType, PropertyType = cp.PropertyType,
Value = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver) 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(ccd, customTypeResolver)
}; };
} }

View file

@ -12,7 +12,7 @@ public class Loader
{ {
private readonly IResourceReader _resourceReader; private readonly IResourceReader _resourceReader;
private readonly IResourceCache _resourceCache; private readonly IResourceCache _resourceCache;
private readonly IDictionary<string, ICustomTypeDefinition> _customTypeDefinitions; private readonly Dictionary<string, ICustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Loader"/> class with the given <paramref name="resourceReader"/>, <paramref name="resourceCache"/>, and <paramref name="customTypeDefinitions"/>. /// Initializes a new instance of the <see cref="Loader"/> class with the given <paramref name="resourceReader"/>, <paramref name="resourceCache"/>, and <paramref name="customTypeDefinitions"/>.
@ -114,5 +114,11 @@ public class Loader
return templateReader.ReadTemplate(); return templateReader.ReadTemplate();
}); });
private ICustomTypeDefinition CustomTypeResolver(string name) => _customTypeDefinitions[name]; private Optional<ICustomTypeDefinition> CustomTypeResolver(string name)
{
if (_customTypeDefinitions.TryGetValue(name, out var customTypeDefinition))
return new Optional<ICustomTypeDefinition>(customTypeDefinition);
return Optional<ICustomTypeDefinition>.Empty;
}
} }

View file

@ -14,7 +14,7 @@ public class MapReader : IMapReader
// External resolvers // External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver; private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver; private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver; private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _mapStringReader; private readonly StringReader _mapStringReader;
private readonly XmlReader _xmlReader; private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class MapReader : IMapReader
string map, string map,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -14,7 +14,7 @@ public class TemplateReader : ITemplateReader
// External resolvers // External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver; private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver; private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver; private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _templateStringReader; private readonly StringReader _templateStringReader;
private readonly XmlReader _xmlReader; private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class TemplateReader : ITemplateReader
string template, string template,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -14,7 +14,7 @@ public class TilesetReader : ITilesetReader
// External resolvers // External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver; private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver; private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver; private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _tilesetStringReader; private readonly StringReader _tilesetStringReader;
private readonly XmlReader _xmlReader; private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class TilesetReader : ITilesetReader
string tileset, string tileset,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -19,7 +19,7 @@ public class TjTemplateReader : TmjReaderBase, ITemplateReader
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base( Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ } { }

View file

@ -19,7 +19,7 @@ public class TmjMapReader : TmjReaderBase, IMapReader
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base( Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ } { }

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -63,8 +64,28 @@ public abstract partial class TmjReaderBase
var propertyType = element.GetRequiredProperty<string>("propertytype"); var propertyType = element.GetRequiredProperty<string>("propertytype");
var customTypeDef = _customTypeResolver(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 (!element.TryGetProperty("value", out var valueElement))
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Value = ReadPropertiesInsideClass(valueElement, new CustomClassDefinition
{
Name = propertyType,
Members = []
})
};
}
if (customTypeDef.Value is not CustomClassDefinition ccd)
throw new JsonException($"Custom type {propertyType} is not a class.");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = element.GetOptionalPropertyCustom<List<IProperty>>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]); var props = element.GetOptionalPropertyCustom<List<IProperty>>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]);
var mergedProps = Helpers.MergeProperties(propsInType, props); var mergedProps = Helpers.MergeProperties(propsInType, props);
@ -77,9 +98,6 @@ public abstract partial class TmjReaderBase
}; };
} }
throw new JsonException($"Unknown custom class '{propertyType}'.");
}
internal List<IProperty> ReadPropertiesInsideClass( internal List<IProperty> ReadPropertiesInsideClass(
JsonElement element, JsonElement element,
CustomClassDefinition customClassDefinition) CustomClassDefinition customClassDefinition)
@ -91,6 +109,33 @@ public abstract partial class TmjReaderBase
if (!element.TryGetProperty(prop.Name, out var propElement)) if (!element.TryGetProperty(prop.Name, out var propElement))
continue; 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 IProperty property = prop.Type switch
{ {
PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() }, PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
@ -100,8 +145,8 @@ public abstract partial class TmjReaderBase
PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs<string>(), CultureInfo.InvariantCulture) }, PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs<string>(), CultureInfo.InvariantCulture) },
PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() }, PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs<uint>() }, PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs<uint>() },
PropertyType.Class => new ClassProperty { Name = prop.Name, PropertyType = ((ClassProperty)prop).PropertyType, Value = ReadPropertiesInsideClass(propElement, (CustomClassDefinition)_customTypeResolver(((ClassProperty)prop).PropertyType)) },
PropertyType.Enum => ReadEnumProperty(propElement), PropertyType.Enum => ReadEnumProperty(propElement),
PropertyType.Class => throw new NotImplementedException("Class properties should be handled elsewhere"),
_ => throw new JsonException("Invalid property type") _ => throw new JsonException("Invalid property type")
}; };
@ -115,7 +160,7 @@ public abstract partial class TmjReaderBase
{ {
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype"); var propertyType = element.GetRequiredProperty<string>("propertytype");
var typeInXml = element.GetOptionalPropertyParseable<PropertyType>("type", (s) => s switch var typeInJson = element.GetOptionalPropertyParseable<PropertyType>("type", (s) => s switch
{ {
"string" => PropertyType.String, "string" => PropertyType.String,
"int" => PropertyType.Int, "int" => PropertyType.Int,
@ -123,8 +168,24 @@ public abstract partial class TmjReaderBase
}).GetValueOr(PropertyType.String); }).GetValueOr(PropertyType.String);
var customTypeDef = _customTypeResolver(propertyType); var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced) if (!customTypeDef.HasValue)
throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); {
if (typeInJson == PropertyType.String)
{
var value = element.GetRequiredProperty<string>("value");
var values = value.Split(',').Select(v => v.Trim()).ToHashSet();
return new EnumProperty { Name = name, PropertyType = propertyType, Value = values };
}
else
{
var value = element.GetRequiredProperty<int>("value");
var values = new HashSet<string> { 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) if (ced.StorageType == CustomEnumStorageType.String)
{ {

View file

@ -13,7 +13,7 @@ public abstract partial class TmjReaderBase : IDisposable
// External resolvers // External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver; private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver; private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver; private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
/// <summary> /// <summary>
/// The root element of the JSON document being read. /// The root element of the JSON document being read.
@ -34,7 +34,7 @@ public abstract partial class TmjReaderBase : IDisposable
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement; RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement;
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));

View file

@ -19,7 +19,7 @@ public class TsjTilesetReader : TmjReaderBase, ITilesetReader
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base( Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ } { }

View file

@ -16,7 +16,7 @@ public class TmxMapReader : TmxReaderBase, IMapReader
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base( Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ } { }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
@ -91,25 +92,35 @@ public abstract partial class TmxReaderBase
var propertyType = _reader.GetRequiredAttribute("propertytype"); var propertyType = _reader.GetRequiredAttribute("propertytype");
var customTypeDef = _customTypeResolver(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) if (!_reader.IsEmptyElement)
{ {
_reader.ReadStartElement("property"); _reader.ReadStartElement("property");
var props = ReadProperties();
_reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = props };
}
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
}
if (customTypeDef.Value is not CustomClassDefinition ccd)
throw new XmlException($"Custom type {propertyType} is not a class.");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
if (!_reader.IsEmptyElement)
{
_reader.ReadStartElement("property");
var props = ReadProperties(); var props = ReadProperties();
var mergedProps = Helpers.MergeProperties(propsInType, props); var mergedProps = Helpers.MergeProperties(propsInType, props);
_reader.ReadEndElement(); _reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; 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}"); return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType };
} }
internal EnumProperty ReadEnumProperty() internal EnumProperty ReadEnumProperty()
@ -124,8 +135,26 @@ public abstract partial class TmxReaderBase
}) ?? PropertyType.String; }) ?? PropertyType.String;
var customTypeDef = _customTypeResolver(propertyType); var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced) // If the custom enum definition is not found,
throw new XmlException($"Unknown custom enum definition: {propertyType}. Enums must be defined"); // 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<int>("value");
var values = new HashSet<string> { 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) 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}");
} }
} }

View file

@ -11,7 +11,7 @@ public abstract partial class TmxReaderBase : IDisposable
// External resolvers // External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver; private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver; private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver; private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly XmlReader _reader; private readonly XmlReader _reader;
private bool disposedValue; private bool disposedValue;
@ -28,7 +28,7 @@ public abstract partial class TmxReaderBase : IDisposable
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{ {
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); _reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));

View file

@ -16,7 +16,7 @@ public class TsxTilesetReader : TmxReaderBase, ITilesetReader
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base( Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ } { }

View file

@ -16,7 +16,7 @@ public class TxTemplateReader : TmxReaderBase, ITemplateReader
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base( Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ } { }