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 analyzers --verify-no-changes src/DotTiled.sln
BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj
BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts
BENCHMARK_SOURCES = src/DotTiled.Benchmark/Program.cs src/DotTiled.Benchmark/DotTiled.Benchmark.csproj
BENCHMARK_OUTPUTDIR = src/DotTiled.Benchmark/BenchmarkDotNet.Artifacts
.PHONY: benchmark
benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md
$(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES)
dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR)
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.
@ -66,15 +66,15 @@ Tiled supports a variety of property types, which are represented in the DotTile
- `object` - <xref:DotTiled.Model.ObjectProperty>
- `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:
@ -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
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.
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
- 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")]
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();
}

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_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_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)
{
var templateJson = TestData.GetRawStringFor($"{fileDir}/{source}");
using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions);
using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, ResolveCustomType);
return templateReader.ReadTemplate();
}
Tileset ResolveTileset(string source)
{
var tilesetJson = TestData.GetRawStringFor($"{fileDir}/{source}");
using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions);
using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions);
ICustomTypeDefinition ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType);
// Act
var map = mapReader.ReadMap();

View file

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

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj;
/// <summary>
/// A template reader for reading Tiled JSON templates.
/// </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>
/// Constructs a new <see cref="TjTemplateReader"/>.
/// </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="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>
public TjTemplateReader(
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }
/// <inheritdoc/>
public Template ReadTemplate()
{
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);
}
public Template ReadTemplate() => ReadTemplate(RootElement);
}

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.Collections.Generic;
using System.Text.Json;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
@ -8,73 +6,24 @@ namespace DotTiled.Serialization.Tmj;
/// <summary>
/// A map reader for reading Tiled JSON maps.
/// </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>
/// 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="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>
public TmjMapReader(
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }
/// <inheritdoc/>
public Map ReadMap()
{
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);
}
public Map ReadMap() => ReadMap(RootElement);
}

View file

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

View file

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

View file

@ -1,15 +1,12 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
internal partial class Tmj
public abstract partial class TmjReaderBase
{
internal static ImageLayer ReadImageLayer(
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal ImageLayer ReadImageLayer(JsonElement element)
{
var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name");
@ -21,7 +18,7 @@ internal partial class Tmj
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 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 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.Globalization;
using System.Text.Json;
@ -6,13 +5,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
internal partial class Tmj
public abstract partial class TmjReaderBase
{
internal static Map ReadMap(
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal Map ReadMap(JsonElement element)
{
var version = element.GetRequiredProperty<string>("version");
var tiledVersion = element.GetRequiredProperty<string>("tiledversion");
@ -58,10 +53,10 @@ internal partial class Tmj
var nextObjectID = element.GetRequiredProperty<uint>("nextobjectid");
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<Tileset> tilesets = element.GetOptionalPropertyCustom<List<Tileset>>("tilesets", e => e.GetValueAsList<Tileset>(el => ReadTileset(el, externalTilesetResolver, 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)), []);
return new Map
{

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
@ -7,12 +6,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
internal partial class Tmj
public abstract partial class TmjReaderBase
{
internal static ObjectLayer ReadObjectLayer(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal ObjectLayer ReadObjectLayer(JsonElement element)
{
var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name");
@ -24,7 +20,7 @@ internal partial class Tmj
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 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 y = element.GetOptionalProperty<uint>("y", 0);
@ -38,7 +34,7 @@ internal partial class Tmj
_ => throw new JsonException($"Unknown draw order '{s}'.")
}, 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
{
@ -63,10 +59,7 @@ internal partial class Tmj
};
}
internal static Model.Object ReadObject(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal Model.Object ReadObject(JsonElement element)
{
uint? idDefault = null;
string nameDefault = "";
@ -87,7 +80,7 @@ internal partial class Tmj
var template = element.GetOptionalProperty<string?>("template", null);
if (template is not null)
{
var resolvedTemplate = externalTemplateResolver(template);
var resolvedTemplate = _externalTemplateResolver(template);
var templObj = resolvedTemplate.Object;
idDefault = templObj.ID;
@ -114,7 +107,7 @@ internal partial class Tmj
var point = element.GetOptionalProperty<bool>("point", pointDefault);
var polygon = element.GetOptionalPropertyCustom<List<Vector2>?>("polygon", ReadPoints, polygonDefault);
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 text = element.GetOptionalPropertyCustom<TextObject?>("text", ReadText, null);
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.Text.Json;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
internal partial class Tmj
public abstract partial class TmjReaderBase
{
internal static TileLayer ReadTileLayer(
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal TileLayer ReadTileLayer(JsonElement element)
{
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 parallaxx = element.GetOptionalProperty<float>("parallaxx", 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 repeatY = element.GetOptionalProperty<bool>("repeaty", false);
var startX = element.GetOptionalProperty<int>("startx", 0);

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
@ -6,13 +5,9 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
internal partial class Tmj
public abstract partial class TmjReaderBase
{
internal static Tileset ReadTileset(
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal Tileset ReadTileset(JsonElement element)
{
var backgroundColor = element.GetOptionalPropertyParseable<Color?>("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var @class = element.GetOptionalProperty<string>("class", "");
@ -44,7 +39,7 @@ internal partial class Tmj
"bottomright" => ObjectAlignment.BottomRight,
_ => throw new JsonException($"Unknown object alignment '{s}'")
}, ObjectAlignment.Unspecified);
var properties = element.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []);
var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var source = element.GetOptionalProperty<string?>("source", null);
var spacing = element.GetOptionalProperty<uint?>("spacing", null);
var tileCount = element.GetOptionalProperty<uint?>("tilecount", null);
@ -57,20 +52,17 @@ internal partial class Tmj
"grid" => TileRenderSize.Grid,
_ => throw new JsonException($"Unknown tile render size '{s}'")
}, TileRenderSize.Tile);
var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []);
var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", ReadTiles, []);
var tileWidth = element.GetOptionalProperty<uint?>("tilewidth", null);
var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var type = element.GetOptionalProperty<string?>("type", null);
var version = element.GetOptionalProperty<string?>("version", 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 (externalTilesetResolver is null)
throw new JsonException("External tileset resolver is required to resolve external tilesets.");
var resolvedTileset = externalTilesetResolver(source);
var resolvedTileset = _externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source;
return resolvedTileset;
@ -159,10 +151,7 @@ internal partial class Tmj
};
}
internal static List<Tile> ReadTiles(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) =>
internal List<Tile> ReadTiles(JsonElement element) =>
element.GetValueAsList<Tile>(e =>
{
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 width = e.GetOptionalProperty<uint>("width", imageWidth ?? 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 properties = e.GetOptionalPropertyCustom("properties", el => ReadProperties(el, customTypeDefinitions), []);
var properties = e.GetOptionalPropertyCustom("properties", ReadProperties, []);
// var terrain, replaced by wangsets
var type = e.GetOptionalProperty<string>("type", "");
@ -216,14 +205,12 @@ internal partial class Tmj
};
}
internal static Wangset ReadWangset(
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal Wangset ReadWangset(JsonElement element)
{
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 properties = element.GetOptionalPropertyCustom("properties", e => ReadProperties(e, customTypeDefinitions), []);
var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []);
var tile = element.GetOptionalProperty<int>("tile", 0);
var type = element.GetOptionalProperty<string>("type", "");
var wangTiles = element.GetOptionalPropertyCustom<List<WangTile>>("wangtiles", e => e.GetValueAsList<WangTile>(ReadWangTile), []);
@ -239,15 +226,13 @@ internal partial class Tmj
};
}
internal static WangColor ReadWangColor(
JsonElement element,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal WangColor ReadWangColor(JsonElement element)
{
var @class = element.GetOptionalProperty<string>("class", "");
var color = element.GetRequiredPropertyParseable<Color>("color", s => Color.Parse(s, CultureInfo.InvariantCulture));
var name = element.GetRequiredProperty<string>("name");
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);
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.Collections.Generic;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmj;
@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj;
/// <summary>
/// A tileset reader for the Tiled JSON format.
/// </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>
/// Constructs a new <see cref="TsjTilesetReader"/>.
/// </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="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>
public TsjTilesetReader(
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }
/// <inheritdoc/>
public Tileset ReadTileset()
{
var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement;
return Tmj.ReadTileset(
rootElement,
_ => throw new NotSupportedException("External tilesets cannot refer to other external tilesets."),
_externalTemplateResolver,
_customTypeDefinitions);
}
/// <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);
}
public Tileset ReadTileset() => ReadTileset(RootElement);
}

View file

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