Tmj reader is now base class and new properties docs

This commit is contained in:
Daniel Cronqvist 2024-08-26 21:36:44 +02:00
parent 11f1ef783e
commit ab8173bb06
29 changed files with 702 additions and 433 deletions

View file

@ -13,10 +13,10 @@ lint:
dotnet format style --verify-no-changes src/DotTiled.sln dotnet format style --verify-no-changes src/DotTiled.sln
dotnet format analyzers --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_SOURCES = src/DotTiled.Benchmark/Program.cs src/DotTiled.Benchmark/DotTiled.Benchmark.csproj
BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts BENCHMARK_OUTPUTDIR = src/DotTiled.Benchmark/BenchmarkDotNet.Artifacts
.PHONY: benchmark .PHONY: benchmark
benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md
$(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES) $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES)
dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) dotnet run --project src/DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR)

View file

@ -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. [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` - <xref:DotTiled.Model.ObjectProperty> - `object` - <xref:DotTiled.Model.ObjectProperty>
- `string` - <xref:DotTiled.Model.StringProperty> - `string` - <xref:DotTiled.Model.StringProperty>
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 <xref:DotTiled.Model.ICustomTypeDefinition>. 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 <xref:DotTiled.Model.ICustomTypeDefinition>. 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 <xref:DotTiled.Model.ClassProperty> 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 <xref:DotTiled.Model.StringProperty>. Similarly, if the values are stored as integers, DotTiled will parse those as <xref:DotTiled.Model.IntProperty>. 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 <xref:DotTiled.Model.ClassProperty> class. For all your custom `class` types in Tiled, you must create an equivalent <xref:DotTiled.Model.CustomClassDefinition> 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 <xref:DotTiled.Model.ClassProperty> 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: 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 <xref:DotTiled.Model.CustomEnumDefinition>. 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 ### [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. 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 <xref:DotTiled.Model.IHasProperties> interface with a method like `GetMappedProperty<T>(string propertyName)`, where `T` is a class that matches the structure of the `class` property in Tiled. The idea is to expand on the <xref:DotTiled.Model.IHasProperties> interface with a method like `GetMappedProperty<T>(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 <xref:DotTiled.Model.ICustomTypeDefinition> 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<MonsterSpawner>();
var entityTypeDefinition = CustomEnumDefinition.FromEnum<EntityType>();
// ...
var map = LoadMap();
var monsterSpawner = map.GetMappedProperty<MonsterSpawner>("monsterSpawnerPropertyInMap");
var entityType = map.GetMappedProperty<EntityType>("entityTypePropertyInMap");
```
Finally, it might be possible to also make some kind of exporting functionality for <xref:DotTiled.Model.ICustomTypeDefinition>. 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.

View file

@ -4,4 +4,4 @@
- name: Essentials - name: Essentials
- href: loading-a-map.md - href: loading-a-map.md
- href: accessing-properties.md - href: custom-properties.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -47,7 +47,7 @@ namespace DotTiled.Benchmark
[Benchmark(Baseline = true, Description = "DotTiled")] [Benchmark(Baseline = true, Description = "DotTiled")]
public DotTiled.Model.Map LoadWithDotTiledFromInMemoryTmjString() 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(); return mapReader.ReadMap();
} }

View file

@ -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<ICustomTypeDefinition>()], ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty<ICustomTypeDefinition>()], ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty<ICustomTypeDefinition>()], ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_with_deep_props/map-with-deep-props", (string f) => MapWithDeepProps(), MapWithDeepPropsCustomTypeDefinitions()],
]; ];
} }

View file

@ -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<ICustomTypeDefinition> 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
}
]
}
];
}

View file

@ -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
}

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<properties>
<property name="customouterclassprop" type="class" propertytype="CustomOuterClass"/>
<property name="customouterclasspropset" type="class" propertytype="CustomOuterClass">
<properties>
<property name="customclasspropinclass" type="class" propertytype="CustomClass">
<properties>
<property name="boolinclass" type="bool" value="true"/>
<property name="floatinclass" type="float" value="13.37"/>
</properties>
</property>
</properties>
</property>
</properties>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">
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
</data>
</layer>
</map>

View file

@ -20,16 +20,20 @@ public partial class TmjMapReaderTests
Template ResolveTemplate(string source) Template ResolveTemplate(string source)
{ {
var templateJson = TestData.GetRawStringFor($"{fileDir}/{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(); return templateReader.ReadTemplate();
} }
Tileset ResolveTileset(string source) Tileset ResolveTileset(string source)
{ {
var tilesetJson = TestData.GetRawStringFor($"{fileDir}/{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(); 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 // Act
var map = mapReader.ReadMap(); var map = mapReader.ReadMap();

View file

@ -73,8 +73,25 @@ internal static partial class Helpers
}; };
} }
internal static List<IProperty> CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => internal static List<IProperty> CreateInstanceOfCustomClass(
customClassDefinition.Members.Select(x => x.Clone()).ToList(); CustomClassDefinition customClassDefinition,
Func<string, ICustomTypeDefinition> 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<IProperty> MergeProperties(IList<IProperty>? baseProperties, IList<IProperty>? overrideProperties) internal static IList<IProperty> MergeProperties(IList<IProperty>? baseProperties, IList<IProperty>? overrideProperties)
{ {

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj;
/// <summary> /// <summary>
/// A template reader for reading Tiled JSON templates. /// A template reader for reading Tiled JSON templates.
/// </summary> /// </summary>
public class TjTemplateReader : ITemplateReader public class TjTemplateReader : TmjReaderBase, ITemplateReader
{ {
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly string _jsonString;
private bool disposedValue;
private readonly IReadOnlyCollection<ICustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Constructs a new <see cref="TjTemplateReader"/>. /// Constructs a new <see cref="TjTemplateReader"/>.
/// </summary> /// </summary>
/// <param name="jsonString">A string containing a Tiled template in the Tiled JSON format.</param> /// <param name="jsonString">A string containing a Tiled map in the Tiled JSON format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param> /// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param> /// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeDefinitions">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param> /// <param name="customTypeResolver">A function that resolves custom types given their name.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception> /// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
public TjTemplateReader( public TjTemplateReader(
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) Func<string, ICustomTypeDefinition> customTypeResolver) : base(
{ jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
_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));
}
/// <inheritdoc/> /// <inheritdoc/>
public Template ReadTemplate() public Template ReadTemplate() => ReadTemplate(RootElement);
{
var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement;
return Tmj.ReadTemplate(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
}
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
} }

View file

@ -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<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
var type = element.GetRequiredProperty<string>("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}'.")
};
}
}

View file

@ -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<IProperty> ReadProperties(
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) =>
element.GetValueAsList<IProperty>(e =>
{
var name = e.GetRequiredProperty<string>("name");
var type = e.GetOptionalPropertyParseable<PropertyType>("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<string>("value") },
PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty<uint>("value") },
PropertyType.Class => ReadClassProperty(e, customTypeDefinitions),
_ => throw new JsonException("Invalid property type")
};
return property!;
});
internal static ClassProperty ReadClassProperty(
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype");
var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType);
if (customTypeDef is CustomClassDefinition ccd)
{
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd);
var props = element.GetOptionalPropertyCustom<List<IProperty>>("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<IProperty> ReadCustomClassProperties(
JsonElement element,
CustomClassDefinition customClassDefinition,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
List<IProperty> 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<string>() },
PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs<int>() },
PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs<float>() },
PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs<bool>() },
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.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs<uint>() },
PropertyType.Class => ReadClassProperty(propElement, customTypeDefinitions),
_ => throw new JsonException("Invalid property type")
};
Helpers.ReplacePropertyInList(resultingProps, property);
}
return resultingProps;
}
}

View file

@ -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<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
var type = element.GetRequiredProperty<string>("type");
var tileset = element.GetOptionalPropertyCustom<Tileset?>("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null);
var @object = element.GetRequiredPropertyCustom<Model.Object>("object", el => ReadObject(el, externalTemplateResolver, customTypeDefinitions));
return new Template
{
Tileset = tileset,
Object = @object
};
}
}

View file

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
@ -8,73 +6,24 @@ namespace DotTiled.Serialization.Tmj;
/// <summary> /// <summary>
/// A map reader for reading Tiled JSON maps. /// A map reader for reading Tiled JSON maps.
/// </summary> /// </summary>
public class TmjMapReader : IMapReader public class TmjMapReader : TmjReaderBase, IMapReader
{ {
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly string _jsonString;
private bool disposedValue;
private readonly IReadOnlyCollection<ICustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Constructs a new <see cref="TmjMapReader"/>. /// Constructs a new <see cref="TmjMapReader"/>.
/// </summary> /// </summary>
/// <param name="jsonString">A string containing a Tiled map in the Tiled JSON format.</param> /// <param name="jsonString">A string containing a Tiled map in the Tiled JSON format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param> /// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param> /// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeDefinitions">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param> /// <param name="customTypeResolver">A function that resolves custom types given their name.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception> /// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
public TmjMapReader( public TmjMapReader(
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) Func<string, ICustomTypeDefinition> customTypeResolver) : base(
{ jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
_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));
}
/// <inheritdoc/> /// <inheritdoc/>
public Map ReadMap() public Map ReadMap() => ReadMap(RootElement);
{
var jsonDoc = JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement;
return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
}
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
} }

View file

@ -5,7 +5,7 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding)
{ {

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
@ -6,12 +5,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static Group ReadGroup( internal Group ReadGroup(JsonElement element)
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var id = element.GetRequiredProperty<uint>("id"); var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
@ -23,8 +19,8 @@ internal partial class Tmj
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f); var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f); var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f); var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var layers = element.GetOptionalPropertyCustom<List<BaseLayer>>("layers", e => e.GetValueAsList<BaseLayer>(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); var layers = element.GetOptionalPropertyCustom<List<BaseLayer>>("layers", e => e.GetValueAsList<BaseLayer>(ReadLayer), []);
return new Group return new Group
{ {

View file

@ -1,15 +1,12 @@
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static ImageLayer ReadImageLayer( internal ImageLayer ReadImageLayer(JsonElement element)
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var id = element.GetRequiredProperty<uint>("id"); var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
@ -21,7 +18,7 @@ internal partial class Tmj
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f); var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f); var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f); var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var image = element.GetRequiredProperty<string>("image"); var image = element.GetRequiredProperty<string>("image");
var repeatX = element.GetOptionalProperty<bool>("repeatx", false); var repeatX = element.GetOptionalProperty<bool>("repeatx", false);

View file

@ -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<string>("type");
return type switch
{
"tilelayer" => ReadTileLayer(element),
"objectgroup" => ReadObjectLayer(element),
"imagelayer" => ReadImageLayer(element),
"group" => ReadGroup(element),
_ => throw new JsonException($"Unsupported layer type '{type}'.")
};
}
}

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
@ -6,13 +5,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static Map ReadMap( internal Map ReadMap(JsonElement element)
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var version = element.GetRequiredProperty<string>("version"); var version = element.GetRequiredProperty<string>("version");
var tiledVersion = element.GetRequiredProperty<string>("tiledversion"); var tiledVersion = element.GetRequiredProperty<string>("tiledversion");
@ -58,10 +53,10 @@ internal partial class Tmj
var nextObjectID = element.GetRequiredProperty<uint>("nextobjectid"); var nextObjectID = element.GetRequiredProperty<uint>("nextobjectid");
var infinite = element.GetOptionalProperty<bool>("infinite", false); var infinite = element.GetOptionalProperty<bool>("infinite", false);
var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
List<BaseLayer> layers = element.GetOptionalPropertyCustom<List<BaseLayer>>("layers", e => e.GetValueAsList<BaseLayer>(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); List<BaseLayer> layers = element.GetOptionalPropertyCustom<List<BaseLayer>>("layers", e => e.GetValueAsList<BaseLayer>(el => ReadLayer(el)), []);
List<Tileset> tilesets = element.GetOptionalPropertyCustom<List<Tileset>>("tilesets", e => e.GetValueAsList<Tileset>(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []); List<Tileset> tilesets = element.GetOptionalPropertyCustom<List<Tileset>>("tilesets", e => e.GetValueAsList<Tileset>(el => ReadTileset(el)), []);
return new Map return new Map
{ {

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
@ -7,12 +6,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static ObjectLayer ReadObjectLayer( internal ObjectLayer ReadObjectLayer(JsonElement element)
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var id = element.GetRequiredProperty<uint>("id"); var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
@ -24,7 +20,7 @@ internal partial class Tmj
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f); var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f); var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f); var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var x = element.GetOptionalProperty<uint>("x", 0); var x = element.GetOptionalProperty<uint>("x", 0);
var y = element.GetOptionalProperty<uint>("y", 0); var y = element.GetOptionalProperty<uint>("y", 0);
@ -38,7 +34,7 @@ internal partial class Tmj
_ => throw new JsonException($"Unknown draw order '{s}'.") _ => throw new JsonException($"Unknown draw order '{s}'.")
}, DrawOrder.TopDown); }, DrawOrder.TopDown);
var objects = element.GetOptionalPropertyCustom<List<Model.Object>>("objects", e => e.GetValueAsList<Model.Object>(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); var objects = element.GetOptionalPropertyCustom<List<Model.Object>>("objects", e => e.GetValueAsList<Model.Object>(el => ReadObject(el)), []);
return new ObjectLayer return new ObjectLayer
{ {
@ -63,10 +59,7 @@ internal partial class Tmj
}; };
} }
internal static Model.Object ReadObject( internal Model.Object ReadObject(JsonElement element)
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
uint? idDefault = null; uint? idDefault = null;
string nameDefault = ""; string nameDefault = "";
@ -87,7 +80,7 @@ internal partial class Tmj
var template = element.GetOptionalProperty<string?>("template", null); var template = element.GetOptionalProperty<string?>("template", null);
if (template is not null) if (template is not null)
{ {
var resolvedTemplate = externalTemplateResolver(template); var resolvedTemplate = _externalTemplateResolver(template);
var templObj = resolvedTemplate.Object; var templObj = resolvedTemplate.Object;
idDefault = templObj.ID; idDefault = templObj.ID;
@ -114,7 +107,7 @@ internal partial class Tmj
var point = element.GetOptionalProperty<bool>("point", pointDefault); var point = element.GetOptionalProperty<bool>("point", pointDefault);
var polygon = element.GetOptionalPropertyCustom<List<Vector2>?>("polygon", ReadPoints, polygonDefault); var polygon = element.GetOptionalPropertyCustom<List<Vector2>?>("polygon", ReadPoints, polygonDefault);
var polyline = element.GetOptionalPropertyCustom<List<Vector2>?>("polyline", ReadPoints, polylineDefault); var polyline = element.GetOptionalPropertyCustom<List<Vector2>?>("polyline", ReadPoints, polylineDefault);
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, propertiesDefault);
var rotation = element.GetOptionalProperty<float>("rotation", rotationDefault); var rotation = element.GetOptionalProperty<float>("rotation", rotationDefault);
var text = element.GetOptionalPropertyCustom<TextObject?>("text", ReadText, null); var text = element.GetOptionalPropertyCustom<TextObject?>("text", ReadText, null);
var type = element.GetOptionalProperty<string>("type", typeDefault); var type = element.GetOptionalProperty<string>("type", typeDefault);

View file

@ -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<IProperty> ReadProperties(JsonElement element) =>
element.GetValueAsList<IProperty>(e =>
{
var name = e.GetRequiredProperty<string>("name");
var type = e.GetOptionalPropertyParseable<PropertyType>("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<string?>("propertytype", null);
if (propertyType is not null)
{
return ReadPropertyWithCustomType(e);
}
IProperty property = type switch
{
PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty<uint>("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<string?>("type", null) == "class";
if (isClass)
{
return ReadClassProperty(element);
}
return ReadEnumProperty(element);
}
internal ClassProperty ReadClassProperty(JsonElement element)
{
var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype");
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd)
{
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = element.GetOptionalPropertyCustom<List<IProperty>>("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<IProperty> ReadPropertiesInsideClass(
JsonElement element,
CustomClassDefinition customClassDefinition)
{
List<IProperty> 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<string>() },
PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs<int>() },
PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs<float>() },
PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs<bool>() },
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.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),
_ => throw new JsonException("Invalid property type")
};
resultingProps.Add(property);
}
return resultingProps;
}
internal EnumProperty ReadEnumProperty(JsonElement element)
{
var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype");
var typeInXml = element.GetOptionalPropertyParseable<PropertyType>("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<string>("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<string> { value } };
}
}
else if (ced.StorageType == CustomEnumStorageType.Int)
{
var value = element.GetRequiredProperty<int>("value");
if (ced.ValueAsFlags)
{
var allValues = ced.Values;
var enumValues = new HashSet<string>();
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<string> { enumValue } };
}
}
throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined");
}
}

View file

@ -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<string>("type");
var tileset = element.GetOptionalPropertyCustom<Tileset?>("tileset", ReadTileset, null);
var @object = element.GetRequiredPropertyCustom<Model.Object>("object", ReadObject);
return new Template
{
Tileset = tileset,
Object = @object
};
}
}

View file

@ -1,15 +1,12 @@
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static TileLayer ReadTileLayer( internal TileLayer ReadTileLayer(JsonElement element)
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var compression = element.GetOptionalPropertyParseable<DataCompression?>("compression", s => s switch var compression = element.GetOptionalPropertyParseable<DataCompression?>("compression", s => s switch
{ {
@ -35,7 +32,7 @@ internal partial class Tmj
var opacity = element.GetOptionalProperty<float>("opacity", 1.0f); var opacity = element.GetOptionalProperty<float>("opacity", 1.0f);
var parallaxx = element.GetOptionalProperty<float>("parallaxx", 1.0f); var parallaxx = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxy = element.GetOptionalProperty<float>("parallaxy", 1.0f); var parallaxy = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var repeatX = element.GetOptionalProperty<bool>("repeatx", false); var repeatX = element.GetOptionalProperty<bool>("repeatx", false);
var repeatY = element.GetOptionalProperty<bool>("repeaty", false); var repeatY = element.GetOptionalProperty<bool>("repeaty", false);
var startX = element.GetOptionalProperty<int>("startx", 0); var startX = element.GetOptionalProperty<int>("startx", 0);

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
@ -6,13 +5,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
internal partial class Tmj public abstract partial class TmjReaderBase
{ {
internal static Tileset ReadTileset( internal Tileset ReadTileset(JsonElement element)
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var backgroundColor = element.GetOptionalPropertyParseable<Color?>("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var backgroundColor = element.GetOptionalPropertyParseable<Color?>("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var @class = element.GetOptionalProperty<string>("class", ""); var @class = element.GetOptionalProperty<string>("class", "");
@ -44,7 +39,7 @@ internal partial class Tmj
"bottomright" => ObjectAlignment.BottomRight, "bottomright" => ObjectAlignment.BottomRight,
_ => throw new JsonException($"Unknown object alignment '{s}'") _ => throw new JsonException($"Unknown object alignment '{s}'")
}, ObjectAlignment.Unspecified); }, ObjectAlignment.Unspecified);
var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var source = element.GetOptionalProperty<string?>("source", null); var source = element.GetOptionalProperty<string?>("source", null);
var spacing = element.GetOptionalProperty<uint?>("spacing", null); var spacing = element.GetOptionalProperty<uint?>("spacing", null);
var tileCount = element.GetOptionalProperty<uint?>("tilecount", null); var tileCount = element.GetOptionalProperty<uint?>("tilecount", null);
@ -57,20 +52,17 @@ internal partial class Tmj
"grid" => TileRenderSize.Grid, "grid" => TileRenderSize.Grid,
_ => throw new JsonException($"Unknown tile render size '{s}'") _ => throw new JsonException($"Unknown tile render size '{s}'")
}, TileRenderSize.Tile); }, TileRenderSize.Tile);
var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []); var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", ReadTiles, []);
var tileWidth = element.GetOptionalProperty<uint?>("tilewidth", null); var tileWidth = element.GetOptionalProperty<uint?>("tilewidth", null);
var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var type = element.GetOptionalProperty<string?>("type", null); var type = element.GetOptionalProperty<string?>("type", null);
var version = element.GetOptionalProperty<string?>("version", null); var version = element.GetOptionalProperty<string?>("version", null);
var transformations = element.GetOptionalPropertyCustom<Transformations?>("transformations", ReadTransformations, null); var transformations = element.GetOptionalPropertyCustom<Transformations?>("transformations", ReadTransformations, null);
var wangsets = element.GetOptionalPropertyCustom<List<Wangset>?>("wangsets", el => el.GetValueAsList<Wangset>(e => ReadWangset(e, customTypeDefinitions)), null); var wangsets = element.GetOptionalPropertyCustom<List<Wangset>?>("wangsets", el => el.GetValueAsList<Wangset>(e => ReadWangset(e)), null);
if (source is not null) if (source is not null)
{ {
if (externalTilesetResolver is null) var resolvedTileset = _externalTilesetResolver(source);
throw new JsonException("External tileset resolver is required to resolve external tilesets.");
var resolvedTileset = externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID; resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source; resolvedTileset.Source = source;
return resolvedTileset; return resolvedTileset;
@ -159,10 +151,7 @@ internal partial class Tmj
}; };
} }
internal static List<Tile> ReadTiles( internal List<Tile> ReadTiles(JsonElement element) =>
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) =>
element.GetValueAsList<Tile>(e => element.GetValueAsList<Tile>(e =>
{ {
var animation = e.GetOptionalPropertyCustom<List<Frame>?>("animation", e => e.GetValueAsList<Frame>(ReadFrame), null); var animation = e.GetOptionalPropertyCustom<List<Frame>?>("animation", e => e.GetValueAsList<Frame>(ReadFrame), null);
@ -174,9 +163,9 @@ internal partial class Tmj
var y = e.GetOptionalProperty<uint>("y", 0); var y = e.GetOptionalProperty<uint>("y", 0);
var width = e.GetOptionalProperty<uint>("width", imageWidth ?? 0); var width = e.GetOptionalProperty<uint>("width", imageWidth ?? 0);
var height = e.GetOptionalProperty<uint>("height", imageHeight ?? 0); var height = e.GetOptionalProperty<uint>("height", imageHeight ?? 0);
var objectGroup = e.GetOptionalPropertyCustom<ObjectLayer?>("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null); var objectGroup = e.GetOptionalPropertyCustom<ObjectLayer?>("objectgroup", e => ReadObjectLayer(e), null);
var probability = e.GetOptionalProperty<float>("probability", 0.0f); var probability = e.GetOptionalProperty<float>("probability", 0.0f);
var properties = e.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []); var properties = e.GetOptionalPropertyCustom("properties", ReadProperties, []);
// var terrain, replaced by wangsets // var terrain, replaced by wangsets
var type = e.GetOptionalProperty<string>("type", ""); var type = e.GetOptionalProperty<string>("type", "");
@ -216,14 +205,12 @@ internal partial class Tmj
}; };
} }
internal static Wangset ReadWangset( internal Wangset ReadWangset(JsonElement element)
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var @clalss = element.GetOptionalProperty<string>("class", ""); var @clalss = element.GetOptionalProperty<string>("class", "");
var colors = element.GetOptionalPropertyCustom<List<WangColor>>("colors", e => e.GetValueAsList<WangColor>(el => ReadWangColor(el, customTypeDefinitions)), []); var colors = element.GetOptionalPropertyCustom<List<WangColor>>("colors", e => e.GetValueAsList<WangColor>(el => ReadWangColor(el)), []);
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var tile = element.GetOptionalProperty<int>("tile", 0); var tile = element.GetOptionalProperty<int>("tile", 0);
var type = element.GetOptionalProperty<string>("type", ""); var type = element.GetOptionalProperty<string>("type", "");
var wangTiles = element.GetOptionalPropertyCustom<List<WangTile>>("wangtiles", e => e.GetValueAsList<WangTile>(ReadWangTile), []); var wangTiles = element.GetOptionalPropertyCustom<List<WangTile>>("wangtiles", e => e.GetValueAsList<WangTile>(ReadWangTile), []);
@ -239,15 +226,13 @@ internal partial class Tmj
}; };
} }
internal static WangColor ReadWangColor( internal WangColor ReadWangColor(JsonElement element)
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
var @class = element.GetOptionalProperty<string>("class", ""); var @class = element.GetOptionalProperty<string>("class", "");
var color = element.GetRequiredPropertyParseable<Color>("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); var color = element.GetRequiredPropertyParseable<Color>("color", s => Color.Parse(s, CultureInfo.InvariantCulture));
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
var probability = element.GetOptionalProperty<float>("probability", 1.0f); var probability = element.GetOptionalProperty<float>("probability", 1.0f);
var properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []); var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var tile = element.GetOptionalProperty<int>("tile", 0); var tile = element.GetOptionalProperty<int>("tile", 0);
return new WangColor return new WangColor

View file

@ -0,0 +1,74 @@
using System;
using System.Text.Json;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
/// <summary>
/// Base class for Tiled JSON format readers.
/// </summary>
public abstract partial class TmjReaderBase : IDisposable
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
/// <summary>
/// The root element of the JSON document being read.
/// </summary>
protected JsonElement RootElement { get; private set; }
private bool disposedValue;
/// <summary>
/// Constructs a new <see cref="TmjMapReader"/>.
/// </summary>
/// <param name="jsonString">A string containing a Tiled map in the Tiled JSON format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeResolver">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
protected TmjReaderBase(
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> 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));
}
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmj; namespace DotTiled.Serialization.Tmj;
@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj;
/// <summary> /// <summary>
/// A tileset reader for the Tiled JSON format. /// A tileset reader for the Tiled JSON format.
/// </summary> /// </summary>
public class TsjTilesetReader : ITilesetReader public class TsjTilesetReader : TmjReaderBase, ITilesetReader
{ {
// External resolvers
private readonly Func<string, Template> _externalTemplateResolver;
private readonly string _jsonString;
private bool disposedValue;
private readonly IReadOnlyCollection<ICustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Constructs a new <see cref="TsjTilesetReader"/>. /// Constructs a new <see cref="TsjTilesetReader"/>.
/// </summary> /// </summary>
/// <param name="jsonString">A string containing a Tiled tileset in the Tiled JSON format.</param> /// <param name="jsonString">A string containing a Tiled map in the Tiled JSON format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param> /// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeDefinitions">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param> /// <param name="customTypeResolver">A function that resolves custom types given their name.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception> /// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
public TsjTilesetReader( public TsjTilesetReader(
string jsonString, string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) Func<string, ICustomTypeDefinition> customTypeResolver) : base(
{ jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); { }
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
/// <inheritdoc/> /// <inheritdoc/>
public Tileset ReadTileset() public Tileset ReadTileset() => ReadTileset(RootElement);
{
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);
}
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
} }

View file

@ -9,6 +9,9 @@ public abstract partial class TmxReaderBase
{ {
internal List<IProperty> ReadProperties() internal List<IProperty> ReadProperties()
{ {
if (!_reader.IsStartElement("properties"))
return [];
return _reader.ReadList("properties", "property", (r) => return _reader.ReadList("properties", "property", (r) =>
{ {
var name = r.GetRequiredAttribute("name"); var name = r.GetRequiredAttribute("name");
@ -39,7 +42,8 @@ public abstract partial class TmxReaderBase
PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") }, PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("value") }, PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("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") _ => throw new XmlException("Invalid property type")
}; };
return property; return property;
@ -49,7 +53,6 @@ public abstract partial class TmxReaderBase
internal IProperty ReadPropertyWithCustomType() internal IProperty ReadPropertyWithCustomType()
{ {
var isClass = _reader.GetOptionalAttribute("type") == "class"; var isClass = _reader.GetOptionalAttribute("type") == "class";
if (isClass) if (isClass)
{ {
return ReadClassProperty(); return ReadClassProperty();
@ -62,17 +65,24 @@ public abstract partial class TmxReaderBase
{ {
var name = _reader.GetRequiredAttribute("name"); var name = _reader.GetRequiredAttribute("name");
var propertyType = _reader.GetRequiredAttribute("propertytype"); var propertyType = _reader.GetRequiredAttribute("propertytype");
var customTypeDef = _customTypeResolver(propertyType); var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd) if (customTypeDef is CustomClassDefinition ccd)
{ {
_reader.ReadStartElement("property"); if (!_reader.IsEmptyElement)
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); {
var props = ReadProperties(); _reader.ReadStartElement("property");
var mergedProps = Helpers.MergeProperties(propsInType, props); var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = ReadProperties();
_reader.ReadEndElement(); var mergedProps = Helpers.MergeProperties(propsInType, props);
return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; _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}"); throw new XmlException($"Unkonwn custom class definition: {propertyType}");