diff --git a/Makefile b/Makefile index 9b64573..14a40ab 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,10 @@ lint: dotnet format style --verify-no-changes src/DotTiled.sln dotnet format analyzers --verify-no-changes src/DotTiled.sln -BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj -BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts +BENCHMARK_SOURCES = src/DotTiled.Benchmark/Program.cs src/DotTiled.Benchmark/DotTiled.Benchmark.csproj +BENCHMARK_OUTPUTDIR = src/DotTiled.Benchmark/BenchmarkDotNet.Artifacts .PHONY: benchmark benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES) - dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file + dotnet run --project src/DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file diff --git a/docs/docs/custom-properties.md b/docs/docs/custom-properties.md new file mode 100644 index 0000000..78cbd00 --- /dev/null +++ b/docs/docs/custom-properties.md @@ -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 in some way. Below is an exhaustive list of all classes that can contain custom properties: + +- + - + - + - + - +- (allows for recursive property objects) +- (used to define custom Tiled property types) +- + - + - + - + - + - + - + - +- +- +- +- + +## How to access properties + +To access the properties on one of the classes listed above, you will make use of the interface. + +In situations where you know that a property must exist, and you simply want to retrieve it, you can use the method like so: + +```csharp +var map = LoadMap(); +var propertyValue = map.GetProperty("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 method like so: + +```csharp +var map = LoadMap(); +if (map.TryGetProperty("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 interface. Below is a list of all property types that Tiled supports and their corresponding classes in DotTiled: + +- `bool` - +- `color` - +- `float` - +- `file` - +- `int` - +- `object` - +- `string` - + +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 . 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 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 . 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 interface with a method like `GetMappedProperty(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 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(); +var entityTypeDefinition = CustomEnumDefinition.FromEnum(); + +// ... + +var map = LoadMap(); +var monsterSpawner = map.GetMappedProperty("monsterSpawnerPropertyInMap"); +var entityType = map.GetMappedProperty("entityTypePropertyInMap"); +``` + +Finally, it might be possible to also make some kind of exporting functionality for . 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. \ No newline at end of file diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml index 13cc1f7..62d2c9d 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -3,4 +3,5 @@ - href: quickstart.md - name: Essentials -- href: loading-a-map.md \ No newline at end of file +- href: loading-a-map.md +- href: custom-properties.md \ No newline at end of file diff --git a/docs/images/entity-type-enum.png b/docs/images/entity-type-enum.png new file mode 100644 index 0000000..459689b Binary files /dev/null and b/docs/images/entity-type-enum.png differ diff --git a/docs/images/monster-spawner-class.png b/docs/images/monster-spawner-class.png new file mode 100644 index 0000000..c3b5ef4 Binary files /dev/null and b/docs/images/monster-spawner-class.png differ diff --git a/docs/images/resolve-types.png b/docs/images/resolve-types.png new file mode 100644 index 0000000..ea66d43 Binary files /dev/null and b/docs/images/resolve-types.png differ diff --git a/src/DotTiled.Benchmark/Program.cs b/src/DotTiled.Benchmark/Program.cs index bf67fd0..523a7f1 100644 --- a/src/DotTiled.Benchmark/Program.cs +++ b/src/DotTiled.Benchmark/Program.cs @@ -39,7 +39,7 @@ namespace DotTiled.Benchmark { using var stringReader = new StringReader(_tmxContents); 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(); } @@ -47,7 +47,7 @@ namespace DotTiled.Benchmark [Benchmark(Baseline = true, Description = "DotTiled")] public DotTiled.Model.Map LoadWithDotTiledFromInMemoryTmjString() { - using var mapReader = new DotTiled.Serialization.Tmj.TmjMapReader(_tmjContents, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), []); + using var mapReader = new DotTiled.Serialization.Tmj.TmjMapReader(_tmjContents, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException()); return mapReader.ReadMap(); } diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index 6984b79..358029b 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -91,7 +91,7 @@ public static partial class DotTiledAssert AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); Assert.NotNull(actual.Tilesets); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); diff --git a/src/DotTiled.Tests/Assert/AssertProperties.cs b/src/DotTiled.Tests/Assert/AssertProperties.cs index 646d6eb..84af365 100644 --- a/src/DotTiled.Tests/Assert/AssertProperties.cs +++ b/src/DotTiled.Tests/Assert/AssertProperties.cs @@ -4,7 +4,7 @@ namespace DotTiled.Tests; public static partial class DotTiledAssert { - internal static void AssertProperties(Dictionary? expected, Dictionary? actual) + internal static void AssertProperties(IList? expected, IList? actual) { if (expected is null) { @@ -14,18 +14,16 @@ public static partial class DotTiledAssert Assert.NotNull(actual); AssertEqual(expected.Count, actual.Count, "Properties.Count"); - foreach (var kvp in expected) + foreach (var prop in expected) { - Assert.Contains(kvp.Key, actual.Keys); - AssertProperty((dynamic)kvp.Value, (dynamic)actual[kvp.Key]); - } - } + Assert.Contains(actual, p => p.Name == prop.Name); - private static void AssertProperty(IProperty expected, IProperty actual) - { - AssertEqual(expected.Type, actual.Type, "Property.Type"); - AssertEqual(expected.Name, actual.Name, "Property.Name"); - AssertProperties((dynamic)actual, (dynamic)expected); + var actualProp = actual.First(p => p.Name == prop.Name); + AssertEqual(prop.Type, actualProp.Type, "Property.Type"); + AssertEqual(prop.Name, actualProp.Name, "Property.Name"); + + AssertProperty((dynamic)prop, (dynamic)actualProp); + } } 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) { 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); + } } } diff --git a/src/DotTiled.Tests/Assert/AssertTileset.cs b/src/DotTiled.Tests/Assert/AssertTileset.cs index 134cc30..befc79a 100644 --- a/src/DotTiled.Tests/Assert/AssertTileset.cs +++ b/src/DotTiled.Tests/Assert/AssertTileset.cs @@ -141,9 +141,9 @@ public static partial class DotTiledAssert AssertEqual(expected.Height, actual.Height, nameof(Tile.Height)); // Elements - AssertProperties(actual.Properties, expected.Properties); - AssertImage(actual.Image, expected.Image); - AssertLayer((BaseLayer?)actual.ObjectLayer, (BaseLayer?)expected.ObjectLayer); + AssertProperties(expected.Properties, actual.Properties); + AssertImage(expected.Image, actual.Image); + AssertLayer((BaseLayer?)expected.ObjectLayer, (BaseLayer?)actual.ObjectLayer); if (expected.Animation is not null) { Assert.NotNull(actual.Animation); diff --git a/src/DotTiled.Tests/Serialization/TestData.cs b/src/DotTiled.Tests/Serialization/TestData.cs index f6e49b5..1c0b885 100644 --- a/src/DotTiled.Tests/Serialization/TestData.cs +++ b/src/DotTiled.Tests/Serialization/TestData.cs @@ -32,14 +32,15 @@ public static partial class TestData public static IEnumerable MapTests => [ - ["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty()], - ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty()], + ["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty()], + ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty()], ["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()], - ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty()], - ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty()], - ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], - ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty()], - ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => MapWithEmbeddedTileset(), Array.Empty()], + ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_deep_props/map-with-deep-props", (string f) => MapWithDeepProps(), MapWithDeepPropsCustomTypeDefinitions()], ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs index 6dac137..a937685 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs @@ -49,16 +49,15 @@ public partial class TestData Width = 1, Height = 1 }, - Properties = new Dictionary - { - ["tilesetbool"] = new BoolProperty { Name = "tilesetbool", Value = true }, - ["tilesetcolor"] = new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, - ["tilesetfile"] = new FileProperty { Name = "tilesetfile", Value = "" }, - ["tilesetfloat"] = new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, - ["tilesetint"] = new IntProperty { Name = "tilesetint", Value = 9 }, - ["tilesetobject"] = new ObjectProperty { Name = "tilesetobject", Value = 0 }, - ["tilesetstring"] = new StringProperty { Name = "tilesetstring", Value = "hello world!" } - }, + Properties = [ + new BoolProperty { Name = "tilesetbool", Value = true }, + new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "tilesetfile", Value = "" }, + new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, + new IntProperty { Name = "tilesetint", Value = 9 }, + new ObjectProperty { Name = "tilesetobject", Value = 0 }, + new StringProperty { Name = "tilesetstring", Value = "hello world!" } + ], Tiles = [ new Tile { diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs index ba5cf4f..d5d8c1e 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs @@ -55,15 +55,15 @@ public partial class TestData } } ], - Properties = new Dictionary - { - ["boolprop"] = new BoolProperty { Name = "boolprop", Value = true }, - ["colorprop"] = new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, - ["fileprop"] = new FileProperty { Name = "fileprop", Value = "file.txt" }, - ["floatprop"] = new FloatProperty { Name = "floatprop", Value = 4.2f }, - ["intprop"] = new IntProperty { Name = "intprop", Value = 8 }, - ["objectprop"] = new ObjectProperty { Name = "objectprop", Value = 5 }, - ["stringprop"] = new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } - } + Properties = + [ + new BoolProperty { Name = "boolprop", Value = true }, + new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileprop", Value = "file.txt" }, + new FloatProperty { Name = "floatprop", Value = 4.2f }, + new IntProperty { Name = "intprop", Value = 8 }, + new ObjectProperty { Name = "objectprop", Value = 5 }, + new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } + ] }; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs index 758c5c3..9a965c2 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -55,28 +55,50 @@ public partial class TestData } } ], - Properties = new Dictionary - { - ["customclassprop"] = new ClassProperty + Properties = [ + new ClassProperty { Name = "customclassprop", PropertyType = "CustomClass", - Properties = new Dictionary - { - ["boolinclass"] = new BoolProperty { Name = "boolinclass", Value = true }, - ["colorinclass"] = new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, - ["fileinclass"] = new FileProperty { Name = "fileinclass", Value = "" }, - ["floatinclass"] = new FloatProperty { Name = "floatinclass", Value = 13.37f }, - ["intinclass"] = new IntProperty { Name = "intinclass", Value = 0 }, - ["objectinclass"] = new ObjectProperty { Name = "objectinclass", Value = 0 }, - ["stringinclass"] = new StringProperty { Name = "stringinclass", Value = "This is a set string" } - } + 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 = "This is a set string" } + ] + }, + new EnumProperty + { + Name = "customenumstringprop", + PropertyType = "CustomEnumString", + Value = new HashSet { "CustomEnumString_2" } + }, + new EnumProperty + { + Name = "customenumstringflagsprop", + PropertyType = "CustomEnumStringFlags", + Value = new HashSet { "CustomEnumStringFlags_1", "CustomEnumStringFlags_2" } + }, + new EnumProperty + { + Name = "customenumintprop", + PropertyType = "CustomEnumInt", + Value = new HashSet { "CustomEnumInt_4" } + }, + new EnumProperty + { + Name = "customenumintflagsprop", + PropertyType = "CustomEnumIntFlags", + Value = new HashSet { "CustomEnumIntFlags_2", "CustomEnumIntFlags_3" } } - } + ] }; // This comes from map-with-custom-type-props/propertytypes.json - public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ + public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ new CustomClassDefinition { Name = "CustomClass", @@ -118,6 +140,50 @@ public partial class TestData 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" + ] } ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj index a8c7f43..74f892b 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj @@ -32,6 +32,30 @@ "floatinclass":13.37, "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", "tiledversion":"1.11.0", diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx index c364577..cadc2fa 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx @@ -8,6 +8,10 @@ + + + + diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs new file mode 100644 index 0000000..1b36b4e --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs @@ -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 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 + } + ] + } + ]; +} diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj new file mode 100644 index 0000000..9fa2bba --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj @@ -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 +} \ No newline at end of file diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx new file mode 100644 index 0000000..56e8f2e --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + +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 + + + diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs index 6e4ae55..ba0013a 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs @@ -95,10 +95,9 @@ public partial class TestData new Vector2(35.6667f, 32.3333f) ], Template = fileExt == "tmx" ? "poly.tx" : "poly.tj", - Properties = new Dictionary - { - ["templateprop"] = new StringProperty { Name = "templateprop", Value = "helo there" } - } + Properties = [ + new StringProperty { Name = "templateprop", Value = "helo there" } + ] }, new TileObject { diff --git a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index bdd19e8..48cc13f 100644 --- a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -11,7 +11,7 @@ public partial class TmjMapReaderTests public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( string testDataFile, Func expectedMap, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Arrange testDataFile += ".tmj"; @@ -20,16 +20,20 @@ public partial class TmjMapReaderTests Template ResolveTemplate(string source) { var templateJson = TestData.GetRawStringFor($"{fileDir}/{source}"); - using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions); + using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return templateReader.ReadTemplate(); } Tileset ResolveTileset(string source) { var tilesetJson = TestData.GetRawStringFor($"{fileDir}/{source}"); - using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions); + using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions); + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType); // Act var map = mapReader.ReadMap(); diff --git a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index ee74718..748d4e3 100644 --- a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -11,7 +11,7 @@ public partial class TmxMapReaderTests public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( string testDataFile, Func expectedMap, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Arrange testDataFile += ".tmx"; @@ -20,16 +20,20 @@ public partial class TmxMapReaderTests Template ResolveTemplate(string 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(); } Tileset ResolveTileset(string 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(); } - 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 var map = mapReader.ReadMap(); diff --git a/src/DotTiled/Model/Layers/BaseLayer.cs b/src/DotTiled/Model/Layers/BaseLayer.cs index aa78191..a16b41a 100644 --- a/src/DotTiled/Model/Layers/BaseLayer.cs +++ b/src/DotTiled/Model/Layers/BaseLayer.cs @@ -7,7 +7,7 @@ namespace DotTiled.Model; /// To check the type of a layer, use C# pattern matching, /// or some other mechanism to determine the type of the layer at runtime. /// -public abstract class BaseLayer +public abstract class BaseLayer : HasPropertiesBase { /// /// 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 /// /// Layer properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; } diff --git a/src/DotTiled/Model/Layers/Objects/Object.cs b/src/DotTiled/Model/Layers/Objects/Object.cs index fad05db..f2990dc 100644 --- a/src/DotTiled/Model/Layers/Objects/Object.cs +++ b/src/DotTiled/Model/Layers/Objects/Object.cs @@ -5,7 +5,7 @@ namespace DotTiled.Model; /// /// Base class for objects in object layers. /// -public abstract class Object +public abstract class Object : HasPropertiesBase { /// /// 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 /// /// Object properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; } diff --git a/src/DotTiled/Model/Map.cs b/src/DotTiled/Model/Map.cs index c9c72dc..48ed975 100644 --- a/src/DotTiled/Model/Map.cs +++ b/src/DotTiled/Model/Map.cs @@ -90,7 +90,7 @@ public enum StaggerIndex /// /// Represents a Tiled map. /// -public class Map +public class Map : HasPropertiesBase { /// /// The TMX format version. Is incremented to match minor Tiled releases. @@ -191,7 +191,10 @@ public class Map /// /// Map properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; /// /// List of tilesets used by the map. diff --git a/src/DotTiled/Model/Properties/BoolProperty.cs b/src/DotTiled/Model/Properties/BoolProperty.cs index bc60a87..315e820 100644 --- a/src/DotTiled/Model/Properties/BoolProperty.cs +++ b/src/DotTiled/Model/Properties/BoolProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a boolean property. /// -public class BoolProperty : IProperty +public class BoolProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/ClassProperty.cs b/src/DotTiled/Model/Properties/ClassProperty.cs index 50d65ba..5c9d6a5 100644 --- a/src/DotTiled/Model/Properties/ClassProperty.cs +++ b/src/DotTiled/Model/Properties/ClassProperty.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace DotTiled.Model; @@ -6,7 +8,7 @@ namespace DotTiled.Model; /// /// Represents a class property. /// -public class ClassProperty : IProperty +public class ClassProperty : IHasProperties, IProperty> { /// public required string Name { get; set; } @@ -23,13 +25,41 @@ public class ClassProperty : IProperty /// /// The properties of the class property. /// - public required Dictionary Properties { get; set; } + public required IList Value { get; set; } /// public IProperty Clone() => new ClassProperty { Name = Name, PropertyType = PropertyType, - Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) + Value = Value.Select(property => property.Clone()).ToList() }; + + /// + public IList GetProperties() => Value; + + /// + public T GetProperty(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}'."); + } + + /// + public bool TryGetProperty(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; + } } diff --git a/src/DotTiled/Model/Properties/ColorProperty.cs b/src/DotTiled/Model/Properties/ColorProperty.cs index aec43f6..7c4a132 100644 --- a/src/DotTiled/Model/Properties/ColorProperty.cs +++ b/src/DotTiled/Model/Properties/ColorProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a color property. /// -public class ColorProperty : IProperty +public class ColorProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs index b5aa0ec..313081f 100644 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs +++ b/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs @@ -65,8 +65,14 @@ public enum CustomClassUseAs /// Represents a custom class definition in Tiled. Refer to the /// documentation of custom types to understand how they work. /// -public class CustomClassDefinition : CustomTypeDefinition +public class CustomClassDefinition : HasPropertiesBase, ICustomTypeDefinition { + /// + public uint ID { get; set; } + + /// + public required string Name { get; set; } + /// /// The color of the custom class inside the Tiled editor. /// @@ -86,4 +92,7 @@ public class CustomClassDefinition : CustomTypeDefinition /// The members of the custom class, with their names, types and default values. /// public List Members { get; set; } = []; + + /// + public override IList GetProperties() => Members; } diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs index ee40be0..60b7f5b 100644 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs +++ b/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs @@ -22,8 +22,14 @@ public enum CustomEnumStorageType /// Represents a custom enum definition in Tiled. Refer to the /// documentation of custom types to understand how they work. /// -public class CustomEnumDefinition : CustomTypeDefinition +public class CustomEnumDefinition : ICustomTypeDefinition { + /// + public uint ID { get; set; } + + /// + public required string Name { get; set; } + /// /// The storage type of the custom enum. /// diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs index 3a91b0a..1e595cc 100644 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs +++ b/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Base class for custom type definitions. /// -public abstract class CustomTypeDefinition +public interface ICustomTypeDefinition { /// /// The ID of the custom type. @@ -13,5 +13,5 @@ public abstract class CustomTypeDefinition /// /// The name of the custom type. /// - public string Name { get; set; } = ""; + public string Name { get; set; } } diff --git a/src/DotTiled/Model/Properties/EnumProperty.cs b/src/DotTiled/Model/Properties/EnumProperty.cs new file mode 100644 index 0000000..19e1b1f --- /dev/null +++ b/src/DotTiled/Model/Properties/EnumProperty.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotTiled.Model; + +/// +/// Represents an enum property. +/// +public class EnumProperty : IProperty> +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => Model.PropertyType.Enum; + + /// + /// The type of the class property. This will be the name of a custom defined + /// type in Tiled. + /// + public required string PropertyType { get; set; } + + /// + /// The value of the enum property. + /// + public required ISet Value { get; set; } + + /// + public IProperty Clone() => new EnumProperty + { + Name = Name, + PropertyType = PropertyType, + Value = Value.ToHashSet() + }; + + /// + /// 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. + /// + /// The value to check. + /// True if the enum property is equal to the specified value; otherwise, false. + public bool IsValue(string value) => Value.Contains(value) && Value.Count == 1; + + /// + /// Determines whether the enum property has the specified value. This method is very similar to the common method. + /// + /// The value to check. + /// True if the enum property has the specified value as one of its values; otherwise, false. + public bool HasValue(string value) => Value.Contains(value); +} diff --git a/src/DotTiled/Model/Properties/FileProperty.cs b/src/DotTiled/Model/Properties/FileProperty.cs index 4ed8642..7c65121 100644 --- a/src/DotTiled/Model/Properties/FileProperty.cs +++ b/src/DotTiled/Model/Properties/FileProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a file property. /// -public class FileProperty : IProperty +public class FileProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/FloatProperty.cs b/src/DotTiled/Model/Properties/FloatProperty.cs index 4c6b51f..57ba928 100644 --- a/src/DotTiled/Model/Properties/FloatProperty.cs +++ b/src/DotTiled/Model/Properties/FloatProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a float property. /// -public class FloatProperty : IProperty +public class FloatProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/IHasProperties.cs b/src/DotTiled/Model/Properties/IHasProperties.cs new file mode 100644 index 0000000..1b124a9 --- /dev/null +++ b/src/DotTiled/Model/Properties/IHasProperties.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace DotTiled.Model; + +/// +/// Interface for objects that have properties attached to them. +/// +public interface IHasProperties +{ + /// + /// The properties attached to the object. + /// + IList GetProperties(); + + /// + /// Tries to get a property of the specified type with the specified name. + /// + /// The type of the property to get. + /// The name of the property to get. + /// The property with the specified name, if found. + /// True if a property with the specified name was found; otherwise, false. + bool TryGetProperty(string name, out T? property) where T : IProperty; + + /// + /// Gets a property of the specified type with the specified name. + /// + /// The type of the property to get. + /// The name of the property to get. + /// The property with the specified name. + T GetProperty(string name) where T : IProperty; +} + +/// +/// Interface for objects that have properties attached to them. +/// +public abstract class HasPropertiesBase : IHasProperties +{ + /// + public abstract IList GetProperties(); + + /// + /// Thrown when a property with the specified name is not found. + /// Thrown when a property with the specified name is not of the specified type. + public T GetProperty(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}'."); + } + + /// + public bool TryGetProperty(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; + } +} diff --git a/src/DotTiled/Model/Properties/IProperty.cs b/src/DotTiled/Model/Properties/IProperty.cs index 262ee09..d2d98e8 100644 --- a/src/DotTiled/Model/Properties/IProperty.cs +++ b/src/DotTiled/Model/Properties/IProperty.cs @@ -22,3 +22,15 @@ public interface IProperty /// An identical, but non-reference-equal, instance of the same property. IProperty Clone(); } + +/// +/// Interface for properties that can be attached to objects, tiles, tilesets, maps etc. +/// +/// The type of the property value. +public interface IProperty : IProperty +{ + /// + /// The value of the property. + /// + public T Value { get; set; } +} diff --git a/src/DotTiled/Model/Properties/IntProperty.cs b/src/DotTiled/Model/Properties/IntProperty.cs index 29f7a1d..27a7afc 100644 --- a/src/DotTiled/Model/Properties/IntProperty.cs +++ b/src/DotTiled/Model/Properties/IntProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents an integer property. /// -public class IntProperty : IProperty +public class IntProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/ObjectProperty.cs b/src/DotTiled/Model/Properties/ObjectProperty.cs index 04b15ba..de8a1e0 100644 --- a/src/DotTiled/Model/Properties/ObjectProperty.cs +++ b/src/DotTiled/Model/Properties/ObjectProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents an object property. /// -public class ObjectProperty : IProperty +public class ObjectProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Properties/PropertyType.cs b/src/DotTiled/Model/Properties/PropertyType.cs index d6057cc..451ca5e 100644 --- a/src/DotTiled/Model/Properties/PropertyType.cs +++ b/src/DotTiled/Model/Properties/PropertyType.cs @@ -43,5 +43,10 @@ public enum PropertyType /// /// A class property. /// - Class + Class, + + /// + /// An enum property. + /// + Enum } diff --git a/src/DotTiled/Model/Properties/StringProperty.cs b/src/DotTiled/Model/Properties/StringProperty.cs index 49d7aec..0c20b29 100644 --- a/src/DotTiled/Model/Properties/StringProperty.cs +++ b/src/DotTiled/Model/Properties/StringProperty.cs @@ -3,7 +3,7 @@ namespace DotTiled.Model; /// /// Represents a string property. /// -public class StringProperty : IProperty +public class StringProperty : IProperty { /// public required string Name { get; set; } diff --git a/src/DotTiled/Model/Tilesets/Tile.cs b/src/DotTiled/Model/Tilesets/Tile.cs index c6b964d..9919cfd 100644 --- a/src/DotTiled/Model/Tilesets/Tile.cs +++ b/src/DotTiled/Model/Tilesets/Tile.cs @@ -6,7 +6,7 @@ namespace DotTiled.Model; /// Represents a single tile in a tileset, when using a collection of images to represent the tileset. /// Tiled documentation for Tileset tiles /// -public class Tile +public class Tile : HasPropertiesBase { /// /// The local tile ID within its tileset. @@ -46,7 +46,10 @@ public class Tile /// /// Tile properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; /// /// The image representing this tile. Only used for tilesets that composed of a collection of images. diff --git a/src/DotTiled/Model/Tilesets/Tileset.cs b/src/DotTiled/Model/Tilesets/Tileset.cs index 147f3d2..47f2021 100644 --- a/src/DotTiled/Model/Tilesets/Tileset.cs +++ b/src/DotTiled/Model/Tilesets/Tileset.cs @@ -8,7 +8,7 @@ namespace DotTiled.Model; public enum ObjectAlignment { /// - /// The alignment is unspecified. Tile objects will use in orthogonal maps, and in isometric maps. + /// The alignment is unspecified. Tile objects will use in orthogonal maps, and in isometric maps. /// Unspecified, @@ -93,7 +93,7 @@ public enum FillMode /// /// A tileset is a collection of tiles that can be used in a tile layer, or by tile objects. /// -public class Tileset +public class Tileset : HasPropertiesBase { /// /// The TMX format version. Is incremented to match minor Tiled releases. @@ -161,7 +161,7 @@ public class Tileset public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; /// - /// The size to use when rendering tiles from thie tileset on a tile layer. When set to , the tile is drawn at the tile grid size of the map. + /// The size to use when rendering tiles from thie tileset on a tile layer. When set to , the tile is drawn at the tile grid size of the map. /// public TileRenderSize RenderSize { get; set; } = TileRenderSize.Tile; @@ -188,7 +188,10 @@ public class Tileset /// /// Tileset properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; // public List? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization diff --git a/src/DotTiled/Model/Tilesets/WangColor.cs b/src/DotTiled/Model/Tilesets/WangColor.cs index 20678cb..f5d1186 100644 --- a/src/DotTiled/Model/Tilesets/WangColor.cs +++ b/src/DotTiled/Model/Tilesets/WangColor.cs @@ -5,7 +5,7 @@ namespace DotTiled.Model; /// /// Represents a Wang color in a Wang set. /// -public class WangColor +public class WangColor : HasPropertiesBase { /// /// The name of this color. @@ -35,5 +35,8 @@ public class WangColor /// /// The Wang color properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; } diff --git a/src/DotTiled/Model/Tilesets/Wangset.cs b/src/DotTiled/Model/Tilesets/Wangset.cs index 1a6f7c3..66952e8 100644 --- a/src/DotTiled/Model/Tilesets/Wangset.cs +++ b/src/DotTiled/Model/Tilesets/Wangset.cs @@ -5,7 +5,7 @@ namespace DotTiled.Model; /// /// Defines a list of colors and any number of Wang tiles using these colors. /// -public class Wangset +public class Wangset : HasPropertiesBase { /// /// The name of the Wang set. @@ -25,7 +25,10 @@ public class Wangset /// /// The Wang set properties. /// - public Dictionary? Properties { get; set; } + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; // Up to 254 Wang colors /// diff --git a/src/DotTiled/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index c6cdefe..1e95695 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -73,31 +73,52 @@ internal static partial class Helpers }; } - internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary? overrideProperties) + internal static List CreateInstanceOfCustomClass( + CustomClassDefinition customClassDefinition, + Func 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 MergeProperties(IList? baseProperties, IList? overrideProperties) { if (baseProperties is null) - return overrideProperties ?? new Dictionary(); + return overrideProperties ?? []; if (overrideProperties is null) return baseProperties; - var result = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); - foreach (var (key, value) in overrideProperties) + var result = baseProperties.Select(x => x.Clone()).ToList(); + 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; } 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 { - result[key] = value; + ReplacePropertyInList(result, overrideProp); } } } @@ -105,6 +126,15 @@ internal static partial class Helpers return result; } + internal static void ReplacePropertyInList(List 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(ref T? field, T value, string fieldName) { if (field is not null) diff --git a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs index 3a66f1b..6a71eb7 100644 --- a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; @@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj; /// /// A template reader for reading Tiled JSON templates. /// -public class TjTemplateReader : ITemplateReader +public class TjTemplateReader : TmjReaderBase, ITemplateReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// A string containing a Tiled template in the Tiled JSON format. + /// A string containing a Tiled map in the Tiled JSON format. /// A function that resolves external tilesets given their source. /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// A function that resolves custom types given their name. /// Thrown when any of the arguments are null. public TjTemplateReader( string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Template ReadTemplate() - { - var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadTemplate(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - } - - /// - 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); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public Template ReadTemplate() => ReadTemplate(RootElement); } diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs deleted file mode 100644 index b9f8de0..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs +++ /dev/null @@ -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 externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var type = element.GetRequiredProperty("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}'.") - }; - } -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs deleted file mode 100644 index 0c35bbe..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ /dev/null @@ -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 ReadProperties( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) => - element.GetValueAsList(e => - { - var name = e.GetRequiredProperty("name"); - var type = e.GetOptionalPropertyParseable("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("value") }, - PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable("value") }, - PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty("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 customTypeDefinitions) - { - var name = element.GetRequiredProperty("name"); - var propertyType = element.GetRequiredProperty("propertytype"); - - var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - - if (customTypeDef is CustomClassDefinition ccd) - { - var propsInType = CreateInstanceOfCustomClass(ccd); - var props = element.GetOptionalPropertyCustom>("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 ReadCustomClassProperties( - JsonElement element, - CustomClassDefinition customClassDefinition, - IReadOnlyCollection customTypeDefinitions) - { - Dictionary 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() }, - PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs(), CultureInfo.InvariantCulture) }, - PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Class => ReadClassProperty(propElement, customTypeDefinitions), - _ => throw new JsonException("Invalid property type") - }; - - resultingProps[prop.Name] = property; - } - - return resultingProps; - } - - internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => - customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs b/src/DotTiled/Serialization/Tmj/Tmj.Template.cs deleted file mode 100644 index 3746b76..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs +++ /dev/null @@ -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 externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var type = element.GetRequiredProperty("type"); - var tileset = element.GetOptionalPropertyCustom("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null); - var @object = element.GetRequiredPropertyCustom("object", el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)); - - return new Template - { - Tileset = tileset, - Object = @object - }; - } -} diff --git a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs index c26311b..7ec12e1 100644 --- a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs +++ b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text.Json; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; @@ -8,73 +6,24 @@ namespace DotTiled.Serialization.Tmj; /// /// A map reader for reading Tiled JSON maps. /// -public class TmjMapReader : IMapReader +public class TmjMapReader : TmjReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// /// A string containing a Tiled map in the Tiled JSON format. /// A function that resolves external tilesets given their source. /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// A function that resolves custom types given their name. /// Thrown when any of the arguments are null. public TmjMapReader( string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Map ReadMap() - { - var jsonDoc = JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - } - - /// - 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); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public Map ReadMap() => ReadMap(RootElement); } diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Data.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs similarity index 98% rename from src/DotTiled/Serialization/Tmj/Tmj.Data.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs index c021a6c..22a35e2 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Data.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs @@ -5,7 +5,7 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs similarity index 72% rename from src/DotTiled/Serialization/Tmj/Tmj.Group.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs index 0885167..e132d0c 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; @@ -6,12 +5,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Group ReadGroup( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Group ReadGroup(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -23,8 +19,8 @@ internal partial class Tmj var offsetY = element.GetOptionalProperty("offsety", 0.0f); var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); - var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); + var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(ReadLayer), []); return new Group { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs similarity index 84% rename from src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs index 794f2b5..576fa52 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; using System.Globalization; using System.Text.Json; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static ImageLayer ReadImageLayer( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal ImageLayer ReadImageLayer(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -21,7 +18,7 @@ internal partial class Tmj var offsetY = element.GetOptionalProperty("offsety", 0.0f); var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var image = element.GetRequiredProperty("image"); var repeatX = element.GetOptionalProperty("repeatx", false); diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs new file mode 100644 index 0000000..9e01d84 --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs @@ -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("type"); + + return type switch + { + "tilelayer" => ReadTileLayer(element), + "objectgroup" => ReadObjectLayer(element), + "imagelayer" => ReadImageLayer(element), + "group" => ReadGroup(element), + _ => throw new JsonException($"Unsupported layer type '{type}'.") + }; + } +} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs similarity index 85% rename from src/DotTiled/Serialization/Tmj/Tmj.Map.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs index 47dde3f..47abc66 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; @@ -6,13 +5,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Map ReadMap( - JsonElement element, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Map ReadMap(JsonElement element) { var version = element.GetRequiredProperty("version"); var tiledVersion = element.GetRequiredProperty("tiledversion"); @@ -58,10 +53,10 @@ internal partial class Tmj var nextObjectID = element.GetRequiredProperty("nextobjectid"); var infinite = element.GetOptionalProperty("infinite", false); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); - List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); - List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []); + List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el)), []); + List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el)), []); return new Map { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs similarity index 90% rename from src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs index 6775389..cae4790 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Numerics; @@ -7,12 +6,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static ObjectLayer ReadObjectLayer( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal ObjectLayer ReadObjectLayer(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -24,7 +20,7 @@ internal partial class Tmj var offsetY = element.GetOptionalProperty("offsety", 0.0f); var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var x = element.GetOptionalProperty("x", 0); var y = element.GetOptionalProperty("y", 0); @@ -38,7 +34,7 @@ internal partial class Tmj _ => throw new JsonException($"Unknown draw order '{s}'.") }, DrawOrder.TopDown); - var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); + var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el)), []); return new ObjectLayer { @@ -63,10 +59,7 @@ internal partial class Tmj }; } - internal static Model.Object ReadObject( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Model.Object ReadObject(JsonElement element) { uint? idDefault = null; string nameDefault = ""; @@ -82,12 +75,12 @@ internal partial class Tmj bool pointDefault = false; List? polygonDefault = null; List? polylineDefault = null; - Dictionary? propertiesDefault = null; + List propertiesDefault = []; var template = element.GetOptionalProperty("template", null); if (template is not null) { - var resolvedTemplate = externalTemplateResolver(template); + var resolvedTemplate = _externalTemplateResolver(template); var templObj = resolvedTemplate.Object; idDefault = templObj.ID; @@ -114,7 +107,7 @@ internal partial class Tmj var point = element.GetOptionalProperty("point", pointDefault); var polygon = element.GetOptionalPropertyCustom?>("polygon", ReadPoints, polygonDefault); var polyline = element.GetOptionalPropertyCustom?>("polyline", ReadPoints, polylineDefault); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, propertiesDefault); var rotation = element.GetOptionalProperty("rotation", rotationDefault); var text = element.GetOptionalPropertyCustom("text", ReadText, null); var type = element.GetOptionalProperty("type", typeDefault); diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs new file mode 100644 index 0000000..c5b6a7d --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs @@ -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 ReadProperties(JsonElement element) => + element.GetValueAsList(e => + { + var name = e.GetRequiredProperty("name"); + var type = e.GetOptionalPropertyParseable("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("propertytype", null); + if (propertyType is not null) + { + return ReadPropertyWithCustomType(e); + } + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable("value") }, + PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty("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("type", null) == "class"; + if (isClass) + { + return ReadClassProperty(element); + } + + return ReadEnumProperty(element); + } + + internal ClassProperty ReadClassProperty(JsonElement element) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is CustomClassDefinition ccd) + { + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + var props = element.GetOptionalPropertyCustom>("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 ReadPropertiesInsideClass( + JsonElement element, + CustomClassDefinition customClassDefinition) + { + List 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() }, + PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs(), CultureInfo.InvariantCulture) }, + PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Class => new ClassProperty { Name = prop.Name, PropertyType = ((ClassProperty)prop).PropertyType, Value = ReadPropertiesInsideClass(propElement, (CustomClassDefinition)_customTypeResolver(((ClassProperty)prop).PropertyType)) }, + PropertyType.Enum => ReadEnumProperty(propElement), + _ => throw new JsonException("Invalid property type") + }; + + resultingProps.Add(property); + } + + return resultingProps; + } + + internal EnumProperty ReadEnumProperty(JsonElement element) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + var typeInXml = element.GetOptionalPropertyParseable("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("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 { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = element.GetRequiredProperty("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + 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 { enumValue } }; + } + } + + throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); + } +} diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs new file mode 100644 index 0000000..45031db --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs @@ -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("type"); + var tileset = element.GetOptionalPropertyCustom("tileset", ReadTileset, null); + var @object = element.GetRequiredPropertyCustom("object", ReadObject); + + return new Template + { + Tileset = tileset, + Object = @object + }; + } +} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs similarity index 88% rename from src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs index 353f7fd..ecc74d8 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; using System.Globalization; using System.Text.Json; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static TileLayer ReadTileLayer( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal TileLayer ReadTileLayer(JsonElement element) { var compression = element.GetOptionalPropertyParseable("compression", s => s switch { @@ -35,7 +32,7 @@ internal partial class Tmj var opacity = element.GetOptionalProperty("opacity", 1.0f); var parallaxx = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxy = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var repeatX = element.GetOptionalProperty("repeatx", false); var repeatY = element.GetOptionalProperty("repeaty", false); var startX = element.GetOptionalProperty("startx", 0); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs similarity index 83% rename from src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs index e0e2bd2..5fef5f3 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; @@ -6,13 +5,9 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Tileset ReadTileset( - JsonElement element, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Tileset ReadTileset(JsonElement element) { var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var @class = element.GetOptionalProperty("class", ""); @@ -44,7 +39,7 @@ internal partial class Tmj "bottomright" => ObjectAlignment.BottomRight, _ => throw new JsonException($"Unknown object alignment '{s}'") }, ObjectAlignment.Unspecified); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var source = element.GetOptionalProperty("source", null); var spacing = element.GetOptionalProperty("spacing", null); var tileCount = element.GetOptionalProperty("tilecount", null); @@ -57,20 +52,17 @@ internal partial class Tmj "grid" => TileRenderSize.Grid, _ => throw new JsonException($"Unknown tile render size '{s}'") }, TileRenderSize.Tile); - var tiles = element.GetOptionalPropertyCustom>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []); + var tiles = element.GetOptionalPropertyCustom>("tiles", ReadTiles, []); var tileWidth = element.GetOptionalProperty("tilewidth", null); var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var type = element.GetOptionalProperty("type", null); var version = element.GetOptionalProperty("version", null); var transformations = element.GetOptionalPropertyCustom("transformations", ReadTransformations, null); - var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e, customTypeDefinitions)), null); + var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e)), null); if (source is not null) { - if (externalTilesetResolver is null) - throw new JsonException("External tileset resolver is required to resolve external tilesets."); - - var resolvedTileset = externalTilesetResolver(source); + var resolvedTileset = _externalTilesetResolver(source); resolvedTileset.FirstGID = firstGID; resolvedTileset.Source = source; return resolvedTileset; @@ -159,10 +151,7 @@ internal partial class Tmj }; } - internal static List ReadTiles( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) => + internal List ReadTiles(JsonElement element) => element.GetValueAsList(e => { var animation = e.GetOptionalPropertyCustom?>("animation", e => e.GetValueAsList(ReadFrame), null); @@ -174,9 +163,9 @@ internal partial class Tmj var y = e.GetOptionalProperty("y", 0); var width = e.GetOptionalProperty("width", imageWidth ?? 0); var height = e.GetOptionalProperty("height", imageHeight ?? 0); - var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null); + var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e), null); var probability = e.GetOptionalProperty("probability", 0.0f); - var properties = e.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = e.GetOptionalPropertyCustom("properties", ReadProperties, []); // var terrain, replaced by wangsets var type = e.GetOptionalProperty("type", ""); @@ -216,14 +205,12 @@ internal partial class Tmj }; } - internal static Wangset ReadWangset( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal Wangset ReadWangset(JsonElement element) { var @clalss = element.GetOptionalProperty("class", ""); - var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); + var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el)), []); var name = element.GetRequiredProperty("name"); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var tile = element.GetOptionalProperty("tile", 0); var type = element.GetOptionalProperty("type", ""); var wangTiles = element.GetOptionalPropertyCustom>("wangtiles", e => e.GetValueAsList(ReadWangTile), []); @@ -239,15 +226,13 @@ internal partial class Tmj }; } - internal static WangColor ReadWangColor( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal WangColor ReadWangColor(JsonElement element) { var @class = element.GetOptionalProperty("class", ""); var color = element.GetRequiredPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); var name = element.GetRequiredProperty("name"); var probability = element.GetOptionalProperty("probability", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var tile = element.GetOptionalProperty("tile", 0); return new WangColor diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs new file mode 100644 index 0000000..4ae338f --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Text.Json; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmj; + +/// +/// Base class for Tiled JSON format readers. +/// +public abstract partial class TmjReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + /// + /// The root element of the JSON document being read. + /// + protected JsonElement RootElement { get; private set; } + + private bool disposedValue; + + /// + /// Constructs a new . + /// + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// Thrown when any of the arguments are null. + protected TmjReaderBase( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func 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)); + } + + /// + 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); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs index aca5556..8c918b5 100644 --- a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using DotTiled.Model; namespace DotTiled.Serialization.Tmj; @@ -7,73 +6,24 @@ namespace DotTiled.Serialization.Tmj; /// /// A tileset reader for the Tiled JSON format. /// -public class TsjTilesetReader : ITilesetReader +public class TsjTilesetReader : TmjReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// A string containing a Tiled tileset in the Tiled JSON format. + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// A function that resolves custom types given their name. /// Thrown when any of the arguments are null. public TsjTilesetReader( string jsonString, + Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Tileset ReadTileset() - { - var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadTileset( - rootElement, - _ => throw new NotSupportedException("External tilesets cannot refer to other external tilesets."), - _externalTemplateResolver, - _customTypeDefinitions); - } - - /// - 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); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public Tileset ReadTileset() => ReadTileset(RootElement); } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs deleted file mode 100644 index 1987bb1..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ /dev/null @@ -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 externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var version = reader.GetRequiredAttribute("version"); - var tiledVersion = reader.GetRequiredAttribute("tiledversion"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var orientation = reader.GetRequiredAttributeEnum("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", 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("compressionlevel") ?? -1; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - var tileWidth = reader.GetRequiredAttributeParseable("tilewidth"); - var tileHeight = reader.GetRequiredAttributeParseable("tileheight"); - var hexSideLength = reader.GetOptionalAttributeParseable("hexsidelength"); - var staggerAxis = reader.GetOptionalAttributeEnum("staggeraxis", s => s switch - { - "x" => StaggerAxis.X, - "y" => StaggerAxis.Y, - _ => throw new InvalidOperationException($"Unknown stagger axis '{s}'") - }); - var staggerIndex = reader.GetOptionalAttributeEnum("staggerindex", s => s switch - { - "odd" => StaggerIndex.Odd, - "even" => StaggerIndex.Even, - _ => throw new InvalidOperationException($"Unknown stagger index '{s}'") - }); - var parallaxOriginX = reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; - var parallaxOriginY = reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; - var backgroundColor = reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); - var nextLayerID = reader.GetRequiredAttributeParseable("nextlayerid"); - var nextObjectID = reader.GetRequiredAttributeParseable("nextobjectid"); - var infinite = (reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; - - // At most one of - Dictionary? properties = null; - - // Any number of - List layers = []; - List 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 - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs deleted file mode 100644 index 9a748d6..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ /dev/null @@ -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 ReadProperties( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - return reader.ReadList("properties", "property", (r) => - { - var name = r.GetRequiredAttribute("name"); - var type = r.GetOptionalAttributeEnum("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("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, - PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("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 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 CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => - customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs deleted file mode 100644 index 8b972a3..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ /dev/null @@ -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 customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - - Dictionary? 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 customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - var repeatX = (reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; - var repeatY = (reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; - - Dictionary? 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 externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - - Dictionary? properties = null; - List 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 - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs deleted file mode 100644 index 198f32d..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ /dev/null @@ -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? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var version = reader.GetOptionalAttribute("version"); - var tiledVersion = reader.GetOptionalAttribute("tiledversion"); - var firstGID = reader.GetOptionalAttributeParseable("firstgid"); - var source = reader.GetOptionalAttribute("source"); - var name = reader.GetOptionalAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tileWidth = reader.GetOptionalAttributeParseable("tilewidth"); - var tileHeight = reader.GetOptionalAttributeParseable("tileheight"); - var spacing = reader.GetOptionalAttributeParseable("spacing") ?? 0; - var margin = reader.GetOptionalAttributeParseable("margin") ?? 0; - var tileCount = reader.GetOptionalAttributeParseable("tilecount"); - var columns = reader.GetOptionalAttributeParseable("columns"); - var objectAlignment = reader.GetOptionalAttributeEnum("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("rendersize", s => s switch - { - "tile" => TileRenderSize.Tile, - "grid" => TileRenderSize.Grid, - _ => throw new InvalidOperationException($"Unknown render size '{s}'") - }) ?? TileRenderSize.Tile; - var fillMode = reader.GetOptionalAttributeEnum("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? properties = null; - List? wangsets = null; - Transformations? transformations = null; - List 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("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("trans"); - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("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("x") ?? 0f; - var y = reader.GetOptionalAttributeParseable("y") ?? 0f; - - reader.ReadStartElement("tileoffset"); - return new TileOffset { X = x, Y = y }; - } - - internal static Grid ReadGrid(XmlReader reader) - { - // Attributes - var orientation = reader.GetOptionalAttributeEnum("orientation", s => s switch - { - "orthogonal" => GridOrientation.Orthogonal, - "isometric" => GridOrientation.Isometric, - _ => throw new InvalidOperationException($"Unknown orientation '{s}'") - }) ?? GridOrientation.Orthogonal; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - - reader.ReadStartElement("grid"); - return new Grid { Orientation = orientation, Width = width, Height = height }; - } - - internal static Transformations ReadTransformations(XmlReader reader) - { - // Attributes - var hFlip = (reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; - var vFlip = (reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; - var rotate = (reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; - var preferUntransformed = (reader.GetOptionalAttributeParseable("preferuntransformed") ?? 0) == 1; - - reader.ReadStartElement("transformations"); - return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; - } - - internal static Tile ReadTile( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var type = reader.GetOptionalAttribute("type") ?? ""; - var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - - // Elements - Dictionary? properties = null; - Image? image = null; - ObjectLayer? objectLayer = null; - List? 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("animation", "frame", (ar) => - { - var tileID = ar.GetRequiredAttributeParseable("tileid"); - var duration = ar.GetRequiredAttributeParseable("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 ReadWangsets( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) => - reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); - - internal static Wangset ReadWangset( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var name = reader.GetRequiredAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tile = reader.GetRequiredAttributeParseable("tile"); - - // Elements - Dictionary? properties = null; - List wangColors = []; - List 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 customTypeDefinitions) - { - // Attributes - var name = reader.GetRequiredAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var color = reader.GetRequiredAttributeParseable("color"); - var tile = reader.GetRequiredAttributeParseable("tile"); - var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; - - // Elements - Dictionary? 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("tileid"); - var wangID = reader.GetRequiredAttributeParseable("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 - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs index 5a99ff6..e90caa1 100644 --- a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A map reader for the Tiled XML format. /// -public class TmxMapReader : IMapReader +public class TmxMapReader : TmxReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled map in the Tiled XML format. - /// A function that resolves external tilesets given their source. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TmxMapReader( XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _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(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Map ReadMap() => Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - - /// - 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); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public new Map ReadMap() => base.ReadMap(); } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs similarity index 59% rename from src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs index b42ebb0..73c8052 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs @@ -1,27 +1,26 @@ -using System.Xml; using DotTiled.Model; 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("x"); - var y = reader.GetRequiredAttributeParseable("y"); - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); + var x = _reader.GetRequiredAttributeParseable("x"); + var y = _reader.GetRequiredAttributeParseable("y"); + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); var usesTileChildrenInsteadOfRawData = encoding is null; if (usesTileChildrenInsteadOfRawData) { - var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", reader); + var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", _reader); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; } else { - var globalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var globalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs similarity index 88% rename from src/DotTiled/Serialization/Tmx/Tmx.Data.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs index e1b6111..254c1d3 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs @@ -8,17 +8,17 @@ using DotTiled.Model; 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("encoding", e => e switch + var encoding = _reader.GetOptionalAttributeEnum("encoding", e => e switch { "csv" => DataEncoding.Csv, "base64" => DataEncoding.Base64, _ => throw new XmlException("Invalid encoding") }); - var compression = reader.GetOptionalAttributeEnum("compression", c => c switch + var compression = _reader.GetOptionalAttributeEnum("compression", c => c switch { "gzip" => DataCompression.GZip, "zlib" => DataCompression.ZLib, @@ -28,8 +28,8 @@ internal partial class Tmx if (usesChunks) { - var chunks = reader - .ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression)) + var chunks = _reader + .ReadList("data", "chunk", (r) => ReadChunk(encoding, compression)) .ToArray(); 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; if (usesTileChildrenInsteadOfRawData) { - var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", reader); + var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", _reader); var (tileChildrenGlobalTileIDs, tileChildrenFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(tileChildrenGlobalTileIDsWithFlippingFlags); 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); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null }; } diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs new file mode 100644 index 0000000..89f0c9b --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +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("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", 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("compressionlevel") ?? -1; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + var tileWidth = _reader.GetRequiredAttributeParseable("tilewidth"); + var tileHeight = _reader.GetRequiredAttributeParseable("tileheight"); + var hexSideLength = _reader.GetOptionalAttributeParseable("hexsidelength"); + var staggerAxis = _reader.GetOptionalAttributeEnum("staggeraxis", s => s switch + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new InvalidOperationException($"Unknown stagger axis '{s}'") + }); + var staggerIndex = _reader.GetOptionalAttributeEnum("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new InvalidOperationException($"Unknown stagger index '{s}'") + }); + var parallaxOriginX = _reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; + var parallaxOriginY = _reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; + var backgroundColor = _reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); + var nextLayerID = _reader.GetRequiredAttributeParseable("nextlayerid"); + var nextObjectID = _reader.GetRequiredAttributeParseable("nextobjectid"); + var infinite = (_reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; + + // At most one of + List? properties = null; + + // Any number of + List layers = []; + List 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 + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs similarity index 56% rename from src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs index 7a375d9..a680544 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs @@ -3,35 +3,31 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; -using System.Xml; using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static ObjectLayer ReadObjectLayer( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal ObjectLayer ReadObjectLayer() { // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - var color = reader.GetOptionalAttributeClass("color"); - var drawOrder = reader.GetOptionalAttributeEnum("draworder", s => s switch + var id = _reader.GetRequiredAttributeParseable("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var color = _reader.GetOptionalAttributeClass("color"); + var drawOrder = _reader.GetOptionalAttributeEnum("draworder", s => s switch { "topdown" => DrawOrder.TopDown, "index" => DrawOrder.Index, @@ -39,13 +35,13 @@ internal partial class Tmx }) ?? DrawOrder.TopDown; // Elements - Dictionary? properties = null; + List? properties = null; List objects = []; - reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch + _reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "object" => () => objects.Add(ReadObject()), _ => r.Skip }); @@ -66,22 +62,19 @@ internal partial class Tmx ParallaxX = parallaxX, ParallaxY = parallaxY, Color = color, - Properties = properties, + Properties = properties ?? [], DrawOrder = drawOrder, Objects = objects }; } - internal static Model.Object ReadObject( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Model.Object ReadObject() { // Attributes - var template = reader.GetOptionalAttribute("template"); + var template = _reader.GetOptionalAttribute("template"); Model.Object? obj = null; if (template is not null) - obj = externalTemplateResolver(template).Object; + obj = _externalTemplateResolver(template).Object; uint? idDefault = obj?.ID ?? null; string nameDefault = obj?.Name ?? ""; @@ -93,32 +86,32 @@ internal partial class Tmx float rotationDefault = obj?.Rotation ?? 0f; uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null; bool visibleDefault = obj?.Visible ?? true; - Dictionary? propertiesDefault = obj?.Properties ?? null; + List? propertiesDefault = obj?.Properties ?? null; - var id = reader.GetOptionalAttributeParseable("id") ?? idDefault; - var name = reader.GetOptionalAttribute("name") ?? nameDefault; - var type = reader.GetOptionalAttribute("type") ?? typeDefault; - var x = reader.GetOptionalAttributeParseable("x") ?? xDefault; - var y = reader.GetOptionalAttributeParseable("y") ?? yDefault; - var width = reader.GetOptionalAttributeParseable("width") ?? widthDefault; - var height = reader.GetOptionalAttributeParseable("height") ?? heightDefault; - var rotation = reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; - var gid = reader.GetOptionalAttributeParseable("gid") ?? gidDefault; - var visible = reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; + var id = _reader.GetOptionalAttributeParseable("id") ?? idDefault; + var name = _reader.GetOptionalAttribute("name") ?? nameDefault; + var type = _reader.GetOptionalAttribute("type") ?? typeDefault; + var x = _reader.GetOptionalAttributeParseable("x") ?? xDefault; + var y = _reader.GetOptionalAttributeParseable("y") ?? yDefault; + var width = _reader.GetOptionalAttributeParseable("width") ?? widthDefault; + var height = _reader.GetOptionalAttributeParseable("height") ?? heightDefault; + var rotation = _reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; + var gid = _reader.GetOptionalAttributeParseable("gid") ?? gidDefault; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; // Elements Model.Object? foundObject = null; int propertiesCounter = 0; - Dictionary? properties = propertiesDefault; + List? 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), - "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(r), "Object marker"), - "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(r), "Object marker"), - "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(r), "Object marker"), - "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(r), "Object marker"), - "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(r), "Object marker"), + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties()).ToList(), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(), "Object marker"), _ => throw new InvalidOperationException($"Unknown object marker '{elementName}'") }); @@ -139,7 +132,7 @@ internal partial class Tmx foundObject.Height = height; foundObject.Rotation = rotation; foundObject.Visible = visible; - foundObject.Properties = properties; + foundObject.Properties = properties ?? []; foundObject.Template = template; return OverrideObject(obj, foundObject); @@ -161,7 +154,7 @@ internal partial class Tmx obj.Height = foundObject.Height; obj.Rotation = foundObject.Rotation; 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; return obj; } @@ -169,26 +162,26 @@ internal partial class Tmx return OverrideObject((dynamic)obj, (dynamic)foundObject); } - internal static EllipseObject ReadEllipseObject(XmlReader reader) + internal EllipseObject ReadEllipseObject() { - reader.Skip(); + _reader.Skip(); return new EllipseObject { }; } internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject _) => obj; - internal static PointObject ReadPointObject(XmlReader reader) + internal PointObject ReadPointObject() { - reader.Skip(); + _reader.Skip(); return new PointObject { }; } internal static PointObject OverrideObject(PointObject obj, PointObject _) => obj; - internal static PolygonObject ReadPolygonObject(XmlReader reader) + internal PolygonObject ReadPolygonObject() { // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => + var points = _reader.GetRequiredAttributeParseable>("points", s => { // Takes on format "x1,y1 x2,y2 x3,y3 ..." var coords = s.Split(' '); @@ -199,7 +192,7 @@ internal partial class Tmx }).ToList(); }); - reader.ReadStartElement("polygon"); + _reader.ReadStartElement("polygon"); return new PolygonObject { Points = points }; } @@ -209,10 +202,10 @@ internal partial class Tmx return obj; } - internal static PolylineObject ReadPolylineObject(XmlReader reader) + internal PolylineObject ReadPolylineObject() { // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => + var points = _reader.GetRequiredAttributeParseable>("points", s => { // Takes on format "x1,y1 x2,y2 x3,y3 ..." var coords = s.Split(' '); @@ -223,7 +216,7 @@ internal partial class Tmx }).ToList(); }); - reader.ReadStartElement("polyline"); + _reader.ReadStartElement("polyline"); return new PolylineObject { Points = points }; } @@ -233,19 +226,19 @@ internal partial class Tmx return obj; } - internal static TextObject ReadTextObject(XmlReader reader) + internal TextObject ReadTextObject() { // Attributes - var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; - var pixelSize = reader.GetOptionalAttributeParseable("pixelsize") ?? 16; - var wrap = reader.GetOptionalAttributeParseable("wrap") ?? false; - var color = reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); - var bold = reader.GetOptionalAttributeParseable("bold") ?? false; - var italic = reader.GetOptionalAttributeParseable("italic") ?? false; - var underline = reader.GetOptionalAttributeParseable("underline") ?? false; - var strikeout = reader.GetOptionalAttributeParseable("strikeout") ?? false; - var kerning = reader.GetOptionalAttributeParseable("kerning") ?? true; - var hAlign = reader.GetOptionalAttributeEnum("halign", s => s switch + var fontFamily = _reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; + var pixelSize = _reader.GetOptionalAttributeParseable("pixelsize") ?? 16; + var wrap = _reader.GetOptionalAttributeParseable("wrap") ?? false; + var color = _reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); + var bold = _reader.GetOptionalAttributeParseable("bold") ?? false; + var italic = _reader.GetOptionalAttributeParseable("italic") ?? false; + var underline = _reader.GetOptionalAttributeParseable("underline") ?? false; + var strikeout = _reader.GetOptionalAttributeParseable("strikeout") ?? false; + var kerning = _reader.GetOptionalAttributeParseable("kerning") ?? true; + var hAlign = _reader.GetOptionalAttributeEnum("halign", s => s switch { "left" => TextHorizontalAlignment.Left, "center" => TextHorizontalAlignment.Center, @@ -253,7 +246,7 @@ internal partial class Tmx "justify" => TextHorizontalAlignment.Justify, _ => throw new InvalidOperationException($"Unknown horizontal alignment '{s}'") }) ?? TextHorizontalAlignment.Left; - var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch + var vAlign = _reader.GetOptionalAttributeEnum("valign", s => s switch { "top" => TextVerticalAlignment.Top, "center" => TextVerticalAlignment.Center, @@ -262,7 +255,7 @@ internal partial class Tmx }) ?? TextVerticalAlignment.Top; // Elements - var text = reader.ReadElementContentAsString("text", ""); + var text = _reader.ReadElementContentAsString("text", ""); return new TextObject { @@ -304,11 +297,7 @@ internal partial class Tmx return obj; } - internal static Template ReadTemplate( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Template ReadTemplate() { // No attributes @@ -318,10 +307,10 @@ internal partial class Tmx // Should contain exactly one of 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"), - "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"), + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(), "Object"), _ => r.Skip }); diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs new file mode 100644 index 0000000..863e125 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -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 ReadProperties() + { + if (!_reader.IsStartElement("properties")) + return []; + + return _reader.ReadList("properties", "property", (r) => + { + var name = r.GetRequiredAttribute("name"); + var type = r.GetOptionalAttributeEnum("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("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, + PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("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("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 { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = _reader.GetRequiredAttributeParseable("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + 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 { enumValue } }; + } + } + + throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}"); + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs new file mode 100644 index 0000000..f69a739 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs @@ -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("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + List? 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("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var repeatX = (_reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; + var repeatY = (_reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; + + List? 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("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + List? properties = null; + List 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 + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs new file mode 100644 index 0000000..72e66e9 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs @@ -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("firstgid"); + var source = _reader.GetOptionalAttribute("source"); + var name = _reader.GetOptionalAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tileWidth = _reader.GetOptionalAttributeParseable("tilewidth"); + var tileHeight = _reader.GetOptionalAttributeParseable("tileheight"); + var spacing = _reader.GetOptionalAttributeParseable("spacing") ?? 0; + var margin = _reader.GetOptionalAttributeParseable("margin") ?? 0; + var tileCount = _reader.GetOptionalAttributeParseable("tilecount"); + var columns = _reader.GetOptionalAttributeParseable("columns"); + var objectAlignment = _reader.GetOptionalAttributeEnum("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("rendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new InvalidOperationException($"Unknown render size '{s}'") + }) ?? TileRenderSize.Tile; + var fillMode = _reader.GetOptionalAttributeEnum("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? properties = null; + List? wangsets = null; + Transformations? transformations = null; + List 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("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("trans"); + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("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("x") ?? 0f; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0f; + + _reader.ReadStartElement("tileoffset"); + return new TileOffset { X = x, Y = y }; + } + + internal Grid ReadGrid() + { + // Attributes + var orientation = _reader.GetOptionalAttributeEnum("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new InvalidOperationException($"Unknown orientation '{s}'") + }) ?? GridOrientation.Orthogonal; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + + _reader.ReadStartElement("grid"); + return new Grid { Orientation = orientation, Width = width, Height = height }; + } + + internal Transformations ReadTransformations() + { + // Attributes + var hFlip = (_reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; + var vFlip = (_reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; + var rotate = (_reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; + var preferUntransformed = (_reader.GetOptionalAttributeParseable("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("id"); + var type = _reader.GetOptionalAttribute("type") ?? ""; + var probability = _reader.GetOptionalAttributeParseable("probability") ?? 0f; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + + // Elements + List? properties = null; + Image? image = null; + ObjectLayer? objectLayer = null; + List? 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("animation", "frame", (ar) => + { + var tileID = ar.GetRequiredAttributeParseable("tileid"); + var duration = ar.GetRequiredAttributeParseable("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 ReadWangsets() => + _reader.ReadList("wangsets", "wangset", r => ReadWangset()); + + internal Wangset ReadWangset() + { + // Attributes + var name = _reader.GetRequiredAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tile = _reader.GetRequiredAttributeParseable("tile"); + + // Elements + List? properties = null; + List wangColors = []; + List 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"); + var tile = _reader.GetRequiredAttributeParseable("tile"); + var probability = _reader.GetOptionalAttributeParseable("probability") ?? 0f; + + // Elements + List? 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("tileid"); + var wangID = _reader.GetRequiredAttributeParseable("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 + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs new file mode 100644 index 0000000..5851d76 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + /// + /// Constructs a new , which is the base class for all Tiled XML format readers. + /// + /// An XML reader for reading a Tiled map in the Tiled XML format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + protected TmxReaderBase( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func 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(); + } + + /// + 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); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs index 0034150..176872b 100644 --- a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,68 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A tileset reader for the Tiled XML format. /// -public class TsxTilesetReader : ITilesetReader +public class TsxTilesetReader : TmxReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled tileset in the Tiled XML format. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TsxTilesetReader( XmlReader reader, + Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _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(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions); - - /// - 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); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public new Tileset ReadTileset() => base.ReadTileset(); } diff --git a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs index fbaf85c..1ff8445 100644 --- a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A template reader for the Tiled XML format. /// -public class TxTemplateReader : ITemplateReader +public class TxTemplateReader : TmxReaderBase, ITemplateReader { - // Resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled template in the Tiled XML format. - /// A function that resolves external tilesets given their source. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TxTemplateReader( XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _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(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - - /// - 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); - // } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public new Template ReadTemplate() => base.ReadTemplate(); }