Merge pull request #14 from dcronqvist/props-remodel

Remodel how properties work, and make readers a base class
This commit is contained in:
dcronqvist 2024-08-26 21:56:37 +02:00 committed by GitHub
commit 30c0d22f93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 2114 additions and 1484 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

@ -0,0 +1,161 @@
# 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.
## All classes that can contain properties
All classes that can contain custom properties implement the interface <xref:DotTiled.Model.IHasProperties> in some way. Below is an exhaustive list of all classes that can contain custom properties:
- <xref:DotTiled.Model.BaseLayer>
- <xref:DotTiled.Model.TileLayer>
- <xref:DotTiled.Model.ObjectLayer>
- <xref:DotTiled.Model.ImageLayer>
- <xref:DotTiled.Model.Group>
- <xref:DotTiled.Model.ClassProperty> (allows for recursive property objects)
- <xref:DotTiled.Model.CustomClassDefinition> (used to define custom Tiled property types)
- <xref:DotTiled.Model.Object>
- <xref:DotTiled.Model.EllipseObject>
- <xref:DotTiled.Model.PointObject>
- <xref:DotTiled.Model.PolygonObject>
- <xref:DotTiled.Model.PolylineObject>
- <xref:DotTiled.Model.RectangleObject>
- <xref:DotTiled.Model.TextObject>
- <xref:DotTiled.Model.TileObject>
- <xref:DotTiled.Model.Tileset>
- <xref:DotTiled.Model.Tile>
- <xref:DotTiled.Model.WangTile>
- <xref:DotTiled.Model.WangColor>
## How to access properties
To access the properties on one of the classes listed above, you will make use of the <xref:DotTiled.Model.IHasProperties> interface.
In situations where you know that a property must exist, and you simply want to retrieve it, you can use the <xref:DotTiled.Model.IHasProperties.GetProperty``1(System.String)> method like so:
```csharp
var map = LoadMap();
var propertyValue = map.GetProperty<BoolProperty>("boolPropertyInMap").Value;
```
If you are unsure whether a property exists, or you want to provide some kind of default behaviour if the property is not present, you can instead use the <xref:DotTiled.Model.IHasProperties.TryGetProperty``1(System.String,``0@)> method like so:
```csharp
var map = LoadMap();
if (map.TryGetProperty<BoolProperty>("boolPropertyInMap", out var property))
{
// Do something with existing property
var propertyValue = property.Value;
}
else
{
// Do something if property does not exist
}
```
For both methods, you can replace `BoolProperty` with any of the property types that Tiled supports. You can find a list of all property types and their corresponding classes in the [next section](#all-types-of-properties).
## All types of properties
Tiled supports a variety of property types, which are represented in the DotTiled library as classes that implement the <xref:DotTiled.Model.IProperty`1> interface. Below is a list of all property types that Tiled supports and their corresponding classes in DotTiled:
- `bool` - <xref:DotTiled.Model.BoolProperty>
- `color` - <xref:DotTiled.Model.ColorProperty>
- `float` - <xref:DotTiled.Model.FloatProperty>
- `file` - <xref:DotTiled.Model.FileProperty>
- `int` - <xref:DotTiled.Model.IntProperty>
- `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 <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.
## Custom types
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
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:
![MonsterSpawner class in Tiled UI](../images/monster-spawner-class.png)
The equivalent definition in DotTiled would look like the following:
```csharp
var monsterSpawnerDefinition = new CustomClassDefinition
{
Name = "MonsterSpawner",
UseAs = CustomClassUseAs.All, // Not really validated by DotTiled
Members = [ // Make sure that the default values match the Tiled UI
new BoolProperty { Name = "enabled", Value = true },
new IntProperty { Name = "maxSpawnAmount", Value = 10 },
new IntProperty { Name = "minSpawnAmount", Value = 0 },
new StringProperty { Name = "monsterNames", Value = "" }
]
};
```
### Enum properties
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.
For example, if you have a custom property type in Tiled that looks like this:
![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,3 +4,4 @@
- name: Essentials - name: Essentials
- href: loading-a-map.md - href: loading-a-map.md
- href: custom-properties.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -39,7 +39,7 @@ namespace DotTiled.Benchmark
{ {
using var stringReader = new StringReader(_tmxContents); using var stringReader = new StringReader(_tmxContents);
using var xmlReader = XmlReader.Create(stringReader); using var xmlReader = XmlReader.Create(stringReader);
using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), []); using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException());
return mapReader.ReadMap(); return mapReader.ReadMap();
} }
@ -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

@ -91,7 +91,7 @@ public static partial class DotTiledAssert
AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID));
AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite));
AssertProperties(actual.Properties, expected.Properties); AssertProperties(expected.Properties, actual.Properties);
Assert.NotNull(actual.Tilesets); Assert.NotNull(actual.Tilesets);
AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count");

View file

@ -4,7 +4,7 @@ namespace DotTiled.Tests;
public static partial class DotTiledAssert public static partial class DotTiledAssert
{ {
internal static void AssertProperties(Dictionary<string, IProperty>? expected, Dictionary<string, IProperty>? actual) internal static void AssertProperties(IList<IProperty>? expected, IList<IProperty>? actual)
{ {
if (expected is null) if (expected is null)
{ {
@ -14,18 +14,16 @@ public static partial class DotTiledAssert
Assert.NotNull(actual); Assert.NotNull(actual);
AssertEqual(expected.Count, actual.Count, "Properties.Count"); AssertEqual(expected.Count, actual.Count, "Properties.Count");
foreach (var kvp in expected) foreach (var prop in expected)
{ {
Assert.Contains(kvp.Key, actual.Keys); Assert.Contains(actual, p => p.Name == prop.Name);
AssertProperty((dynamic)kvp.Value, (dynamic)actual[kvp.Key]);
}
}
private static void AssertProperty(IProperty expected, IProperty actual) var actualProp = actual.First(p => p.Name == prop.Name);
{ AssertEqual(prop.Type, actualProp.Type, "Property.Type");
AssertEqual(expected.Type, actual.Type, "Property.Type"); AssertEqual(prop.Name, actualProp.Name, "Property.Name");
AssertEqual(expected.Name, actual.Name, "Property.Name");
AssertProperties((dynamic)actual, (dynamic)expected); AssertProperty((dynamic)prop, (dynamic)actualProp);
}
} }
private static void AssertProperty(StringProperty expected, StringProperty actual) => AssertEqual(expected.Value, actual.Value, "StringProperty.Value"); private static void AssertProperty(StringProperty expected, StringProperty actual) => AssertEqual(expected.Value, actual.Value, "StringProperty.Value");
@ -45,6 +43,16 @@ public static partial class DotTiledAssert
private static void AssertProperty(ClassProperty expected, ClassProperty actual) private static void AssertProperty(ClassProperty expected, ClassProperty actual)
{ {
AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType"); AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType");
AssertProperties(expected.Properties, actual.Properties); AssertProperties(expected.Value, actual.Value);
}
private static void AssertProperty(EnumProperty expected, EnumProperty actual)
{
AssertEqual(expected.PropertyType, actual.PropertyType, "EnumProperty.PropertyType");
AssertEqual(expected.Value.Count, actual.Value.Count, "EnumProperty.Value.Count");
foreach (var value in expected.Value)
{
Assert.Contains(actual.Value, v => v == value);
}
} }
} }

View file

@ -141,9 +141,9 @@ public static partial class DotTiledAssert
AssertEqual(expected.Height, actual.Height, nameof(Tile.Height)); AssertEqual(expected.Height, actual.Height, nameof(Tile.Height));
// Elements // Elements
AssertProperties(actual.Properties, expected.Properties); AssertProperties(expected.Properties, actual.Properties);
AssertImage(actual.Image, expected.Image); AssertImage(expected.Image, actual.Image);
AssertLayer((BaseLayer?)actual.ObjectLayer, (BaseLayer?)expected.ObjectLayer); AssertLayer((BaseLayer?)expected.ObjectLayer, (BaseLayer?)actual.ObjectLayer);
if (expected.Animation is not null) if (expected.Animation is not null)
{ {
Assert.NotNull(actual.Animation); Assert.NotNull(actual.Animation);

View file

@ -32,14 +32,15 @@ public static partial class TestData
public static IEnumerable<object[]> MapTests => public static IEnumerable<object[]> MapTests =>
[ [
["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty<CustomTypeDefinition>()], ["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty<CustomTypeDefinition>()], ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_with_custom_type_props/map-with-custom-type-props", (string f) => MapWithCustomTypeProps(), MapWithCustomTypePropsCustomTypeDefinitions()], ["Serialization/TestData/Map/map_with_custom_type_props/map-with-custom-type-props", (string f) => MapWithCustomTypeProps(), MapWithCustomTypePropsCustomTypeDefinitions()],
["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => MapWithEmbeddedTileset(), Array.Empty<CustomTypeDefinition>()], ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => MapWithEmbeddedTileset(), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty<CustomTypeDefinition>()], ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty<CustomTypeDefinition>()], ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty<ICustomTypeDefinition>()],
["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty<CustomTypeDefinition>()], ["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<CustomTypeDefinition>()], ["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<CustomTypeDefinition>()], ["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

@ -49,16 +49,15 @@ public partial class TestData
Width = 1, Width = 1,
Height = 1 Height = 1
}, },
Properties = new Dictionary<string, IProperty> Properties = [
{ new BoolProperty { Name = "tilesetbool", Value = true },
["tilesetbool"] = new BoolProperty { Name = "tilesetbool", Value = true }, new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) },
["tilesetcolor"] = new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, new FileProperty { Name = "tilesetfile", Value = "" },
["tilesetfile"] = new FileProperty { Name = "tilesetfile", Value = "" }, new FloatProperty { Name = "tilesetfloat", Value = 5.2f },
["tilesetfloat"] = new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, new IntProperty { Name = "tilesetint", Value = 9 },
["tilesetint"] = new IntProperty { Name = "tilesetint", Value = 9 }, new ObjectProperty { Name = "tilesetobject", Value = 0 },
["tilesetobject"] = new ObjectProperty { Name = "tilesetobject", Value = 0 }, new StringProperty { Name = "tilesetstring", Value = "hello world!" }
["tilesetstring"] = new StringProperty { Name = "tilesetstring", Value = "hello world!" } ],
},
Tiles = [ Tiles = [
new Tile new Tile
{ {

View file

@ -55,15 +55,15 @@ public partial class TestData
} }
} }
], ],
Properties = new Dictionary<string, IProperty> Properties =
{ [
["boolprop"] = new BoolProperty { Name = "boolprop", Value = true }, new BoolProperty { Name = "boolprop", Value = true },
["colorprop"] = new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) },
["fileprop"] = new FileProperty { Name = "fileprop", Value = "file.txt" }, new FileProperty { Name = "fileprop", Value = "file.txt" },
["floatprop"] = new FloatProperty { Name = "floatprop", Value = 4.2f }, new FloatProperty { Name = "floatprop", Value = 4.2f },
["intprop"] = new IntProperty { Name = "intprop", Value = 8 }, new IntProperty { Name = "intprop", Value = 8 },
["objectprop"] = new ObjectProperty { Name = "objectprop", Value = 5 }, new ObjectProperty { Name = "objectprop", Value = 5 },
["stringprop"] = new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" }
} ]
}; };
} }

View file

@ -55,28 +55,50 @@ public partial class TestData
} }
} }
], ],
Properties = new Dictionary<string, IProperty> Properties = [
{ new ClassProperty
["customclassprop"] = new ClassProperty
{ {
Name = "customclassprop", Name = "customclassprop",
PropertyType = "CustomClass", PropertyType = "CustomClass",
Properties = new Dictionary<string, IProperty> Value = [
{ new BoolProperty { Name = "boolinclass", Value = true },
["boolinclass"] = new BoolProperty { Name = "boolinclass", Value = true }, new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) },
["colorinclass"] = new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, new FileProperty { Name = "fileinclass", Value = "" },
["fileinclass"] = new FileProperty { Name = "fileinclass", Value = "" }, new FloatProperty { Name = "floatinclass", Value = 13.37f },
["floatinclass"] = new FloatProperty { Name = "floatinclass", Value = 13.37f }, new IntProperty { Name = "intinclass", Value = 0 },
["intinclass"] = new IntProperty { Name = "intinclass", Value = 0 }, new ObjectProperty { Name = "objectinclass", Value = 0 },
["objectinclass"] = new ObjectProperty { Name = "objectinclass", Value = 0 }, new StringProperty { Name = "stringinclass", Value = "This is a set string" }
["stringinclass"] = new StringProperty { Name = "stringinclass", Value = "This is a set string" } ]
} },
new EnumProperty
{
Name = "customenumstringprop",
PropertyType = "CustomEnumString",
Value = new HashSet<string> { "CustomEnumString_2" }
},
new EnumProperty
{
Name = "customenumstringflagsprop",
PropertyType = "CustomEnumStringFlags",
Value = new HashSet<string> { "CustomEnumStringFlags_1", "CustomEnumStringFlags_2" }
},
new EnumProperty
{
Name = "customenumintprop",
PropertyType = "CustomEnumInt",
Value = new HashSet<string> { "CustomEnumInt_4" }
},
new EnumProperty
{
Name = "customenumintflagsprop",
PropertyType = "CustomEnumIntFlags",
Value = new HashSet<string> { "CustomEnumIntFlags_2", "CustomEnumIntFlags_3" }
} }
} ]
}; };
// This comes from map-with-custom-type-props/propertytypes.json // This comes from map-with-custom-type-props/propertytypes.json
public static IReadOnlyCollection<CustomTypeDefinition> MapWithCustomTypePropsCustomTypeDefinitions() => [ public static IReadOnlyCollection<ICustomTypeDefinition> MapWithCustomTypePropsCustomTypeDefinitions() => [
new CustomClassDefinition new CustomClassDefinition
{ {
Name = "CustomClass", Name = "CustomClass",
@ -118,6 +140,50 @@ public partial class TestData
Value = "" Value = ""
} }
] ]
},
new CustomEnumDefinition
{
Name = "CustomEnumString",
StorageType = CustomEnumStorageType.String,
ValueAsFlags = false,
Values = [
"CustomEnumString_1",
"CustomEnumString_2",
"CustomEnumString_3"
]
},
new CustomEnumDefinition
{
Name = "CustomEnumStringFlags",
StorageType = CustomEnumStorageType.String,
ValueAsFlags = true,
Values = [
"CustomEnumStringFlags_1",
"CustomEnumStringFlags_2"
]
},
new CustomEnumDefinition
{
Name = "CustomEnumInt",
StorageType = CustomEnumStorageType.Int,
ValueAsFlags = false,
Values = [
"CustomEnumInt_1",
"CustomEnumInt_2",
"CustomEnumInt_3",
"CustomEnumInt_4",
]
},
new CustomEnumDefinition
{
Name = "CustomEnumIntFlags",
StorageType = CustomEnumStorageType.Int,
ValueAsFlags = true,
Values = [
"CustomEnumIntFlags_1",
"CustomEnumIntFlags_2",
"CustomEnumIntFlags_3"
]
} }
]; ];
} }

View file

@ -32,6 +32,30 @@
"floatinclass":13.37, "floatinclass":13.37,
"stringinclass":"This is a set string" "stringinclass":"This is a set string"
} }
},
{
"name":"customenumintflagsprop",
"propertytype":"CustomEnumIntFlags",
"type":"int",
"value":6
},
{
"name":"customenumintprop",
"propertytype":"CustomEnumInt",
"type":"int",
"value":3
},
{
"name":"customenumstringflagsprop",
"propertytype":"CustomEnumStringFlags",
"type":"string",
"value":"CustomEnumStringFlags_1,CustomEnumStringFlags_2"
},
{
"name":"customenumstringprop",
"propertytype":"CustomEnumString",
"type":"string",
"value":"CustomEnumString_2"
}], }],
"renderorder":"right-down", "renderorder":"right-down",
"tiledversion":"1.11.0", "tiledversion":"1.11.0",

View file

@ -8,6 +8,10 @@
<property name="stringinclass" value="This is a set string"/> <property name="stringinclass" value="This is a set string"/>
</properties> </properties>
</property> </property>
<property name="customenumintflagsprop" type="int" propertytype="CustomEnumIntFlags" value="6"/>
<property name="customenumintprop" type="int" propertytype="CustomEnumInt" value="3"/>
<property name="customenumstringflagsprop" propertytype="CustomEnumStringFlags" value="CustomEnumStringFlags_1,CustomEnumStringFlags_2"/>
<property name="customenumstringprop" propertytype="CustomEnumString" value="CustomEnumString_2"/>
</properties> </properties>
<layer id="1" name="Tile Layer 1" width="5" height="5"> <layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv"> <data encoding="csv">

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

@ -95,10 +95,9 @@ public partial class TestData
new Vector2(35.6667f, 32.3333f) new Vector2(35.6667f, 32.3333f)
], ],
Template = fileExt == "tmx" ? "poly.tx" : "poly.tj", Template = fileExt == "tmx" ? "poly.tx" : "poly.tj",
Properties = new Dictionary<string, IProperty> Properties = [
{ new StringProperty { Name = "templateprop", Value = "helo there" }
["templateprop"] = new StringProperty { Name = "templateprop", Value = "helo there" } ]
}
}, },
new TileObject new TileObject
{ {

View file

@ -11,7 +11,7 @@ public partial class TmjMapReaderTests
public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(
string testDataFile, string testDataFile,
Func<string, Map> expectedMap, Func<string, Map> expectedMap,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
// Arrange // Arrange
testDataFile += ".tmj"; testDataFile += ".tmj";
@ -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

@ -11,7 +11,7 @@ public partial class TmxMapReaderTests
public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(
string testDataFile, string testDataFile,
Func<string, Map> expectedMap, Func<string, Map> expectedMap,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{ {
// Arrange // Arrange
testDataFile += ".tmx"; testDataFile += ".tmx";
@ -20,16 +20,20 @@ public partial class TmxMapReaderTests
Template ResolveTemplate(string source) Template ResolveTemplate(string source)
{ {
using var xmlTemplateReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); using var xmlTemplateReader = TestData.GetXmlReaderFor($"{fileDir}/{source}");
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, ResolveCustomType);
return templateReader.ReadTemplate(); return templateReader.ReadTemplate();
} }
Tileset ResolveTileset(string source) Tileset ResolveTileset(string source)
{ {
using var xmlTilesetReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); using var xmlTilesetReader = TestData.GetXmlReaderFor($"{fileDir}/{source}");
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset(); return tilesetReader.ReadTileset();
} }
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); ICustomTypeDefinition ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
}
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType);
// Act // Act
var map = mapReader.ReadMap(); var map = mapReader.ReadMap();

View file

@ -7,7 +7,7 @@ namespace DotTiled.Model;
/// To check the type of a layer, <see href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching">use C# pattern matching</see>, /// To check the type of a layer, <see href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching">use C# pattern matching</see>,
/// or some other mechanism to determine the type of the layer at runtime. /// or some other mechanism to determine the type of the layer at runtime.
/// </summary> /// </summary>
public abstract class BaseLayer public abstract class BaseLayer : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// Unique ID of the layer. Each layer that is added to a map gets a unique ID. Even if a layer is deleted, no layer ever gets the same ID. /// Unique ID of the layer. Each layer that is added to a map gets a unique ID. Even if a layer is deleted, no layer ever gets the same ID.
@ -62,5 +62,8 @@ public abstract class BaseLayer
/// <summary> /// <summary>
/// Layer properties. /// Layer properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
} }

View file

@ -5,7 +5,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Base class for objects in object layers. /// Base class for objects in object layers.
/// </summary> /// </summary>
public abstract class Object public abstract class Object : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// Unique ID of the objects. Each object that is placed on a map gets a unique ID. Even if an object was deleted, no object gets the same ID. /// Unique ID of the objects. Each object that is placed on a map gets a unique ID. Even if an object was deleted, no object gets the same ID.
@ -60,5 +60,8 @@ public abstract class Object
/// <summary> /// <summary>
/// Object properties. /// Object properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
} }

View file

@ -90,7 +90,7 @@ public enum StaggerIndex
/// <summary> /// <summary>
/// Represents a Tiled map. /// Represents a Tiled map.
/// </summary> /// </summary>
public class Map public class Map : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// The TMX format version. Is incremented to match minor Tiled releases. /// The TMX format version. Is incremented to match minor Tiled releases.
@ -191,7 +191,10 @@ public class Map
/// <summary> /// <summary>
/// Map properties. /// Map properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
/// <summary> /// <summary>
/// List of tilesets used by the map. /// List of tilesets used by the map.

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a boolean property. /// Represents a boolean property.
/// </summary> /// </summary>
public class BoolProperty : IProperty public class BoolProperty : IProperty<bool>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace DotTiled.Model; namespace DotTiled.Model;
@ -6,7 +8,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a class property. /// Represents a class property.
/// </summary> /// </summary>
public class ClassProperty : IProperty public class ClassProperty : IHasProperties, IProperty<IList<IProperty>>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }
@ -23,13 +25,41 @@ public class ClassProperty : IProperty
/// <summary> /// <summary>
/// The properties of the class property. /// The properties of the class property.
/// </summary> /// </summary>
public required Dictionary<string, IProperty> Properties { get; set; } public required IList<IProperty> Value { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IProperty Clone() => new ClassProperty public IProperty Clone() => new ClassProperty
{ {
Name = Name, Name = Name,
PropertyType = PropertyType, PropertyType = PropertyType,
Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) Value = Value.Select(property => property.Clone()).ToList()
}; };
/// <inheritdoc/>
public IList<IProperty> GetProperties() => Value;
/// <inheritdoc/>
public T GetProperty<T>(string name) where T : IProperty
{
var property = Value.FirstOrDefault(_properties => _properties.Name == name) ?? throw new InvalidOperationException($"Property '{name}' not found.");
if (property is T prop)
{
return prop;
}
throw new InvalidOperationException($"Property '{name}' is not of type '{typeof(T).Name}'.");
}
/// <inheritdoc/>
public bool TryGetProperty<T>(string name, [NotNullWhen(true)] out T? property) where T : IProperty
{
if (Value.FirstOrDefault(_properties => _properties.Name == name) is T prop)
{
property = prop;
return true;
}
property = default;
return false;
}
} }

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a color property. /// Represents a color property.
/// </summary> /// </summary>
public class ColorProperty : IProperty public class ColorProperty : IProperty<Color>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -65,8 +65,14 @@ public enum CustomClassUseAs
/// Represents a custom class definition in Tiled. Refer to the /// Represents a custom class definition in Tiled. Refer to the
/// <see href="https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types">documentation of custom types to understand how they work</see>. /// <see href="https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types">documentation of custom types to understand how they work</see>.
/// </summary> /// </summary>
public class CustomClassDefinition : CustomTypeDefinition public class CustomClassDefinition : HasPropertiesBase, ICustomTypeDefinition
{ {
/// <inheritdoc/>
public uint ID { get; set; }
/// <inheritdoc/>
public required string Name { get; set; }
/// <summary> /// <summary>
/// The color of the custom class inside the Tiled editor. /// The color of the custom class inside the Tiled editor.
/// </summary> /// </summary>
@ -86,4 +92,7 @@ public class CustomClassDefinition : CustomTypeDefinition
/// The members of the custom class, with their names, types and default values. /// The members of the custom class, with their names, types and default values.
/// </summary> /// </summary>
public List<IProperty> Members { get; set; } = []; public List<IProperty> Members { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Members;
} }

View file

@ -22,8 +22,14 @@ public enum CustomEnumStorageType
/// Represents a custom enum definition in Tiled. Refer to the /// Represents a custom enum definition in Tiled. Refer to the
/// <see href="https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types">documentation of custom types to understand how they work</see>. /// <see href="https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types">documentation of custom types to understand how they work</see>.
/// </summary> /// </summary>
public class CustomEnumDefinition : CustomTypeDefinition public class CustomEnumDefinition : ICustomTypeDefinition
{ {
/// <inheritdoc/>
public uint ID { get; set; }
/// <inheritdoc/>
public required string Name { get; set; }
/// <summary> /// <summary>
/// The storage type of the custom enum. /// The storage type of the custom enum.
/// </summary> /// </summary>

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Base class for custom type definitions. /// Base class for custom type definitions.
/// </summary> /// </summary>
public abstract class CustomTypeDefinition public interface ICustomTypeDefinition
{ {
/// <summary> /// <summary>
/// The ID of the custom type. /// The ID of the custom type.
@ -13,5 +13,5 @@ public abstract class CustomTypeDefinition
/// <summary> /// <summary>
/// The name of the custom type. /// The name of the custom type.
/// </summary> /// </summary>
public string Name { get; set; } = ""; public string Name { get; set; }
} }

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
namespace DotTiled.Model;
/// <summary>
/// Represents an enum property.
/// </summary>
public class EnumProperty : IProperty<ISet<string>>
{
/// <inheritdoc/>
public required string Name { get; set; }
/// <inheritdoc/>
public PropertyType Type => Model.PropertyType.Enum;
/// <summary>
/// The type of the class property. This will be the name of a custom defined
/// type in Tiled.
/// </summary>
public required string PropertyType { get; set; }
/// <summary>
/// The value of the enum property.
/// </summary>
public required ISet<string> Value { get; set; }
/// <inheritdoc/>
public IProperty Clone() => new EnumProperty
{
Name = Name,
PropertyType = PropertyType,
Value = Value.ToHashSet()
};
/// <summary>
/// Determines whether the enum property is equal to the specified value.
/// For enums which have multiple values (e.g. flag enums), this method will only return true if it is the only value.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the enum property is equal to the specified value; otherwise, false.</returns>
public bool IsValue(string value) => Value.Contains(value) && Value.Count == 1;
/// <summary>
/// Determines whether the enum property has the specified value. This method is very similar to the common <see cref="System.Enum.HasFlag" /> method.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the enum property has the specified value as one of its values; otherwise, false.</returns>
public bool HasValue(string value) => Value.Contains(value);
}

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a file property. /// Represents a file property.
/// </summary> /// </summary>
public class FileProperty : IProperty public class FileProperty : IProperty<string>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a float property. /// Represents a float property.
/// </summary> /// </summary>
public class FloatProperty : IProperty public class FloatProperty : IProperty<float>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace DotTiled.Model;
/// <summary>
/// Interface for objects that have properties attached to them.
/// </summary>
public interface IHasProperties
{
/// <summary>
/// The properties attached to the object.
/// </summary>
IList<IProperty> GetProperties();
/// <summary>
/// Tries to get a property of the specified type with the specified name.
/// </summary>
/// <typeparam name="T">The type of the property to get.</typeparam>
/// <param name="name">The name of the property to get.</param>
/// <param name="property">The property with the specified name, if found.</param>
/// <returns>True if a property with the specified name was found; otherwise, false.</returns>
bool TryGetProperty<T>(string name, out T? property) where T : IProperty;
/// <summary>
/// Gets a property of the specified type with the specified name.
/// </summary>
/// <typeparam name="T">The type of the property to get.</typeparam>
/// <param name="name">The name of the property to get.</param>
/// <returns>The property with the specified name.</returns>
T GetProperty<T>(string name) where T : IProperty;
}
/// <summary>
/// Interface for objects that have properties attached to them.
/// </summary>
public abstract class HasPropertiesBase : IHasProperties
{
/// <inheritdoc/>
public abstract IList<IProperty> GetProperties();
/// <inheritdoc/>
/// <exception cref="KeyNotFoundException">Thrown when a property with the specified name is not found.</exception>
/// <exception cref="InvalidCastException">Thrown when a property with the specified name is not of the specified type.</exception>
public T GetProperty<T>(string name) where T : IProperty
{
var properties = GetProperties();
var property = properties.FirstOrDefault(_properties => _properties.Name == name) ?? throw new KeyNotFoundException($"Property '{name}' not found.");
if (property is T prop)
{
return prop;
}
throw new InvalidCastException($"Property '{name}' is not of type '{typeof(T).Name}'.");
}
/// <inheritdoc/>
public bool TryGetProperty<T>(string name, [NotNullWhen(true)] out T? property) where T : IProperty
{
var properties = GetProperties();
if (properties.FirstOrDefault(_properties => _properties.Name == name) is T prop)
{
property = prop;
return true;
}
property = default;
return false;
}
}

View file

@ -22,3 +22,15 @@ public interface IProperty
/// <returns>An identical, but non-reference-equal, instance of the same property.</returns> /// <returns>An identical, but non-reference-equal, instance of the same property.</returns>
IProperty Clone(); IProperty Clone();
} }
/// <summary>
/// Interface for properties that can be attached to objects, tiles, tilesets, maps etc.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
public interface IProperty<T> : IProperty
{
/// <summary>
/// The value of the property.
/// </summary>
public T Value { get; set; }
}

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents an integer property. /// Represents an integer property.
/// </summary> /// </summary>
public class IntProperty : IProperty public class IntProperty : IProperty<int>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents an object property. /// Represents an object property.
/// </summary> /// </summary>
public class ObjectProperty : IProperty public class ObjectProperty : IProperty<uint>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -43,5 +43,10 @@ public enum PropertyType
/// <summary> /// <summary>
/// A class property. /// A class property.
/// </summary> /// </summary>
Class Class,
/// <summary>
/// An enum property.
/// </summary>
Enum
} }

View file

@ -3,7 +3,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a string property. /// Represents a string property.
/// </summary> /// </summary>
public class StringProperty : IProperty public class StringProperty : IProperty<string>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public required string Name { get; set; } public required string Name { get; set; }

View file

@ -6,7 +6,7 @@ namespace DotTiled.Model;
/// Represents a single tile in a tileset, when using a collection of images to represent the tileset. /// Represents a single tile in a tileset, when using a collection of images to represent the tileset.
/// <see href="https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile">Tiled documentation for Tileset tiles</see> /// <see href="https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile">Tiled documentation for Tileset tiles</see>
/// </summary> /// </summary>
public class Tile public class Tile : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// The local tile ID within its tileset. /// The local tile ID within its tileset.
@ -46,7 +46,10 @@ public class Tile
/// <summary> /// <summary>
/// Tile properties. /// Tile properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
/// <summary> /// <summary>
/// The image representing this tile. Only used for tilesets that composed of a collection of images. /// The image representing this tile. Only used for tilesets that composed of a collection of images.

View file

@ -93,7 +93,7 @@ public enum FillMode
/// <summary> /// <summary>
/// A tileset is a collection of tiles that can be used in a tile layer, or by tile objects. /// A tileset is a collection of tiles that can be used in a tile layer, or by tile objects.
/// </summary> /// </summary>
public class Tileset public class Tileset : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// The TMX format version. Is incremented to match minor Tiled releases. /// The TMX format version. Is incremented to match minor Tiled releases.
@ -188,7 +188,10 @@ public class Tileset
/// <summary> /// <summary>
/// Tileset properties. /// Tileset properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
// public List<Terrain>? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization // public List<Terrain>? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization

View file

@ -5,7 +5,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Represents a Wang color in a Wang set. /// Represents a Wang color in a Wang set.
/// </summary> /// </summary>
public class WangColor public class WangColor : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// The name of this color. /// The name of this color.
@ -35,5 +35,8 @@ public class WangColor
/// <summary> /// <summary>
/// The Wang color properties. /// The Wang color properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
} }

View file

@ -5,7 +5,7 @@ namespace DotTiled.Model;
/// <summary> /// <summary>
/// Defines a list of colors and any number of Wang tiles using these colors. /// Defines a list of colors and any number of Wang tiles using these colors.
/// </summary> /// </summary>
public class Wangset public class Wangset : HasPropertiesBase
{ {
/// <summary> /// <summary>
/// The name of the Wang set. /// The name of the Wang set.
@ -25,7 +25,10 @@ public class Wangset
/// <summary> /// <summary>
/// The Wang set properties. /// The Wang set properties.
/// </summary> /// </summary>
public Dictionary<string, IProperty>? Properties { get; set; } public List<IProperty> Properties { get; set; } = [];
/// <inheritdoc/>
public override IList<IProperty> GetProperties() => Properties;
// Up to 254 Wang colors // Up to 254 Wang colors
/// <summary> /// <summary>

View file

@ -73,31 +73,52 @@ internal static partial class Helpers
}; };
} }
internal static Dictionary<string, IProperty> MergeProperties(Dictionary<string, IProperty>? baseProperties, Dictionary<string, IProperty>? overrideProperties) 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)
{ {
if (baseProperties is null) if (baseProperties is null)
return overrideProperties ?? new Dictionary<string, IProperty>(); return overrideProperties ?? [];
if (overrideProperties is null) if (overrideProperties is null)
return baseProperties; return baseProperties;
var result = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); var result = baseProperties.Select(x => x.Clone()).ToList();
foreach (var (key, value) in overrideProperties) foreach (var overrideProp in overrideProperties)
{ {
if (!result.TryGetValue(key, out var baseProp)) if (!result.Any(x => x.Name == overrideProp.Name))
{ {
result[key] = value; result.Add(overrideProp);
continue; continue;
} }
else else
{ {
if (value is ClassProperty classProp) var existingProp = result.First(x => x.Name == overrideProp.Name);
if (existingProp is ClassProperty classProp)
{ {
((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); classProp.Value = MergeProperties(classProp.Value, ((ClassProperty)overrideProp).Value);
} }
else else
{ {
result[key] = value; ReplacePropertyInList(result, overrideProp);
} }
} }
} }
@ -105,6 +126,15 @@ internal static partial class Helpers
return result; return result;
} }
internal static void ReplacePropertyInList(List<IProperty> properties, IProperty property)
{
var index = properties.FindIndex(p => p.Name == property.Name);
if (index == -1)
properties.Add(property);
else
properties[index] = property;
}
internal static void SetAtMostOnce<T>(ref T? field, T value, string fieldName) internal static void SetAtMostOnce<T>(ref T? field, T value, string fieldName)
{ {
if (field is not null) if (field is not null)

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<CustomTypeDefinition> _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<CustomTypeDefinition> 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<CustomTypeDefinition> 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,106 +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 Dictionary<string, IProperty> ReadProperties(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> 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!;
}).ToDictionary(p => p.Name);
internal static ClassProperty ReadClassProperty(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> 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 = CreateInstanceOfCustomClass(ccd);
var props = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []);
var mergedProps = Helpers.MergeProperties(propsInType, props);
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Properties = mergedProps
};
}
throw new JsonException($"Unknown custom class '{propertyType}'.");
}
internal static Dictionary<string, IProperty> ReadCustomClassProperties(
JsonElement element,
CustomClassDefinition customClassDefinition,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
Dictionary<string, IProperty> resultingProps = [];
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")
};
resultingProps[prop.Name] = property;
}
return resultingProps;
}
internal static Dictionary<string, IProperty> CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) =>
customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone());
}

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<CustomTypeDefinition> 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<CustomTypeDefinition> _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<CustomTypeDefinition> 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null); 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<CustomTypeDefinition> customTypeDefinitions)
{ {
uint? idDefault = null; uint? idDefault = null;
string nameDefault = ""; string nameDefault = "";
@ -82,12 +75,12 @@ internal partial class Tmj
bool pointDefault = false; bool pointDefault = false;
List<Vector2>? polygonDefault = null; List<Vector2>? polygonDefault = null;
List<Vector2>? polylineDefault = null; List<Vector2>? polylineDefault = null;
Dictionary<string, IProperty>? propertiesDefault = null; List<IProperty> propertiesDefault = [];
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<Dictionary<string, IProperty>?>("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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null); 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<CustomTypeDefinition> 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<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null); 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<CustomTypeDefinition> _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<CustomTypeDefinition> 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

@ -1,107 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
internal partial class Tmx
{
internal static Map ReadMap(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var version = reader.GetRequiredAttribute("version");
var tiledVersion = reader.GetRequiredAttribute("tiledversion");
var @class = reader.GetOptionalAttribute("class") ?? "";
var orientation = reader.GetRequiredAttributeEnum<MapOrientation>("orientation", s => s switch
{
"orthogonal" => MapOrientation.Orthogonal,
"isometric" => MapOrientation.Isometric,
"staggered" => MapOrientation.Staggered,
"hexagonal" => MapOrientation.Hexagonal,
_ => throw new InvalidOperationException($"Unknown orientation '{s}'")
});
var renderOrder = reader.GetOptionalAttributeEnum<RenderOrder>("renderorder", s => s switch
{
"right-down" => RenderOrder.RightDown,
"right-up" => RenderOrder.RightUp,
"left-down" => RenderOrder.LeftDown,
"left-up" => RenderOrder.LeftUp,
_ => throw new InvalidOperationException($"Unknown render order '{s}'")
}) ?? RenderOrder.RightDown;
var compressionLevel = reader.GetOptionalAttributeParseable<int>("compressionlevel") ?? -1;
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
var tileWidth = reader.GetRequiredAttributeParseable<uint>("tilewidth");
var tileHeight = reader.GetRequiredAttributeParseable<uint>("tileheight");
var hexSideLength = reader.GetOptionalAttributeParseable<uint>("hexsidelength");
var staggerAxis = reader.GetOptionalAttributeEnum<StaggerAxis>("staggeraxis", s => s switch
{
"x" => StaggerAxis.X,
"y" => StaggerAxis.Y,
_ => throw new InvalidOperationException($"Unknown stagger axis '{s}'")
});
var staggerIndex = reader.GetOptionalAttributeEnum<StaggerIndex>("staggerindex", s => s switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new InvalidOperationException($"Unknown stagger index '{s}'")
});
var parallaxOriginX = reader.GetOptionalAttributeParseable<float>("parallaxoriginx") ?? 0.0f;
var parallaxOriginY = reader.GetOptionalAttributeParseable<float>("parallaxoriginy") ?? 0.0f;
var backgroundColor = reader.GetOptionalAttributeClass<Color>("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture);
var nextLayerID = reader.GetRequiredAttributeParseable<uint>("nextlayerid");
var nextObjectID = reader.GetRequiredAttributeParseable<uint>("nextobjectid");
var infinite = (reader.GetOptionalAttributeParseable<uint>("infinite") ?? 0) == 1;
// At most one of
Dictionary<string, IProperty>? properties = null;
// Any number of
List<BaseLayer> layers = [];
List<Tileset> tilesets = [];
reader.ProcessChildren("map", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)),
"layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)),
"imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip
});
return new Map
{
Version = version,
TiledVersion = tiledVersion,
Class = @class,
Orientation = orientation,
RenderOrder = renderOrder,
CompressionLevel = compressionLevel,
Width = width,
Height = height,
TileWidth = tileWidth,
TileHeight = tileHeight,
HexSideLength = hexSideLength,
StaggerAxis = staggerAxis,
StaggerIndex = staggerIndex,
ParallaxOriginX = parallaxOriginX,
ParallaxOriginY = parallaxOriginY,
BackgroundColor = backgroundColor,
NextLayerID = nextLayerID,
NextObjectID = nextObjectID,
Infinite = infinite,
Properties = properties,
Tilesets = tilesets,
Layers = layers
};
}
}

View file

@ -1,71 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
internal partial class Tmx
{
internal static Dictionary<string, IProperty> ReadProperties(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
return reader.ReadList("properties", "property", (r) =>
{
var name = r.GetRequiredAttribute("name");
var type = r.GetOptionalAttributeEnum<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 XmlException("Invalid property type")
}) ?? PropertyType.String;
IProperty property = type switch
{
PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable<bool>("value") },
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(r, customTypeDefinitions),
_ => throw new XmlException("Invalid property type")
};
return (name, property);
}).ToDictionary(x => x.name, x => x.property);
}
internal static ClassProperty ReadClassProperty(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var name = reader.GetRequiredAttribute("name");
var propertyType = reader.GetRequiredAttribute("propertytype");
var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType);
if (customTypeDef is CustomClassDefinition ccd)
{
reader.ReadStartElement("property");
var propsInType = CreateInstanceOfCustomClass(ccd);
var props = ReadProperties(reader, customTypeDefinitions);
var mergedProps = Helpers.MergeProperties(propsInType, props);
reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Properties = mergedProps };
}
throw new XmlException($"Unkonwn custom class definition: {propertyType}");
}
internal static Dictionary<string, IProperty> CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) =>
customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone());
}

View file

@ -1,157 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
internal partial class Tmx
{
internal static TileLayer ReadTileLayer(
XmlReader reader,
bool dataUsesChunks,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
Dictionary<string, IProperty>? properties = null;
Data? data = null;
reader.ProcessChildren("layer", (r, elementName) => elementName switch
{
"data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
_ => r.Skip
});
return new TileLayer
{
ID = id,
Name = name,
Class = @class,
X = x,
Y = y,
Width = width,
Height = height,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Data = data,
Properties = properties
};
}
internal static ImageLayer ReadImageLayer(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var repeatX = (reader.GetOptionalAttributeParseable<uint>("repeatx") ?? 0) == 1;
var repeatY = (reader.GetOptionalAttributeParseable<uint>("repeaty") ?? 0) == 1;
Dictionary<string, IProperty>? properties = null;
Image? image = null;
reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch
{
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
_ => r.Skip
});
return new ImageLayer
{
ID = id,
Name = name,
Class = @class,
X = x,
Y = y,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Properties = properties,
Image = image,
RepeatX = repeatX,
RepeatY = repeatY
};
}
internal static Group ReadGroup(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
Dictionary<string, IProperty>? properties = null;
List<BaseLayer> layers = [];
reader.ProcessChildren("group", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"layer" => () => layers.Add(ReadTileLayer(r, false, customTypeDefinitions)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)),
"imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip
});
return new Group
{
ID = id,
Name = name,
Class = @class,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Properties = properties,
Layers = layers
};
}
}

View file

@ -1,350 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
internal partial class Tmx
{
internal static Tileset ReadTileset(
XmlReader reader,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var version = reader.GetOptionalAttribute("version");
var tiledVersion = reader.GetOptionalAttribute("tiledversion");
var firstGID = reader.GetOptionalAttributeParseable<uint>("firstgid");
var source = reader.GetOptionalAttribute("source");
var name = reader.GetOptionalAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var tileWidth = reader.GetOptionalAttributeParseable<uint>("tilewidth");
var tileHeight = reader.GetOptionalAttributeParseable<uint>("tileheight");
var spacing = reader.GetOptionalAttributeParseable<uint>("spacing") ?? 0;
var margin = reader.GetOptionalAttributeParseable<uint>("margin") ?? 0;
var tileCount = reader.GetOptionalAttributeParseable<uint>("tilecount");
var columns = reader.GetOptionalAttributeParseable<uint>("columns");
var objectAlignment = reader.GetOptionalAttributeEnum<ObjectAlignment>("objectalignment", s => s switch
{
"unspecified" => ObjectAlignment.Unspecified,
"topleft" => ObjectAlignment.TopLeft,
"top" => ObjectAlignment.Top,
"topright" => ObjectAlignment.TopRight,
"left" => ObjectAlignment.Left,
"center" => ObjectAlignment.Center,
"right" => ObjectAlignment.Right,
"bottomleft" => ObjectAlignment.BottomLeft,
"bottom" => ObjectAlignment.Bottom,
"bottomright" => ObjectAlignment.BottomRight,
_ => throw new InvalidOperationException($"Unknown object alignment '{s}'")
}) ?? ObjectAlignment.Unspecified;
var renderSize = reader.GetOptionalAttributeEnum<TileRenderSize>("rendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,
_ => throw new InvalidOperationException($"Unknown render size '{s}'")
}) ?? TileRenderSize.Tile;
var fillMode = reader.GetOptionalAttributeEnum<FillMode>("fillmode", s => s switch
{
"stretch" => FillMode.Stretch,
"preserve-aspect-fit" => FillMode.PreserveAspectFit,
_ => throw new InvalidOperationException($"Unknown fill mode '{s}'")
}) ?? FillMode.Stretch;
// Elements
Image? image = null;
TileOffset? tileOffset = null;
Grid? grid = null;
Dictionary<string, IProperty>? properties = null;
List<Wangset>? wangsets = null;
Transformations? transformations = null;
List<Tile> tiles = [];
reader.ProcessChildren("tileset", (r, elementName) => elementName switch
{
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(r), "TileOffset"),
"grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(r), "Grid"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r, customTypeDefinitions), "Wangsets"),
"transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"),
"tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip
});
// Check if tileset is referring to external file
if (source is not null)
{
if (externalTilesetResolver is null)
throw new InvalidOperationException("External tileset resolver is required to resolve external tilesets.");
var resolvedTileset = externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source;
return resolvedTileset;
}
return new Tileset
{
Version = version,
TiledVersion = tiledVersion,
FirstGID = firstGID,
Source = source,
Name = name,
Class = @class,
TileWidth = tileWidth,
TileHeight = tileHeight,
Spacing = spacing,
Margin = margin,
TileCount = tileCount,
Columns = columns,
ObjectAlignment = objectAlignment,
RenderSize = renderSize,
FillMode = fillMode,
Image = image,
TileOffset = tileOffset,
Grid = grid,
Properties = properties,
Wangsets = wangsets,
Transformations = transformations,
Tiles = tiles
};
}
internal static Image ReadImage(XmlReader reader)
{
// Attributes
var format = reader.GetOptionalAttributeEnum<ImageFormat>("format", s => s switch
{
"png" => ImageFormat.Png,
"jpg" => ImageFormat.Jpg,
"bmp" => ImageFormat.Bmp,
"gif" => ImageFormat.Gif,
_ => throw new InvalidOperationException($"Unknown image format '{s}'")
});
var source = reader.GetOptionalAttribute("source");
var transparentColor = reader.GetOptionalAttributeClass<Color>("trans");
var width = reader.GetOptionalAttributeParseable<uint>("width");
var height = reader.GetOptionalAttributeParseable<uint>("height");
reader.ProcessChildren("image", (r, elementName) => elementName switch
{
"data" => throw new NotSupportedException("Embedded image data is not supported."),
_ => r.Skip
});
if (format is null && source is not null)
format = ParseImageFormatFromSource(source);
return new Image
{
Format = format,
Source = source,
TransparentColor = transparentColor,
Width = width,
Height = height,
};
}
private static ImageFormat ParseImageFormatFromSource(string source)
{
var extension = Path.GetExtension(source).ToLowerInvariant();
return extension switch
{
".png" => ImageFormat.Png,
".gif" => ImageFormat.Gif,
".jpg" => ImageFormat.Jpg,
".jpeg" => ImageFormat.Jpg,
".bmp" => ImageFormat.Bmp,
_ => throw new XmlException($"Unsupported image format '{extension}'")
};
}
internal static TileOffset ReadTileOffset(XmlReader reader)
{
// Attributes
var x = reader.GetOptionalAttributeParseable<float>("x") ?? 0f;
var y = reader.GetOptionalAttributeParseable<float>("y") ?? 0f;
reader.ReadStartElement("tileoffset");
return new TileOffset { X = x, Y = y };
}
internal static Grid ReadGrid(XmlReader reader)
{
// Attributes
var orientation = reader.GetOptionalAttributeEnum<GridOrientation>("orientation", s => s switch
{
"orthogonal" => GridOrientation.Orthogonal,
"isometric" => GridOrientation.Isometric,
_ => throw new InvalidOperationException($"Unknown orientation '{s}'")
}) ?? GridOrientation.Orthogonal;
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
reader.ReadStartElement("grid");
return new Grid { Orientation = orientation, Width = width, Height = height };
}
internal static Transformations ReadTransformations(XmlReader reader)
{
// Attributes
var hFlip = (reader.GetOptionalAttributeParseable<uint>("hflip") ?? 0) == 1;
var vFlip = (reader.GetOptionalAttributeParseable<uint>("vflip") ?? 0) == 1;
var rotate = (reader.GetOptionalAttributeParseable<uint>("rotate") ?? 0) == 1;
var preferUntransformed = (reader.GetOptionalAttributeParseable<uint>("preferuntransformed") ?? 0) == 1;
reader.ReadStartElement("transformations");
return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed };
}
internal static Tile ReadTile(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id");
var type = reader.GetOptionalAttribute("type") ?? "";
var probability = reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = reader.GetOptionalAttributeParseable<uint>("width");
var height = reader.GetOptionalAttributeParseable<uint>("height");
// Elements
Dictionary<string, IProperty>? properties = null;
Image? image = null;
ObjectLayer? objectLayer = null;
List<Frame>? animation = null;
reader.ProcessChildren("tile", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions), "ObjectLayer"),
"animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList<Frame>("animation", "frame", (ar) =>
{
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
var duration = ar.GetRequiredAttributeParseable<uint>("duration");
return new Frame { TileID = tileID, Duration = duration };
}), "Animation"),
_ => r.Skip
});
return new Tile
{
ID = id,
Type = type,
Probability = probability,
X = x,
Y = y,
Width = width ?? image?.Width ?? 0,
Height = height ?? image?.Height ?? 0,
Properties = properties,
Image = image,
ObjectLayer = objectLayer,
Animation = animation
};
}
internal static List<Wangset> ReadWangsets(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) =>
reader.ReadList<Wangset>("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions));
internal static Wangset ReadWangset(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var tile = reader.GetRequiredAttributeParseable<int>("tile");
// Elements
Dictionary<string, IProperty>? properties = null;
List<WangColor> wangColors = [];
List<WangTile> wangTiles = [];
reader.ProcessChildren("wangset", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)),
"wangtile" => () => wangTiles.Add(ReadWangTile(r)),
_ => r.Skip
});
if (wangColors.Count > 254)
throw new ArgumentException("Wangset can have at most 254 Wang colors.");
return new Wangset
{
Name = name,
Class = @class,
Tile = tile,
Properties = properties,
WangColors = wangColors,
WangTiles = wangTiles
};
}
internal static WangColor ReadWangColor(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var color = reader.GetRequiredAttributeParseable<Color>("color");
var tile = reader.GetRequiredAttributeParseable<int>("tile");
var probability = reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
// Elements
Dictionary<string, IProperty>? properties = null;
reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
_ => r.Skip
});
return new WangColor
{
Name = name,
Class = @class,
Color = color,
Tile = tile,
Probability = probability,
Properties = properties
};
}
internal static WangTile ReadWangTile(XmlReader reader)
{
// Attributes
var tileID = reader.GetRequiredAttributeParseable<uint>("tileid");
var wangID = reader.GetRequiredAttributeParseable<byte[]>("wangid", s =>
{
// Comma-separated list of indices (0-254)
var indices = s.Split(',').Select(i => byte.Parse(i, CultureInfo.InvariantCulture)).ToArray();
if (indices.Length > 8)
throw new ArgumentException("Wang ID can have at most 8 indices.");
return indices;
});
reader.ReadStartElement("wangtile");
return new WangTile
{
TileID = tileID,
WangID = wangID
};
}
}

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Xml; using System.Xml;
using DotTiled.Model; using DotTiled.Model;
@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx;
/// <summary> /// <summary>
/// A map reader for the Tiled XML format. /// A map reader for the Tiled XML format.
/// </summary> /// </summary>
public class TmxMapReader : IMapReader public class TmxMapReader : TmxReaderBase, IMapReader
{ {
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Constructs a new <see cref="TmxMapReader"/>. /// Constructs a new <see cref="TmxMapReader"/>.
/// </summary> /// </summary>
/// <param name="reader">An XML reader for reading a Tiled map in the Tiled XML format.</param> /// <inheritdoc />
/// <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>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
public TmxMapReader( public TmxMapReader(
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) Func<string, ICustomTypeDefinition> customTypeResolver) : base(
{ reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); { }
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
// Prepare reader
_ = _reader.MoveToContent();
}
/// <inheritdoc/> /// <inheritdoc/>
public Map ReadMap() => Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); public new Map ReadMap() => base.ReadMap();
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
_reader.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~TmxTiledMapReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
/// <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,27 +1,26 @@
using System.Xml;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmx; namespace DotTiled.Serialization.Tmx;
internal partial class Tmx public abstract partial class TmxReaderBase
{ {
internal static Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression) internal Chunk ReadChunk(DataEncoding? encoding, DataCompression? compression)
{ {
var x = reader.GetRequiredAttributeParseable<int>("x"); var x = _reader.GetRequiredAttributeParseable<int>("x");
var y = reader.GetRequiredAttributeParseable<int>("y"); var y = _reader.GetRequiredAttributeParseable<int>("y");
var width = reader.GetRequiredAttributeParseable<uint>("width"); var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height"); var height = _reader.GetRequiredAttributeParseable<uint>("height");
var usesTileChildrenInsteadOfRawData = encoding is null; var usesTileChildrenInsteadOfRawData = encoding is null;
if (usesTileChildrenInsteadOfRawData) if (usesTileChildrenInsteadOfRawData)
{ {
var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", reader); var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", _reader);
var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags);
return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags };
} }
else else
{ {
var globalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); var globalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression);
var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags);
return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags };
} }

View file

@ -8,17 +8,17 @@ using DotTiled.Model;
namespace DotTiled.Serialization.Tmx; namespace DotTiled.Serialization.Tmx;
internal partial class Tmx public abstract partial class TmxReaderBase
{ {
internal static Data ReadData(XmlReader reader, bool usesChunks) internal Data ReadData(bool usesChunks)
{ {
var encoding = reader.GetOptionalAttributeEnum<DataEncoding>("encoding", e => e switch var encoding = _reader.GetOptionalAttributeEnum<DataEncoding>("encoding", e => e switch
{ {
"csv" => DataEncoding.Csv, "csv" => DataEncoding.Csv,
"base64" => DataEncoding.Base64, "base64" => DataEncoding.Base64,
_ => throw new XmlException("Invalid encoding") _ => throw new XmlException("Invalid encoding")
}); });
var compression = reader.GetOptionalAttributeEnum<DataCompression>("compression", c => c switch var compression = _reader.GetOptionalAttributeEnum<DataCompression>("compression", c => c switch
{ {
"gzip" => DataCompression.GZip, "gzip" => DataCompression.GZip,
"zlib" => DataCompression.ZLib, "zlib" => DataCompression.ZLib,
@ -28,8 +28,8 @@ internal partial class Tmx
if (usesChunks) if (usesChunks)
{ {
var chunks = reader var chunks = _reader
.ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression)) .ReadList("data", "chunk", (r) => ReadChunk(encoding, compression))
.ToArray(); .ToArray();
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks }; return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks };
} }
@ -37,12 +37,12 @@ internal partial class Tmx
var usesTileChildrenInsteadOfRawData = encoding is null && compression is null; var usesTileChildrenInsteadOfRawData = encoding is null && compression is null;
if (usesTileChildrenInsteadOfRawData) if (usesTileChildrenInsteadOfRawData)
{ {
var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", reader); var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", _reader);
var (tileChildrenGlobalTileIDs, tileChildrenFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(tileChildrenGlobalTileIDsWithFlippingFlags); var (tileChildrenGlobalTileIDs, tileChildrenFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(tileChildrenGlobalTileIDsWithFlippingFlags);
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = tileChildrenGlobalTileIDs, FlippingFlags = tileChildrenFlippingFlags, Chunks = null }; return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = tileChildrenGlobalTileIDs, FlippingFlags = tileChildrenFlippingFlags, Chunks = null };
} }
var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression);
var (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags); var (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags);
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null }; return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null };
} }

View file

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
/// <summary>
/// Base class for Tiled XML format readers.
/// </summary>
public abstract partial class TmxReaderBase
{
internal Map ReadMap()
{
// Attributes
var version = _reader.GetRequiredAttribute("version");
var tiledVersion = _reader.GetRequiredAttribute("tiledversion");
var @class = _reader.GetOptionalAttribute("class") ?? "";
var orientation = _reader.GetRequiredAttributeEnum<MapOrientation>("orientation", s => s switch
{
"orthogonal" => MapOrientation.Orthogonal,
"isometric" => MapOrientation.Isometric,
"staggered" => MapOrientation.Staggered,
"hexagonal" => MapOrientation.Hexagonal,
_ => throw new InvalidOperationException($"Unknown orientation '{s}'")
});
var renderOrder = _reader.GetOptionalAttributeEnum<RenderOrder>("renderorder", s => s switch
{
"right-down" => RenderOrder.RightDown,
"right-up" => RenderOrder.RightUp,
"left-down" => RenderOrder.LeftDown,
"left-up" => RenderOrder.LeftUp,
_ => throw new InvalidOperationException($"Unknown render order '{s}'")
}) ?? RenderOrder.RightDown;
var compressionLevel = _reader.GetOptionalAttributeParseable<int>("compressionlevel") ?? -1;
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("height");
var tileWidth = _reader.GetRequiredAttributeParseable<uint>("tilewidth");
var tileHeight = _reader.GetRequiredAttributeParseable<uint>("tileheight");
var hexSideLength = _reader.GetOptionalAttributeParseable<uint>("hexsidelength");
var staggerAxis = _reader.GetOptionalAttributeEnum<StaggerAxis>("staggeraxis", s => s switch
{
"x" => StaggerAxis.X,
"y" => StaggerAxis.Y,
_ => throw new InvalidOperationException($"Unknown stagger axis '{s}'")
});
var staggerIndex = _reader.GetOptionalAttributeEnum<StaggerIndex>("staggerindex", s => s switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new InvalidOperationException($"Unknown stagger index '{s}'")
});
var parallaxOriginX = _reader.GetOptionalAttributeParseable<float>("parallaxoriginx") ?? 0.0f;
var parallaxOriginY = _reader.GetOptionalAttributeParseable<float>("parallaxoriginy") ?? 0.0f;
var backgroundColor = _reader.GetOptionalAttributeClass<Color>("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture);
var nextLayerID = _reader.GetRequiredAttributeParseable<uint>("nextlayerid");
var nextObjectID = _reader.GetRequiredAttributeParseable<uint>("nextobjectid");
var infinite = (_reader.GetOptionalAttributeParseable<uint>("infinite") ?? 0) == 1;
// At most one of
List<IProperty>? properties = null;
// Any number of
List<BaseLayer> layers = [];
List<Tileset> tilesets = [];
_reader.ProcessChildren("map", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
"tileset" => () => tilesets.Add(ReadTileset()),
"layer" => () => layers.Add(ReadTileLayer(infinite)),
"objectgroup" => () => layers.Add(ReadObjectLayer()),
"imagelayer" => () => layers.Add(ReadImageLayer()),
"group" => () => layers.Add(ReadGroup()),
_ => r.Skip
});
return new Map
{
Version = version,
TiledVersion = tiledVersion,
Class = @class,
Orientation = orientation,
RenderOrder = renderOrder,
CompressionLevel = compressionLevel,
Width = width,
Height = height,
TileWidth = tileWidth,
TileHeight = tileHeight,
HexSideLength = hexSideLength,
StaggerAxis = staggerAxis,
StaggerIndex = staggerIndex,
ParallaxOriginX = parallaxOriginX,
ParallaxOriginY = parallaxOriginY,
BackgroundColor = backgroundColor,
NextLayerID = nextLayerID,
NextObjectID = nextObjectID,
Infinite = infinite,
Properties = properties ?? [],
Tilesets = tilesets,
Layers = layers
};
}
}

View file

@ -3,35 +3,31 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Xml;
using DotTiled.Model; using DotTiled.Model;
namespace DotTiled.Serialization.Tmx; namespace DotTiled.Serialization.Tmx;
internal partial class Tmx public abstract partial class TmxReaderBase
{ {
internal static ObjectLayer ReadObjectLayer( internal ObjectLayer ReadObjectLayer()
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id"); var id = _reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? ""; var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? ""; var @class = _reader.GetOptionalAttribute("class") ?? "";
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0; var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0; var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = reader.GetOptionalAttributeParseable<uint>("width"); var width = _reader.GetOptionalAttributeParseable<uint>("width");
var height = reader.GetOptionalAttributeParseable<uint>("height"); var height = _reader.GetOptionalAttributeParseable<uint>("height");
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f; var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true; var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor"); var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f; var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f; var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f; var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f; var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var color = reader.GetOptionalAttributeClass<Color>("color"); var color = _reader.GetOptionalAttributeClass<Color>("color");
var drawOrder = reader.GetOptionalAttributeEnum<DrawOrder>("draworder", s => s switch var drawOrder = _reader.GetOptionalAttributeEnum<DrawOrder>("draworder", s => s switch
{ {
"topdown" => DrawOrder.TopDown, "topdown" => DrawOrder.TopDown,
"index" => DrawOrder.Index, "index" => DrawOrder.Index,
@ -39,13 +35,13 @@ internal partial class Tmx
}) ?? DrawOrder.TopDown; }) ?? DrawOrder.TopDown;
// Elements // Elements
Dictionary<string, IProperty>? properties = null; List<IProperty>? properties = null;
List<Model.Object> objects = []; List<Model.Object> objects = [];
reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch _reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
"object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), "object" => () => objects.Add(ReadObject()),
_ => r.Skip _ => r.Skip
}); });
@ -66,22 +62,19 @@ internal partial class Tmx
ParallaxX = parallaxX, ParallaxX = parallaxX,
ParallaxY = parallaxY, ParallaxY = parallaxY,
Color = color, Color = color,
Properties = properties, Properties = properties ?? [],
DrawOrder = drawOrder, DrawOrder = drawOrder,
Objects = objects Objects = objects
}; };
} }
internal static Model.Object ReadObject( internal Model.Object ReadObject()
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var template = reader.GetOptionalAttribute("template"); var template = _reader.GetOptionalAttribute("template");
Model.Object? obj = null; Model.Object? obj = null;
if (template is not null) if (template is not null)
obj = externalTemplateResolver(template).Object; obj = _externalTemplateResolver(template).Object;
uint? idDefault = obj?.ID ?? null; uint? idDefault = obj?.ID ?? null;
string nameDefault = obj?.Name ?? ""; string nameDefault = obj?.Name ?? "";
@ -93,32 +86,32 @@ internal partial class Tmx
float rotationDefault = obj?.Rotation ?? 0f; float rotationDefault = obj?.Rotation ?? 0f;
uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null; uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null;
bool visibleDefault = obj?.Visible ?? true; bool visibleDefault = obj?.Visible ?? true;
Dictionary<string, IProperty>? propertiesDefault = obj?.Properties ?? null; List<IProperty>? propertiesDefault = obj?.Properties ?? null;
var id = reader.GetOptionalAttributeParseable<uint>("id") ?? idDefault; var id = _reader.GetOptionalAttributeParseable<uint>("id") ?? idDefault;
var name = reader.GetOptionalAttribute("name") ?? nameDefault; var name = _reader.GetOptionalAttribute("name") ?? nameDefault;
var type = reader.GetOptionalAttribute("type") ?? typeDefault; var type = _reader.GetOptionalAttribute("type") ?? typeDefault;
var x = reader.GetOptionalAttributeParseable<float>("x") ?? xDefault; var x = _reader.GetOptionalAttributeParseable<float>("x") ?? xDefault;
var y = reader.GetOptionalAttributeParseable<float>("y") ?? yDefault; var y = _reader.GetOptionalAttributeParseable<float>("y") ?? yDefault;
var width = reader.GetOptionalAttributeParseable<float>("width") ?? widthDefault; var width = _reader.GetOptionalAttributeParseable<float>("width") ?? widthDefault;
var height = reader.GetOptionalAttributeParseable<float>("height") ?? heightDefault; var height = _reader.GetOptionalAttributeParseable<float>("height") ?? heightDefault;
var rotation = reader.GetOptionalAttributeParseable<float>("rotation") ?? rotationDefault; var rotation = _reader.GetOptionalAttributeParseable<float>("rotation") ?? rotationDefault;
var gid = reader.GetOptionalAttributeParseable<uint>("gid") ?? gidDefault; var gid = _reader.GetOptionalAttributeParseable<uint>("gid") ?? gidDefault;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? visibleDefault; var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? visibleDefault;
// Elements // Elements
Model.Object? foundObject = null; Model.Object? foundObject = null;
int propertiesCounter = 0; int propertiesCounter = 0;
Dictionary<string, IProperty>? properties = propertiesDefault; List<IProperty>? properties = propertiesDefault;
reader.ProcessChildren("object", (r, elementName) => elementName switch _reader.ProcessChildren("object", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)), "Properties", ref propertiesCounter), "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties()).ToList(), "Properties", ref propertiesCounter),
"ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(r), "Object marker"), "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(), "Object marker"),
"point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(r), "Object marker"), "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(), "Object marker"),
"polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(r), "Object marker"), "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(), "Object marker"),
"polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(r), "Object marker"), "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(), "Object marker"),
"text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(r), "Object marker"), "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(), "Object marker"),
_ => throw new InvalidOperationException($"Unknown object marker '{elementName}'") _ => throw new InvalidOperationException($"Unknown object marker '{elementName}'")
}); });
@ -139,7 +132,7 @@ internal partial class Tmx
foundObject.Height = height; foundObject.Height = height;
foundObject.Rotation = rotation; foundObject.Rotation = rotation;
foundObject.Visible = visible; foundObject.Visible = visible;
foundObject.Properties = properties; foundObject.Properties = properties ?? [];
foundObject.Template = template; foundObject.Template = template;
return OverrideObject(obj, foundObject); return OverrideObject(obj, foundObject);
@ -161,7 +154,7 @@ internal partial class Tmx
obj.Height = foundObject.Height; obj.Height = foundObject.Height;
obj.Rotation = foundObject.Rotation; obj.Rotation = foundObject.Rotation;
obj.Visible = foundObject.Visible; obj.Visible = foundObject.Visible;
obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties); obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties).ToList();
obj.Template = foundObject.Template; obj.Template = foundObject.Template;
return obj; return obj;
} }
@ -169,26 +162,26 @@ internal partial class Tmx
return OverrideObject((dynamic)obj, (dynamic)foundObject); return OverrideObject((dynamic)obj, (dynamic)foundObject);
} }
internal static EllipseObject ReadEllipseObject(XmlReader reader) internal EllipseObject ReadEllipseObject()
{ {
reader.Skip(); _reader.Skip();
return new EllipseObject { }; return new EllipseObject { };
} }
internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject _) => obj; internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject _) => obj;
internal static PointObject ReadPointObject(XmlReader reader) internal PointObject ReadPointObject()
{ {
reader.Skip(); _reader.Skip();
return new PointObject { }; return new PointObject { };
} }
internal static PointObject OverrideObject(PointObject obj, PointObject _) => obj; internal static PointObject OverrideObject(PointObject obj, PointObject _) => obj;
internal static PolygonObject ReadPolygonObject(XmlReader reader) internal PolygonObject ReadPolygonObject()
{ {
// Attributes // Attributes
var points = reader.GetRequiredAttributeParseable<List<Vector2>>("points", s => var points = _reader.GetRequiredAttributeParseable<List<Vector2>>("points", s =>
{ {
// Takes on format "x1,y1 x2,y2 x3,y3 ..." // Takes on format "x1,y1 x2,y2 x3,y3 ..."
var coords = s.Split(' '); var coords = s.Split(' ');
@ -199,7 +192,7 @@ internal partial class Tmx
}).ToList(); }).ToList();
}); });
reader.ReadStartElement("polygon"); _reader.ReadStartElement("polygon");
return new PolygonObject { Points = points }; return new PolygonObject { Points = points };
} }
@ -209,10 +202,10 @@ internal partial class Tmx
return obj; return obj;
} }
internal static PolylineObject ReadPolylineObject(XmlReader reader) internal PolylineObject ReadPolylineObject()
{ {
// Attributes // Attributes
var points = reader.GetRequiredAttributeParseable<List<Vector2>>("points", s => var points = _reader.GetRequiredAttributeParseable<List<Vector2>>("points", s =>
{ {
// Takes on format "x1,y1 x2,y2 x3,y3 ..." // Takes on format "x1,y1 x2,y2 x3,y3 ..."
var coords = s.Split(' '); var coords = s.Split(' ');
@ -223,7 +216,7 @@ internal partial class Tmx
}).ToList(); }).ToList();
}); });
reader.ReadStartElement("polyline"); _reader.ReadStartElement("polyline");
return new PolylineObject { Points = points }; return new PolylineObject { Points = points };
} }
@ -233,19 +226,19 @@ internal partial class Tmx
return obj; return obj;
} }
internal static TextObject ReadTextObject(XmlReader reader) internal TextObject ReadTextObject()
{ {
// Attributes // Attributes
var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; var fontFamily = _reader.GetOptionalAttribute("fontfamily") ?? "sans-serif";
var pixelSize = reader.GetOptionalAttributeParseable<int>("pixelsize") ?? 16; var pixelSize = _reader.GetOptionalAttributeParseable<int>("pixelsize") ?? 16;
var wrap = reader.GetOptionalAttributeParseable<bool>("wrap") ?? false; var wrap = _reader.GetOptionalAttributeParseable<bool>("wrap") ?? false;
var color = reader.GetOptionalAttributeClass<Color>("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); var color = _reader.GetOptionalAttributeClass<Color>("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture);
var bold = reader.GetOptionalAttributeParseable<bool>("bold") ?? false; var bold = _reader.GetOptionalAttributeParseable<bool>("bold") ?? false;
var italic = reader.GetOptionalAttributeParseable<bool>("italic") ?? false; var italic = _reader.GetOptionalAttributeParseable<bool>("italic") ?? false;
var underline = reader.GetOptionalAttributeParseable<bool>("underline") ?? false; var underline = _reader.GetOptionalAttributeParseable<bool>("underline") ?? false;
var strikeout = reader.GetOptionalAttributeParseable<bool>("strikeout") ?? false; var strikeout = _reader.GetOptionalAttributeParseable<bool>("strikeout") ?? false;
var kerning = reader.GetOptionalAttributeParseable<bool>("kerning") ?? true; var kerning = _reader.GetOptionalAttributeParseable<bool>("kerning") ?? true;
var hAlign = reader.GetOptionalAttributeEnum<TextHorizontalAlignment>("halign", s => s switch var hAlign = _reader.GetOptionalAttributeEnum<TextHorizontalAlignment>("halign", s => s switch
{ {
"left" => TextHorizontalAlignment.Left, "left" => TextHorizontalAlignment.Left,
"center" => TextHorizontalAlignment.Center, "center" => TextHorizontalAlignment.Center,
@ -253,7 +246,7 @@ internal partial class Tmx
"justify" => TextHorizontalAlignment.Justify, "justify" => TextHorizontalAlignment.Justify,
_ => throw new InvalidOperationException($"Unknown horizontal alignment '{s}'") _ => throw new InvalidOperationException($"Unknown horizontal alignment '{s}'")
}) ?? TextHorizontalAlignment.Left; }) ?? TextHorizontalAlignment.Left;
var vAlign = reader.GetOptionalAttributeEnum<TextVerticalAlignment>("valign", s => s switch var vAlign = _reader.GetOptionalAttributeEnum<TextVerticalAlignment>("valign", s => s switch
{ {
"top" => TextVerticalAlignment.Top, "top" => TextVerticalAlignment.Top,
"center" => TextVerticalAlignment.Center, "center" => TextVerticalAlignment.Center,
@ -262,7 +255,7 @@ internal partial class Tmx
}) ?? TextVerticalAlignment.Top; }) ?? TextVerticalAlignment.Top;
// Elements // Elements
var text = reader.ReadElementContentAsString("text", ""); var text = _reader.ReadElementContentAsString("text", "");
return new TextObject return new TextObject
{ {
@ -304,11 +297,7 @@ internal partial class Tmx
return obj; return obj;
} }
internal static Template ReadTemplate( internal Template ReadTemplate()
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// No attributes // No attributes
@ -318,10 +307,10 @@ internal partial class Tmx
// Should contain exactly one of // Should contain exactly one of
Model.Object? obj = null; Model.Object? obj = null;
reader.ProcessChildren("template", (r, elementName) => elementName switch _reader.ProcessChildren("template", (r, elementName) => elementName switch
{ {
"tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), "Tileset"), "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(), "Tileset"),
"object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"), "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(), "Object"),
_ => r.Skip _ => r.Skip
}); });

View file

@ -0,0 +1,150 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
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");
var type = r.GetOptionalAttributeEnum<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 XmlException("Invalid property type")
}) ?? PropertyType.String;
var propertyType = r.GetOptionalAttribute("propertytype");
if (propertyType is not null)
{
return ReadPropertyWithCustomType();
}
IProperty property = type switch
{
PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable<bool>("value") },
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 => 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;
});
}
internal IProperty ReadPropertyWithCustomType()
{
var isClass = _reader.GetOptionalAttribute("type") == "class";
if (isClass)
{
return ReadClassProperty();
}
return ReadEnumProperty();
}
internal ClassProperty ReadClassProperty()
{
var name = _reader.GetRequiredAttribute("name");
var propertyType = _reader.GetRequiredAttribute("propertytype");
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd)
{
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}");
}
internal EnumProperty ReadEnumProperty()
{
var name = _reader.GetRequiredAttribute("name");
var propertyType = _reader.GetRequiredAttribute("propertytype");
var typeInXml = _reader.GetOptionalAttributeEnum<PropertyType>("type", (s) => s switch
{
"string" => PropertyType.String,
"int" => PropertyType.Int,
_ => throw new XmlException("Invalid property type")
}) ?? PropertyType.String;
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced)
throw new XmlException($"Unknown custom enum definition: {propertyType}. Enums must be defined");
if (ced.StorageType == CustomEnumStorageType.String)
{
var value = _reader.GetRequiredAttribute("value");
if (value.Contains(',') && !ced.ValueAsFlags)
throw new XmlException("Enum value must not contain ',' if not ValueAsFlags is set to true.");
if (ced.ValueAsFlags)
{
var values = value.Split(',').Select(v => v.Trim()).ToHashSet();
return new EnumProperty { Name = name, PropertyType = propertyType, Value = values };
}
else
{
return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet<string> { value } };
}
}
else if (ced.StorageType == CustomEnumStorageType.Int)
{
var value = _reader.GetRequiredAttributeParseable<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 XmlException($"Unknown custom enum storage type: {ced.StorageType}");
}
}

View file

@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Linq;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
public abstract partial class TmxReaderBase
{
internal TileLayer ReadTileLayer(bool dataUsesChunks)
{
var id = _reader.GetRequiredAttributeParseable<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("height");
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
List<IProperty>? properties = null;
Data? data = null;
_reader.ProcessChildren("layer", (r, elementName) => elementName switch
{
"data" => () => Helpers.SetAtMostOnce(ref data, ReadData(dataUsesChunks), "Data"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
_ => r.Skip
});
return new TileLayer
{
ID = id,
Name = name,
Class = @class,
X = x,
Y = y,
Width = width,
Height = height,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Data = data,
Properties = properties ?? []
};
}
internal ImageLayer ReadImageLayer()
{
var id = _reader.GetRequiredAttributeParseable<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var repeatX = (_reader.GetOptionalAttributeParseable<uint>("repeatx") ?? 0) == 1;
var repeatY = (_reader.GetOptionalAttributeParseable<uint>("repeaty") ?? 0) == 1;
List<IProperty>? properties = null;
Image? image = null;
_reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch
{
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
_ => r.Skip
});
return new ImageLayer
{
ID = id,
Name = name,
Class = @class,
X = x,
Y = y,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Properties = properties ?? [],
Image = image,
RepeatX = repeatX,
RepeatY = repeatY
};
}
internal Group ReadGroup()
{
var id = _reader.GetRequiredAttributeParseable<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
List<IProperty>? properties = null;
List<BaseLayer> layers = [];
_reader.ProcessChildren("group", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
"layer" => () => layers.Add(ReadTileLayer(false)),
"objectgroup" => () => layers.Add(ReadObjectLayer()),
"imagelayer" => () => layers.Add(ReadImageLayer()),
"group" => () => layers.Add(ReadGroup()),
_ => r.Skip
});
return new Group
{
ID = id,
Name = name,
Class = @class,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Properties = properties ?? [],
Layers = layers
};
}
}

View file

@ -0,0 +1,317 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
public abstract partial class TmxReaderBase
{
internal Tileset ReadTileset()
{
// Attributes
var version = _reader.GetOptionalAttribute("version");
var tiledVersion = _reader.GetOptionalAttribute("tiledversion");
var firstGID = _reader.GetOptionalAttributeParseable<uint>("firstgid");
var source = _reader.GetOptionalAttribute("source");
var name = _reader.GetOptionalAttribute("name");
var @class = _reader.GetOptionalAttribute("class") ?? "";
var tileWidth = _reader.GetOptionalAttributeParseable<uint>("tilewidth");
var tileHeight = _reader.GetOptionalAttributeParseable<uint>("tileheight");
var spacing = _reader.GetOptionalAttributeParseable<uint>("spacing") ?? 0;
var margin = _reader.GetOptionalAttributeParseable<uint>("margin") ?? 0;
var tileCount = _reader.GetOptionalAttributeParseable<uint>("tilecount");
var columns = _reader.GetOptionalAttributeParseable<uint>("columns");
var objectAlignment = _reader.GetOptionalAttributeEnum<ObjectAlignment>("objectalignment", s => s switch
{
"unspecified" => ObjectAlignment.Unspecified,
"topleft" => ObjectAlignment.TopLeft,
"top" => ObjectAlignment.Top,
"topright" => ObjectAlignment.TopRight,
"left" => ObjectAlignment.Left,
"center" => ObjectAlignment.Center,
"right" => ObjectAlignment.Right,
"bottomleft" => ObjectAlignment.BottomLeft,
"bottom" => ObjectAlignment.Bottom,
"bottomright" => ObjectAlignment.BottomRight,
_ => throw new InvalidOperationException($"Unknown object alignment '{s}'")
}) ?? ObjectAlignment.Unspecified;
var renderSize = _reader.GetOptionalAttributeEnum<TileRenderSize>("rendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,
_ => throw new InvalidOperationException($"Unknown render size '{s}'")
}) ?? TileRenderSize.Tile;
var fillMode = _reader.GetOptionalAttributeEnum<FillMode>("fillmode", s => s switch
{
"stretch" => FillMode.Stretch,
"preserve-aspect-fit" => FillMode.PreserveAspectFit,
_ => throw new InvalidOperationException($"Unknown fill mode '{s}'")
}) ?? FillMode.Stretch;
// Elements
Image? image = null;
TileOffset? tileOffset = null;
Grid? grid = null;
List<IProperty>? properties = null;
List<Wangset>? wangsets = null;
Transformations? transformations = null;
List<Tile> tiles = [];
_reader.ProcessChildren("tileset", (r, elementName) => elementName switch
{
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"),
"tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(), "TileOffset"),
"grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(), "Grid"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
"wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(), "Wangsets"),
"transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(), "Transformations"),
"tile" => () => tiles.Add(ReadTile()),
_ => r.Skip
});
// Check if tileset is referring to external file
if (source is not null)
{
var resolvedTileset = _externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source;
return resolvedTileset;
}
return new Tileset
{
Version = version,
TiledVersion = tiledVersion,
FirstGID = firstGID,
Source = source,
Name = name,
Class = @class,
TileWidth = tileWidth,
TileHeight = tileHeight,
Spacing = spacing,
Margin = margin,
TileCount = tileCount,
Columns = columns,
ObjectAlignment = objectAlignment,
RenderSize = renderSize,
FillMode = fillMode,
Image = image,
TileOffset = tileOffset,
Grid = grid,
Properties = properties ?? [],
Wangsets = wangsets,
Transformations = transformations,
Tiles = tiles
};
}
internal Image ReadImage()
{
// Attributes
var format = _reader.GetOptionalAttributeEnum<ImageFormat>("format", s => s switch
{
"png" => ImageFormat.Png,
"jpg" => ImageFormat.Jpg,
"bmp" => ImageFormat.Bmp,
"gif" => ImageFormat.Gif,
_ => throw new InvalidOperationException($"Unknown image format '{s}'")
});
var source = _reader.GetOptionalAttribute("source");
var transparentColor = _reader.GetOptionalAttributeClass<Color>("trans");
var width = _reader.GetOptionalAttributeParseable<uint>("width");
var height = _reader.GetOptionalAttributeParseable<uint>("height");
_reader.ProcessChildren("image", (r, elementName) => elementName switch
{
"data" => throw new NotSupportedException("Embedded image data is not supported."),
_ => r.Skip
});
if (format is null && source is not null)
format = Helpers.ParseImageFormatFromSource(source);
return new Image
{
Format = format,
Source = source,
TransparentColor = transparentColor,
Width = width,
Height = height,
};
}
internal TileOffset ReadTileOffset()
{
// Attributes
var x = _reader.GetOptionalAttributeParseable<float>("x") ?? 0f;
var y = _reader.GetOptionalAttributeParseable<float>("y") ?? 0f;
_reader.ReadStartElement("tileoffset");
return new TileOffset { X = x, Y = y };
}
internal Grid ReadGrid()
{
// Attributes
var orientation = _reader.GetOptionalAttributeEnum<GridOrientation>("orientation", s => s switch
{
"orthogonal" => GridOrientation.Orthogonal,
"isometric" => GridOrientation.Isometric,
_ => throw new InvalidOperationException($"Unknown orientation '{s}'")
}) ?? GridOrientation.Orthogonal;
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("height");
_reader.ReadStartElement("grid");
return new Grid { Orientation = orientation, Width = width, Height = height };
}
internal Transformations ReadTransformations()
{
// Attributes
var hFlip = (_reader.GetOptionalAttributeParseable<uint>("hflip") ?? 0) == 1;
var vFlip = (_reader.GetOptionalAttributeParseable<uint>("vflip") ?? 0) == 1;
var rotate = (_reader.GetOptionalAttributeParseable<uint>("rotate") ?? 0) == 1;
var preferUntransformed = (_reader.GetOptionalAttributeParseable<uint>("preferuntransformed") ?? 0) == 1;
_reader.ReadStartElement("transformations");
return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed };
}
internal Tile ReadTile()
{
// Attributes
var id = _reader.GetRequiredAttributeParseable<uint>("id");
var type = _reader.GetOptionalAttribute("type") ?? "";
var probability = _reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = _reader.GetOptionalAttributeParseable<uint>("width");
var height = _reader.GetOptionalAttributeParseable<uint>("height");
// Elements
List<IProperty>? properties = null;
Image? image = null;
ObjectLayer? objectLayer = null;
List<Frame>? animation = null;
_reader.ProcessChildren("tile", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"),
"objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(), "ObjectLayer"),
"animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList<Frame>("animation", "frame", (ar) =>
{
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
var duration = ar.GetRequiredAttributeParseable<uint>("duration");
return new Frame { TileID = tileID, Duration = duration };
}), "Animation"),
_ => r.Skip
});
return new Tile
{
ID = id,
Type = type,
Probability = probability,
X = x,
Y = y,
Width = width ?? image?.Width ?? 0,
Height = height ?? image?.Height ?? 0,
Properties = properties ?? [],
Image = image,
ObjectLayer = objectLayer,
Animation = animation
};
}
internal List<Wangset> ReadWangsets() =>
_reader.ReadList<Wangset>("wangsets", "wangset", r => ReadWangset());
internal Wangset ReadWangset()
{
// Attributes
var name = _reader.GetRequiredAttribute("name");
var @class = _reader.GetOptionalAttribute("class") ?? "";
var tile = _reader.GetRequiredAttributeParseable<int>("tile");
// Elements
List<IProperty>? properties = null;
List<WangColor> wangColors = [];
List<WangTile> wangTiles = [];
_reader.ProcessChildren("wangset", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
"wangcolor" => () => wangColors.Add(ReadWangColor()),
"wangtile" => () => wangTiles.Add(ReadWangTile()),
_ => r.Skip
});
if (wangColors.Count > 254)
throw new ArgumentException("Wangset can have at most 254 Wang colors.");
return new Wangset
{
Name = name,
Class = @class,
Tile = tile,
Properties = properties ?? [],
WangColors = wangColors,
WangTiles = wangTiles
};
}
internal WangColor ReadWangColor()
{
// Attributes
var name = _reader.GetRequiredAttribute("name");
var @class = _reader.GetOptionalAttribute("class") ?? "";
var color = _reader.GetRequiredAttributeParseable<Color>("color");
var tile = _reader.GetRequiredAttributeParseable<int>("tile");
var probability = _reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
// Elements
List<IProperty>? properties = null;
_reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"),
_ => r.Skip
});
return new WangColor
{
Name = name,
Class = @class,
Color = color,
Tile = tile,
Probability = probability,
Properties = properties ?? []
};
}
internal WangTile ReadWangTile()
{
// Attributes
var tileID = _reader.GetRequiredAttributeParseable<uint>("tileid");
var wangID = _reader.GetRequiredAttributeParseable<byte[]>("wangid", s =>
{
// Comma-separated list of indices (0-254)
var indices = s.Split(',').Select(i => byte.Parse(i, CultureInfo.InvariantCulture)).ToArray();
if (indices.Length > 8)
throw new ArgumentException("Wang ID can have at most 8 indices.");
return indices;
});
_reader.ReadStartElement("wangtile");
return new WangTile
{
TileID = tileID,
WangID = wangID
};
}
}

View file

@ -0,0 +1,74 @@
using System;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
/// <summary>
/// Base class for Tiled XML format readers.
/// </summary>
public abstract partial class TmxReaderBase : IDisposable
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly XmlReader _reader;
private bool disposedValue;
/// <summary>
/// Constructs a new <see cref="TmxReaderBase"/>, which is the base class for all Tiled XML format readers.
/// </summary>
/// <param name="reader">An XML reader for reading a Tiled map in the Tiled XML 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 function that resolves custom types given their source.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
protected TmxReaderBase(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver));
// Prepare reader
_ = _reader.MoveToContent();
}
/// <inheritdoc />
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
_reader.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~TmxReaderBase()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
/// <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 System.Xml; using System.Xml;
using DotTiled.Model; using DotTiled.Model;
@ -8,68 +7,20 @@ namespace DotTiled.Serialization.Tmx;
/// <summary> /// <summary>
/// A tileset reader for the Tiled XML format. /// A tileset reader for the Tiled XML format.
/// </summary> /// </summary>
public class TsxTilesetReader : ITilesetReader public class TsxTilesetReader : TmxReaderBase, ITilesetReader
{ {
// External resolvers
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Constructs a new <see cref="TsxTilesetReader"/>. /// Constructs a new <see cref="TsxTilesetReader"/>.
/// </summary> /// </summary>
/// <param name="reader">An XML reader for reading a Tiled tileset in the Tiled XML format.</param> /// <inheritdoc />
/// <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>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
public TsxTilesetReader( public TsxTilesetReader(
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) Func<string, ICustomTypeDefinition> customTypeResolver) : base(
{ reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); { }
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
// Prepare reader
_ = _reader.MoveToContent();
}
/// <inheritdoc/> /// <inheritdoc/>
public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions); public new Tileset ReadTileset() => base.ReadTileset();
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
_reader.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~TsxTilesetReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
/// <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 System.Xml; using System.Xml;
using DotTiled.Model; using DotTiled.Model;
@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx;
/// <summary> /// <summary>
/// A template reader for the Tiled XML format. /// A template reader for the Tiled XML format.
/// </summary> /// </summary>
public class TxTemplateReader : ITemplateReader public class TxTemplateReader : TmxReaderBase, ITemplateReader
{ {
// Resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
/// <summary> /// <summary>
/// Constructs a new <see cref="TxTemplateReader"/>. /// Constructs a new <see cref="TxTemplateReader"/>.
/// </summary> /// </summary>
/// <param name="reader">An XML reader for reading a Tiled template in the Tiled XML format.</param> /// <inheritdoc />
/// <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>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
public TxTemplateReader( public TxTemplateReader(
XmlReader reader, XmlReader reader,
Func<string, Tileset> externalTilesetResolver, Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver, Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) Func<string, ICustomTypeDefinition> customTypeResolver) : base(
{ reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); { }
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
// Prepare reader
_ = _reader.MoveToContent();
}
/// <inheritdoc/> /// <inheritdoc/>
public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); public new Template ReadTemplate() => base.ReadTemplate();
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
_reader.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~TxTemplateReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
} }