diff --git a/DotTiled.Benchmark/DotTiled.Benchmark.csproj b/DotTiled.Benchmark/DotTiled.Benchmark.csproj
new file mode 100644
index 0000000..739d619
--- /dev/null
+++ b/DotTiled.Benchmark/DotTiled.Benchmark.csproj
@@ -0,0 +1,20 @@
+ïŧŋ
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DotTiled.Benchmark/Program.cs b/DotTiled.Benchmark/Program.cs
new file mode 100644
index 0000000..2acbce2
--- /dev/null
+++ b/DotTiled.Benchmark/Program.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text;
+using System.Xml;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Order;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+
+namespace MyBenchmarks
+{
+ [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+ [CategoriesColumn]
+ [Orderer(SummaryOrderPolicy.FastestToSlowest)]
+ [HideColumns(["StdDev", "Error", "RatioSD"])]
+ public class MapLoading
+ {
+ private string _tmxPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmx";
+ private string _tmxContents = "";
+
+ private string _tmjPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj";
+ private string _tmjContents = "";
+
+ public MapLoading()
+ {
+ var basePath = Path.GetDirectoryName(WhereAmI())!;
+ var tmxPath = Path.Combine(basePath, $"../{_tmxPath}");
+ var tmjPath = Path.Combine(basePath, $"../{_tmjPath}");
+
+ _tmxContents = System.IO.File.ReadAllText(tmxPath);
+ _tmjContents = System.IO.File.ReadAllText(tmjPath);
+ }
+
+ static string WhereAmI([CallerFilePath] string callerFilePath = "") => callerFilePath;
+
+ [BenchmarkCategory("MapFromInMemoryTmxString")]
+ [Benchmark(Baseline = true, Description = "DotTiled")]
+ 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(), []);
+ return mapReader.ReadMap();
+ }
+
+ [BenchmarkCategory("MapFromInMemoryTmjString")]
+ [Benchmark(Baseline = true, Description = "DotTiled")]
+ public DotTiled.Map LoadWithDotTiledFromInMemoryTmjString()
+ {
+ using var mapReader = new DotTiled.TmjMapReader(_tmjContents, _ => throw new Exception(), _ => throw new Exception(), []);
+ return mapReader.ReadMap();
+ }
+
+ [BenchmarkCategory("MapFromInMemoryTmxString")]
+ [Benchmark(Description = "TiledLib")]
+ public TiledLib.Map LoadWithTiledLibFromInMemoryTmxString()
+ {
+ using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents));
+ return TiledLib.Map.FromStream(memStream);
+ }
+
+ [BenchmarkCategory("MapFromInMemoryTmjString")]
+ [Benchmark(Description = "TiledLib")]
+ public TiledLib.Map LoadWithTiledLibFromInMemoryTmjString()
+ {
+ using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmjContents));
+ return TiledLib.Map.FromStream(memStream);
+ }
+
+ [BenchmarkCategory("MapFromInMemoryTmxString")]
+ [Benchmark(Description = "TiledCSPlus")]
+ public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromInMemoryTmxString()
+ {
+ using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents));
+ return new TiledCSPlus.TiledMap(memStream);
+ }
+ }
+
+ public class Program
+ {
+ 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/AssertData.cs b/DotTiled.Tests/Assert/AssertData.cs
new file mode 100644
index 0000000..31ffff2
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertData.cs
@@ -0,0 +1,43 @@
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ internal static void AssertData(Data? expected, Data? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ // Attributes
+ Assert.NotNull(actual);
+ AssertEqual(expected.Encoding, actual.Encoding, nameof(Data.Encoding));
+ AssertEqual(expected.Compression, actual.Compression, nameof(Data.Compression));
+
+ // Data
+ AssertEqual(expected.GlobalTileIDs, actual.GlobalTileIDs, nameof(Data.GlobalTileIDs));
+ AssertEqual(expected.FlippingFlags, actual.FlippingFlags, nameof(Data.FlippingFlags));
+
+ if (expected.Chunks is not null)
+ {
+ Assert.NotNull(actual.Chunks);
+ AssertEqual(expected.Chunks.Length, actual.Chunks.Length, "Chunks.Length");
+ for (var i = 0; i < expected.Chunks.Length; i++)
+ AssertChunk(expected.Chunks[i], actual.Chunks[i]);
+ }
+ }
+
+ private static void AssertChunk(Chunk expected, Chunk actual)
+ {
+ // Attributes
+ AssertEqual(expected.X, actual.X, nameof(Chunk.X));
+ AssertEqual(expected.Y, actual.Y, nameof(Chunk.Y));
+ AssertEqual(expected.Width, actual.Width, nameof(Chunk.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(Chunk.Height));
+
+ // Data
+ AssertEqual(expected.GlobalTileIDs, actual.GlobalTileIDs, nameof(Chunk.GlobalTileIDs));
+ AssertEqual(expected.FlippingFlags, actual.FlippingFlags, nameof(Chunk.FlippingFlags));
+ }
+}
diff --git a/DotTiled.Tests/Assert/AssertImage.cs b/DotTiled.Tests/Assert/AssertImage.cs
new file mode 100644
index 0000000..a674faa
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertImage.cs
@@ -0,0 +1,21 @@
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ internal static void AssertImage(Image? expected, Image? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ // Attributes
+ Assert.NotNull(actual);
+ AssertEqual(expected.Format, actual.Format, nameof(Image.Format));
+ AssertEqual(expected.Source, actual.Source, nameof(Image.Source));
+ AssertEqual(expected.TransparentColor, actual.TransparentColor, nameof(Image.TransparentColor));
+ AssertEqual(expected.Width, actual.Width, nameof(Image.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(Image.Height));
+ }
+}
diff --git a/DotTiled.Tests/Assert/AssertLayer.cs b/DotTiled.Tests/Assert/AssertLayer.cs
new file mode 100644
index 0000000..5432d62
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertLayer.cs
@@ -0,0 +1,75 @@
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ internal static void AssertLayer(BaseLayer? expected, BaseLayer? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ // Attributes
+ Assert.NotNull(actual);
+ AssertEqual(expected.ID, actual.ID, nameof(BaseLayer.ID));
+ AssertEqual(expected.Name, actual.Name, nameof(BaseLayer.Name));
+ AssertEqual(expected.Class, actual.Class, nameof(BaseLayer.Class));
+ AssertEqual(expected.Opacity, actual.Opacity, nameof(BaseLayer.Opacity));
+ AssertEqual(expected.Visible, actual.Visible, nameof(BaseLayer.Visible));
+ AssertEqual(expected.TintColor, actual.TintColor, nameof(BaseLayer.TintColor));
+ AssertEqual(expected.OffsetX, actual.OffsetX, nameof(BaseLayer.OffsetX));
+ AssertEqual(expected.OffsetY, actual.OffsetY, nameof(BaseLayer.OffsetY));
+ AssertEqual(expected.ParallaxX, actual.ParallaxX, nameof(BaseLayer.ParallaxX));
+ AssertEqual(expected.ParallaxY, actual.ParallaxY, nameof(BaseLayer.ParallaxY));
+
+ AssertProperties(expected.Properties, actual.Properties);
+ AssertLayer((dynamic)expected, (dynamic)actual);
+ }
+
+ private static void AssertLayer(TileLayer expected, TileLayer actual)
+ {
+ // Attributes
+ AssertEqual(expected.Width, actual.Width, nameof(TileLayer.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(TileLayer.Height));
+ AssertEqual(expected.X, actual.X, nameof(TileLayer.X));
+ AssertEqual(expected.Y, actual.Y, nameof(TileLayer.Y));
+
+ Assert.NotNull(actual.Data);
+ AssertData(expected.Data, actual.Data);
+ }
+
+ private static void AssertLayer(ObjectLayer expected, ObjectLayer actual)
+ {
+ // Attributes
+ AssertEqual(expected.DrawOrder, actual.DrawOrder, nameof(ObjectLayer.DrawOrder));
+ AssertEqual(expected.X, actual.X, nameof(ObjectLayer.X));
+ AssertEqual(expected.Y, actual.Y, nameof(ObjectLayer.Y));
+
+ Assert.NotNull(actual.Objects);
+ AssertEqual(expected.Objects.Count, actual.Objects.Count, "Objects.Count");
+ for (var i = 0; i < expected.Objects.Count; i++)
+ AssertObject(expected.Objects[i], actual.Objects[i]);
+ }
+
+ private static void AssertLayer(ImageLayer expected, ImageLayer actual)
+ {
+ // Attributes
+ AssertEqual(expected.RepeatX, actual.RepeatX, nameof(ImageLayer.RepeatX));
+ AssertEqual(expected.RepeatY, actual.RepeatY, nameof(ImageLayer.RepeatY));
+ AssertEqual(expected.X, actual.X, nameof(ImageLayer.X));
+ AssertEqual(expected.Y, actual.Y, nameof(ImageLayer.Y));
+
+ Assert.NotNull(actual.Image);
+ AssertImage(expected.Image, actual.Image);
+ }
+
+ private static void AssertLayer(Group expected, Group actual)
+ {
+ // Attributes
+ Assert.NotNull(actual.Layers);
+ AssertEqual(expected.Layers.Count, actual.Layers.Count, "Layers.Count");
+ for (var i = 0; i < expected.Layers.Count; i++)
+ AssertLayer(expected.Layers[i], actual.Layers[i]);
+ }
+}
diff --git a/DotTiled.Tests/Assert/AssertMap.cs b/DotTiled.Tests/Assert/AssertMap.cs
new file mode 100644
index 0000000..e9ad8be
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertMap.cs
@@ -0,0 +1,105 @@
+using System.Collections;
+using System.Numerics;
+
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ private static void AssertEqual(T expected, T actual, string nameof)
+ {
+ if (expected == null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ if (typeof(T) == typeof(float))
+ {
+ var expectedFloat = (float)(object)expected;
+ var actualFloat = (float)(object)actual!;
+
+ var expecRounded = MathF.Round(expectedFloat, 3);
+ var actRounded = MathF.Round(actualFloat, 3);
+
+ Assert.True(expecRounded == actRounded, $"Expected {nameof} '{expecRounded}' but got '{actRounded}'");
+ return;
+ }
+
+ if (expected is Vector2)
+ {
+ var expectedVector = (Vector2)(object)expected;
+ var actualVector = (Vector2)(object)actual!;
+
+ AssertEqual(expectedVector.X, actualVector.X, $"{nameof}.X");
+ AssertEqual(expectedVector.Y, actualVector.Y, $"{nameof}.Y");
+
+ return;
+ }
+
+ if (typeof(T).IsArray)
+ {
+ var expectedArray = (Array)(object)expected;
+ var actualArray = (Array)(object)actual!;
+
+ Assert.NotNull(actualArray);
+ AssertEqual(expectedArray.Length, actualArray.Length, $"{nameof}.Length");
+
+ for (var i = 0; i < expectedArray.Length; i++)
+ AssertEqual(expectedArray.GetValue(i), actualArray.GetValue(i), $"{nameof}[{i}]");
+
+ return;
+ }
+
+ if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(List<>))
+ {
+ var expectedList = (IList)(object)expected;
+ var actualList = (IList)(object)actual!;
+
+ Assert.NotNull(actualList);
+ AssertEqual(expectedList.Count, actualList.Count, $"{nameof}.Count");
+
+ for (var i = 0; i < expectedList.Count; i++)
+ AssertEqual(expectedList[i], actualList[i], $"{nameof}[{i}]");
+
+ return;
+ }
+
+ Assert.True(expected.Equals(actual), $"Expected {nameof} '{expected}' but got '{actual}'");
+ }
+
+ internal static void AssertMap(Map expected, Map actual)
+ {
+ // Attributes
+ AssertEqual(expected.Version, actual.Version, nameof(Map.Version));
+ AssertEqual(expected.TiledVersion, actual.TiledVersion, nameof(Map.TiledVersion));
+ AssertEqual(expected.Class, actual.Class, nameof(Map.Class));
+ AssertEqual(expected.Orientation, actual.Orientation, nameof(Map.Orientation));
+ AssertEqual(expected.RenderOrder, actual.RenderOrder, nameof(Map.RenderOrder));
+ AssertEqual(expected.CompressionLevel, actual.CompressionLevel, nameof(Map.CompressionLevel));
+ AssertEqual(expected.Width, actual.Width, nameof(Map.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(Map.Height));
+ AssertEqual(expected.TileWidth, actual.TileWidth, nameof(Map.TileWidth));
+ AssertEqual(expected.TileHeight, actual.TileHeight, nameof(Map.TileHeight));
+ AssertEqual(expected.HexSideLength, actual.HexSideLength, nameof(Map.HexSideLength));
+ AssertEqual(expected.StaggerAxis, actual.StaggerAxis, nameof(Map.StaggerAxis));
+ AssertEqual(expected.StaggerIndex, actual.StaggerIndex, nameof(Map.StaggerIndex));
+ AssertEqual(expected.ParallaxOriginX, actual.ParallaxOriginX, nameof(Map.ParallaxOriginX));
+ AssertEqual(expected.ParallaxOriginY, actual.ParallaxOriginY, nameof(Map.ParallaxOriginY));
+ AssertEqual(expected.BackgroundColor, actual.BackgroundColor, nameof(Map.BackgroundColor));
+ AssertEqual(expected.NextLayerID, actual.NextLayerID, nameof(Map.NextLayerID));
+ AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID));
+ AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite));
+
+ AssertProperties(actual.Properties, expected.Properties);
+
+ Assert.NotNull(actual.Tilesets);
+ AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count");
+ for (var i = 0; i < expected.Tilesets.Count; i++)
+ AssertTileset(expected.Tilesets[i], actual.Tilesets[i]);
+
+ Assert.NotNull(actual.Layers);
+ AssertEqual(expected.Layers.Count, actual.Layers.Count, "Layers.Count");
+ for (var i = 0; i < expected.Layers.Count; i++)
+ AssertLayer(expected.Layers[i], actual.Layers[i]);
+ }
+}
diff --git a/DotTiled.Tests/Assert/AssertObject.cs b/DotTiled.Tests/Assert/AssertObject.cs
new file mode 100644
index 0000000..6c586bb
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertObject.cs
@@ -0,0 +1,73 @@
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ internal static void AssertObject(Object expected, Object actual)
+ {
+ // Attributes
+ AssertEqual(expected.ID, actual.ID, nameof(Object.ID));
+ AssertEqual(expected.Name, actual.Name, nameof(Object.Name));
+ AssertEqual(expected.Type, actual.Type, nameof(Object.Type));
+ AssertEqual(expected.X, actual.X, nameof(Object.X));
+ AssertEqual(expected.Y, actual.Y, nameof(Object.Y));
+ AssertEqual(expected.Width, actual.Width, nameof(Object.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(Object.Height));
+ AssertEqual(expected.Rotation, actual.Rotation, nameof(Object.Rotation));
+ AssertEqual(expected.Visible, actual.Visible, nameof(Object.Visible));
+ AssertEqual(expected.Template, actual.Template, nameof(Object.Template));
+
+ AssertProperties(expected.Properties, actual.Properties);
+
+ Assert.True(expected.GetType() == actual.GetType(), $"Expected object type {expected.GetType()} but got {actual.GetType()}");
+ AssertObject((dynamic)expected, (dynamic)actual);
+ }
+
+ private static void AssertObject(RectangleObject expected, RectangleObject actual)
+ {
+ Assert.True(true); // A rectangle object is the same as the abstract Object
+ }
+
+ private static void AssertObject(EllipseObject expected, EllipseObject actual)
+ {
+ Assert.True(true); // An ellipse object is the same as the abstract Object
+ }
+
+ private static void AssertObject(PointObject expected, PointObject actual)
+ {
+ Assert.True(true); // A point object is the same as the abstract Object
+ }
+
+ private static void AssertObject(PolygonObject expected, PolygonObject actual)
+ {
+ AssertEqual(expected.Points, actual.Points, nameof(PolygonObject.Points));
+ }
+
+ private static void AssertObject(PolylineObject expected, PolylineObject actual)
+ {
+ AssertEqual(expected.Points, actual.Points, nameof(PolylineObject.Points));
+ }
+
+ private static void AssertObject(TextObject expected, TextObject actual)
+ {
+ // Attributes
+ AssertEqual(expected.FontFamily, actual.FontFamily, nameof(TextObject.FontFamily));
+ AssertEqual(expected.PixelSize, actual.PixelSize, nameof(TextObject.PixelSize));
+ AssertEqual(expected.Wrap, actual.Wrap, nameof(TextObject.Wrap));
+ AssertEqual(expected.Color, actual.Color, nameof(TextObject.Color));
+ AssertEqual(expected.Bold, actual.Bold, nameof(TextObject.Bold));
+ AssertEqual(expected.Italic, actual.Italic, nameof(TextObject.Italic));
+ AssertEqual(expected.Underline, actual.Underline, nameof(TextObject.Underline));
+ AssertEqual(expected.Strikeout, actual.Strikeout, nameof(TextObject.Strikeout));
+ AssertEqual(expected.Kerning, actual.Kerning, nameof(TextObject.Kerning));
+ AssertEqual(expected.HorizontalAlignment, actual.HorizontalAlignment, nameof(TextObject.HorizontalAlignment));
+ AssertEqual(expected.VerticalAlignment, actual.VerticalAlignment, nameof(TextObject.VerticalAlignment));
+
+ AssertEqual(expected.Text, actual.Text, nameof(TextObject.Text));
+ }
+
+ private static void AssertObject(TileObject expected, TileObject actual)
+ {
+ // Attributes
+ AssertEqual(expected.GID, actual.GID, nameof(TileObject.GID));
+ }
+}
diff --git a/DotTiled.Tests/Assert/AssertProperties.cs b/DotTiled.Tests/Assert/AssertProperties.cs
new file mode 100644
index 0000000..740ba2b
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertProperties.cs
@@ -0,0 +1,69 @@
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ internal static void AssertProperties(Dictionary? expected, Dictionary? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ Assert.NotNull(actual);
+ AssertEqual(expected.Count, actual.Count, "Properties.Count");
+ foreach (var kvp in expected)
+ {
+ Assert.Contains(kvp.Key, actual.Keys);
+ AssertProperty((dynamic)kvp.Value, (dynamic)actual[kvp.Key]);
+ }
+ }
+
+ private static void AssertProperty(IProperty expected, IProperty actual)
+ {
+ AssertEqual(expected.Type, actual.Type, "Property.Type");
+ AssertEqual(expected.Name, actual.Name, "Property.Name");
+ AssertProperties((dynamic)actual, (dynamic)expected);
+ }
+
+ private static void AssertProperty(StringProperty expected, StringProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "StringProperty.Value");
+ }
+
+ private static void AssertProperty(IntProperty expected, IntProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "IntProperty.Value");
+ }
+
+ private static void AssertProperty(FloatProperty expected, FloatProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "FloatProperty.Value");
+ }
+
+ private static void AssertProperty(BoolProperty expected, BoolProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "BoolProperty.Value");
+ }
+
+ private static void AssertProperty(ColorProperty expected, ColorProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "ColorProperty.Value");
+ }
+
+ private static void AssertProperty(FileProperty expected, FileProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "FileProperty.Value");
+ }
+
+ private static void AssertProperty(ObjectProperty expected, ObjectProperty actual)
+ {
+ AssertEqual(expected.Value, actual.Value, "ObjectProperty.Value");
+ }
+
+ private static void AssertProperty(ClassProperty expected, ClassProperty actual)
+ {
+ AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType");
+ AssertProperties(expected.Properties, actual.Properties);
+ }
+}
diff --git a/DotTiled.Tests/Assert/AssertTileset.cs b/DotTiled.Tests/Assert/AssertTileset.cs
new file mode 100644
index 0000000..e6b39bb
--- /dev/null
+++ b/DotTiled.Tests/Assert/AssertTileset.cs
@@ -0,0 +1,160 @@
+namespace DotTiled.Tests;
+
+public static partial class DotTiledAssert
+{
+ internal static void AssertTileset(Tileset expected, Tileset actual)
+ {
+ // Attributes
+ AssertEqual(expected.Version, actual.Version, nameof(Tileset.Version));
+ AssertEqual(expected.TiledVersion, actual.TiledVersion, nameof(Tileset.TiledVersion));
+ AssertEqual(expected.FirstGID, actual.FirstGID, nameof(Tileset.FirstGID));
+ AssertEqual(expected.Source, actual.Source, nameof(Tileset.Source));
+ AssertEqual(expected.Name, actual.Name, nameof(Tileset.Name));
+ AssertEqual(expected.Class, actual.Class, nameof(Tileset.Class));
+ AssertEqual(expected.TileWidth, actual.TileWidth, nameof(Tileset.TileWidth));
+ AssertEqual(expected.TileHeight, actual.TileHeight, nameof(Tileset.TileHeight));
+ AssertEqual(expected.Spacing, actual.Spacing, nameof(Tileset.Spacing));
+ AssertEqual(expected.Margin, actual.Margin, nameof(Tileset.Margin));
+ AssertEqual(expected.TileCount, actual.TileCount, nameof(Tileset.TileCount));
+ AssertEqual(expected.Columns, actual.Columns, nameof(Tileset.Columns));
+ AssertEqual(expected.ObjectAlignment, actual.ObjectAlignment, nameof(Tileset.ObjectAlignment));
+ AssertEqual(expected.RenderSize, actual.RenderSize, nameof(Tileset.RenderSize));
+ AssertEqual(expected.FillMode, actual.FillMode, nameof(Tileset.FillMode));
+
+ // At most one of
+ AssertImage(expected.Image, actual.Image);
+ AssertTileOffset(expected.TileOffset, actual.TileOffset);
+ AssertGrid(expected.Grid, actual.Grid);
+ AssertProperties(expected.Properties, actual.Properties);
+ // TODO: AssertTerrainTypes(actual.TerrainTypes, expected.TerrainTypes);
+ if (expected.Wangsets is not null)
+ {
+ Assert.NotNull(actual.Wangsets);
+ AssertEqual(expected.Wangsets.Count, actual.Wangsets.Count, "Wangsets.Count");
+ for (var i = 0; i < expected.Wangsets.Count; i++)
+ AssertWangset(expected.Wangsets[i], actual.Wangsets[i]);
+ }
+ AssertTransformations(expected.Transformations, actual.Transformations);
+
+ // Any number of
+ Assert.NotNull(actual.Tiles);
+ AssertEqual(expected.Tiles.Count, actual.Tiles.Count, "Tiles.Count");
+ for (var i = 0; i < expected.Tiles.Count; i++)
+ AssertTile(expected.Tiles[i], actual.Tiles[i]);
+ }
+
+ private static void AssertTileOffset(TileOffset? expected, TileOffset? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ // Attributes
+ Assert.NotNull(actual);
+ AssertEqual(expected.X, actual.X, nameof(TileOffset.X));
+ AssertEqual(expected.Y, actual.Y, nameof(TileOffset.Y));
+ }
+
+ private static void AssertGrid(Grid? expected, Grid? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ // Attributes
+ Assert.NotNull(actual);
+ AssertEqual(expected.Orientation, actual.Orientation, nameof(Grid.Orientation));
+ AssertEqual(expected.Width, actual.Width, nameof(Grid.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(Grid.Height));
+ }
+
+ private static void AssertWangset(Wangset expected, Wangset actual)
+ {
+ // Attributes
+ AssertEqual(expected.Name, actual.Name, nameof(Wangset.Name));
+ AssertEqual(expected.Class, actual.Class, nameof(Wangset.Class));
+ AssertEqual(expected.Tile, actual.Tile, nameof(Wangset.Tile));
+
+ // At most one of
+ AssertProperties(expected.Properties, actual.Properties);
+ if (expected.WangColors is not null)
+ {
+ Assert.NotNull(actual.WangColors);
+ AssertEqual(expected.WangColors.Count, actual.WangColors.Count, "WangColors.Count");
+ for (var i = 0; i < expected.WangColors.Count; i++)
+ AssertWangColor(expected.WangColors[i], actual.WangColors[i]);
+ }
+ for (var i = 0; i < expected.WangTiles.Count; i++)
+ AssertWangTile(expected.WangTiles[i], actual.WangTiles[i]);
+ }
+
+ private static void AssertWangColor(WangColor expected, WangColor actual)
+ {
+ // Attributes
+ AssertEqual(expected.Name, actual.Name, nameof(WangColor.Name));
+ AssertEqual(expected.Class, actual.Class, nameof(WangColor.Class));
+ AssertEqual(expected.Color, actual.Color, nameof(WangColor.Color));
+ AssertEqual(expected.Tile, actual.Tile, nameof(WangColor.Tile));
+ AssertEqual(expected.Probability, actual.Probability, nameof(WangColor.Probability));
+
+ AssertProperties(expected.Properties, actual.Properties);
+ }
+
+ private static void AssertWangTile(WangTile expected, WangTile actual)
+ {
+ // Attributes
+ AssertEqual(expected.TileID, actual.TileID, nameof(WangTile.TileID));
+ AssertEqual(expected.WangID, actual.WangID, nameof(WangTile.WangID));
+ }
+
+ private static void AssertTransformations(Transformations? expected, Transformations? actual)
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ // Attributes
+ Assert.NotNull(actual);
+ AssertEqual(expected.HFlip, actual.HFlip, nameof(Transformations.HFlip));
+ AssertEqual(expected.VFlip, actual.VFlip, nameof(Transformations.VFlip));
+ AssertEqual(expected.Rotate, actual.Rotate, nameof(Transformations.Rotate));
+ AssertEqual(expected.PreferUntransformed, actual.PreferUntransformed, nameof(Transformations.PreferUntransformed));
+ }
+
+ private static void AssertTile(Tile expected, Tile actual)
+ {
+ // Attributes
+ AssertEqual(expected.ID, actual.ID, nameof(Tile.ID));
+ AssertEqual(expected.Type, actual.Type, nameof(Tile.Type));
+ AssertEqual(expected.Probability, actual.Probability, nameof(Tile.Probability));
+ AssertEqual(expected.X, actual.X, nameof(Tile.X));
+ AssertEqual(expected.Y, actual.Y, nameof(Tile.Y));
+ AssertEqual(expected.Width, actual.Width, nameof(Tile.Width));
+ AssertEqual(expected.Height, actual.Height, nameof(Tile.Height));
+
+ // Elements
+ AssertProperties(actual.Properties, expected.Properties);
+ AssertImage(actual.Image, expected.Image);
+ AssertLayer((BaseLayer?)actual.ObjectLayer, (BaseLayer?)expected.ObjectLayer);
+ if (expected.Animation is not null)
+ {
+ Assert.NotNull(actual.Animation);
+ AssertEqual(expected.Animation.Count, actual.Animation.Count, "Animation.Count");
+ for (var i = 0; i < expected.Animation.Count; i++)
+ AssertFrame(expected.Animation[i], actual.Animation[i]);
+ }
+ }
+
+ private static void AssertFrame(Frame expected, Frame actual)
+ {
+ // Attributes
+ AssertEqual(expected.TileID, actual.TileID, nameof(Frame.TileID));
+ AssertEqual(expected.Duration, actual.Duration, nameof(Frame.Duration));
+ }
+}
diff --git a/DotTiled.Tests/DotTiled.Tests.csproj b/DotTiled.Tests/DotTiled.Tests.csproj
index c110013..faa22d4 100644
--- a/DotTiled.Tests/DotTiled.Tests.csproj
+++ b/DotTiled.Tests/DotTiled.Tests.csproj
@@ -25,8 +25,8 @@
-
-
+
+
diff --git a/DotTiled.Tests/Serialization/TestData.cs b/DotTiled.Tests/Serialization/TestData.cs
new file mode 100644
index 0000000..c3d52f8
--- /dev/null
+++ b/DotTiled.Tests/Serialization/TestData.cs
@@ -0,0 +1,79 @@
+using System.Xml;
+
+namespace DotTiled.Tests;
+
+public static partial class TestData
+{
+ public static XmlReader GetXmlReaderFor(string testDataFile)
+ {
+ var fullyQualifiedTestDataFile = $"DotTiled.Tests.{ConvertPathToAssemblyResourcePath(testDataFile)}";
+ using var stream = typeof(TestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
+ ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found");
+
+ using var stringReader = new StreamReader(stream);
+ var xml = stringReader.ReadToEnd();
+ var xmlStringReader = new StringReader(xml);
+ return XmlReader.Create(xmlStringReader);
+ }
+
+ public static string GetRawStringFor(string testDataFile)
+ {
+ var fullyQualifiedTestDataFile = $"DotTiled.Tests.{ConvertPathToAssemblyResourcePath(testDataFile)}";
+ using var stream = typeof(TestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
+ ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found");
+
+ using var stringReader = new StreamReader(stream);
+ return stringReader.ReadToEnd();
+ }
+
+ private static string ConvertPathToAssemblyResourcePath(string path) =>
+ path.Replace("/", ".").Replace("\\", ".").Replace(" ", "_");
+
+ public static IEnumerable