From ab8173bb06f4230a4e09aa672ea18419e04dd76c Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Mon, 26 Aug 2024 21:36:44 +0200 Subject: [PATCH] Tmj reader is now base class and new properties docs --- Makefile | 6 +- ...ing-properties.md => custom-properties.md} | 72 ++++++-- docs/docs/toc.yml | 2 +- docs/images/entity-type-enum.png | Bin 0 -> 11906 bytes src/DotTiled.Benchmark/Program.cs | 2 +- src/DotTiled.Tests/Serialization/TestData.cs | 1 + .../map-with-deep-props.cs | 161 ++++++++++++++++ .../map-with-deep-props.tmj | 55 ++++++ .../map-with-deep-props.tmx | 25 +++ .../Serialization/Tmj/TmjMapReaderTests.cs | 10 +- src/DotTiled/Serialization/Helpers.cs | 21 ++- .../Serialization/Tmj/TjTemplateReader.cs | 64 +------ src/DotTiled/Serialization/Tmj/Tmj.Layer.cs | 26 --- .../Serialization/Tmj/Tmj.Properties.cs | 103 ----------- .../Serialization/Tmj/Tmj.Template.cs | 26 --- .../Serialization/Tmj/TmjMapReader.cs | 63 +------ .../{Tmj.Data.cs => TmjReaderBase.Data.cs} | 2 +- .../{Tmj.Group.cs => TmjReaderBase.Group.cs} | 12 +- ...geLayer.cs => TmjReaderBase.ImageLayer.cs} | 9 +- .../Serialization/Tmj/TmjReaderBase.Layer.cs | 21 +++ .../Tmj/{Tmj.Map.cs => TmjReaderBase.Map.cs} | 15 +- ...tLayer.cs => TmjReaderBase.ObjectLayer.cs} | 21 +-- .../Tmj/TmjReaderBase.Properties.cs | 174 ++++++++++++++++++ .../Tmj/TmjReaderBase.Template.cs | 20 ++ ...ileLayer.cs => TmjReaderBase.TileLayer.cs} | 9 +- ...mj.Tileset.cs => TmjReaderBase.Tileset.cs} | 43 ++--- .../Serialization/Tmj/TmjReaderBase.cs | 74 ++++++++ .../Serialization/Tmj/TsjTilesetReader.cs | 68 +------ .../Tmx/TmxReaderBase.Properties.cs | 30 ++- 29 files changed, 702 insertions(+), 433 deletions(-) rename docs/docs/{accessing-properties.md => custom-properties.md} (59%) create mode 100644 docs/images/entity-type-enum.png create mode 100644 src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs create mode 100644 src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj create mode 100644 src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx delete mode 100644 src/DotTiled/Serialization/Tmj/Tmj.Layer.cs delete mode 100644 src/DotTiled/Serialization/Tmj/Tmj.Properties.cs delete mode 100644 src/DotTiled/Serialization/Tmj/Tmj.Template.cs rename src/DotTiled/Serialization/Tmj/{Tmj.Data.cs => TmjReaderBase.Data.cs} (98%) rename src/DotTiled/Serialization/Tmj/{Tmj.Group.cs => TmjReaderBase.Group.cs} (77%) rename src/DotTiled/Serialization/Tmj/{Tmj.ImageLayer.cs => TmjReaderBase.ImageLayer.cs} (88%) create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs rename src/DotTiled/Serialization/Tmj/{Tmj.Map.cs => TmjReaderBase.Map.cs} (88%) rename src/DotTiled/Serialization/Tmj/{Tmj.ObjectLayer.cs => TmjReaderBase.ObjectLayer.cs} (93%) create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs rename src/DotTiled/Serialization/Tmj/{Tmj.TileLayer.cs => TmjReaderBase.TileLayer.cs} (91%) rename src/DotTiled/Serialization/Tmj/{Tmj.Tileset.cs => TmjReaderBase.Tileset.cs} (86%) create mode 100644 src/DotTiled/Serialization/Tmj/TmjReaderBase.cs 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/accessing-properties.md b/docs/docs/custom-properties.md similarity index 59% rename from docs/docs/accessing-properties.md rename to docs/docs/custom-properties.md index 59fcfea..78cbd00 100644 --- a/docs/docs/accessing-properties.md +++ b/docs/docs/custom-properties.md @@ -1,4 +1,4 @@ -# Accessing properties +# Custom properties [Tiled facilitates a very flexible way to store custom data in your maps using properties](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-properties). Accessing these properties is a common task when working with Tiled maps in your game since it will allow you to fully utilize the strengths of Tiled, such as customizing the behavior of your game objects or setting up the initial state of your game world. @@ -66,15 +66,15 @@ Tiled supports a variety of property types, which are represented in the DotTile - `object` - - `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 collection of . This collection of definitions shall then be passed to the corresponding reader when loading a map, tileset, or template. +In addition to these primitive property types, [Tiled also supports more complex property types](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types). These custom property types are defined in Tiled according to the linked documentation, and to work with them in DotTiled, you *must* define their equivalences as a . You must then provide a resolving function to a defined type given a custom type name, as it is defined in Tiled. -Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will attempt to find the corresponding definition, and if it does not find one, it will throw an exception. However, if it does find the definition, it will use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file when populating a instance. More information about these `class` properties can be found in [the next section](#class-properties). +## Custom types -Finally, Tiled also allows you to define custom property types that work as enums. These custom property types are just parsed and retrieved as their corresponding storage type. So for a custom property type that is defined as an enum where the values are stored as strings, DotTiled will just parse those as . Similarly, if the values are stored as integers, DotTiled will parse those as . +Tiled allows you to define custom property types that can be used in your maps. These custom property types can be of type `class` or `enum`. DotTiled supports custom property types by allowing you to define the equivalent in C# and then providing a custom type resolver function that will return the equivalent definition given a custom type name. -## Class properties +### Class properties -As mentioned, Tiled supports `class` properties which allow you to create hierarchical structures of properties. DotTiled supports this feature through the class. For all your custom `class` types in Tiled, you must create an equivalent and pass it to the corresponding reader when loading a map, tileset, or template. +Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will use the supplied custom type resolver function to retrieve the custom type definition. It will then use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file when populating a instance. `class` properties allow you to create hierarchical structures of properties. For example, if you have a `class` property in Tiled that looks like this: @@ -96,16 +96,66 @@ var monsterSpawnerDefinition = new CustomClassDefinition }; ``` -### Resolve object types and properties automatically +### Enum properties -If you don't want to have to rely on creating an equivalent definition for every `class` property that you may be using in your Tiled maps, you can check the `Resolve object types and properties` checkbox in `Edit > Preferences > General | Export Options` in Tiled. +Tiled also allows you to define custom property types that work as enums. Similarly to `class` properties, you must define the equivalent in DotTiled as a . You can then return the corresponding definition in the resolving function. -![Resolve object types and properties](../images/resolve-types.png) +For example, if you have a custom property type in Tiled that looks like this: -This will make sure that all properties, even those that do not differ from their default values, are included in the exported map, tileset, or template file. This will allow DotTiled to resolve the properties of the `class` property without needing an equivalent definition. However, you *must* enable a similar configuration flag in DotTiled when loading the map, tileset, or template to make sure that DotTiled knows to not throw an exception when it encounters a `class` property without an equivalent definition. +![EntityType enum in Tiled UI](../images/entity-type-enum.png) + +The equivalent definition in DotTiled would look like the following: + +```csharp +var entityTypeDefinition = new CustomEnumDefinition +{ + Name = "EntityType", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = false, + Values = [ + "Bomb", + "Chest", + "Flower", + "Chair" + ] +}; +``` ### [Future] Automatically map custom property `class` types to C# classes In the future, DotTiled will support automatically mapping custom property `class` types to C# classes. This will allow you to define a C# class that matches the structure of the `class` property in Tiled, and DotTiled will automatically map the properties of the `class` property to the properties of the C# class. This will make working with `class` properties much easier and more intuitive. -The idea is to expand on the interface with a method like `GetMappedProperty(string propertyName)`, where `T` is a class that matches the structure of the `class` property in Tiled. \ No newline at end of file +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 d582202..62d2c9d 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -4,4 +4,4 @@ - name: Essentials - href: loading-a-map.md -- href: accessing-properties.md \ No newline at end of file +- 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 0000000000000000000000000000000000000000..459689b3e63e007155e0d2fd55a9ce1597106305 GIT binary patch literal 11906 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 literal 0 HcmV?d00001 diff --git a/src/DotTiled.Benchmark/Program.cs b/src/DotTiled.Benchmark/Program.cs index 3cd6e15..523a7f1 100644 --- a/src/DotTiled.Benchmark/Program.cs +++ b/src/DotTiled.Benchmark/Program.cs @@ -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/Serialization/TestData.cs b/src/DotTiled.Tests/Serialization/TestData.cs index b007913..1c0b885 100644 --- a/src/DotTiled.Tests/Serialization/TestData.cs +++ b/src/DotTiled.Tests/Serialization/TestData.cs @@ -41,5 +41,6 @@ public static partial class TestData ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], ["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-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/Tmj/TmjMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index 3fe6843..48cc13f 100644 --- a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -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/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index 073a19f..1e95695 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -73,8 +73,25 @@ internal static partial class Helpers }; } - internal static List CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) => - customClassDefinition.Members.Select(x => x.Clone()).ToList(); + 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) { diff --git a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs index c4ada75..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 aeef011..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 d9777e7..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.Json; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - internal static List 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!; - }); - - 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 = Helpers.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, - Value = props - }; - } - - throw new JsonException($"Unknown custom class '{propertyType}'."); - } - - internal static List ReadCustomClassProperties( - JsonElement element, - CustomClassDefinition customClassDefinition, - IReadOnlyCollection customTypeDefinitions) - { - List resultingProps = Helpers.CreateInstanceOfCustomClass(customClassDefinition); - - foreach (var prop in customClassDefinition.Members) - { - if (!element.TryGetProperty(prop.Name, out var propElement)) - continue; // Property not present in element, therefore will use default value - - IProperty property = prop.Type switch - { - PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - 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") - }; - - Helpers.ReplacePropertyInList(resultingProps, property); - } - - return resultingProps; - } -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs b/src/DotTiled/Serialization/Tmj/Tmj.Template.cs deleted file mode 100644 index 71ba1e7..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 1d277b1..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 77% rename from src/DotTiled/Serialization/Tmj/Tmj.Group.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs index a714038..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), []); - 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 88% rename from src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs index 74fb230..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), []); + 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 88% rename from src/DotTiled/Serialization/Tmj/Tmj.Map.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs index eeb47b0..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), []); + 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 93% rename from src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs index 589c151..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), []); + 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 = ""; @@ -87,7 +80,7 @@ internal partial class Tmj 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 91% rename from src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs index 905e447..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), []); + 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 86% rename from src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs index 466b5d3..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), []); + 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), []); + 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), []); + 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), []); + 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 dde9075..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/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs index 1335d75..863e125 100644 --- a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -9,6 +9,9 @@ public abstract partial class TmxReaderBase { internal List ReadProperties() { + if (!_reader.IsStartElement("properties")) + return []; + return _reader.ReadList("properties", "property", (r) => { var name = r.GetRequiredAttribute("name"); @@ -39,7 +42,8 @@ public abstract partial class TmxReaderBase PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Class => ReadClassProperty(), + PropertyType.Class => throw new XmlException("Class property must have a property type"), + PropertyType.Enum => throw new XmlException("Enum property must have a property type"), _ => throw new XmlException("Invalid property type") }; return property; @@ -49,7 +53,6 @@ public abstract partial class TmxReaderBase internal IProperty ReadPropertyWithCustomType() { var isClass = _reader.GetOptionalAttribute("type") == "class"; - if (isClass) { return ReadClassProperty(); @@ -62,17 +65,24 @@ public abstract partial class TmxReaderBase { var name = _reader.GetRequiredAttribute("name"); var propertyType = _reader.GetRequiredAttribute("propertytype"); - var customTypeDef = _customTypeResolver(propertyType); + if (customTypeDef is CustomClassDefinition ccd) { - _reader.ReadStartElement("property"); - var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); - var props = ReadProperties(); - var mergedProps = Helpers.MergeProperties(propsInType, props); - - _reader.ReadEndElement(); - return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + if (!_reader.IsEmptyElement) + { + _reader.ReadStartElement("property"); + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + var props = ReadProperties(); + var mergedProps = Helpers.MergeProperties(propsInType, props); + _reader.ReadEndElement(); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + } + else + { + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType }; + } } throw new XmlException($"Unkonwn custom class definition: {propertyType}");