Merge pull request #1 from dcronqvist/readers

Move to a reader/writer based approach instead of a standalone serializer
This commit is contained in:
dcronqvist 2024-08-04 16:58:50 +02:00 committed by GitHub
commit 34dcca7f35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 619 additions and 506 deletions

View file

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="TiledCSPlus" Version="4.2.0" />
<PackageReference Include="TiledLib" Version="4.0.3" />
</ItemGroup>

View file

@ -1,26 +1,81 @@
using System;
using System.Collections.Immutable;
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
{
public class MapLoader
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class MapLoading
{
public MapLoader()
private string _tmxPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\Tmx\TestData\Map\empty-map-csv.tmx";
private string _tmxContents = "";
public MapLoading()
{
_tmxContents = System.IO.File.ReadAllText(_tmxPath);
}
[Benchmark]
public DotTiled.Map LoadWithDotTiled()
[BenchmarkCategory("MapFromInMemoryTmxString")]
[Benchmark(Baseline = true, Description = "DotTiled")]
public DotTiled.Map LoadWithDotTiledFromInMemoryString()
{
throw new NotImplementedException();
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();
}
[Benchmark]
public TiledLib.Map LoadWithTiledLib()
[BenchmarkCategory("MapFromTmxFile")]
[Benchmark(Baseline = true, Description = "DotTiled")]
public DotTiled.Map LoadWithDotTiledFromFile()
{
throw new NotImplementedException();
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());
return mapReader.ReadMap();
}
[BenchmarkCategory("MapFromInMemoryTmxString")]
[Benchmark(Description = "TiledLib")]
public TiledLib.Map LoadWithTiledLibFromInMemoryString()
{
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()
{
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);
}
}
@ -28,7 +83,10 @@ namespace MyBenchmarks
{
public static void Main(string[] args)
{
//var summary = BenchmarkRunner.Run<Md5VsSha256>();
var config = BenchmarkDotNet.Configs.DefaultConfig.Instance
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
.AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default);
var summary = BenchmarkRunner.Run<MapLoading>(config);
}
}
}

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests;
public partial class TmxSerializerDataTests
public static partial class DotTiledAssert
{
public static void AssertData(Data? actual, Data? expected)
internal static void AssertData(Data? expected, Data? actual)
{
if (expected is null)
{
@ -24,11 +24,11 @@ public partial class TmxSerializerDataTests
Assert.NotNull(actual.Chunks);
Assert.Equal(expected.Chunks.Length, actual.Chunks.Length);
for (var i = 0; i < expected.Chunks.Length; i++)
AssertChunk(actual.Chunks[i], expected.Chunks[i]);
AssertChunk(expected.Chunks[i], actual.Chunks[i]);
}
}
private static void AssertChunk(Chunk actual, Chunk expected)
private static void AssertChunk(Chunk expected, Chunk actual)
{
// Attributes
Assert.Equal(expected.X, actual.X);

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests;
public partial class TmxSerializerImageTests
public static partial class DotTiledAssert
{
public static void AssertImage(Image? actual, Image? expected)
internal static void AssertImage(Image? expected, Image? actual)
{
if (expected is null)
{

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests;
public partial class TmxSerializerLayerTests
public static partial class DotTiledAssert
{
public static void AssertLayer(BaseLayer? actual, BaseLayer? expected)
internal static void AssertLayer(BaseLayer? expected, BaseLayer? actual)
{
if (expected is null)
{
@ -23,11 +23,11 @@ public partial class TmxSerializerLayerTests
Assert.Equal(expected.ParallaxX, actual.ParallaxX);
Assert.Equal(expected.ParallaxY, actual.ParallaxY);
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
AssertLayer((dynamic)actual, (dynamic)expected);
AssertProperties(expected.Properties, actual.Properties);
AssertLayer((dynamic)expected, (dynamic)actual);
}
private static void AssertLayer(TileLayer actual, TileLayer expected)
private static void AssertLayer(TileLayer expected, TileLayer actual)
{
// Attributes
Assert.Equal(expected.Width, actual.Width);
@ -36,10 +36,10 @@ public partial class TmxSerializerLayerTests
Assert.Equal(expected.Y, actual.Y);
Assert.NotNull(actual.Data);
TmxSerializerDataTests.AssertData(actual.Data, expected.Data);
AssertData(expected.Data, actual.Data);
}
private static void AssertLayer(ObjectLayer actual, ObjectLayer expected)
private static void AssertLayer(ObjectLayer expected, ObjectLayer actual)
{
// Attributes
Assert.Equal(expected.DrawOrder, actual.DrawOrder);
@ -49,10 +49,10 @@ public partial class TmxSerializerLayerTests
Assert.NotNull(actual.Objects);
Assert.Equal(expected.Objects.Count, actual.Objects.Count);
for (var i = 0; i < expected.Objects.Count; i++)
TmxSerializerObjectTests.AssertObject(actual.Objects[i], expected.Objects[i]);
AssertObject(expected.Objects[i], actual.Objects[i]);
}
private static void AssertLayer(ImageLayer actual, ImageLayer expected)
private static void AssertLayer(ImageLayer expected, ImageLayer actual)
{
// Attributes
Assert.Equal(expected.RepeatX, actual.RepeatX);
@ -61,15 +61,15 @@ public partial class TmxSerializerLayerTests
Assert.Equal(expected.Y, actual.Y);
Assert.NotNull(actual.Image);
TmxSerializerImageTests.AssertImage(actual.Image, expected.Image);
AssertImage(expected.Image, actual.Image);
}
private static void AssertLayer(Group actual, Group expected)
private static void AssertLayer(Group expected, Group actual)
{
// Attributes
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]);
}
}

View file

@ -0,0 +1,40 @@
namespace DotTiled.Tests;
public static partial class DotTiledAssert
{
internal static void AssertMap(Map expected, Map actual)
{
// Attributes
Assert.Equal(expected.Version, actual.Version);
Assert.Equal(expected.TiledVersion, actual.TiledVersion);
Assert.Equal(expected.Class, actual.Class);
Assert.Equal(expected.Orientation, actual.Orientation);
Assert.Equal(expected.RenderOrder, actual.RenderOrder);
Assert.Equal(expected.CompressionLevel, actual.CompressionLevel);
Assert.Equal(expected.Width, actual.Width);
Assert.Equal(expected.Height, actual.Height);
Assert.Equal(expected.TileWidth, actual.TileWidth);
Assert.Equal(expected.TileHeight, actual.TileHeight);
Assert.Equal(expected.HexSideLength, actual.HexSideLength);
Assert.Equal(expected.StaggerAxis, actual.StaggerAxis);
Assert.Equal(expected.StaggerIndex, actual.StaggerIndex);
Assert.Equal(expected.ParallaxOriginX, actual.ParallaxOriginX);
Assert.Equal(expected.ParallaxOriginY, actual.ParallaxOriginY);
Assert.Equal(expected.BackgroundColor, actual.BackgroundColor);
Assert.Equal(expected.NextLayerID, actual.NextLayerID);
Assert.Equal(expected.NextObjectID, actual.NextObjectID);
Assert.Equal(expected.Infinite, actual.Infinite);
AssertProperties(actual.Properties, expected.Properties);
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]);
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]);
}
}

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests;
public partial class TmxSerializerObjectTests
public static partial class DotTiledAssert
{
public static void AssertObject(Object actual, Object expected)
internal static void AssertObject(Object expected, Object actual)
{
// Attributes
Assert.Equal(expected.ID, actual.ID);
@ -17,36 +17,36 @@ public partial class TmxSerializerObjectTests
Assert.Equal(expected.Visible, actual.Visible);
Assert.Equal(expected.Template, actual.Template);
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
AssertObject((dynamic)actual, (dynamic)expected);
AssertProperties(actual.Properties, expected.Properties);
AssertObject((dynamic)expected, (dynamic)actual);
}
private static void AssertObject(RectangleObject actual, RectangleObject expected)
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 actual, EllipseObject expected)
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 actual, PointObject expected)
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 actual, PolygonObject expected)
private static void AssertObject(PolygonObject expected, PolygonObject actual)
{
Assert.Equal(expected.Points, actual.Points);
}
private static void AssertObject(PolylineObject actual, PolylineObject expected)
private static void AssertObject(PolylineObject expected, PolylineObject actual)
{
Assert.Equal(expected.Points, actual.Points);
}
private static void AssertObject(TextObject actual, TextObject expected)
private static void AssertObject(TextObject expected, TextObject actual)
{
// Attributes
Assert.Equal(expected.FontFamily, actual.FontFamily);

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests;
public partial class TmxSerializerPropertiesTests
public static partial class DotTiledAssert
{
public static void AssertProperties(Dictionary<string, IProperty>? actual, Dictionary<string, IProperty>? expected)
internal static void AssertProperties(Dictionary<string, IProperty>? expected, Dictionary<string, IProperty>? actual)
{
if (expected is null)
{

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests;
public partial class TmxSerializerTilesetTests
public static partial class DotTiledAssert
{
public static void AssertTileset(Tileset actual, Tileset expected)
internal static void AssertTileset(Tileset expected, Tileset actual)
{
// Attributes
Assert.Equal(expected.Version, actual.Version);
@ -22,28 +22,28 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.FillMode, actual.FillMode);
// At most one of
TmxSerializerImageTests.AssertImage(actual.Image, expected.Image);
AssertTileOffset(actual.TileOffset, expected.TileOffset);
AssertGrid(actual.Grid, expected.Grid);
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
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);
Assert.Equal(expected.Wangsets.Count, actual.Wangsets.Count);
for (var i = 0; i < expected.Wangsets.Count; i++)
AssertWangset(actual.Wangsets[i], expected.Wangsets[i]);
AssertWangset(expected.Wangsets[i], actual.Wangsets[i]);
}
AssertTransformations(actual.Transformations, expected.Transformations);
AssertTransformations(expected.Transformations, actual.Transformations);
// Any number of
Assert.NotNull(actual.Tiles);
Assert.Equal(expected.Tiles.Count, actual.Tiles.Count);
for (var i = 0; i < expected.Tiles.Count; i++)
AssertTile(actual.Tiles[i], expected.Tiles[i]);
AssertTile(expected.Tiles[i], actual.Tiles[i]);
}
private static void AssertTileOffset(TileOffset? actual, TileOffset? expected)
private static void AssertTileOffset(TileOffset? expected, TileOffset? actual)
{
if (expected is null)
{
@ -57,7 +57,7 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.Y, actual.Y);
}
private static void AssertGrid(Grid? actual, Grid? expected)
private static void AssertGrid(Grid? expected, Grid? actual)
{
if (expected is null)
{
@ -72,7 +72,7 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.Height, actual.Height);
}
private static void AssertWangset(Wangset actual, Wangset expected)
private static void AssertWangset(Wangset expected, Wangset actual)
{
// Attributes
Assert.Equal(expected.Name, actual.Name);
@ -80,19 +80,19 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.Tile, actual.Tile);
// At most one of
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
AssertProperties(expected.Properties, actual.Properties);
if (expected.WangColors is not null)
{
Assert.NotNull(actual.WangColors);
Assert.Equal(expected.WangColors.Count, actual.WangColors.Count);
for (var i = 0; i < expected.WangColors.Count; i++)
AssertWangColor(actual.WangColors[i], expected.WangColors[i]);
AssertWangColor(expected.WangColors[i], actual.WangColors[i]);
}
for (var i = 0; i < expected.WangTiles.Count; i++)
AssertWangTile(actual.WangTiles[i], expected.WangTiles[i]);
AssertWangTile(expected.WangTiles[i], actual.WangTiles[i]);
}
private static void AssertWangColor(WangColor actual, WangColor expected)
private static void AssertWangColor(WangColor expected, WangColor actual)
{
// Attributes
Assert.Equal(expected.Name, actual.Name);
@ -101,17 +101,17 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.Tile, actual.Tile);
Assert.Equal(expected.Probability, actual.Probability);
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
AssertProperties(expected.Properties, actual.Properties);
}
private static void AssertWangTile(WangTile actual, WangTile expected)
private static void AssertWangTile(WangTile expected, WangTile actual)
{
// Attributes
Assert.Equal(expected.TileID, actual.TileID);
Assert.Equal(expected.WangID, actual.WangID);
}
private static void AssertTransformations(Transformations? actual, Transformations? expected)
private static void AssertTransformations(Transformations? expected, Transformations? actual)
{
if (expected is null)
{
@ -127,7 +127,7 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.PreferUntransformed, actual.PreferUntransformed);
}
private static void AssertTile(Tile actual, Tile expected)
private static void AssertTile(Tile expected, Tile actual)
{
// Attributes
Assert.Equal(expected.ID, actual.ID);
@ -139,19 +139,19 @@ public partial class TmxSerializerTilesetTests
Assert.Equal(expected.Height, actual.Height);
// Elements
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
TmxSerializerImageTests.AssertImage(actual.Image, expected.Image);
TmxSerializerLayerTests.AssertLayer(actual.ObjectLayer, expected.ObjectLayer);
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);
Assert.Equal(expected.Animation.Count, actual.Animation.Count);
for (var i = 0; i < expected.Animation.Count; i++)
AssertFrame(actual.Animation[i], expected.Animation[i]);
AssertFrame(expected.Animation[i], actual.Animation[i]);
}
}
private static void AssertFrame(Frame actual, Frame expected)
private static void AssertFrame(Frame expected, Frame actual)
{
// Attributes
Assert.Equal(expected.TileID, actual.TileID);

View file

@ -26,7 +26,7 @@
<ItemGroup>
<!-- TmxSerializer test data -->
<EmbeddedResource Include="TmxSerializer/TestData/**/*" />
<EmbeddedResource Include="Serialization/Tmx/TestData/**/*" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,6 @@
namespace DotTiled.Tests;
public partial class TmxSerializerMapTests
public partial class TmxMapReaderTests
{
private static Map EmptyMapWithProperties() => new Map
{

View file

@ -1,6 +1,6 @@
namespace DotTiled.Tests;
public partial class TmxSerializerMapTests
public partial class TmxMapReaderTests
{
private static Map EmptyMapWithEncodingAndCompression(DataEncoding dataEncoding, DataCompression? compression) => new Map
{

View file

@ -1,6 +1,6 @@
namespace DotTiled.Tests;
public partial class TmxSerializerMapTests
public partial class TmxMapReaderTests
{
private static Map MapWithGroup() => new Map
{

View file

@ -1,6 +1,6 @@
namespace DotTiled.Tests;
public partial class TmxSerializerMapTests
public partial class TmxMapReaderTests
{
private static Map MapWithObjectTemplate() => new Map
{

View file

@ -1,6 +1,6 @@
namespace DotTiled.Tests;
public partial class TmxSerializerMapTests
public partial class TmxMapReaderTests
{
private static Map SimpleMapWithEmbeddedTileset() => new Map
{

View file

@ -2,12 +2,12 @@ using System.Xml;
namespace DotTiled.Tests;
public static class TmxSerializerTestData
public static class TmxMapReaderTestData
{
public static XmlReader GetReaderFor(string testDataFile)
public static XmlReader GetXmlReaderFor(string testDataFile)
{
var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}";
using var stream = typeof(TmxSerializerTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
using var stream = typeof(TmxMapReaderTestData).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 TmxSerializerTestData
public static string GetRawStringFor(string testDataFile)
{
var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}";
using var stream = typeof(TmxSerializerTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
using var stream = typeof(TmxMapReaderTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found");
using var stringReader = new StreamReader(stream);

View file

@ -0,0 +1,150 @@
using System.Xml;
namespace DotTiled.Tests;
public partial class TmxMapReaderTests
{
[Fact]
public void TmxMapReaderConstructor_XmlReaderIsNull_ThrowsArgumentNullException()
{
// Arrange
XmlReader xmlReader = null!;
Func<string, Tileset> externalTilesetResolver = (_) => new Tileset();
Func<string, Template> externalTemplateResolver = (_) => new Template { Object = new RectangleObject { } };
// Act
Action act = () =>
{
using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver);
};
// Assert
Assert.Throws<ArgumentNullException>(act);
}
[Fact]
public void TmxMapReaderConstructor_ExternalTilesetResolverIsNull_ThrowsArgumentNullException()
{
// Arrange
using var stringReader = new StringReader("<map></map>");
using var xmlReader = XmlReader.Create(stringReader);
Func<string, Tileset> externalTilesetResolver = null!;
Func<string, Template> externalTemplateResolver = (_) => new Template { Object = new RectangleObject { } };
// Act
Action act = () =>
{
using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver);
};
// Assert
Assert.Throws<ArgumentNullException>(act);
}
[Fact]
public void TmxMapReaderConstructor_ExternalTemplateResolverIsNull_ThrowsArgumentNullException()
{
// Arrange
using var stringReader = new StringReader("<map></map>");
using var xmlReader = XmlReader.Create(stringReader);
Func<string, Tileset> externalTilesetResolver = (_) => new Tileset();
Func<string, Template> externalTemplateResolver = null!;
// Act
Action act = () =>
{
using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver);
};
// Assert
Assert.Throws<ArgumentNullException>(act);
}
[Fact]
public void TmxMapReaderConstructor_NoneNull_DoesNotThrow()
{
// Arrange
using var stringReader = new StringReader("<map></map>");
using var xmlReader = XmlReader.Create(stringReader);
Func<string, Tileset> externalTilesetResolver = (_) => new Tileset();
Func<string, Template> externalTemplateResolver = (_) => new Template { Object = new RectangleObject { } };
// Act
using var tmxMapReader = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver);
// Assert
Assert.NotNull(tmxMapReader);
}
public static IEnumerable<object[]> 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()],
];
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))]
public void TmxMapReaderReadMap_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
using var reader = TmxMapReaderTestData.GetXmlReaderFor(testDataFile);
static Template ResolveTemplate(string source)
{
using var xmlTemplateReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Template.{source}");
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate);
return templateReader.ReadTemplate();
}
static Tileset ResolveTileset(string source)
{
using var xmlTilesetReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Tileset.{source}");
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate);
return tilesetReader.ReadTileset();
}
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate);
// Act
var map = mapReader.ReadMap();
// Assert
Assert.NotNull(map);
DotTiledAssert.AssertMap(expectedMap, map);
}
public static IEnumerable<object[]> DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data =>
[
["Serialization.Tmx.TestData.Map.map-with-object-template.tmx", MapWithObjectTemplate()],
["Serialization.Tmx.TestData.Map.map-with-group.tmx", MapWithGroup()],
];
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))]
public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
using var reader = TmxMapReaderTestData.GetXmlReaderFor(testDataFile);
static Template ResolveTemplate(string source)
{
using var xmlTemplateReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Template.{source}");
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate);
return templateReader.ReadTemplate();
}
static Tileset ResolveTileset(string source)
{
using var xmlTilesetReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Tileset.{source}");
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate);
return tilesetReader.ReadTileset();
}
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate);
// Act
var map = mapReader.ReadMap();
// Assert
Assert.NotNull(map);
DotTiledAssert.AssertMap(expectedMap, map);
}
}

View file

@ -1,225 +0,0 @@
namespace DotTiled.Tests;
public partial class TmxSerializerMapTests
{
private static void AssertMap(Map actual, Map expected)
{
// Attributes
Assert.Equal(expected.Version, actual.Version);
Assert.Equal(expected.TiledVersion, actual.TiledVersion);
Assert.Equal(expected.Class, actual.Class);
Assert.Equal(expected.Orientation, actual.Orientation);
Assert.Equal(expected.RenderOrder, actual.RenderOrder);
Assert.Equal(expected.CompressionLevel, actual.CompressionLevel);
Assert.Equal(expected.Width, actual.Width);
Assert.Equal(expected.Height, actual.Height);
Assert.Equal(expected.TileWidth, actual.TileWidth);
Assert.Equal(expected.TileHeight, actual.TileHeight);
Assert.Equal(expected.HexSideLength, actual.HexSideLength);
Assert.Equal(expected.StaggerAxis, actual.StaggerAxis);
Assert.Equal(expected.StaggerIndex, actual.StaggerIndex);
Assert.Equal(expected.ParallaxOriginX, actual.ParallaxOriginX);
Assert.Equal(expected.ParallaxOriginY, actual.ParallaxOriginY);
Assert.Equal(expected.BackgroundColor, actual.BackgroundColor);
Assert.Equal(expected.NextLayerID, actual.NextLayerID);
Assert.Equal(expected.NextObjectID, actual.NextObjectID);
Assert.Equal(expected.Infinite, actual.Infinite);
TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties);
Assert.NotNull(actual.Tilesets);
Assert.Equal(expected.Tilesets.Count, actual.Tilesets.Count);
for (var i = 0; i < expected.Tilesets.Count; i++)
TmxSerializerTilesetTests.AssertTileset(actual.Tilesets[i], expected.Tilesets[i]);
Assert.NotNull(actual.Layers);
Assert.Equal(expected.Layers.Count, actual.Layers.Count);
for (var i = 0; i < expected.Layers.Count; i++)
TmxSerializerLayerTests.AssertLayer(actual.Layers[i], expected.Layers[i]);
}
public static IEnumerable<object[]> DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data =>
[
["TmxSerializer.TestData.Map.empty-map-csv.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)],
["TmxSerializer.TestData.Map.empty-map-base64.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)],
["TmxSerializer.TestData.Map.empty-map-base64-gzip.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)],
["TmxSerializer.TestData.Map.empty-map-base64-zlib.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)],
["TmxSerializer.TestData.Map.simple-tileset-embed.tmx", SimpleMapWithEmbeddedTileset()],
["TmxSerializer.TestData.Map.empty-map-properties.tmx", EmptyMapWithProperties()],
];
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))]
public void DeserializeMapFromXmlReader_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
using var reader = TmxSerializerTestData.GetReaderFor(testDataFile);
var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile);
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (TmxSerializer serializer, string s) =>
throw new NotSupportedException("External tilesets are not supported in this test");
Func<TmxSerializer, string, Template> externalTemplateResolver = (TmxSerializer serializer, string s) =>
throw new NotSupportedException("External templates are not supported in this test");
var tmxSerializer = new TmxSerializer(
externalTilesetResolver,
externalTemplateResolver);
// Act
var map = tmxSerializer.DeserializeMap(reader);
var raw = tmxSerializer.DeserializeMap(testDataFileText);
// Assert
Assert.NotNull(map);
AssertMap(map, expectedMap);
Assert.NotNull(raw);
AssertMap(raw, expectedMap);
AssertMap(map, raw);
}
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))]
public void DeserializeMapFromString_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile);
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (TmxSerializer serializer, string s) =>
throw new NotSupportedException("External tilesets are not supported in this test");
Func<TmxSerializer, string, Template> externalTemplateResolver = (TmxSerializer serializer, string s) =>
throw new NotSupportedException("External templates are not supported in this test");
var tmxSerializer = new TmxSerializer(
externalTilesetResolver,
externalTemplateResolver);
// Act
var raw = tmxSerializer.DeserializeMap(testDataFileText);
// Assert
Assert.NotNull(raw);
AssertMap(raw, expectedMap);
}
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data))]
public void DeserializeMapFromStringFromXmlReader_ValidXmlNoExternalTilesets_Equal(string testDataFile, Map expectedMap)
{
// Arrange
using var reader = TmxSerializerTestData.GetReaderFor(testDataFile);
var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile);
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (TmxSerializer serializer, string s) =>
throw new NotSupportedException("External tilesets are not supported in this test");
Func<TmxSerializer, string, Template> externalTemplateResolver = (TmxSerializer serializer, string s) =>
throw new NotSupportedException("External templates are not supported in this test");
var tmxSerializer = new TmxSerializer(
externalTilesetResolver,
externalTemplateResolver);
// Act
var map = tmxSerializer.DeserializeMap(reader);
var raw = tmxSerializer.DeserializeMap(testDataFileText);
// Assert
Assert.NotNull(map);
Assert.NotNull(raw);
AssertMap(map, raw);
AssertMap(map, expectedMap);
AssertMap(raw, expectedMap);
}
public static IEnumerable<object[]> DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data =>
[
["TmxSerializer.TestData.Map.map-with-object-template.tmx", MapWithObjectTemplate()],
["TmxSerializer.TestData.Map.map-with-group.tmx", MapWithGroup()],
];
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))]
public void DeserializeMapFromXmlReader_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
using var reader = TmxSerializerTestData.GetReaderFor(testDataFile);
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (TmxSerializer serializer, string s) =>
{
using var tilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{s}");
return serializer.DeserializeTileset(tilesetReader);
};
Func<TmxSerializer, string, Template> externalTemplateResolver = (TmxSerializer serializer, string s) =>
{
using var templateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{s}");
return serializer.DeserializeTemplate(templateReader);
};
var tmxSerializer = new TmxSerializer(
externalTilesetResolver,
externalTemplateResolver);
// Act
var map = tmxSerializer.DeserializeMap(reader);
// Assert
Assert.NotNull(map);
AssertMap(map, expectedMap);
}
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))]
public void DeserializeMapFromString_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile);
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (TmxSerializer serializer, string s) =>
{
using var tilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{s}");
return serializer.DeserializeTileset(tilesetReader);
};
Func<TmxSerializer, string, Template> externalTemplateResolver = (TmxSerializer serializer, string s) =>
{
using var templateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{s}");
return serializer.DeserializeTemplate(templateReader);
};
var tmxSerializer = new TmxSerializer(
externalTilesetResolver,
externalTemplateResolver);
// Act
var map = tmxSerializer.DeserializeMap(testDataFileText);
// Assert
Assert.NotNull(map);
AssertMap(map, expectedMap);
}
[Theory]
[MemberData(nameof(DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))]
public void DeserializeMapFromStringFromXmlReader_ValidXmlExternalTilesetsAndTemplates_Equal(string testDataFile, Map expectedMap)
{
// Arrange
using var reader = TmxSerializerTestData.GetReaderFor(testDataFile);
var testDataFileText = TmxSerializerTestData.GetRawStringFor(testDataFile);
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (TmxSerializer serializer, string s) =>
{
using var tilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{s}");
return serializer.DeserializeTileset(tilesetReader);
};
Func<TmxSerializer, string, Template> externalTemplateResolver = (TmxSerializer serializer, string s) =>
{
using var templateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{s}");
return serializer.DeserializeTemplate(templateReader);
};
var tmxSerializer = new TmxSerializer(
externalTilesetResolver,
externalTemplateResolver);
// Act
var map = tmxSerializer.DeserializeMap(reader);
var raw = tmxSerializer.DeserializeMap(testDataFileText);
// Assert
Assert.NotNull(map);
Assert.NotNull(raw);
AssertMap(map, raw);
AssertMap(map, expectedMap);
AssertMap(raw, expectedMap);
}
}

View file

@ -1,32 +0,0 @@
namespace DotTiled.Tests;
public class TmxSerializerTests
{
[Fact]
public void TmxSerializerConstructor_ExternalTilesetResolverIsNull_ThrowsArgumentNullException()
{
// Arrange
Func<TmxSerializer, string, Tileset> externalTilesetResolver = null!;
Func<TmxSerializer, string, Template> externalTemplateResolver = null!;
// Act
Action act = () => _ = new TmxSerializer(externalTilesetResolver, externalTemplateResolver);
// Assert
Assert.Throws<ArgumentNullException>(act);
}
[Fact]
public void TmxSerializerConstructor_ExternalTilesetResolverIsNotNull_DoesNotThrow()
{
// Arrange
Func<TmxSerializer, string, Tileset> externalTilesetResolver = (_, _) => new Tileset();
Func<TmxSerializer, string, Template> externalTemplateResolver = (_, _) => new Template { Object = new RectangleObject { } };
// Act
var tmxSerializer = new TmxSerializer(externalTilesetResolver, externalTemplateResolver);
// Assert
Assert.NotNull(tmxSerializer);
}
}

View file

@ -60,5 +60,5 @@ public class Map
// Any number of
public List<Tileset> Tilesets { get; set; } = [];
public List<BaseLayer> Layers { get; set; } = [];
// public List<Group> Groups { get; set; } = [];
public List<Group> Groups { get; set; } = [];
}

View file

@ -0,0 +1,8 @@
using System;
namespace DotTiled;
public interface IMapReader : IDisposable
{
Map ReadMap();
}

View file

@ -0,0 +1,8 @@
using System;
namespace DotTiled;
public interface ITemplateReader : IDisposable
{
Template ReadTemplate();
}

View file

@ -0,0 +1,8 @@
using System;
namespace DotTiled;
public interface ITilesetReader : IDisposable
{
Tileset ReadTileset();
}

View file

@ -2,9 +2,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression)
internal static Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression)
{
var x = reader.GetRequiredAttributeParseable<int>("x");
var y = reader.GetRequiredAttributeParseable<int>("y");

View file

@ -7,9 +7,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private Data ReadData(XmlReader reader, bool usesChunks)
internal static Data ReadData(XmlReader reader, bool usesChunks)
{
var encoding = reader.GetOptionalAttributeEnum<DataEncoding>("encoding", e => e switch
{
@ -46,7 +46,7 @@ public partial class TmxSerializer
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null };
}
private (uint[] GlobalTileIDs, FlippingFlags[] FlippingFlags) ReadAndClearFlippingFlagsFromGIDs(uint[] globalTileIDs)
internal static (uint[] GlobalTileIDs, FlippingFlags[] FlippingFlags) ReadAndClearFlippingFlagsFromGIDs(uint[] globalTileIDs)
{
var clearedGlobalTileIDs = new uint[globalTileIDs.Length];
var flippingFlags = new FlippingFlags[globalTileIDs.Length];
@ -61,12 +61,12 @@ public partial class TmxSerializer
return (clearedGlobalTileIDs, flippingFlags);
}
private uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader)
internal static uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader)
{
return reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable<uint>("gid") ?? 0).ToArray();
}
private uint[] ReadRawData(XmlReader reader, DataEncoding encoding, DataCompression? compression)
internal static uint[] ReadRawData(XmlReader reader, DataEncoding encoding, DataCompression? compression)
{
var data = reader.ReadElementContentAsString();
if (encoding == DataEncoding.Csv)
@ -87,7 +87,7 @@ public partial class TmxSerializer
return decompressed;
}
private uint[] ParseCsvData(string data)
internal static uint[] ParseCsvData(string data)
{
var values = data
.Split((char[])['\n', '\r', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
@ -96,7 +96,7 @@ public partial class TmxSerializer
return values;
}
private uint[] ReadMemoryStreamAsInt32Array(Stream stream)
internal static uint[] ReadMemoryStreamAsInt32Array(Stream stream)
{
var finalValues = new List<uint>();
var int32Bytes = new byte[4];
@ -108,13 +108,13 @@ public partial class TmxSerializer
return finalValues.ToArray();
}
private uint[] DecompressGZip(MemoryStream stream)
internal static uint[] DecompressGZip(MemoryStream stream)
{
using var decompressedStream = new GZipStream(stream, CompressionMode.Decompress);
return ReadMemoryStreamAsInt32Array(decompressedStream);
}
private uint[] DecompressZLib(MemoryStream stream)
internal static uint[] DecompressZLib(MemoryStream stream)
{
using var decompressedStream = new ZLibStream(stream, CompressionMode.Decompress);
return ReadMemoryStreamAsInt32Array(decompressedStream);

View file

@ -2,7 +2,7 @@ using System;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private static class Helpers
{

View file

@ -6,9 +6,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private Map ReadMap(XmlReader reader)
internal static Map ReadMap(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
// Attributes
var version = reader.GetRequiredAttribute("version");
@ -65,11 +65,11 @@ public partial class TmxSerializer
reader.ProcessChildren("map", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"tileset" => () => tilesets.Add(ReadTileset(r)),
"tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver)),
"layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)),
"imagelayer" => () => layers.Add(ReadImageLayer(r)),
"group" => () => layers.Add(ReadGroup(r)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)),
_ => r.Skip
});

View file

@ -7,9 +7,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private ObjectLayer ReadObjectLayer(XmlReader reader)
internal static ObjectLayer ReadObjectLayer(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
// Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id");
@ -41,7 +41,7 @@ public partial class TmxSerializer
reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"object" => () => objects.Add(ReadObject(r)),
"object" => () => objects.Add(ReadObject(r, externalTemplateResolver)),
_ => r.Skip
});
@ -68,7 +68,7 @@ public partial class TmxSerializer
};
}
private Object ReadObject(XmlReader reader)
internal static Object ReadObject(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
// Attributes
var template = reader.GetOptionalAttribute("template");
@ -88,7 +88,7 @@ public partial class TmxSerializer
// Perform template copy first
if (template is not null)
{
var resolvedTemplate = _externalTemplateResolver(this, template);
var resolvedTemplate = externalTemplateResolver(template);
var templObj = resolvedTemplate.Object;
idDefault = templObj.ID;
@ -152,7 +152,7 @@ public partial class TmxSerializer
return obj;
}
private Dictionary<string, IProperty> MergeProperties(Dictionary<string, IProperty>? baseProperties, Dictionary<string, IProperty> overrideProperties)
internal static Dictionary<string, IProperty> MergeProperties(Dictionary<string, IProperty>? baseProperties, Dictionary<string, IProperty> overrideProperties)
{
if (baseProperties is null)
return overrideProperties ?? new Dictionary<string, IProperty>();
@ -184,19 +184,19 @@ public partial class TmxSerializer
return result;
}
private EllipseObject ReadEllipseObject(XmlReader reader)
internal static EllipseObject ReadEllipseObject(XmlReader reader)
{
reader.Skip();
return new EllipseObject { };
}
private PointObject ReadPointObject(XmlReader reader)
internal static PointObject ReadPointObject(XmlReader reader)
{
reader.Skip();
return new PointObject { };
}
private PolygonObject ReadPolygonObject(XmlReader reader)
internal static PolygonObject ReadPolygonObject(XmlReader reader)
{
// Attributes
var points = reader.GetRequiredAttributeParseable<List<Vector2>>("points", s =>
@ -214,7 +214,7 @@ public partial class TmxSerializer
return new PolygonObject { Points = points };
}
private PolylineObject ReadPolylineObject(XmlReader reader)
internal static PolylineObject ReadPolylineObject(XmlReader reader)
{
// Attributes
var points = reader.GetRequiredAttributeParseable<List<Vector2>>("points", s =>
@ -232,7 +232,7 @@ public partial class TmxSerializer
return new PolylineObject { Points = points };
}
private TextObject ReadTextObject(XmlReader reader)
internal static TextObject ReadTextObject(XmlReader reader)
{
// Attributes
var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif";
@ -280,7 +280,7 @@ public partial class TmxSerializer
};
}
private Template ReadTemplate(XmlReader reader)
internal static Template ReadTemplate(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
// No attributes
@ -292,8 +292,8 @@ public partial class TmxSerializer
reader.ProcessChildren("template", (r, elementName) => elementName switch
{
"tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r), "Tileset"),
"object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r), "Object"),
"tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver), "Tileset"),
"object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver), "Object"),
_ => r.Skip
});

View file

@ -4,9 +4,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private Dictionary<string, IProperty> ReadProperties(XmlReader reader)
internal static Dictionary<string, IProperty> ReadProperties(XmlReader reader)
{
return reader.ReadList("properties", "property", (r) =>
{
@ -40,7 +40,7 @@ public partial class TmxSerializer
}).ToDictionary(x => x.name, x => x.property);
}
private ClassProperty ReadClassProperty(XmlReader reader)
internal static ClassProperty ReadClassProperty(XmlReader reader)
{
var name = reader.GetRequiredAttribute("name");
var propertyType = reader.GetRequiredAttribute("propertytype");

View file

@ -5,9 +5,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private TileLayer ReadTileLayer(XmlReader reader, bool dataUsesChunks)
internal static TileLayer ReadTileLayer(XmlReader reader, bool dataUsesChunks)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
@ -55,7 +55,7 @@ public partial class TmxSerializer
};
}
private ImageLayer ReadImageLayer(XmlReader reader)
internal static ImageLayer ReadImageLayer(XmlReader reader)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
@ -103,7 +103,7 @@ public partial class TmxSerializer
};
}
private Group ReadGroup(XmlReader reader)
internal static Group ReadGroup(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
@ -123,9 +123,9 @@ public partial class TmxSerializer
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: false)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)),
"imagelayer" => () => layers.Add(ReadImageLayer(r)),
"group" => () => layers.Add(ReadGroup(r)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)),
_ => r.Skip
});

View file

@ -5,9 +5,9 @@ using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
internal partial class Tmx
{
private Tileset ReadTileset(XmlReader reader)
internal static Tileset ReadTileset(XmlReader reader, Func<string, Tileset>? externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
// Attributes
var version = reader.GetOptionalAttribute("version");
@ -66,14 +66,17 @@ public partial class TmxSerializer
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r), "Wangsets"),
"transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"),
"tile" => () => tiles.Add(ReadTile(r)),
"tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver)),
_ => r.Skip
});
// Check if tileset is referring to external file
if (source is not null)
{
var resolvedTileset = _externalTilesetResolver(this, source);
if (externalTilesetResolver is null)
throw new InvalidOperationException("External tileset resolver is required to resolve external tilesets.");
var resolvedTileset = externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = null;
return resolvedTileset;
@ -106,7 +109,7 @@ public partial class TmxSerializer
};
}
private Image ReadImage(XmlReader reader)
internal static Image ReadImage(XmlReader reader)
{
// Attributes
var format = reader.GetOptionalAttributeEnum<ImageFormat>("format", s => s switch
@ -138,7 +141,7 @@ public partial class TmxSerializer
};
}
private TileOffset ReadTileOffset(XmlReader reader)
internal static TileOffset ReadTileOffset(XmlReader reader)
{
// Attributes
var x = reader.GetOptionalAttributeParseable<float>("x") ?? 0f;
@ -148,7 +151,7 @@ public partial class TmxSerializer
return new TileOffset { X = x, Y = y };
}
private Grid ReadGrid(XmlReader reader)
internal static Grid ReadGrid(XmlReader reader)
{
// Attributes
var orientation = reader.GetOptionalAttributeEnum<GridOrientation>("orientation", s => s switch
@ -164,7 +167,7 @@ public partial class TmxSerializer
return new Grid { Orientation = orientation, Width = width, Height = height };
}
private Transformations ReadTransformations(XmlReader reader)
internal static Transformations ReadTransformations(XmlReader reader)
{
// Attributes
var hFlip = reader.GetOptionalAttributeParseable<bool>("hflip") ?? false;
@ -176,7 +179,7 @@ public partial class TmxSerializer
return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed };
}
private Tile ReadTile(XmlReader reader)
internal static Tile ReadTile(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
// Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id");
@ -197,7 +200,7 @@ public partial class TmxSerializer
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r), "ObjectLayer"),
"objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver), "ObjectLayer"),
"animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList<Frame>("animation", "frame", (ar) =>
{
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
@ -223,12 +226,12 @@ public partial class TmxSerializer
};
}
private List<Wangset> ReadWangsets(XmlReader reader)
internal static List<Wangset> ReadWangsets(XmlReader reader)
{
return reader.ReadList<Wangset>("wangsets", "wangset", ReadWangset);
}
private Wangset ReadWangset(XmlReader reader)
internal static Wangset ReadWangset(XmlReader reader)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
@ -262,7 +265,7 @@ public partial class TmxSerializer
};
}
private WangColor ReadWangColor(XmlReader reader)
internal static WangColor ReadWangColor(XmlReader reader)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
@ -291,7 +294,7 @@ public partial class TmxSerializer
};
}
private WangTile ReadWangTile(XmlReader reader)
internal static WangTile ReadWangTile(XmlReader reader)
{
// Attributes
var tileID = reader.GetRequiredAttributeParseable<uint>("tileid");

View file

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Xml;
namespace DotTiled;
public class TmxMapReader : IMapReader
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
public TmxMapReader(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
// Prepare reader
_reader.MoveToContent();
}
public Map ReadMap()
{
return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
_reader.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~TmxTiledMapReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
System.GC.SuppressFinalize(this);
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Xml;
namespace DotTiled;
public class TsxTilesetReader : ITilesetReader
{
// External resolvers
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
public TsxTilesetReader(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
}
public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver);
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
// ~TsxTilesetReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
System.GC.SuppressFinalize(this);
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Xml;
namespace DotTiled;
public class TxTemplateReader : ITemplateReader
{
// Resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
public TxTemplateReader(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
// Prepare reader
_reader.MoveToContent();
}
public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver);
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
// ~TxTemplateReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
System.GC.SuppressFinalize(this);
}
}

View file

@ -1,45 +0,0 @@
using System;
using System.IO;
using System.Xml;
namespace DotTiled;
public partial class TmxSerializer
{
private readonly Func<TmxSerializer, string, Tileset> _externalTilesetResolver;
private readonly Func<TmxSerializer, string, Template> _externalTemplateResolver;
public TmxSerializer(
Func<TmxSerializer, string, Tileset> externalTilesetResolver,
Func<TmxSerializer, string, Template> externalTemplateResolver
)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
}
public Map DeserializeMap(XmlReader reader)
{
reader.ReadToFollowing("map");
return ReadMap(reader);
}
public Map DeserializeMap(string xml)
{
using var stringReader = new StringReader(xml);
using var reader = XmlReader.Create(stringReader);
return DeserializeMap(reader);
}
public Tileset DeserializeTileset(XmlReader reader)
{
reader.ReadToFollowing("tileset");
return ReadTileset(reader);
}
public Template DeserializeTemplate(XmlReader reader)
{
reader.ReadToFollowing("template");
return ReadTemplate(reader);
}
}

124
README.md
View file

@ -4,45 +4,71 @@
DotTiled is a simple and easy-to-use library for loading, saving, and managing [Tiled maps and tilesets](https://mapeditor.org) in your .NET projects. After [TiledCS](https://github.com/TheBoneJarmer/TiledCS) unfortunately became unmaintained (since 2022), I aimed to create a new library that could fill its shoes. DotTiled is the result of that effort.
- [Feature coverage](#feature-coverage)
DotTiled is designed to be a lightweight and efficient library that provides a simple API for loading and managing Tiled maps and tilesets. It is built with performance in mind and aims to be as fast and memory-efficient as possible. Targeting `netstandard2.0` and `net8.0` allows DotTiled to be used in popular game engines like Unity and Godot, as well as in popular game development frameworks like MonoGame.
- [Alternative libraries and comparison + benchmarks](#alternative-libraries-and-comparison)
- [Quickstart](#quickstart)
- [Installing DotTiled](#installing-dottiled)
- [Constructing a `TmxSerializer`](#constructing-a-tmxserializer)
- [Loading a `.tmx` map](#loading-a-tmx-map)
- [Loading a `.tsx` tileset](#loading-a-tsx-tileset)
- [Constructing a `TmjSerializer`](#constructing-a-tmjserializer)
- [Loading a `.tmj` map](#loading-a-tmj-map)
- [Loading a `.tsj` tileset](#loading-a-tsj-tileset)
# Feature coverage
**TBD**: Add a table displaying the features that DotTiled supports, and which features are not yet supported, and perhaps why they are not supported (yet or ever).
# Alternative libraries and comparison
Other similar libraries exist, and you may want to consider them for your project as well:
| Library | Actively maintained | Supported formats | Feature coverage | Tiled version compatibility | Docs | License | Benchmark rank* |
| --- | --- | --- | --- | --- | --- | --- | --- |
| **DotTiled** | ✅ | `.tmx` `.tsx` `.tmj` `.tsj` `.tx` | | | Usage, API, XML docs | | |
| [TiledLib](https://github.com/Ragath/TiledLib.Net) |✅| | | | | | |
| [TiledCSPlus](https://github.com/nolemretaWxd/TiledCSPlus) |✅| | | | | | |
| [TiledSharp](https://github.com/marshallward/TiledSharp) |❌| | | | | | |
| [TiledCS](https://github.com/TheBoneJarmer/TiledCS) |❌| | | | | | |
| [TiledNet](https://github.com/napen123/Tiled.Net) |❌| | | | | | |
|**Comparison**|**DotTiled**|[TiledLib](https://github.com/Ragath/TiledLib.Net)|[TiledCSPlus](https://github.com/nolemretaWxd/TiledCSPlus)|[TiledSharp](https://github.com/marshallward/TiledSharp)|[TiledCS](https://github.com/TheBoneJarmer/TiledCS)|[TiledNet](https://github.com/napen123/Tiled.Net)|
|---------------------------------|:-----------------------:|:--------:|:-----------:|:----------:|:-------:|:------:|
| Actively maintained | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Benchmark (time)* | 1.00 | 1.81 | 2.12 | - | - | - |
| Benchmark (memory)* | 1.00 | 1.42 | 2.03 | - | - | - |
| .NET Targets | `net8.0`<br>`netstandard2.0` |`net6.0`<br>`net7.0`|`netstandard2.1`|`netstandard2.0`|`netstandard2.0`|`net45`|
| Docs |Usage,<br>XML Docs|Usage|Usage, API,<br>XML Docs|Usage, API|Usage, XML Docs|Usage, XML Docs|
| License | MIT | MIT | MIT | Apache-2.0 | MIT | BSD 3-Clause |
| *Feature coverage<br>comparison below*|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
> [!NOTE]
> *Benchmark rank is based on the libraries' speed and memory usage when loading different types of maps and tilesets. Further explanations and details can be found in the below collapsible section.
> *Both benchmark time and memory ratios are relative to DotTiled. Lower is better. Benchmark (time) refers to the execution time of loading the same map from an in-memory string that contains XML data in the `.tmx` format. Benchmark (memory) refers to the memory allocated during that loading process. For further details on the benchmark results, see the collapsible section below.
<details>
<summary>
Comparison and benchmark details
Feature coverage comparison
</summary>
**TODO: Add table displaying feature availability**
| **Comparison**|**DotTiled**|[TiledLib](https://github.com/Ragath/TiledLib.Net)|[TiledCSPlus](https://github.com/nolemretaWxd/TiledCSPlus)|[TiledSharp](https://github.com/marshallward/TiledSharp)|[TiledCS](https://github.com/TheBoneJarmer/TiledCS)|[TiledNet](https://github.com/napen123/Tiled.Net)|
|---------------------------------|:-:|:-:|:-:|:-:|:-:|:-:|
| Full XML support `.tmx` |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Full JSON support `.tmj` |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Load from string (implies file) |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Load from file |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| External tilesets |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Template files |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Property custom types |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Hierarchical layers (groups) |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
| Infinite maps |✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|✅/❌|
**TODO: Add table displaying benchmark results**
</details>
<details>
<summary>
Benchmark details
</summary>
The following benchmark results were gathered using the `DotTiled.Benchmark` project which uses [BenchmarkDotNet](https://benchmarkdotnet.org/) to compare the performance of DotTiled with other similar libraries. The benchmark results are grouped by category and show the mean execution time, memory consumption metrics, and ratio to DotTiled.
```
BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4651/22H2/2022Update)
12th Gen Intel Core i7-12700K, 1 CPU, 20 logical and 12 physical cores
.NET SDK 8.0.202
[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 |
</details>
@ -57,55 +83,3 @@ DotTiled is available as a NuGet package. You can install it by using the NuGet
```pwsh
dotnet add package DotTiled
```
### Constructing a `TmxSerializer`
There are few details to be aware of for your `TmxSerializer`:
```csharp
// A map may or may not contain tilesets that are stored in external
// files. To deserialize a map, you must provide a way to resolve such
// tilesets.
Func<TmxSerializer, string, Tileset> tilesetResolver =
(TmxSerializer serializer, string path) =>
{
string tilesetXml = fileSystem.ReadAllText(path);
return serializer.DeserializeTileset(tilesetXml);
};
// A map may or may not contain objects that reference template files.
// To deserialize a map, you must provide a way to resolve such templates.
Func<TmxSerializer, string, Template> templateResolver =
(TmxSerializer serializer, string path) =>
{
string templateXml = fileSystem.ReadAllText(path);
return serializer.DeserializeTemplate(templateXml);
};
var tmxSerializer = new TmxSerializer(tilesetResolver, templateResolver);
```
### Loading a `.tmx` map
The `TmxSerializer` has several overloads for `DeserializeMap` that allow you to load a map from a number of different sources.
```csharp
string mapXml = fileSystem.ReadAllText("path/to/map.tmx");
Map mapFromRaw = tmxSerializer.DeserializeMap(mapXml); // From raw XML string in memory
using var reader = fileSystem.OpenXmlReader("path/to/map.tmx");
Map mapFromReader = tmxSerializer.DeserializeMap(reader); // From XML reader
```
### Loading a `.tsx` tileset
Similar to maps, the `TmxSerializer` has several overloads for `DeserializeTileset` that allow you to load a tileset from a number of different sources.
```csharp
string tilesetXml = fileSystem.ReadAllText("path/to/tileset.tsx");
Tileset tileset = tmxSerializer.DeserializeTileset(tilesetXml); // From raw XML string in memory
using var reader = fileSystem.OpenXmlReader("path/to/tileset.tsx");
Tileset tileset = tmxSerializer.DeserializeTileset(reader); // From XML reader
```