diff --git a/DotTiled.Benchmark/Program.cs b/DotTiled.Benchmark/Program.cs index 1abb982..8397495 100644 --- a/DotTiled.Benchmark/Program.cs +++ b/DotTiled.Benchmark/Program.cs @@ -15,68 +15,54 @@ namespace MyBenchmarks [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [CategoriesColumn] [Orderer(SummaryOrderPolicy.FastestToSlowest)] + [HideColumns(["StdDev", "Error", "RatioSD"])] public class MapLoading { - private string _tmxPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\Tmx\TestData\Map\empty-map-csv.tmx"; + private string _tmxPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\TestData\Map\empty-map-csv.tmx"; private string _tmxContents = ""; + private string _tmjPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\TestData\Map\empty-map-csv.tmj"; + private string _tmjContents = ""; + public MapLoading() { _tmxContents = System.IO.File.ReadAllText(_tmxPath); + _tmjContents = System.IO.File.ReadAllText(_tmjPath); } [BenchmarkCategory("MapFromInMemoryTmxString")] [Benchmark(Baseline = true, Description = "DotTiled")] - public DotTiled.Map LoadWithDotTiledFromInMemoryString() + public DotTiled.Map LoadWithDotTiledFromInMemoryTmxString() { using var stringReader = new StringReader(_tmxContents); using var xmlReader = XmlReader.Create(stringReader); - using var mapReader = new DotTiled.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception()); + using var mapReader = new DotTiled.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception(), []); return mapReader.ReadMap(); } - [BenchmarkCategory("MapFromTmxFile")] + [BenchmarkCategory("MapFromInMemoryTmjString")] [Benchmark(Baseline = true, Description = "DotTiled")] - public DotTiled.Map LoadWithDotTiledFromFile() + public DotTiled.Map LoadWithDotTiledFromInMemoryTmjString() { - using var fileStream = System.IO.File.OpenRead(_tmxPath); - using var xmlReader = XmlReader.Create(fileStream); - using var mapReader = new DotTiled.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception()); + using var mapReader = new DotTiled.TmjMapReader(_tmjContents, _ => throw new Exception(), _ => throw new Exception(), []); return mapReader.ReadMap(); } [BenchmarkCategory("MapFromInMemoryTmxString")] [Benchmark(Description = "TiledLib")] - public TiledLib.Map LoadWithTiledLibFromInMemoryString() + public TiledLib.Map LoadWithTiledLibFromInMemoryTmxString() { using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents)); return TiledLib.Map.FromStream(memStream); } - [BenchmarkCategory("MapFromTmxFile")] - [Benchmark(Description = "TiledLib")] - public TiledLib.Map LoadWithTiledLibFromFile() - { - using var fileStream = System.IO.File.OpenRead(_tmxPath); - var map = TiledLib.Map.FromStream(fileStream); - return map; - } - [BenchmarkCategory("MapFromInMemoryTmxString")] [Benchmark(Description = "TiledCSPlus")] - public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromInMemoryString() + public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromInMemoryTmxString() { using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents)); return new TiledCSPlus.TiledMap(memStream); } - - [BenchmarkCategory("MapFromTmxFile")] - [Benchmark(Description = "TiledCSPlus")] - public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromFile() - { - using var fileStream = System.IO.File.OpenRead(_tmxPath); - return new TiledCSPlus.TiledMap(fileStream); - } } public class Program @@ -84,6 +70,7 @@ namespace MyBenchmarks public static void Main(string[] args) { var config = BenchmarkDotNet.Configs.DefaultConfig.Instance + .WithArtifactsPath(args[0]) .WithOptions(ConfigOptions.DisableOptimizationsValidator) .AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default); var summary = BenchmarkRunner.Run(config); diff --git a/DotTiled.Tests/Assert/AssertMap.cs b/DotTiled.Tests/Assert/AssertMap.cs index 167d0ad..e831063 100644 --- a/DotTiled.Tests/Assert/AssertMap.cs +++ b/DotTiled.Tests/Assert/AssertMap.cs @@ -30,11 +30,11 @@ public static partial class DotTiledAssert Assert.NotNull(actual.Tilesets); Assert.Equal(expected.Tilesets.Count, actual.Tilesets.Count); for (var i = 0; i < expected.Tilesets.Count; i++) - AssertTileset(actual.Tilesets[i], expected.Tilesets[i]); + AssertTileset(expected.Tilesets[i], actual.Tilesets[i]); Assert.NotNull(actual.Layers); Assert.Equal(expected.Layers.Count, actual.Layers.Count); for (var i = 0; i < expected.Layers.Count; i++) - AssertLayer(actual.Layers[i], expected.Layers[i]); + AssertLayer(expected.Layers[i], actual.Layers[i]); } } diff --git a/DotTiled.Tests/Assert/AssertObject.cs b/DotTiled.Tests/Assert/AssertObject.cs index 68d74eb..3b08744 100644 --- a/DotTiled.Tests/Assert/AssertObject.cs +++ b/DotTiled.Tests/Assert/AssertObject.cs @@ -17,7 +17,7 @@ public static partial class DotTiledAssert Assert.Equal(expected.Visible, actual.Visible); Assert.Equal(expected.Template, actual.Template); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); AssertObject((dynamic)expected, (dynamic)actual); } diff --git a/DotTiled.Tests/Assert/AssertProperties.cs b/DotTiled.Tests/Assert/AssertProperties.cs index d0a8269..afd28c2 100644 --- a/DotTiled.Tests/Assert/AssertProperties.cs +++ b/DotTiled.Tests/Assert/AssertProperties.cs @@ -19,51 +19,51 @@ public static partial class DotTiledAssert } } - private static void AssertProperty(IProperty actual, IProperty expected) + private static void AssertProperty(IProperty expected, IProperty actual) { Assert.Equal(expected.Type, actual.Type); Assert.Equal(expected.Name, actual.Name); AssertProperties((dynamic)actual, (dynamic)expected); } - private static void AssertProperty(StringProperty actual, StringProperty expected) + private static void AssertProperty(StringProperty expected, StringProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(IntProperty actual, IntProperty expected) + private static void AssertProperty(IntProperty expected, IntProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(FloatProperty actual, FloatProperty expected) + private static void AssertProperty(FloatProperty expected, FloatProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(BoolProperty actual, BoolProperty expected) + private static void AssertProperty(BoolProperty expected, BoolProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(ColorProperty actual, ColorProperty expected) + private static void AssertProperty(ColorProperty expected, ColorProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(FileProperty actual, FileProperty expected) + private static void AssertProperty(FileProperty expected, FileProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(ObjectProperty actual, ObjectProperty expected) + private static void AssertProperty(ObjectProperty expected, ObjectProperty actual) { Assert.Equal(expected.Value, actual.Value); } - private static void AssertProperty(ClassProperty actual, ClassProperty expected) + private static void AssertProperty(ClassProperty expected, ClassProperty actual) { Assert.Equal(expected.PropertyType, actual.PropertyType); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); } } diff --git a/DotTiled.Tests/DotTiled.Tests.csproj b/DotTiled.Tests/DotTiled.Tests.csproj index 5e36559..faa22d4 100644 --- a/DotTiled.Tests/DotTiled.Tests.csproj +++ b/DotTiled.Tests/DotTiled.Tests.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/TestData.cs b/DotTiled.Tests/Serialization/TestData.cs similarity index 75% rename from DotTiled.Tests/Serialization/Tmx/TestData/TestData.cs rename to DotTiled.Tests/Serialization/TestData.cs index 667fec0..d31956f 100644 --- a/DotTiled.Tests/Serialization/Tmx/TestData/TestData.cs +++ b/DotTiled.Tests/Serialization/TestData.cs @@ -2,12 +2,12 @@ using System.Xml; namespace DotTiled.Tests; -public static class TmxMapReaderTestData +public static partial class TestData { public static XmlReader GetXmlReaderFor(string testDataFile) { var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}"; - using var stream = typeof(TmxMapReaderTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) + using var stream = typeof(TestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found"); using var stringReader = new StreamReader(stream); @@ -19,7 +19,7 @@ public static class TmxMapReaderTestData public static string GetRawStringFor(string testDataFile) { var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}"; - using var stream = typeof(TmxMapReaderTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) + using var stream = typeof(TestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found"); using var stringReader = new StreamReader(stream); diff --git a/DotTiled.Tests/Serialization/TestData/CustomTypes/large-propertytypes.json b/DotTiled.Tests/Serialization/TestData/CustomTypes/large-propertytypes.json new file mode 100644 index 0000000..e21cf83 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/CustomTypes/large-propertytypes.json @@ -0,0 +1,103 @@ +[ + { + "id": 4, + "name": "Enum0String", + "storageType": "string", + "type": "enum", + "values": [ + "Enum0_1", + "Enum0_2", + "Enum0_3" + ], + "valuesAsFlags": false + }, + { + "id": 5, + "name": "Enum1Num", + "storageType": "int", + "type": "enum", + "values": [ + "Enum1Num_1", + "Enum1Num_2", + "Enum1Num_3", + "Enum1Num_4" + ], + "valuesAsFlags": false + }, + { + "id": 6, + "name": "Enum2StringFlags", + "storageType": "string", + "type": "enum", + "values": [ + "Enum2StringFlags_1", + "Enum2StringFlags_2", + "Enum2StringFlags_3", + "Enum2StringFlags_4" + ], + "valuesAsFlags": true + }, + { + "id": 7, + "name": "Enum3NumFlags", + "storageType": "int", + "type": "enum", + "values": [ + "Enum3NumFlags_1", + "Enum3NumFlags_2", + "Enum3NumFlags_3", + "Enum3NumFlags_4", + "Enum3NumFlags_5" + ], + "valuesAsFlags": true + }, + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 2, + "members": [ + { + "name": "Yep", + "propertyType": "TestClass", + "type": "class", + "value": { + } + } + ], + "name": "Test", + "type": "class", + "useAs": [ + "property", + "map", + "layer", + "object", + "tile", + "tileset", + "wangcolor", + "wangset", + "project" + ] + }, + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 1, + "members": [ + { + "name": "Amount", + "type": "float", + "value": 0 + }, + { + "name": "Name", + "type": "string", + "value": "" + } + ], + "name": "TestClass", + "type": "class", + "useAs": [ + "property" + ] + } +] diff --git a/DotTiled.Tests/Serialization/TestData/CustomTypes/map-with-object-template-propertytypes.json b/DotTiled.Tests/Serialization/TestData/CustomTypes/map-with-object-template-propertytypes.json new file mode 100644 index 0000000..3505dce --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/CustomTypes/map-with-object-template-propertytypes.json @@ -0,0 +1,24 @@ +[ + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 1, + "members": [ + { + "name": "Amount", + "type": "float", + "value": 0 + }, + { + "name": "Name", + "type": "string", + "value": "" + } + ], + "name": "TestClass", + "type": "class", + "useAs": [ + "property" + ] + } +] diff --git a/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-gzip.tmj b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-gzip.tmj new file mode 100644 index 0000000..de94421 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-gzip.tmj @@ -0,0 +1,30 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "compression":"gzip", + "data":"H4sIAAAAAAAACmNgoD0AAMrGiJlkAAAA", + "encoding":"base64", + "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", + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-base64-gzip.tmx b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-gzip.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-base64-gzip.tmx rename to DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-gzip.tmx diff --git a/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-zlib.tmj b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-zlib.tmj new file mode 100644 index 0000000..4cf9e84 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-zlib.tmj @@ -0,0 +1,30 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "compression":"zlib", + "data":"eJxjYKA9AAAAZAAB", + "encoding":"base64", + "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", + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-base64-zlib.tmx b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-zlib.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-base64-zlib.tmx rename to DotTiled.Tests/Serialization/TestData/Map/empty-map-base64-zlib.tmx diff --git a/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64.tmj b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64.tmj new file mode 100644 index 0000000..b13707c --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64.tmj @@ -0,0 +1,30 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "compression":"", + "data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "encoding":"base64", + "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", + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-base64.tmx b/DotTiled.Tests/Serialization/TestData/Map/empty-map-base64.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-base64.tmx rename to DotTiled.Tests/Serialization/TestData/Map/empty-map-base64.tmx diff --git a/DotTiled.Tests/Serialization/TestData/Map/empty-map-csv.tmj b/DotTiled.Tests/Serialization/TestData/Map/empty-map-csv.tmj new file mode 100644 index 0000000..896df6c --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map-csv.tmj @@ -0,0 +1,32 @@ +{ "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", + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-csv.tmx b/DotTiled.Tests/Serialization/TestData/Map/empty-map-csv.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-csv.tmx rename to DotTiled.Tests/Serialization/TestData/Map/empty-map-csv.tmx diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-properties.cs b/DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.cs similarity index 95% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-properties.cs rename to DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.cs index 795b920..79df5a5 100644 --- a/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-properties.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.cs @@ -1,8 +1,8 @@ namespace DotTiled.Tests; -public partial class TmxMapReaderTests +public partial class TestData { - private static Map EmptyMapWithProperties() => new Map + public static Map EmptyMapWithProperties() => new Map { Version = "1.10", TiledVersion = "1.11.0", diff --git a/DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.tmj b/DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.tmj new file mode 100644 index 0000000..237546d --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.tmj @@ -0,0 +1,69 @@ +{ "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":"MapBool", + "type":"bool", + "value":true + }, + { + "name":"MapColor", + "type":"color", + "value":"#ffff0000" + }, + { + "name":"MapFile", + "type":"file", + "value":"file.png" + }, + { + "name":"MapFloat", + "type":"float", + "value":5.2 + }, + { + "name":"MapInt", + "type":"int", + "value":42 + }, + + { + "name":"MapObject", + "type":"object", + "value":5 + }, + { + "name":"MapString", + "type":"string", + "value":"string in map" + }], + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-properties.tmx b/DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map-properties.tmx rename to DotTiled.Tests/Serialization/TestData/Map/empty-map-properties.tmx diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map.cs b/DotTiled.Tests/Serialization/TestData/Map/empty-map.cs similarity index 89% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map.cs rename to DotTiled.Tests/Serialization/TestData/Map/empty-map.cs index 12cfc00..b51aeba 100644 --- a/DotTiled.Tests/Serialization/Tmx/TestData/Map/empty-map.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/empty-map.cs @@ -1,8 +1,8 @@ namespace DotTiled.Tests; -public partial class TmxMapReaderTests +public partial class TestData { - private static Map EmptyMapWithEncodingAndCompression(DataEncoding dataEncoding, DataCompression? compression) => new Map + public static Map EmptyMapWithEncodingAndCompression(DataEncoding dataEncoding, DataCompression? compression) => new Map { Version = "1.10", TiledVersion = "1.11.0", diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-group.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-group.cs similarity index 97% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-group.cs rename to DotTiled.Tests/Serialization/TestData/Map/map-with-group.cs index 515312e..14a2c8c 100644 --- a/DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-group.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-group.cs @@ -1,8 +1,8 @@ namespace DotTiled.Tests; -public partial class TmxMapReaderTests +public partial class TestData { - private static Map MapWithGroup() => new Map + public static Map MapWithGroup() => new Map { Version = "1.10", TiledVersion = "1.11.0", diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-group.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-group.tmj new file mode 100644 index 0000000..6bce8c8 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-group.tmj @@ -0,0 +1,80 @@ +{ "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":4, + "name":"Tile Layer 2", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }, + { + "id":3, + "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 + }, + { + "draworder":"topdown", + "id":2, + "name":"Object Layer 1", + "objects":[ + { + "height":64.5, + "id":1, + "name":"Name", + "rotation":0, + "type":"", + "visible":true, + "width":64.5, + "x":35.5, + "y":26 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "name":"Group 1", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":2, + "orientation":"orthogonal", + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-group.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-group.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-group.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-with-group.tmx diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-object-template.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs similarity index 91% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-object-template.cs rename to DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs index 8f4459e..1f9cb1c 100644 --- a/DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-object-template.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.cs @@ -1,8 +1,8 @@ namespace DotTiled.Tests; -public partial class TmxMapReaderTests +public partial class TestData { - private static Map MapWithObjectTemplate() => new Map + public static Map MapWithObjectTemplate(string templateExtension) => new Map { Version = "1.10", TiledVersion = "1.11.0", @@ -49,7 +49,7 @@ public partial class TmxMapReaderTests new RectangleObject { ID = 1, - Template = "map-with-object-template.tx", + Template = $"map-with-object-template.{templateExtension}", Name = "Thingy 2", X = 94.5749f, Y = 33.6842f, @@ -73,7 +73,7 @@ public partial class TmxMapReaderTests new RectangleObject { ID = 2, - Template = "map-with-object-template.tx", + Template = $"map-with-object-template.{templateExtension}", Name = "Thingy", X = 29.7976f, Y = 33.8693f, @@ -97,7 +97,7 @@ public partial class TmxMapReaderTests new RectangleObject { ID = 3, - Template = "map-with-object-template.tx", + Template = $"map-with-object-template.{templateExtension}", Name = "Thingy 3", X = 5, Y = 5, @@ -112,7 +112,7 @@ public partial class TmxMapReaderTests PropertyType = "TestClass", Properties = new Dictionary { - ["Amount"] = new FloatProperty { Name = "Amount", Value = 4.2f }, + ["Amount"] = new FloatProperty { Name = "Amount", Value = 0.0f }, ["Name"] = new StringProperty { Name = "Name", Value = "I am here 3" } } } diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.tmj new file mode 100644 index 0000000..398403b --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.tmj @@ -0,0 +1,104 @@ +{ "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 + }, + { + "draworder":"topdown", + "id":2, + "name":"Object Layer 1", + "objects":[ + { + "height":37.0156, + "id":1, + "template":"map-with-object-template.tj", + "name":"Thingy 2", + "properties":[ + { + "name":"Bool", + "type":"bool", + "value":true + }, + { + "name":"TestClassInTemplate", + "propertytype":"TestClass", + "type":"class", + "value": + { + "Amount":37, + "Name":"I am here" + } + }], + "rotation":0, + "type":"", + "visible":true, + "width":37.0156, + "x":94.5749, + "y":33.6842 + }, + { + "id":2, + "template":"map-with-object-template.tj", + "x":29.7976, + "y":33.8693 + }, + { + "height":37.0156, + "id":3, + "template":"map-with-object-template.tj", + "name":"Thingy 3", + "properties":[ + { + "name":"Bool", + "type":"bool", + "value":true + }, + { + "name":"TestClassInTemplate", + "propertytype":"TestClass", + "type":"class", + "value": + { + "Name":"I am here 3" + } + }], + "rotation":0, + "type":"", + "visible":true, + "width":37.0156, + "x":5, + "y":5 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":3, + "nextobjectid":3, + "orientation":"orthogonal", + "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/DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-object-template.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/map-with-object-template.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-with-object-template.tmx diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Map/simple-tileset-embed.cs b/DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.cs similarity index 92% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/simple-tileset-embed.cs rename to DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.cs index 7dfb740..d6a5f10 100644 --- a/DotTiled.Tests/Serialization/Tmx/TestData/Map/simple-tileset-embed.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.cs @@ -1,8 +1,8 @@ namespace DotTiled.Tests; -public partial class TmxMapReaderTests +public partial class TestData { - private static Map SimpleMapWithEmbeddedTileset() => new Map + public static Map SimpleMapWithEmbeddedTileset() => new Map { Version = "1.10", TiledVersion = "1.11.0", @@ -26,6 +26,7 @@ public partial class TmxMapReaderTests Columns = 4, Image = new Image { + Format = ImageFormat.Png, Source = "tiles.png", Width = 128, Height = 64 diff --git a/DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.tmj b/DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.tmj new file mode 100644 index 0000000..fa5a4ef --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.tmj @@ -0,0 +1,45 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2], + "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", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[ + { + "columns":4, + "firstgid":1, + "image":"tiles.png", + "imageheight":64, + "imagewidth":128, + "margin":0, + "name":"Tileset 1", + "spacing":0, + "tilecount":8, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Map/simple-tileset-embed.tmx b/DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.tmx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Map/simple-tileset-embed.tmx rename to DotTiled.Tests/Serialization/TestData/Map/simple-tileset-embed.tmx diff --git a/DotTiled.Tests/Serialization/TestData/Template/map-with-object-template.tj b/DotTiled.Tests/Serialization/TestData/Template/map-with-object-template.tj new file mode 100644 index 0000000..ec2b065 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Template/map-with-object-template.tj @@ -0,0 +1,28 @@ +{ "object": + { + "height":37.0156, + "id":2, + "name":"Thingy", + "properties":[ + { + "name":"Bool", + "type":"bool", + "value":true + }, + { + "name":"TestClassInTemplate", + "propertytype":"TestClass", + "type":"class", + "value": + { + "Amount":4.2, + "Name":"Hello there" + } + }], + "rotation":0, + "type":"", + "visible":true, + "width":37.0156 + }, + "type":"template" +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/Tmx/TestData/Template/map-with-object-template.tx b/DotTiled.Tests/Serialization/TestData/Template/map-with-object-template.tx similarity index 100% rename from DotTiled.Tests/Serialization/Tmx/TestData/Template/map-with-object-template.tx rename to DotTiled.Tests/Serialization/TestData/Template/map-with-object-template.tx diff --git a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs new file mode 100644 index 0000000..7e220a9 --- /dev/null +++ b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -0,0 +1,107 @@ +namespace DotTiled.Tests; + +public partial class TmjMapReaderTests +{ + public static IEnumerable DeserializeMap_ValidTmjNoExternalTilesets_ReturnsMapWithoutThrowing_Data => + [ + ["Serialization.TestData.Map.empty-map-csv.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)], + ["Serialization.TestData.Map.empty-map-base64.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)], + ["Serialization.TestData.Map.empty-map-base64-gzip.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)], + ["Serialization.TestData.Map.empty-map-base64-zlib.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)], + ["Serialization.TestData.Map.simple-tileset-embed.tmj", TestData.SimpleMapWithEmbeddedTileset()], + ["Serialization.TestData.Map.empty-map-properties.tmj", TestData.EmptyMapWithProperties()], + ]; + + [Theory] + [MemberData(nameof(DeserializeMap_ValidTmjNoExternalTilesets_ReturnsMapWithoutThrowing_Data))] + public void TmxMapReaderReadMap_ValidTmjNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) + { + // Arrange + var json = TestData.GetRawStringFor(testDataFile); + static Template ResolveTemplate(string source) + { + throw new NotSupportedException("External templates are not supported in this test."); + } + static Tileset ResolveTileset(string source) + { + throw new NotSupportedException("External tilesets are not supported in this test."); + } + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, []); + + // Act + var map = mapReader.ReadMap(); + + // Assert + Assert.NotNull(map); + DotTiledAssert.AssertMap(expectedMap, map); + } + + public static IEnumerable DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => + [ + ["Serialization.TestData.Map.map-with-object-template.tmj", TestData.MapWithObjectTemplate("tj")], + ["Serialization.TestData.Map.map-with-group.tmj", TestData.MapWithGroup()], + ]; + + [Theory] + [MemberData(nameof(DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))] + public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) + { + // Arrange + CustomTypeDefinition[] customTypeDefinitions = [ + new CustomClassDefinition + { + Name = "TestClass", + ID = 1, + UseAs = CustomClassUseAs.Property, + Members = [ + new StringProperty + { + Name = "Name", + Value = "" + }, + new FloatProperty + { + Name = "Amount", + Value = 0f + } + ] + }, + new CustomClassDefinition + { + Name = "Test", + ID = 2, + UseAs = CustomClassUseAs.All, + Members = [ + new ClassProperty + { + Name = "Yep", + PropertyType = "TestClass", + Properties = [] + } + ] + } + ]; + + var json = TestData.GetRawStringFor(testDataFile); + Template ResolveTemplate(string source) + { + var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}"); + using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions); + return templateReader.ReadTemplate(); + } + Tileset ResolveTileset(string source) + { + var tilesetJson = TestData.GetRawStringFor($"Serialization.TestData.Tileset.{source}"); + using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions); + return tilesetReader.ReadTileset(); + } + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions); + + // Act + var map = mapReader.ReadMap(); + + // Assert + Assert.NotNull(map); + DotTiledAssert.AssertMap(expectedMap, map); + } +} diff --git a/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index 1924ac2..3556893 100644 --- a/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -15,7 +15,7 @@ public partial class TmxMapReaderTests // Act Action act = () => { - using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); + using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []); }; // Assert @@ -34,7 +34,7 @@ public partial class TmxMapReaderTests // Act Action act = () => { - using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); + using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []); }; // Assert @@ -53,7 +53,7 @@ public partial class TmxMapReaderTests // Act Action act = () => { - using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); + using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []); }; // Assert @@ -70,7 +70,7 @@ public partial class TmxMapReaderTests Func externalTemplateResolver = (_) => new Template { Object = new RectangleObject { } }; // Act - using var tmxMapReader = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); + using var tmxMapReader = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []); // Assert Assert.NotNull(tmxMapReader); @@ -78,12 +78,12 @@ public partial class TmxMapReaderTests public static IEnumerable DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data => [ - ["Serialization.Tmx.TestData.Map.empty-map-csv.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)], - ["Serialization.Tmx.TestData.Map.empty-map-base64.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)], - ["Serialization.Tmx.TestData.Map.empty-map-base64-gzip.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)], - ["Serialization.Tmx.TestData.Map.empty-map-base64-zlib.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)], - ["Serialization.Tmx.TestData.Map.simple-tileset-embed.tmx", SimpleMapWithEmbeddedTileset()], - ["Serialization.Tmx.TestData.Map.empty-map-properties.tmx", EmptyMapWithProperties()], + ["Serialization.TestData.Map.empty-map-csv.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)], + ["Serialization.TestData.Map.empty-map-base64.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)], + ["Serialization.TestData.Map.empty-map-base64-gzip.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)], + ["Serialization.TestData.Map.empty-map-base64-zlib.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)], + ["Serialization.TestData.Map.simple-tileset-embed.tmx", TestData.SimpleMapWithEmbeddedTileset()], + ["Serialization.TestData.Map.empty-map-properties.tmx", TestData.EmptyMapWithProperties()], ]; [Theory] @@ -91,20 +91,55 @@ public partial class TmxMapReaderTests public void TmxMapReaderReadMap_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) { // Arrange - using var reader = TmxMapReaderTestData.GetXmlReaderFor(testDataFile); - static Template ResolveTemplate(string source) + CustomTypeDefinition[] customTypeDefinitions = [ + new CustomClassDefinition + { + Name = "TestClass", + ID = 1, + UseAs = CustomClassUseAs.Property, + Members = [ + new StringProperty + { + Name = "Name", + Value = "" + }, + new FloatProperty + { + Name = "Amount", + Value = 0f + } + ] + }, + new CustomClassDefinition + { + Name = "Test", + ID = 2, + UseAs = CustomClassUseAs.All, + Members = [ + new ClassProperty + { + Name = "Yep", + PropertyType = "TestClass", + Properties = [] + } + ] + } + ]; + + using var reader = TestData.GetXmlReaderFor(testDataFile); + Template ResolveTemplate(string source) { - using var xmlTemplateReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Template.{source}"); - using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate); + using var xmlTemplateReader = TestData.GetXmlReaderFor($"Serialization.TestData.Template.{source}"); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); return templateReader.ReadTemplate(); } - static Tileset ResolveTileset(string source) + Tileset ResolveTileset(string source) { - using var xmlTilesetReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Tileset.{source}"); - using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate); + using var xmlTilesetReader = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}"); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); return tilesetReader.ReadTileset(); } - using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate); + using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); // Act var map = mapReader.ReadMap(); @@ -116,8 +151,8 @@ public partial class TmxMapReaderTests public static IEnumerable DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => [ - ["Serialization.Tmx.TestData.Map.map-with-object-template.tmx", MapWithObjectTemplate()], - ["Serialization.Tmx.TestData.Map.map-with-group.tmx", MapWithGroup()], + ["Serialization.TestData.Map.map-with-object-template.tmx", TestData.MapWithObjectTemplate("tx")], + ["Serialization.TestData.Map.map-with-group.tmx", TestData.MapWithGroup()], ]; [Theory] @@ -125,20 +160,54 @@ public partial class TmxMapReaderTests public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) { // Arrange - using var reader = TmxMapReaderTestData.GetXmlReaderFor(testDataFile); - static Template ResolveTemplate(string source) + CustomTypeDefinition[] customTypeDefinitions = [ + new CustomClassDefinition + { + Name = "TestClass", + ID = 1, + UseAs = CustomClassUseAs.Property, + Members = [ + new StringProperty + { + Name = "Name", + Value = "" + }, + new FloatProperty + { + Name = "Amount", + Value = 0f + } + ] + }, + new CustomClassDefinition + { + Name = "Test", + ID = 2, + UseAs = CustomClassUseAs.All, + Members = [ + new ClassProperty + { + Name = "Yep", + PropertyType = "TestClass", + Properties = [] + } + ] + } + ]; + using var reader = TestData.GetXmlReaderFor(testDataFile); + Template ResolveTemplate(string source) { - using var xmlTemplateReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Template.{source}"); - using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate); + using var xmlTemplateReader = TestData.GetXmlReaderFor($"Serialization.TestData.Template.{source}"); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); return templateReader.ReadTemplate(); } - static Tileset ResolveTileset(string source) + Tileset ResolveTileset(string source) { - using var xmlTilesetReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Tileset.{source}"); - using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate); + using var xmlTilesetReader = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}"); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); return tilesetReader.ReadTileset(); } - using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate); + using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); // Act var map = mapReader.ReadMap(); diff --git a/DotTiled/Model/IProperty.cs b/DotTiled/Model/IProperty.cs index 9558ee2..ae522f2 100644 --- a/DotTiled/Model/IProperty.cs +++ b/DotTiled/Model/IProperty.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; namespace DotTiled; @@ -18,6 +20,8 @@ public interface IProperty { public string Name { get; set; } public PropertyType Type { get; } + + IProperty Clone(); } public class StringProperty : IProperty @@ -25,6 +29,12 @@ public class StringProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.String; public required string Value { get; set; } + + public IProperty Clone() => new StringProperty + { + Name = Name, + Value = Value + }; } public class IntProperty : IProperty @@ -32,6 +42,12 @@ public class IntProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Int; public required int Value { get; set; } + + public IProperty Clone() => new IntProperty + { + Name = Name, + Value = Value + }; } public class FloatProperty : IProperty @@ -39,6 +55,12 @@ public class FloatProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Float; public required float Value { get; set; } + + public IProperty Clone() => new FloatProperty + { + Name = Name, + Value = Value + }; } public class BoolProperty : IProperty @@ -46,6 +68,12 @@ public class BoolProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Bool; public required bool Value { get; set; } + + public IProperty Clone() => new BoolProperty + { + Name = Name, + Value = Value + }; } public class ColorProperty : IProperty @@ -53,6 +81,12 @@ public class ColorProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Color; public required Color Value { get; set; } + + public IProperty Clone() => new ColorProperty + { + Name = Name, + Value = Value + }; } public class FileProperty : IProperty @@ -60,6 +94,12 @@ public class FileProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.File; public required string Value { get; set; } + + public IProperty Clone() => new FileProperty + { + Name = Name, + Value = Value + }; } public class ObjectProperty : IProperty @@ -67,6 +107,12 @@ public class ObjectProperty : IProperty public required string Name { get; set; } public PropertyType Type => PropertyType.Object; public required uint Value { get; set; } + + public IProperty Clone() => new ObjectProperty + { + Name = Name, + Value = Value + }; } public class ClassProperty : IProperty @@ -75,4 +121,53 @@ public class ClassProperty : IProperty public PropertyType Type => DotTiled.PropertyType.Class; public required string PropertyType { get; set; } public required Dictionary Properties { get; set; } + + public IProperty Clone() => new ClassProperty + { + Name = Name, + PropertyType = PropertyType, + Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) + }; +} + +public abstract class CustomTypeDefinition +{ + public uint ID { get; set; } + public string Name { get; set; } = ""; +} + +[Flags] +public enum CustomClassUseAs +{ + Property, + Map, + Layer, + Object, + Tile, + Tileset, + WangColor, + Wangset, + Project, + All = Property | Map | Layer | Object | Tile | Tileset | WangColor | Wangset | Project +} + +public class CustomClassDefinition : CustomTypeDefinition +{ + public Color Color { get; set; } + public bool DrawFill { get; set; } + public CustomClassUseAs UseAs { get; set; } + public List Members { get; set; } +} + +public enum CustomEnumStorageType +{ + Int, + String +} + +public class CustomEnumDefinition : CustomTypeDefinition +{ + public CustomEnumStorageType StorageType { get; set; } + public List Values { get; set; } = []; + public bool ValueAsFlags { get; set; } } diff --git a/DotTiled/Model/Tileset/Tileset.cs b/DotTiled/Model/Tileset/Tileset.cs index ac1da0d..7b1a982 100644 --- a/DotTiled/Model/Tileset/Tileset.cs +++ b/DotTiled/Model/Tileset/Tileset.cs @@ -39,8 +39,8 @@ public class Tileset public string Class { get; set; } = ""; public uint? TileWidth { get; set; } public uint? TileHeight { get; set; } - public float? Spacing { get; set; } - public float? Margin { get; set; } + public float? Spacing { get; set; } = 0f; + public float? Margin { get; set; } = 0f; public uint? TileCount { get; set; } public uint? Columns { get; set; } public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; diff --git a/DotTiled/Serialization/Helpers.cs b/DotTiled/Serialization/Helpers.cs new file mode 100644 index 0000000..905cb9f --- /dev/null +++ b/DotTiled/Serialization/Helpers.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace DotTiled; + +internal static partial class Helpers +{ + internal static uint[] ReadMemoryStreamAsInt32Array(Stream stream) + { + var finalValues = new List(); + var int32Bytes = new byte[4]; + while (stream.Read(int32Bytes, 0, 4) == 4) + { + var value = BitConverter.ToUInt32(int32Bytes, 0); + finalValues.Add(value); + } + return [.. finalValues]; + } + + internal static uint[] DecompressGZip(MemoryStream stream) + { + using var decompressedStream = new GZipStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } + + internal static uint[] DecompressZLib(MemoryStream stream) + { + using var decompressedStream = new ZLibStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } + + internal static uint[] ReadBytesAsInt32Array(byte[] bytes) + { + var intArray = new uint[bytes.Length / 4]; + for (var i = 0; i < intArray.Length; i++) + { + intArray[i] = BitConverter.ToUInt32(bytes, i * 4); + } + + return intArray; + } + + internal static (uint[] GlobalTileIDs, FlippingFlags[] FlippingFlags) ReadAndClearFlippingFlagsFromGIDs(uint[] globalTileIDs) + { + var clearedGlobalTileIDs = new uint[globalTileIDs.Length]; + var flippingFlags = new FlippingFlags[globalTileIDs.Length]; + for (var i = 0; i < globalTileIDs.Length; i++) + { + var gid = globalTileIDs[i]; + var flags = gid & 0xF0000000u; + flippingFlags[i] = (FlippingFlags)flags; + clearedGlobalTileIDs[i] = gid & 0x0FFFFFFFu; + } + + return (clearedGlobalTileIDs, flippingFlags); + } + + internal 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 NotSupportedException($"Unsupported image format '{extension}'") + }; + } + + internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary overrideProperties) + { + if (baseProperties is null) + return overrideProperties ?? new Dictionary(); + + if (overrideProperties is null) + return baseProperties; + + var result = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); + foreach (var (key, value) in overrideProperties) + { + if (!result.TryGetValue(key, out var baseProp)) + { + result[key] = value; + continue; + } + else + { + if (value is ClassProperty classProp) + { + ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); + } + else + { + result[key] = value; + } + } + } + + return result; + } + + internal static void SetAtMostOnce(ref T? field, T value, string fieldName) + { + if (field is not null) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + } + + internal static void SetAtMostOnceUsingCounter(ref T? field, T value, string fieldName, ref int counter) + { + if (counter > 0) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + counter++; + } +} diff --git a/DotTiled/Serialization/Tmj/ExtensionsJsonElement.cs b/DotTiled/Serialization/Tmj/ExtensionsJsonElement.cs new file mode 100644 index 0000000..7462c56 --- /dev/null +++ b/DotTiled/Serialization/Tmj/ExtensionsJsonElement.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; + +namespace DotTiled; + +internal static class ExtensionsJsonElement +{ + internal static T GetRequiredProperty(this JsonElement element, string propertyName) + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return property.GetValueAs(); + } + + internal static T GetOptionalProperty(this JsonElement element, string propertyName, T defaultValue) + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + if (property.ValueKind == JsonValueKind.Null) + return defaultValue; + + return property.GetValueAs(); + } + + internal static T GetValueAs(this JsonElement element) + { + bool isNullable = Nullable.GetUnderlyingType(typeof(T)) != null; + + if (isNullable && element.ValueKind == JsonValueKind.Null) + return default!; + + var realType = isNullable ? Nullable.GetUnderlyingType(typeof(T))! : typeof(T); + + string val = realType switch + { + Type t when t == typeof(string) => element.GetString()!, + Type t when t == typeof(int) => element.GetInt32().ToString(CultureInfo.InvariantCulture), + Type t when t == typeof(uint) => element.GetUInt32().ToString(CultureInfo.InvariantCulture), + Type t when t == typeof(float) => element.GetSingle().ToString(CultureInfo.InvariantCulture), + Type t when t == typeof(bool) => element.GetBoolean().ToString(CultureInfo.InvariantCulture), + _ => throw new JsonException($"Unsupported type '{typeof(T)}'.") + }; + + return (T)Convert.ChangeType(val, realType, CultureInfo.InvariantCulture); + } + + internal static T GetRequiredPropertyParseable(this JsonElement element, string propertyName) where T : IParsable + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return T.Parse(property.GetString()!, CultureInfo.InvariantCulture); + } + + internal static T GetRequiredPropertyParseable(this JsonElement element, string propertyName, Func parser) + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return parser(property.GetString()!); + } + + internal static T GetOptionalPropertyParseable(this JsonElement element, string propertyName, T defaultValue) where T : IParsable + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + return T.Parse(property.GetString()!, CultureInfo.InvariantCulture); + } + + internal static T GetOptionalPropertyParseable(this JsonElement element, string propertyName, Func parser, T defaultValue) + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + return parser(property.GetString()!); + } + + internal static T GetRequiredPropertyCustom(this JsonElement element, string propertyName, Func parser) + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return parser(property); + } + + internal static T GetOptionalPropertyCustom(this JsonElement element, string propertyName, Func parser, T defaultValue) + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + return parser(property); + } + + internal static List GetValueAsList(this JsonElement element, Func parser) + { + var list = new List(); + + foreach (var item in element.EnumerateArray()) + list.Add(parser(item)); + + return list; + } +} diff --git a/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/DotTiled/Serialization/Tmj/TjTemplateReader.cs new file mode 100644 index 0000000..0dc1dd8 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +public class TjTemplateReader : ITemplateReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + 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)); + } + + 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); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Data.cs b/DotTiled/Serialization/Tmj/Tmj.Data.cs new file mode 100644 index 0000000..0b05c01 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Data.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) + { + var chunks = element.GetValueAsList(e => ReadChunk(e, compression, encoding)).ToArray(); + return new Data + { + Chunks = chunks, + Compression = compression, + Encoding = encoding, + FlippingFlags = null, + GlobalTileIDs = null + }; + } + + internal static Chunk ReadChunk(JsonElement element, DataCompression? compression, DataEncoding encoding) + { + var data = ReadDataWithoutChunks(element, compression, encoding); + + var x = element.GetRequiredProperty("x"); + var y = element.GetRequiredProperty("y"); + var width = element.GetRequiredProperty("width"); + var height = element.GetRequiredProperty("height"); + + return new Chunk + { + X = x, + Y = y, + Width = width, + Height = height, + GlobalTileIDs = data.GlobalTileIDs!, + FlippingFlags = data.FlippingFlags! + }; + } + + internal static Data ReadDataWithoutChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) + { + if (encoding == DataEncoding.Csv) + { + // Array of uint + var data = element.GetValueAsList(e => e.GetValueAs()).ToArray(); + var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(data); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null }; + } + else if (encoding == DataEncoding.Base64) + { + var base64Data = element.GetBytesFromBase64(); + + if (compression == null) + { + var data = Helpers.ReadBytesAsInt32Array(base64Data); + var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(data); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null }; + } + + using var stream = new MemoryStream(base64Data); + var decompressed = compression switch + { + DataCompression.GZip => Helpers.DecompressGZip(stream), + DataCompression.ZLib => Helpers.DecompressZLib(stream), + _ => throw new JsonException($"Unsupported compression '{compression}'.") + }; + + { + var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(decompressed); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null }; + } + } + + throw new JsonException($"Unsupported encoding '{encoding}'."); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Group.cs b/DotTiled/Serialization/Tmj/Tmj.Group.cs new file mode 100644 index 0000000..44e8b4d --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Group.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Group ReadGroup( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + 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)), []); + + 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/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs new file mode 100644 index 0000000..d315891 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static ImageLayer ReadImageLayer( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + 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 image = element.GetRequiredProperty("image"); + var repeatX = element.GetRequiredProperty("repeatx"); + var repeatY = element.GetRequiredProperty("repeaty"); + var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var x = element.GetOptionalProperty("x", 0); + var y = element.GetOptionalProperty("y", 0); + + var imgModel = new Image + { + Format = Helpers.ParseImageFormatFromSource(image), + Height = 0, + Width = 0, + Source = image, + TransparentColor = transparentColor + }; + + return new ImageLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Image = imgModel, + RepeatX = repeatX, + RepeatY = repeatY, + X = x, + Y = y + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/DotTiled/Serialization/Tmj/Tmj.Layer.cs new file mode 100644 index 0000000..f14d614 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Layer.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +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/DotTiled/Serialization/Tmj/Tmj.Map.cs b/DotTiled/Serialization/Tmj/Tmj.Map.cs new file mode 100644 index 0000000..ea7313f --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Map ReadMap( + JsonElement element, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var version = element.GetRequiredProperty("version"); + var tiledVersion = element.GetRequiredProperty("tiledversion"); + string @class = element.GetOptionalProperty("class", ""); + var orientation = element.GetRequiredPropertyParseable("orientation", s => s switch + { + "orthogonal" => MapOrientation.Orthogonal, + "isometric" => MapOrientation.Isometric, + "staggered" => MapOrientation.Staggered, + "hexagonal" => MapOrientation.Hexagonal, + _ => throw new JsonException($"Unknown orientation '{s}'") + }); + var renderOrder = element.GetOptionalPropertyParseable("renderorder", s => s switch + { + "right-down" => RenderOrder.RightDown, + "right-up" => RenderOrder.RightUp, + "left-down" => RenderOrder.LeftDown, + "left-up" => RenderOrder.LeftUp, + _ => throw new JsonException($"Unknown render order '{s}'") + }, RenderOrder.RightDown); + var compressionLevel = element.GetOptionalProperty("compressionlevel", -1); + var width = element.GetRequiredProperty("width"); + var height = element.GetRequiredProperty("height"); + var tileWidth = element.GetRequiredProperty("tilewidth"); + var tileHeight = element.GetRequiredProperty("tileheight"); + var hexSideLength = element.GetOptionalProperty("hexsidelength", null); + var staggerAxis = element.GetOptionalPropertyParseable("staggeraxis", s => s switch + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new JsonException($"Unknown stagger axis '{s}'") + }, null); + var staggerIndex = element.GetOptionalPropertyParseable("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new JsonException($"Unknown stagger index '{s}'") + }, null); + var parallaxOriginX = element.GetOptionalProperty("parallaxoriginx", 0.0f); + var parallaxOriginY = element.GetOptionalProperty("parallaxoriginy", 0.0f); + var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), Color.Parse("#00000000", CultureInfo.InvariantCulture)); + var nextLayerID = element.GetRequiredProperty("nextlayerid"); + var nextObjectID = element.GetRequiredProperty("nextobjectid"); + var infinite = element.GetOptionalProperty("infinite", false); + + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + + 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)), []); + + 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/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs new file mode 100644 index 0000000..2fdf3c9 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static ObjectLayer ReadObjectLayer( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + 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 x = element.GetOptionalProperty("x", 0); + var y = element.GetOptionalProperty("y", 0); + var width = element.GetOptionalProperty("width", null); + var height = element.GetOptionalProperty("height", null); + var color = element.GetOptionalPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var drawOrder = element.GetOptionalPropertyParseable("draworder", s => s switch + { + "topdown" => DrawOrder.TopDown, + "index" => DrawOrder.Index, + _ => throw new JsonException($"Unknown draw order '{s}'.") + }, DrawOrder.TopDown); + + var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); + + return new ObjectLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + X = x, + Y = y, + Width = width, + Height = height, + Color = color, + DrawOrder = drawOrder, + Objects = objects + }; + } + + internal static Object ReadObject( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + uint? idDefault = null; + string nameDefault = ""; + string typeDefault = ""; + float xDefault = 0f; + float yDefault = 0f; + float widthDefault = 0f; + float heightDefault = 0f; + float rotationDefault = 0f; + uint? gidDefault = null; + bool visibleDefault = true; + bool ellipseDefault = false; + bool pointDefault = false; + List? polygonDefault = null; + List? polylineDefault = null; + Dictionary? propertiesDefault = null; + + var template = element.GetOptionalProperty("template", null); + if (template is not null) + { + var resolvedTemplate = externalTemplateResolver(template); + var templObj = resolvedTemplate.Object; + + idDefault = templObj.ID; + nameDefault = templObj.Name; + typeDefault = templObj.Type; + xDefault = templObj.X; + yDefault = templObj.Y; + widthDefault = templObj.Width; + heightDefault = templObj.Height; + rotationDefault = templObj.Rotation; + gidDefault = templObj.GID; + visibleDefault = templObj.Visible; + propertiesDefault = templObj.Properties; + ellipseDefault = templObj is EllipseObject; + pointDefault = templObj is PointObject; + polygonDefault = (templObj is PolygonObject polygonObj) ? polygonObj.Points : null; + polylineDefault = (templObj is PolylineObject polylineObj) ? polylineObj.Points : null; + } + + var ellipse = element.GetOptionalProperty("ellipse", ellipseDefault); + var gid = element.GetOptionalProperty("gid", gidDefault); + var height = element.GetOptionalProperty("height", heightDefault); + var id = element.GetOptionalProperty("id", idDefault); + var name = element.GetOptionalProperty("name", nameDefault); + var point = element.GetOptionalProperty("point", pointDefault); + var polygon = element.GetOptionalPropertyCustom?>("polygon", e => ReadPoints(e), polygonDefault); + var polyline = element.GetOptionalPropertyCustom?>("polyline", e => ReadPoints(e), polylineDefault); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var rotation = element.GetOptionalProperty("rotation", rotationDefault); + var text = element.GetOptionalPropertyCustom("text", ReadText, null); + var type = element.GetOptionalProperty("type", typeDefault); + var visible = element.GetOptionalProperty("visible", visibleDefault); + var width = element.GetOptionalProperty("width", widthDefault); + var x = element.GetOptionalProperty("x", xDefault); + var y = element.GetOptionalProperty("y", yDefault); + + if (ellipse) + { + return new EllipseObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties + }; + } + + if (point) + { + return new PointObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties + }; + } + + if (polygon is not null) + { + return new PolygonObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties, + Points = polygon + }; + } + + if (polyline is not null) + { + return new PolylineObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties, + Points = polyline + }; + } + + if (text is not null) + { + text.ID = id; + text.Name = name; + text.Type = type; + text.X = x; + text.Y = y; + text.Width = width; + text.Height = height; + text.Rotation = rotation; + text.GID = gid; + text.Visible = visible; + text.Template = template; + text.Properties = properties; + return text; + } + + return new RectangleObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + GID = gid, + Visible = visible, + Template = template, + Properties = properties + }; + } + + internal static List ReadPoints(JsonElement element) => + element.GetValueAsList(e => + { + var x = e.GetRequiredProperty("x"); + var y = e.GetRequiredProperty("y"); + return new Vector2(x, y); + }); + + internal static TextObject ReadText(JsonElement element) + { + var bold = element.GetOptionalProperty("bold", false); + var color = element.GetOptionalPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture), Color.Parse("#00000000", CultureInfo.InvariantCulture)); + var fontfamily = element.GetOptionalProperty("fontfamily", "sans-serif"); + var halign = element.GetOptionalPropertyParseable("halign", s => s switch + { + "left" => TextHorizontalAlignment.Left, + "center" => TextHorizontalAlignment.Center, + "right" => TextHorizontalAlignment.Right, + _ => throw new JsonException($"Unknown horizontal alignment '{s}'.") + }, TextHorizontalAlignment.Left); + var italic = element.GetOptionalProperty("italic", false); + var kerning = element.GetOptionalProperty("kerning", true); + var pixelsize = element.GetOptionalProperty("pixelsize", 16); + var strikeout = element.GetOptionalProperty("strikeout", false); + var text = element.GetRequiredProperty("text"); + var underline = element.GetOptionalProperty("underline", false); + var valign = element.GetOptionalPropertyParseable("valign", s => s switch + { + "top" => TextVerticalAlignment.Top, + "center" => TextVerticalAlignment.Center, + "bottom" => TextVerticalAlignment.Bottom, + _ => throw new JsonException($"Unknown vertical alignment '{s}'.") + }, TextVerticalAlignment.Top); + var wrap = element.GetOptionalProperty("wrap", false); + + return new TextObject + { + Bold = bold, + Color = color, + FontFamily = fontfamily, + HorizontalAlignment = halign, + Italic = italic, + Kerning = kerning, + PixelSize = pixelsize, + Strikeout = strikeout, + Text = text, + Underline = underline, + VerticalAlignment = valign, + Wrap = wrap + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/DotTiled/Serialization/Tmj/Tmj.Properties.cs new file mode 100644 index 0000000..6981521 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Properties.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +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) + { + return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Template.cs b/DotTiled/Serialization/Tmj/Tmj.Template.cs new file mode 100644 index 0000000..79c7860 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Template.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +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/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs new file mode 100644 index 0000000..5528177 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + + internal static TileLayer ReadTileLayer( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var compression = element.GetOptionalPropertyParseable("compression", s => s switch + { + "zlib" => DataCompression.ZLib, + "gzip" => DataCompression.GZip, + "" => null, + _ => throw new JsonException($"Unsupported compression '{s}'.") + }, null); + var encoding = element.GetOptionalPropertyParseable("encoding", s => s switch + { + "csv" => DataEncoding.Csv, + "base64" => DataEncoding.Base64, + _ => throw new JsonException($"Unsupported encoding '{s}'.") + }, DataEncoding.Csv); + var chunks = element.GetOptionalPropertyCustom("chunks", e => ReadDataAsChunks(e, compression, encoding), null); + var @class = element.GetOptionalProperty("class", ""); + var data = element.GetOptionalPropertyCustom("data", e => ReadDataWithoutChunks(e, compression, encoding), null); + var height = element.GetRequiredProperty("height"); + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + 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 repeatX = element.GetOptionalProperty("repeatx", false); + var repeatY = element.GetOptionalProperty("repeaty", false); + var startX = element.GetOptionalProperty("startx", 0); + var startY = element.GetOptionalProperty("starty", 0); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var visible = element.GetOptionalProperty("visible", true); + var width = element.GetRequiredProperty("width"); + var x = element.GetRequiredProperty("x"); + var y = element.GetRequiredProperty("y"); + + if ((data ?? chunks) is null) + throw new JsonException("Tile layer does not contain data."); + + return new TileLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxx, + ParallaxY = parallaxy, + Properties = properties, + X = x, + Y = y, + Width = width, + Height = height, + Data = data ?? chunks + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/DotTiled/Serialization/Tmj/Tmj.Tileset.cs new file mode 100644 index 0000000..fd5088b --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Tileset.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Tileset ReadTileset( + JsonElement element, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var @class = element.GetOptionalProperty("class", ""); + var columns = element.GetOptionalProperty("columns", null); + var fillMode = element.GetOptionalPropertyParseable("fillmode", s => s switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new JsonException($"Unknown fill mode '{s}'") + }, FillMode.Stretch); + var firstGID = element.GetOptionalProperty("firstgid", null); + var grid = element.GetOptionalPropertyCustom("grid", ReadGrid, null); + var image = element.GetOptionalProperty("image", null); + var imageHeight = element.GetOptionalProperty("imageheight", null); + var imageWidth = element.GetOptionalProperty("imagewidth", null); + var margin = element.GetOptionalProperty("margin", null); + var name = element.GetOptionalProperty("name", null); + var objectAlignment = element.GetOptionalPropertyParseable("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 JsonException($"Unknown object alignment '{s}'") + }, ObjectAlignment.Unspecified); + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var source = element.GetOptionalProperty("source", null); + var spacing = element.GetOptionalProperty("spacing", null); + var tileCount = element.GetOptionalProperty("tilecount", null); + var tiledVersion = element.GetOptionalProperty("tiledversion", null); + var tileHeight = element.GetOptionalProperty("tileheight", null); + var tileOffset = element.GetOptionalPropertyCustom("tileoffset", ReadTileOffset, null); + var tileRenderSize = element.GetOptionalPropertyParseable("tilerendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new JsonException($"Unknown tile render size '{s}'") + }, TileRenderSize.Tile); + var tiles = element.GetOptionalPropertyCustom>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []); + 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 wangsets = element.GetOptionalPropertyCustom?>("wangsets", ReadWangSets, 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); + resolvedTileset.FirstGID = firstGID; + resolvedTileset.Source = source; + return resolvedTileset; + } + + var imageModel = new Image + { + Format = Helpers.ParseImageFormatFromSource(image!), + Source = image, + Height = imageHeight, + Width = imageWidth, + TransparentColor = transparentColor + }; + + return new Tileset + { + Class = @class, + Columns = columns, + FillMode = fillMode, + FirstGID = firstGID, + Grid = grid, + Image = imageModel, + Margin = margin, + Name = name, + ObjectAlignment = objectAlignment, + Properties = properties, + Source = source, + Spacing = spacing, + TileCount = tileCount, + TiledVersion = tiledVersion, + TileHeight = tileHeight, + TileOffset = tileOffset, + RenderSize = tileRenderSize, + Tiles = tiles, + TileWidth = tileWidth, + Version = version, + //Wangsets = wangsets + }; + } + + internal static Grid ReadGrid(JsonElement element) + { + var orientation = element.GetOptionalPropertyParseable("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new JsonException($"Unknown grid orientation '{s}'") + }, GridOrientation.Orthogonal); + var height = element.GetRequiredProperty("height"); + var width = element.GetRequiredProperty("width"); + + return new Grid + { + Orientation = orientation, + Height = height, + Width = width + }; + } + + internal static TileOffset ReadTileOffset(JsonElement element) + { + var x = element.GetRequiredProperty("x"); + var y = element.GetRequiredProperty("y"); + + return new TileOffset + { + X = x, + Y = y + }; + } + + internal static List ReadTiles( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) => + element.GetValueAsList(e => + { + var animation = e.GetOptionalPropertyCustom?>("animation", e => e.GetValueAsList(ReadFrame), null); + var id = e.GetRequiredProperty("id"); + var image = e.GetOptionalProperty("image", null); + var imageHeight = e.GetOptionalProperty("imageheight", null); + var imageWidth = e.GetOptionalProperty("imagewidth", null); + var x = e.GetOptionalProperty("x", 0); + 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 probability = e.GetOptionalProperty("probability", 1.0f); + var properties = e.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + // var terrain, replaced by wangsets + var type = e.GetOptionalProperty("type", ""); + + var imageModel = image != null ? new Image + { + Format = Helpers.ParseImageFormatFromSource(image), + Source = image, + Height = imageHeight ?? 0, + Width = imageWidth ?? 0 + } : null; + + return new Tile + { + Animation = animation, + ID = id, + Image = imageModel, + X = x, + Y = y, + Width = width, + Height = height, + ObjectLayer = objectGroup, + Probability = probability, + Properties = properties, + Type = type + }; + }); + + internal static Frame ReadFrame(JsonElement element) + { + var duration = element.GetRequiredProperty("duration"); + var tileID = element.GetRequiredProperty("tileid"); + + return new Frame + { + Duration = duration, + TileID = tileID + }; + } + + internal static Wangset ReadWangset( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var @clalss = element.GetOptionalProperty("class", ""); + var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); + var name = element.GetRequiredProperty("name"); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var tile = element.GetOptionalProperty("tile", 0); + var type = element.GetOptionalProperty("type", ""); + var wangTiles = element.GetOptionalPropertyCustom>("wangtiles", e => e.GetValueAsList(ReadWangTile), []); + + return new Wangset + { + Class = @clalss, + WangColors = colors, + Name = name, + Properties = properties, + Tile = tile, + WangTiles = wangTiles + }; + } + + internal static WangColor ReadWangColor( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + 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 tile = element.GetOptionalProperty("tile", 0); + + return new WangColor + { + Class = @class, + Color = color, + Name = name, + Probability = probability, + Properties = properties, + Tile = tile + }; + } + + internal static WangTile ReadWangTile(JsonElement element) + { + var tileID = element.GetRequiredProperty("tileid"); + var wangID = element.GetOptionalPropertyCustom>("wangid", e => e.GetValueAsList(el => (byte)el.GetUInt32()), []); + + return new WangTile + { + TileID = tileID, + WangID = [.. wangID] + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Wangset.cs b/DotTiled/Serialization/Tmj/Tmj.Wangset.cs new file mode 100644 index 0000000..cf9f024 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Wangset.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ +} diff --git a/DotTiled/Serialization/Tmj/TmjMapReader.cs b/DotTiled/Serialization/Tmj/TmjMapReader.cs new file mode 100644 index 0000000..260cd21 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace DotTiled; + +public class TmjMapReader : IMapReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + 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)); + } + + 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); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/DotTiled/Serialization/Tmj/TsjTilesetReader.cs new file mode 100644 index 0000000..14e5323 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +public class TsjTilesetReader : ITilesetReader +{ + // External resolvers + private readonly Func _externalTemplateResolver; + + private readonly string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TsjTilesetReader( + string jsonString, + 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)); + } + + 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); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmx/Tmx.Helpers.cs b/DotTiled/Serialization/Tmx/Tmx.Helpers.cs deleted file mode 100644 index bfc04f4..0000000 --- a/DotTiled/Serialization/Tmx/Tmx.Helpers.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace DotTiled; - -internal partial class Tmx -{ - private static class Helpers - { - public static void SetAtMostOnce(ref T? field, T value, string fieldName) - { - if (field is not null) - throw new InvalidOperationException($"{fieldName} already set"); - - field = value; - } - - public static void SetAtMostOnceUsingCounter(ref T? field, T value, string fieldName, ref int counter) - { - if (counter > 0) - throw new InvalidOperationException($"{fieldName} already set"); - - field = value; - counter++; - } - } -} diff --git a/DotTiled/Serialization/Tmx/Tmx.Map.cs b/DotTiled/Serialization/Tmx/Tmx.Map.cs index d43d0b1..bb7f1ae 100644 --- a/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Map.cs @@ -8,7 +8,11 @@ namespace DotTiled; internal partial class Tmx { - internal static Map ReadMap(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + internal static Map ReadMap( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var version = reader.GetRequiredAttribute("version"); @@ -64,12 +68,12 @@ internal partial class Tmx reader.ProcessChildren("map", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver)), - "layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)), - "imagelayer" => () => layers.Add(ReadImageLayer(r)), - "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)), + "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 }); diff --git a/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs index 2c52429..2367974 100644 --- a/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs @@ -9,7 +9,10 @@ namespace DotTiled; internal partial class Tmx { - internal static ObjectLayer ReadObjectLayer(XmlReader reader, Func externalTemplateResolver) + internal static ObjectLayer ReadObjectLayer( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var id = reader.GetRequiredAttributeParseable("id"); @@ -40,8 +43,8 @@ internal partial class Tmx reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "object" => () => objects.Add(ReadObject(r, externalTemplateResolver)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), _ => r.Skip }); @@ -68,7 +71,10 @@ internal partial class Tmx }; } - internal static Object ReadObject(XmlReader reader, Func externalTemplateResolver) + internal static Object ReadObject( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var template = reader.GetOptionalAttribute("template"); @@ -122,7 +128,7 @@ internal partial class Tmx reader.ProcessChildren("object", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, MergeProperties(properties, ReadProperties(r)), "Properties", ref propertiesCounter), + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)), "Properties", ref propertiesCounter), "ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r), "Object marker"), "point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r), "Object marker"), "polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r), "Object marker"), @@ -152,38 +158,6 @@ internal partial class Tmx return obj; } - internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary overrideProperties) - { - if (baseProperties is null) - return overrideProperties ?? new Dictionary(); - - if (overrideProperties is null) - return baseProperties; - - var result = new Dictionary(baseProperties); - foreach (var (key, value) in overrideProperties) - { - if (!result.TryGetValue(key, out var baseProp)) - { - result[key] = value; - continue; - } - else - { - if (value is ClassProperty classProp) - { - ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); - } - else - { - result[key] = value; - } - } - } - - return result; - } - internal static EllipseObject ReadEllipseObject(XmlReader reader) { reader.Skip(); @@ -280,7 +254,11 @@ internal partial class Tmx }; } - internal static Template ReadTemplate(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + internal static Template ReadTemplate( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // No attributes @@ -292,8 +270,8 @@ internal partial class Tmx reader.ProcessChildren("template", (r, elementName) => elementName switch { - "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver), "Tileset"), - "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver), "Object"), + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"), _ => r.Skip }); diff --git a/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/DotTiled/Serialization/Tmx/Tmx.Properties.cs index 5609a85..6ea65e5 100644 --- a/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Properties.cs @@ -6,7 +6,9 @@ namespace DotTiled; internal partial class Tmx { - internal static Dictionary ReadProperties(XmlReader reader) + internal static Dictionary ReadProperties( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { return reader.ReadList("properties", "property", (r) => { @@ -33,22 +35,38 @@ internal partial class Tmx 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), + 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) + internal static ClassProperty ReadClassProperty( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { var name = reader.GetRequiredAttribute("name"); var propertyType = reader.GetRequiredAttribute("propertytype"); - reader.ReadStartElement("property"); - var properties = ReadProperties(reader); - reader.ReadEndElement(); + 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); - return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties }; + 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) + { + return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); } } diff --git a/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs index e162542..78096e3 100644 --- a/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs @@ -7,7 +7,10 @@ namespace DotTiled; internal partial class Tmx { - internal static TileLayer ReadTileLayer(XmlReader reader, bool dataUsesChunks) + internal static TileLayer ReadTileLayer( + XmlReader reader, + bool dataUsesChunks, + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -30,7 +33,7 @@ internal partial class Tmx reader.ProcessChildren("layer", (r, elementName) => elementName switch { "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), _ => r.Skip }); @@ -55,7 +58,9 @@ internal partial class Tmx }; } - internal static ImageLayer ReadImageLayer(XmlReader reader) + internal static ImageLayer ReadImageLayer( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -78,7 +83,7 @@ internal partial class Tmx reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch { "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), _ => r.Skip }); @@ -103,7 +108,10 @@ internal partial class Tmx }; } - internal static Group ReadGroup(XmlReader reader, Func externalTemplateResolver) + internal static Group ReadGroup( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -121,11 +129,11 @@ internal partial class Tmx reader.ProcessChildren("group", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: false)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)), - "imagelayer" => () => layers.Add(ReadImageLayer(r)), - "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)), + "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 }); diff --git a/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/DotTiled/Serialization/Tmx/Tmx.Tileset.cs index 1996bc2..3885dac 100644 --- a/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Tileset.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Xml; @@ -7,7 +8,11 @@ namespace DotTiled; internal partial class Tmx { - internal static Tileset ReadTileset(XmlReader reader, Func? externalTilesetResolver, Func externalTemplateResolver) + internal static Tileset ReadTileset( + XmlReader reader, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var version = reader.GetOptionalAttribute("version"); @@ -18,8 +23,8 @@ internal partial class Tmx var @class = reader.GetOptionalAttribute("class") ?? ""; var tileWidth = reader.GetOptionalAttributeParseable("tilewidth"); var tileHeight = reader.GetOptionalAttributeParseable("tileheight"); - var spacing = reader.GetOptionalAttributeParseable("spacing"); - var margin = reader.GetOptionalAttributeParseable("margin"); + 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 @@ -63,10 +68,10 @@ internal partial class Tmx "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), "Properties"), - "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r), "Wangsets"), + "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)), + "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)), _ => r.Skip }); @@ -131,6 +136,9 @@ internal partial class Tmx _ => r.Skip }); + if (format is null && source is not null) + format = ParseImageFormatFromSource(source); + return new Image { Format = format, @@ -141,6 +149,21 @@ internal partial class Tmx }; } + + 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 @@ -179,7 +202,10 @@ internal partial class Tmx return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; } - internal static Tile ReadTile(XmlReader reader, Func externalTemplateResolver) + internal static Tile ReadTile( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var id = reader.GetRequiredAttributeParseable("id"); @@ -198,9 +224,9 @@ internal partial class Tmx reader.ProcessChildren("tile", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "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), "ObjectLayer"), + "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"); @@ -226,12 +252,16 @@ internal partial class Tmx }; } - internal static List ReadWangsets(XmlReader reader) + internal static List ReadWangsets( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { - return reader.ReadList("wangsets", "wangset", ReadWangset); + return reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); } - internal static Wangset ReadWangset(XmlReader reader) + internal static Wangset ReadWangset( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { // Attributes var name = reader.GetRequiredAttribute("name"); @@ -245,8 +275,8 @@ internal partial class Tmx reader.ProcessChildren("wangset", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "wangcolor" => () => wangColors.Add(ReadWangColor(r)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)), "wangtile" => () => wangTiles.Add(ReadWangTile(r)), _ => r.Skip }); @@ -265,7 +295,9 @@ internal partial class Tmx }; } - internal static WangColor ReadWangColor(XmlReader reader) + internal static WangColor ReadWangColor( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { // Attributes var name = reader.GetRequiredAttribute("name"); @@ -279,7 +311,7 @@ internal partial class Tmx reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), _ => r.Skip }); diff --git a/DotTiled/Serialization/Tmx/TmxMapReader.cs b/DotTiled/Serialization/Tmx/TmxMapReader.cs index d7e631a..02388bb 100644 --- a/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -13,11 +13,18 @@ public class TmxMapReader : IMapReader private readonly XmlReader _reader; private bool disposedValue; - public TmxMapReader(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + private readonly IReadOnlyCollection _customTypeDefinitions; + + 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(); @@ -25,7 +32,7 @@ public class TmxMapReader : IMapReader public Map ReadMap() { - return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver); + return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); } protected virtual void Dispose(bool disposing) diff --git a/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/DotTiled/Serialization/Tmx/TsxTilesetReader.cs index c089129..dba516b 100644 --- a/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Xml; namespace DotTiled; @@ -11,13 +12,22 @@ public class TsxTilesetReader : ITilesetReader private readonly XmlReader _reader; private bool disposedValue; - public TsxTilesetReader(XmlReader reader, Func externalTemplateResolver) + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TsxTilesetReader( + XmlReader reader, + 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(); } - public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver); + public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions); protected virtual void Dispose(bool disposing) { diff --git a/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/DotTiled/Serialization/Tmx/TxTemplateReader.cs index 24b95f4..eba6299 100644 --- a/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Xml; namespace DotTiled; @@ -12,17 +13,24 @@ public class TxTemplateReader : ITemplateReader private readonly XmlReader _reader; private bool disposedValue; - public TxTemplateReader(XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver) + private readonly IReadOnlyCollection _customTypeDefinitions; + + 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(); } - public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver); + public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); protected virtual void Dispose(bool disposing) { diff --git a/Makefile b/Makefile index e69de29..e7dc7a5 100644 --- a/Makefile +++ b/Makefile @@ -0,0 +1,10 @@ +test: + dotnet test + +BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj +BENCHMARK_OUTPUTDIR = 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 diff --git a/README.md b/README.md index 2fec131..a629121 100644 --- a/README.md +++ b/README.md @@ -60,15 +60,13 @@ BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4651/22H2/2022Update) [Host] : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2 DefaultJob : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2 ``` -| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|------------ |------------------------- |----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:| -| DotTiled | MapFromInMemoryTmxString | 2.991 μs | 0.0266 μs | 0.0236 μs | 1.00 | 0.00 | 1.2817 | 0.0610 | 16.37 KB | 1.00 | -| TiledLib | MapFromInMemoryTmxString | 5.405 μs | 0.0466 μs | 0.0413 μs | 1.81 | 0.02 | 1.8158 | 0.1068 | 23.32 KB | 1.42 | -| TiledCSPlus | MapFromInMemoryTmxString | 6.354 μs | 0.0703 μs | 0.0587 μs | 2.12 | 0.03 | 2.5940 | 0.1831 | 33.23 KB | 2.03 | -| | | | | | | | | | | | -| DotTiled | MapFromTmxFile | 28.570 μs | 0.1216 μs | 0.1137 μs | 1.00 | 0.00 | 1.0376 | - | 13.88 KB | 1.00 | -| TiledCSPlus | MapFromTmxFile | 33.377 μs | 0.1086 μs | 0.1016 μs | 1.17 | 0.01 | 2.8076 | 0.1221 | 36.93 KB | 2.66 | -| TiledLib | MapFromTmxFile | 36.077 μs | 0.1900 μs | 0.1777 μs | 1.26 | 0.01 | 2.0752 | 0.1221 | 27.1 KB | 1.95 | +| Method | Categories | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------------ |------------------------- |---------:|------:|-------:|-------:|----------:|------------:| +| DotTiled | MapFromInMemoryTmjString | 4.292 μs | 1.00 | 0.4349 | - | 5.62 KB | 1.00 | +| | | | | | | | | +| DotTiled | MapFromInMemoryTmxString | 3.075 μs | 1.00 | 1.2817 | 0.0610 | 16.4 KB | 1.00 | +| TiledLib | MapFromInMemoryTmxString | 5.574 μs | 1.81 | 1.8005 | 0.0916 | 23.32 KB | 1.42 | +| TiledCSPlus | MapFromInMemoryTmxString | 6.546 μs | 2.13 | 2.5940 | 0.1831 | 33.16 KB | 2.02 |