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 MapTests => + [ + ["Serialization/TestData/Map/default_map/default-map", (string f) => TestData.DefaultMap(), Array.Empty()], + ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => TestData.MapWithCommonProps(), Array.Empty()], + ["Serialization/TestData/Map/map_with_custom_type_props/map-with-custom-type-props", (string f) => TestData.MapWithCustomTypeProps(), TestData.MapWithCustomTypePropsCustomTypeDefinitions()], + ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => TestData.MapWithEmbeddedTileset(), Array.Empty()], + ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => TestData.MapWithExternalTileset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => TestData.MapWithFlippingFlags(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => TestData.MapExternalTilesetMulti(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => TestData.MapExternalTilesetWangset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => TestData.MapWithManyLayers(f), Array.Empty()], + ]; + + private static CustomTypeDefinition[] typedefs = [ + new CustomClassDefinition + { + Name = "TestClass", + ID = 1, + UseAs = CustomClassUseAs.Property, + Members = [ + new StringProperty + { + Name = "Name", + Value = "" + }, + new FloatProperty + { + Name = "Amount", + Value = 0f + } + ] + }, + new CustomClassDefinition + { + Name = "Test", + ID = 2, + UseAs = CustomClassUseAs.All, + Members = [ + new ClassProperty + { + Name = "Yep", + PropertyType = "TestClass", + Properties = [] + } + ] + } + ]; +} diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/simple-tileset-embed.cs b/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs similarity index 68% rename from DotTiled.Tests/TmxSerializer/TestData/Map/simple-tileset-embed.cs rename to DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs index ad27d8e..eff73d9 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/simple-tileset-embed.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs @@ -1,37 +1,28 @@ namespace DotTiled.Tests; -public partial class TmxSerializerMapTests +public partial class TestData { - private static Map SimpleMapWithEmbeddedTileset() => new Map + public static Map DefaultMap() => new Map { - Version = "1.10", - TiledVersion = "1.11.0", + Class = "", Orientation = MapOrientation.Orthogonal, - RenderOrder = RenderOrder.RightDown, Width = 5, Height = 5, TileWidth = 32, TileHeight = 32, Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = new Color { R = 0, G = 0, B = 0, A = 0 }, + Version = "1.10", + TiledVersion = "1.11.0", NextLayerID = 2, NextObjectID = 1, - Tilesets = [ - new Tileset - { - FirstGID = 1, - Name = "Tileset 1", - TileWidth = 32, - TileHeight = 32, - TileCount = 8, - Columns = 4, - Image = new Image - { - Source = "tiles.png", - Width = 128, - Height = 64 - } - } - ], Layers = [ new TileLayer { @@ -42,13 +33,14 @@ public partial class TmxSerializerMapTests Data = new Data { Encoding = DataEncoding.Csv, + Chunks = null, Compression = null, GlobalTileIDs = [ - 1,1,1,1,1, - 1,1,1,1,1, - 1,1,1,1,1, - 2,2,2,2,2, - 2,2,2,2,2 + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 ], FlippingFlags = [ FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, @@ -57,7 +49,7 @@ public partial class TmxSerializerMapTests FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None ] - }, + } } ] }; diff --git a/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj b/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj new file mode 100644 index 0000000..896df6c --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj @@ -0,0 +1,32 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-csv.tmx b/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmx similarity index 100% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-csv.tmx rename to DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmx diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs new file mode 100644 index 0000000..24651b6 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs @@ -0,0 +1,121 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapExternalTilesetMulti(string fileExt) => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 2, + NextObjectID = 1, + Tilesets = [ + new Tileset + { + TileOffset = new TileOffset + { + X = 1, + Y = 5 + }, + Version = "1.10", + TiledVersion = "1.11.0", + FirstGID = 1, + Name = "multi-tileset", + TileWidth = 256, + TileHeight = 96, + TileCount = 2, + Columns = 0, + Source = $"multi-tileset.{(fileExt == "tmx" ? "tsx" : "tsj")}", + Grid = new Grid + { + Orientation = GridOrientation.Orthogonal, + Width = 1, + Height = 1 + }, + Properties = new Dictionary + { + ["tilesetbool"] = new BoolProperty { Name = "tilesetbool", Value = true }, + ["tilesetcolor"] = new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, + ["tilesetfile"] = new FileProperty { Name = "tilesetfile", Value = "" }, + ["tilesetfloat"] = new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, + ["tilesetint"] = new IntProperty { Name = "tilesetint", Value = 9 }, + ["tilesetobject"] = new ObjectProperty { Name = "tilesetobject", Value = 0 }, + ["tilesetstring"] = new StringProperty { Name = "tilesetstring", Value = "hello world!" } + }, + Tiles = [ + new Tile + { + ID = 0, + Width = 256, + Height = 96, + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96 + } + }, + new Tile + { + ID = 1, + Width = 256, + Height = 96, + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96 + } + } + ] + } + ], + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ] + }; +} diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.tmj new file mode 100644 index 0000000..da37182 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.tmj @@ -0,0 +1,36 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"multi-tileset.tsj" + }], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64-zlib.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.tmx similarity index 66% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64-zlib.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.tmx index d35b438..477c112 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64-zlib.tmx +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.tmx @@ -1,8 +1,13 @@ + - - eJxjYKA9AAAAZAAB - + +0,0,0,0,0, +0,0,0,0,0, +1,0,0,0,0, +0,0,0,0,0, +0,2,0,0,0 + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/multi-tileset.tsj b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/multi-tileset.tsj new file mode 100644 index 0000000..d190934 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/multi-tileset.tsj @@ -0,0 +1,71 @@ +{ "columns":0, + "grid": + { + "height":1, + "orientation":"orthogonal", + "width":1 + }, + "margin":0, + "name":"multi-tileset", + "properties":[ + { + "name":"tilesetbool", + "type":"bool", + "value":true + }, + { + "name":"tilesetcolor", + "type":"color", + "value":"#ffff0000" + }, + { + "name":"tilesetfile", + "type":"file", + "value":"" + }, + { + "name":"tilesetfloat", + "type":"float", + "value":5.2 + }, + { + "name":"tilesetint", + "type":"int", + "value":9 + }, + { + "name":"tilesetobject", + "type":"object", + "value":0 + }, + { + "name":"tilesetstring", + "type":"string", + "value":"hello world!" + }], + "spacing":0, + "tilecount":2, + "tiledversion":"1.11.0", + "tileheight":96, + "tileoffset": + { + "x":1, + "y":5 + }, + "tiles":[ + { + "id":0, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256 + }, + { + "id":1, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256 + }], + "tilewidth":256, + "type":"tileset", + "version":"1.10" +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/multi-tileset.tsx b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/multi-tileset.tsx new file mode 100644 index 0000000..a28bfac --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/multi-tileset.tsx @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/tileset.png b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/tileset.png new file mode 100644 index 0000000..97c1fb3 Binary files /dev/null and b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/tileset.png differ diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs new file mode 100644 index 0000000..9aaa7d7 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs @@ -0,0 +1,92 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapExternalTilesetWangset(string fileExt) => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 24, + TileHeight = 24, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 2, + NextObjectID = 1, + Tilesets = [ + new Tileset + { + Version = "1.10", + TiledVersion = "1.11.0", + FirstGID = 1, + Name = "tileset", + TileWidth = 24, + TileHeight = 24, + TileCount = 48, + Columns = 10, + Source = $"wangset-tileset.{(fileExt == "tmx" ? "tsx" : "tsj")}", + Transformations = new Transformations + { + HFlip = true, + VFlip = true, + Rotate = false, + PreferUntransformed = false + }, + Grid = new Grid + { + Orientation = GridOrientation.Orthogonal, + Width = 32, + Height = 32 + }, + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96, + } + } + ], + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 2, 2, 12, 11, 0, + 1, 12, 1, 11, 0, + 2, 1, 0, 1, 0, + 12, 11, 12, 2, 0, + 0, 0, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.FlippedHorizontally, FlippingFlags.None, FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.None, + FlippingFlags.FlippedVertically, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedVertically | FlippingFlags.FlippedHorizontally, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedVertically | FlippingFlags.FlippedHorizontally, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedHorizontally, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ] + }; +} diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.tmj new file mode 100644 index 0000000..cea9ad6 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.tmj @@ -0,0 +1,36 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[2147483650, 2, 2147483660, 2147483659, 0, + 1073741825, 12, 1, 3221225483, 0, + 2, 1, 0, 3221225473, 0, + 12, 11, 12, 2147483650, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":24, + "tilesets":[ + { + "firstgid":1, + "source":"wangset-tileset.tsj" + }], + "tilewidth":24, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.tmx new file mode 100644 index 0000000..656fddb --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.tmx @@ -0,0 +1,13 @@ + + + + + +2147483650,2,2147483660,2147483659,0, +1073741825,12,1,3221225483,0, +2,1,0,3221225473,0, +12,11,12,2147483650,0, +0,0,0,0,0 + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/tileset.png b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/tileset.png new file mode 100644 index 0000000..97c1fb3 Binary files /dev/null and b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/tileset.png differ diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/wangset-tileset.tsj b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/wangset-tileset.tsj new file mode 100644 index 0000000..511641a --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/wangset-tileset.tsj @@ -0,0 +1,69 @@ +{ "columns":10, + "grid": + { + "height":32, + "orientation":"orthogonal", + "width":32 + }, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256, + "margin":0, + "name":"tileset", + "spacing":0, + "tilecount":48, + "tiledversion":"1.11.0", + "tileheight":24, + "tilewidth":24, + "transformations": + { + "hflip":true, + "preferuntransformed":false, + "rotate":false, + "vflip":true + }, + "type":"tileset", + "version":"1.10", + "wangsets":[ + { + "colors":[ + { + "color":"#ff0000", + "name":"Water", + "probability":1, + "tile":0 + }, + { + "color":"#00ff00", + "name":"Grass", + "probability":1, + "tile":-1 + }, + { + "color":"#0000ff", + "name":"Stone", + "probability":1, + "tile":29 + }], + "name":"test-terrain", + "tile":-1, + "type":"mixed", + "wangtiles":[ + { + "tileid":0, + "wangid":[1, 1, 0, 0, 0, 1, 1, 1] + }, + { + "tileid":1, + "wangid":[1, 1, 1, 1, 0, 0, 0, 1] + }, + { + "tileid":10, + "wangid":[0, 0, 0, 1, 1, 1, 1, 1] + }, + { + "tileid":11, + "wangid":[0, 1, 1, 1, 1, 1, 0, 0] + }] + }] +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/wangset-tileset.tsx b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/wangset-tileset.tsx new file mode 100644 index 0000000..d2b8666 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/wangset-tileset.tsx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs new file mode 100644 index 0000000..8c3283e --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs @@ -0,0 +1,68 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapWithCommonProps() => new Map + { + Class = "", + Orientation = MapOrientation.Isometric, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 16, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00ff00", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 2, + NextObjectID = 1, + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ], + Properties = new Dictionary + { + ["boolprop"] = new BoolProperty { Name = "boolprop", Value = true }, + ["colorprop"] = new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, + ["fileprop"] = new FileProperty { Name = "fileprop", Value = "file.txt" }, + ["floatprop"] = new FloatProperty { Name = "floatprop", Value = 4.2f }, + ["intprop"] = new IntProperty { Name = "intprop", Value = 8 }, + ["objectprop"] = new ObjectProperty { Name = "objectprop", Value = 5 }, + ["stringprop"] = new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } + } + }; +} diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.tmj new file mode 100644 index 0000000..c7182ef --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.tmj @@ -0,0 +1,70 @@ +{ "backgroundcolor":"#00ff00", + "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"isometric", + "properties":[ + { + "name":"boolprop", + "type":"bool", + "value":true + }, + { + "name":"colorprop", + "type":"color", + "value":"#ff55ffff" + }, + { + "name":"fileprop", + "type":"file", + "value":"file.txt" + }, + { + "name":"floatprop", + "type":"float", + "value":4.2 + }, + { + "name":"intprop", + "type":"int", + "value":8 + }, + + { + "name":"objectprop", + "type":"object", + "value":5 + }, + { + "name":"stringprop", + "type":"string", + "value":"This is a string, hello world!" + }], + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":16, + "tilesets":[], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.tmx new file mode 100644 index 0000000..b4b36cd --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.tmx @@ -0,0 +1,21 @@ + + + + + + + + + + + + + +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0 + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs new file mode 100644 index 0000000..1343f62 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -0,0 +1,122 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapWithCustomTypeProps() => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 2, + NextObjectID = 1, + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ], + Properties = new Dictionary + { + ["customclassprop"] = new ClassProperty + { + Name = "customclassprop", + PropertyType = "CustomClass", + Properties = new Dictionary + { + ["boolinclass"] = new BoolProperty { Name = "boolinclass", Value = true }, + ["colorinclass"] = new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + ["fileinclass"] = new FileProperty { Name = "fileinclass", Value = "" }, + ["floatinclass"] = new FloatProperty { Name = "floatinclass", Value = 13.37f }, + ["intinclass"] = new IntProperty { Name = "intinclass", Value = 0 }, + ["objectinclass"] = new ObjectProperty { Name = "objectinclass", Value = 0 }, + ["stringinclass"] = new StringProperty { Name = "stringinclass", Value = "This is a set string" } + } + } + } + }; + + // This comes from map-with-custom-type-props/propertytypes.json + public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ + new CustomClassDefinition + { + Name = "CustomClass", + UseAs = CustomClassUseAs.Property, + Members = [ + new BoolProperty + { + Name = "boolinclass", + Value = false + }, + new ColorProperty + { + Name = "colorinclass", + Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) + }, + new FileProperty + { + Name = "fileinclass", + Value = "" + }, + new FloatProperty + { + Name = "floatinclass", + Value = 0f + }, + new IntProperty + { + Name = "intinclass", + Value = 0 + }, + new ObjectProperty + { + Name = "objectinclass", + Value = 0 + }, + new StringProperty + { + Name = "stringinclass", + Value = "" + } + ] + } + ]; +} diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj new file mode 100644 index 0000000..a8c7f43 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj @@ -0,0 +1,44 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"customclassprop", + "propertytype":"CustomClass", + "type":"class", + "value": + { + "boolinclass":true, + "floatinclass":13.37, + "stringinclass":"This is a set string" + } + }], + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-properties.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx similarity index 50% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-properties.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx index 5a14d94..c364577 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-properties.tmx +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx @@ -1,13 +1,13 @@ - - - - - - - + + + + + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/propertytypes.json b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/propertytypes.json new file mode 100644 index 0000000..16c42fb --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/propertytypes.json @@ -0,0 +1,49 @@ +[ + { + "color": "#ffa0a0a4", + "drawFill": true, + "id": 8, + "members": [ + { + "name": "boolinclass", + "type": "bool", + "value": false + }, + { + "name": "colorinclass", + "type": "color", + "value": "" + }, + { + "name": "fileinclass", + "type": "file", + "value": "" + }, + { + "name": "floatinclass", + "type": "float", + "value": 0 + }, + { + "name": "intinclass", + "type": "int", + "value": 0 + }, + { + "name": "objectinclass", + "type": "object", + "value": 0 + }, + { + "name": "stringinclass", + "type": "string", + "value": "" + } + ], + "name": "CustomClass", + "type": "class", + "useAs": [ + "property" + ] + } +] diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-properties.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs similarity index 56% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-properties.cs rename to DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs index c87c183..fb3c95f 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-properties.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs @@ -1,20 +1,48 @@ +using System.Globalization; + namespace DotTiled.Tests; -public partial class TmxSerializerMapTests +public partial class TestData { - private static Map EmptyMapWithProperties() => new Map + public static Map MapWithEmbeddedTileset() => new Map { - Version = "1.10", - TiledVersion = "1.11.0", + Class = "", Orientation = MapOrientation.Orthogonal, - RenderOrder = RenderOrder.RightDown, Width = 5, Height = 5, TileWidth = 32, TileHeight = 32, Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", NextLayerID = 2, NextObjectID = 1, + Tilesets = [ + new Tileset + { + FirstGID = 1, + Name = "tileset", + TileWidth = 32, + TileHeight = 32, + TileCount = 24, + Columns = 8, + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96, + } + } + ], Layers = [ new TileLayer { @@ -25,12 +53,14 @@ public partial class TmxSerializerMapTests Data = new Data { Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, GlobalTileIDs = [ - 0,0,0,0,0, - 0,0,0,0,0, - 0,0,0,0,0, - 0,0,0,0,0, - 0,0,0,0,0 + 1, 1, 0, 0, 7, + 1, 1, 0, 0, 7, + 0, 0, 0, 0, 7, + 9, 10, 0, 0, 7, + 17, 18, 0, 0, 0 ], FlippingFlags = [ FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, @@ -41,16 +71,6 @@ public partial class TmxSerializerMapTests ] } } - ], - Properties = new Dictionary - { - ["MapBool"] = new BoolProperty { Name = "MapBool", Value = true }, - ["MapColor"] = new ColorProperty { Name = "MapColor", Value = new Color { R = 255, G = 0, B = 0, A = 255 } }, - ["MapFile"] = new FileProperty { Name = "MapFile", Value = "file.png" }, - ["MapFloat"] = new FloatProperty { Name = "MapFloat", Value = 5.2f }, - ["MapInt"] = new IntProperty { Name = "MapInt", Value = 42 }, - ["MapObject"] = new ObjectProperty { Name = "MapObject", Value = 5 }, - ["MapString"] = new StringProperty { Name = "MapString", Value = "string in map" } - } + ] }; } diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.tmj new file mode 100644 index 0000000..41d5e7b --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.tmj @@ -0,0 +1,45 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[1, 1, 0, 0, 7, + 1, 1, 0, 0, 7, + 0, 0, 0, 0, 7, + 9, 10, 0, 0, 7, + 17, 18, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256, + "margin":0, + "name":"tileset", + "spacing":0, + "tilecount":24, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/simple-tileset-embed.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.tmx similarity index 60% rename from DotTiled.Tests/TmxSerializer/TestData/Map/simple-tileset-embed.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.tmx index 3d91b9d..43ca51c 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/simple-tileset-embed.tmx +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.tmx @@ -1,15 +1,15 @@ - - + + -1,1,1,1,1, -1,1,1,1,1, -1,1,1,1,1, -2,2,2,2,2, -2,2,2,2,2 +1,1,0,0,7, +1,1,0,0,7, +0,0,0,0,7, +9,10,0,0,7, +17,18,0,0,0 diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/tileset.png b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/tileset.png new file mode 100644 index 0000000..97c1fb3 Binary files /dev/null and b/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/tileset.png differ diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs similarity index 51% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs rename to DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs index e01516b..10c4d67 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map.cs +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs @@ -1,20 +1,51 @@ +using System.Globalization; + namespace DotTiled.Tests; -public partial class TmxSerializerMapTests +public partial class TestData { - private static Map EmptyMapWithEncodingAndCompression(DataEncoding dataEncoding, DataCompression? compression) => new Map + public static Map MapWithExternalTileset(string fileExt) => new Map { - Version = "1.10", - TiledVersion = "1.11.0", + Class = "", Orientation = MapOrientation.Orthogonal, - RenderOrder = RenderOrder.RightDown, Width = 5, Height = 5, TileWidth = 32, TileHeight = 32, Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", NextLayerID = 2, NextObjectID = 1, + Tilesets = [ + new Tileset + { + Version = "1.10", + TiledVersion = "1.11.0", + FirstGID = 1, + Name = "tileset", + TileWidth = 32, + TileHeight = 32, + TileCount = 24, + Columns = 8, + Source = $"tileset.{(fileExt == "tmx" ? "tsx" : "tsj")}", + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96, + } + } + ], Layers = [ new TileLayer { @@ -24,14 +55,15 @@ public partial class TmxSerializerMapTests Height = 5, Data = new Data { - Encoding = dataEncoding, - Compression = compression, + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, GlobalTileIDs = [ - 0,0,0,0,0, - 0,0,0,0,0, - 0,0,0,0,0, - 0,0,0,0,0, - 0,0,0,0,0 + 1, 1, 0, 0, 7, + 1, 1, 0, 0, 7, + 0, 0, 1, 0, 7, + 0, 0, 0, 1, 7, + 21, 21, 21, 21, 1 ], FlippingFlags = [ FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.tmj new file mode 100644 index 0000000..89bef93 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.tmj @@ -0,0 +1,36 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[1, 1, 0, 0, 7, + 1, 1, 0, 0, 7, + 0, 0, 1, 0, 7, + 0, 0, 0, 1, 7, + 21, 21, 21, 21, 1], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.tsj" + }], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64-gzip.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.tmx similarity index 67% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64-gzip.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.tmx index d3e0f29..06114fb 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64-gzip.tmx +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.tmx @@ -1,8 +1,13 @@ + - - H4sIAAAAAAAACmNgoD0AAMrGiJlkAAAA - + +1,1,0,0,7, +1,1,0,0,7, +0,0,1,0,7, +0,0,0,1,7, +21,21,21,21,1 + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.png b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.png new file mode 100644 index 0000000..97c1fb3 Binary files /dev/null and b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.png differ diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.tsj b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.tsj new file mode 100644 index 0000000..820e88f --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.tsj @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256, + "margin":0, + "name":"tileset", + "spacing":0, + "tilecount":24, + "tiledversion":"1.11.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.10" +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.tsx b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.tsx new file mode 100644 index 0000000..d730182 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs new file mode 100644 index 0000000..4e181c4 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs @@ -0,0 +1,79 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapWithFlippingFlags(string fileExt) => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture), + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 2, + NextObjectID = 1, + Tilesets = [ + new Tileset + { + Version = "1.10", + TiledVersion = "1.11.0", + FirstGID = 1, + Name = "tileset", + TileWidth = 32, + TileHeight = 32, + TileCount = 24, + Columns = 8, + Source = $"tileset.{(fileExt == "tmx" ? "tsx" : "tsj")}", + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96, + } + } + ], + Layers = [ + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 1, 1, 0, 0, 7, + 1, 1, 0, 0, 7, + 0, 0, 1, 0, 7, + 0, 0, 0, 1, 7, + 21, 21, 21, 21, 1 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.FlippedDiagonally | FlippingFlags.FlippedHorizontally, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedVertically, + FlippingFlags.FlippedDiagonally | FlippingFlags.FlippedVertically, FlippingFlags.FlippedVertically | FlippingFlags.FlippedHorizontally, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedVertically, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedVertically, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.FlippedVertically, + FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.None + ] + } + } + ] + }; +} diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.tmj new file mode 100644 index 0000000..3b74128 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.tmj @@ -0,0 +1,36 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "data":[1, 2684354561, 0, 0, 1073741831, + 1610612737, 3221225473, 0, 0, 1073741831, + 0, 0, 1, 0, 1073741831, + 0, 0, 0, 1, 1073741831, + 2147483669, 2147483669, 2147483669, 2147483669, 1], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.tsj" + }], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.tmx similarity index 55% rename from DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64.tmx rename to DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.tmx index 0e98f67..a72cd1a 100644 --- a/DotTiled.Tests/TmxSerializer/TestData/Map/empty-map-base64.tmx +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.tmx @@ -1,8 +1,13 @@ + - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== - + +1,2684354561,0,0,1073741831, +1610612737,3221225473,0,0,1073741831, +0,0,1,0,1073741831, +0,0,0,1,1073741831, +2147483669,2147483669,2147483669,2147483669,1 + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.png b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.png new file mode 100644 index 0000000..97c1fb3 Binary files /dev/null and b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.png differ diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.tsj b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.tsj new file mode 100644 index 0000000..820e88f --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.tsj @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256, + "margin":0, + "name":"tileset", + "spacing":0, + "tilecount":24, + "tiledversion":"1.11.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.10" +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.tsx b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.tsx new file mode 100644 index 0000000..d730182 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs new file mode 100644 index 0000000..2ef98d0 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs @@ -0,0 +1,219 @@ +using System.Numerics; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapWithManyLayers(string fileExt) => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + ParallaxOriginX = 0, + ParallaxOriginY = 0, + RenderOrder = RenderOrder.RightDown, + CompressionLevel = -1, + BackgroundColor = new Color { R = 0, G = 0, B = 0, A = 0 }, + Version = "1.10", + TiledVersion = "1.11.0", + NextLayerID = 8, + NextObjectID = 7, + Tilesets = [ + new Tileset + { + Version = "1.10", + TiledVersion = "1.11.0", + FirstGID = 1, + Name = "tileset", + TileWidth = 32, + TileHeight = 32, + TileCount = 24, + Columns = 8, + Source = $"tileset.{(fileExt == "tmx" ? "tsx" : "tsj")}", + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = 256, + Height = 96, + } + } + ], + Layers = [ + new Group + { + ID = 2, + Name = "Root", + Layers = [ + new ObjectLayer + { + ID = 3, + Name = "Objects", + Objects = [ + new RectangleObject + { + ID = 1, + Name = "Object 1", + X = 25.6667f, + Y = 28.6667f, + Width = 31.3333f, + Height = 31.3333f + }, + new PointObject + { + ID = 3, + Name = "P1", + X = 117.667f, + Y = 48.6667f + }, + new EllipseObject + { + ID = 4, + Name = "Circle1", + X = 77f, + Y = 72.3333f, + Width = 34.6667f, + Height = 34.6667f + }, + new PolygonObject + { + ID = 5, + Name = "Poly", + X = 20.6667f, + Y = 114.667f, + Points = [ + new Vector2(0, 0), + new Vector2(104,20), + new Vector2(35.6667f, 32.3333f) + ], + Template = fileExt == "tmx" ? "poly.tx" : "poly.tj", + Properties = new Dictionary + { + ["templateprop"] = new StringProperty { Name = "templateprop", Value = "helo there" } + } + }, + new TileObject + { + ID = 6, + Name = "TileObj", + GID = 7, + X = -35, + Y = 110.333f, + Width = 64, + Height = 146 + } + ] + }, + new Group + { + ID = 5, + Name = "Sub", + Layers = [ + new TileLayer + { + ID = 7, + Name = "Tile 3", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + }, + new TileLayer + { + ID = 6, + Name = "Tile 2", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 0, 15, 15, 0, 0, + 0, 15, 15, 0, 0, + 0, 15, 15, 15, 0, + 15, 15, 15, 0, 0, + 0, 0, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ] + }, + new ImageLayer + { + ID = 4, + Name = "ImageLayer", + Image = new Image + { + Format = ImageFormat.Png, + Source = "tileset.png", + Width = fileExt == "tmx" ? 256u : 0, // Currently, json format does not + Height = fileExt == "tmx" ? 96u : 0 // include image dimensions in image layer https://github.com/mapeditor/tiled/issues/4028 + }, + RepeatX = true + }, + new TileLayer + { + ID = 1, + Name = "Tile Layer 1", + Width = 5, + Height = 5, + Data = new Data + { + Encoding = DataEncoding.Csv, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + FlippingFlags = [ + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, + FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None + ] + } + } + ] + } + ] + }; +} diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmj b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmj new file mode 100644 index 0000000..9e9f669 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmj @@ -0,0 +1,163 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "id":2, + "layers":[ + { + "draworder":"topdown", + "id":3, + "name":"Objects", + "objects":[ + { + "height":31.3333, + "id":1, + "name":"Object 1", + "rotation":0, + "type":"", + "visible":true, + "width":31.3333, + "x":25.6667, + "y":28.6667 + }, + { + "height":0, + "id":3, + "name":"P1", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":117.667, + "y":48.6667 + }, + { + "ellipse":true, + "height":34.6667, + "id":4, + "name":"Circle1", + "rotation":0, + "type":"", + "visible":true, + "width":34.6667, + "x":77, + "y":72.3333 + }, + { + "id":5, + "template":"poly.tj", + "x":20.6667, + "y":114.667 + }, + { + "gid":7, + "height":146, + "id":6, + "name":"TileObj", + "rotation":0, + "type":"", + "visible":true, + "width":64, + "x":-35, + "y":110.333 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":5, + "layers":[ + { + "data":[0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":7, + "name":"Tile 3", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }, + { + "data":[0, 15, 15, 0, 0, + 0, 15, 15, 0, 0, + 0, 15, 15, 15, 0, + 15, 15, 15, 0, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":6, + "name":"Tile 2", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "name":"Sub", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }, + { + "id":4, + "image":"tileset.png", + "name":"ImageLayer", + "opacity":1, + "repeatx":true, + "type":"imagelayer", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0], + "height":5, + "id":1, + "name":"Tile Layer 1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "name":"Root", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":8, + "nextobjectid":7, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.tsj" + }], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmx b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmx new file mode 100644 index 0000000..5888069 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmx @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0 + + + + +0,15,15,0,0, +0,15,15,0,0, +0,15,15,15,0, +15,15,15,0,0, +0,0,0,0,0 + + + + + + + + +1,1,1,1,1, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0 + + + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/poly.tj b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/poly.tj new file mode 100644 index 0000000..f23c7d9 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/poly.tj @@ -0,0 +1,31 @@ +{ "object": + { + "height":0, + "id":5, + "name":"Poly", + "polygon":[ + { + "x":0, + "y":0 + }, + { + "x":104, + "y":20 + }, + { + "x":35.6667, + "y":32.3333 + }], + "properties":[ + { + "name":"templateprop", + "type":"string", + "value":"helo there" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0 + }, + "type":"template" +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/poly.tx b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/poly.tx new file mode 100644 index 0000000..a0a2457 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/poly.tx @@ -0,0 +1,9 @@ + + diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.png b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.png new file mode 100644 index 0000000..97c1fb3 Binary files /dev/null and b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.png differ diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.tsj b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.tsj new file mode 100644 index 0000000..820e88f --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.tsj @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"tileset.png", + "imageheight":96, + "imagewidth":256, + "margin":0, + "name":"tileset", + "spacing":0, + "tilecount":24, + "tiledversion":"1.11.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.10" +} \ No newline at end of file diff --git a/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.tsx b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.tsx new file mode 100644 index 0000000..d730182 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs new file mode 100644 index 0000000..670fdf6 --- /dev/null +++ b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -0,0 +1,38 @@ +namespace DotTiled.Tests; + +public partial class TmjMapReaderTests +{ + public static IEnumerable Maps => TestData.MapTests; + [Theory] + [MemberData(nameof(Maps))] + public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( + string testDataFile, + Func expectedMap, + IReadOnlyCollection customTypeDefinitions) + { + // Arrange + testDataFile += ".tmj"; + var fileDir = Path.GetDirectoryName(testDataFile); + var json = TestData.GetRawStringFor(testDataFile); + Template ResolveTemplate(string source) + { + var templateJson = TestData.GetRawStringFor($"{fileDir}/{source}"); + using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions); + return templateReader.ReadTemplate(); + } + Tileset ResolveTileset(string source) + { + var tilesetJson = TestData.GetRawStringFor($"{fileDir}/{source}"); + using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions); + return tilesetReader.ReadTileset(); + } + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions); + + // Act + var map = mapReader.ReadMap(); + + // Assert + Assert.NotNull(map); + DotTiledAssert.AssertMap(expectedMap("tmj"), map); + } +} diff --git a/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs new file mode 100644 index 0000000..a99ee9a --- /dev/null +++ b/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -0,0 +1,40 @@ +using System.Xml; + +namespace DotTiled.Tests; + +public partial class TmxMapReaderTests +{ + public static IEnumerable Maps => TestData.MapTests; + [Theory] + [MemberData(nameof(Maps))] + public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( + string testDataFile, + Func expectedMap, + IReadOnlyCollection customTypeDefinitions) + { + // Arrange + testDataFile += ".tmx"; + var fileDir = Path.GetDirectoryName(testDataFile); + using var reader = TestData.GetXmlReaderFor(testDataFile); + Template ResolveTemplate(string source) + { + using var xmlTemplateReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + return templateReader.ReadTemplate(); + } + Tileset ResolveTileset(string source) + { + using var xmlTilesetReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); + return tilesetReader.ReadTileset(); + } + using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + + // Act + var map = mapReader.ReadMap(); + + // Assert + Assert.NotNull(map); + DotTiledAssert.AssertMap(expectedMap("tmx"), map); + } +} diff --git a/DotTiled.Tests/TmxSerializer/TestData/TestData.cs b/DotTiled.Tests/TmxSerializer/TestData/TestData.cs deleted file mode 100644 index e2e36c9..0000000 --- a/DotTiled.Tests/TmxSerializer/TestData/TestData.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Xml; - -namespace DotTiled.Tests; - -public static class TmxSerializerTestData -{ - public static XmlReader GetReaderFor(string testDataFile) - { - var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}"; - using var stream = typeof(TmxSerializerTestData).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); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.DataTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.DataTests.cs deleted file mode 100644 index e08d402..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.DataTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace DotTiled.Tests; - -public partial class TmxSerializerDataTests -{ - public static void AssertData(Data? actual, Data? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - // Attributes - Assert.NotNull(actual); - Assert.Equal(expected.Encoding, actual.Encoding); - Assert.Equal(expected.Compression, actual.Compression); - - // Data - Assert.Equal(expected.GlobalTileIDs, actual.GlobalTileIDs); - Assert.Equal(expected.FlippingFlags, actual.FlippingFlags); - - if (expected.Chunks is not null) - { - 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]); - } - } - - private static void AssertChunk(Chunk actual, Chunk expected) - { - // Attributes - Assert.Equal(expected.X, actual.X); - Assert.Equal(expected.Y, actual.Y); - Assert.Equal(expected.Width, actual.Width); - Assert.Equal(expected.Height, actual.Height); - - // Data - Assert.Equal(expected.GlobalTileIDs, actual.GlobalTileIDs); - Assert.Equal(expected.FlippingFlags, actual.FlippingFlags); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.ImageTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.ImageTests.cs deleted file mode 100644 index 7d00713..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.ImageTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace DotTiled.Tests; - -public partial class TmxSerializerImageTests -{ - public static void AssertImage(Image? actual, Image? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - // Attributes - Assert.NotNull(actual); - Assert.Equal(expected.Format, actual.Format); - Assert.Equal(expected.Source, actual.Source); - Assert.Equal(expected.TransparentColor, actual.TransparentColor); - Assert.Equal(expected.Width, actual.Width); - Assert.Equal(expected.Height, actual.Height); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs deleted file mode 100644 index 5e36b95..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.LayerTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace DotTiled.Tests; - -public partial class TmxSerializerLayerTests -{ - public static void AssertLayer(BaseLayer? actual, BaseLayer? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - // Attributes - Assert.NotNull(actual); - Assert.Equal(expected.ID, actual.ID); - Assert.Equal(expected.Name, actual.Name); - Assert.Equal(expected.Class, actual.Class); - Assert.Equal(expected.X, actual.X); - Assert.Equal(expected.Y, actual.Y); - Assert.Equal(expected.Opacity, actual.Opacity); - Assert.Equal(expected.Visible, actual.Visible); - Assert.Equal(expected.TintColor, actual.TintColor); - Assert.Equal(expected.OffsetX, actual.OffsetX); - Assert.Equal(expected.OffsetY, actual.OffsetY); - Assert.Equal(expected.ParallaxX, actual.ParallaxX); - Assert.Equal(expected.ParallaxY, actual.ParallaxY); - - TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties); - AssertLayer((dynamic)actual, (dynamic)expected); - } - - private static void AssertLayer(TileLayer actual, TileLayer expected) - { - // Attributes - Assert.Equal(expected.Width, actual.Width); - Assert.Equal(expected.Height, actual.Height); - - Assert.NotNull(actual.Data); - TmxSerializerDataTests.AssertData(actual.Data, expected.Data); - } - - private static void AssertLayer(ObjectLayer actual, ObjectLayer expected) - { - // Attributes - Assert.Equal(expected.DrawOrder, actual.DrawOrder); - - 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]); - } - - private static void AssertLayer(ImageLayer actual, ImageLayer expected) - { - // Attributes - Assert.Equal(expected.RepeatX, actual.RepeatX); - Assert.Equal(expected.RepeatY, actual.RepeatY); - - Assert.NotNull(actual.Image); - TmxSerializerImageTests.AssertImage(actual.Image, expected.Image); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs deleted file mode 100644 index ec3f0e8..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.MapTests.cs +++ /dev/null @@ -1,67 +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 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 DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) - { - // Arrange - using var reader = TmxSerializerTestData.GetReaderFor(testDataFile); - Func externalTilesetResolver = (string s) => throw new NotSupportedException("External tilesets are not supported in this test"); - var tmxSerializer = new TmxSerializer(externalTilesetResolver); - - // Act - var map = tmxSerializer.DeserializeMap(reader); - - // Assert - Assert.NotNull(map); - AssertMap(map, expectedMap); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.ObjectTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.ObjectTests.cs deleted file mode 100644 index c593074..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.ObjectTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace DotTiled.Tests; - -public partial class TmxSerializerObjectTests -{ - public static void AssertObject(Object actual, Object expected) - { - // Attributes - Assert.Equal(expected.ID, actual.ID); - Assert.Equal(expected.Name, actual.Name); - Assert.Equal(expected.Type, actual.Type); - Assert.Equal(expected.X, actual.X); - Assert.Equal(expected.Y, actual.Y); - Assert.Equal(expected.Width, actual.Width); - Assert.Equal(expected.Height, actual.Height); - Assert.Equal(expected.Rotation, actual.Rotation); - Assert.Equal(expected.GID, actual.GID); - Assert.Equal(expected.Visible, actual.Visible); - Assert.Equal(expected.Template, actual.Template); - - TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties); - AssertObject((dynamic)actual, (dynamic)expected); - } - - private static void AssertObject(RectangleObject actual, RectangleObject expected) - { - Assert.True(true); // A rectangle object is the same as the abstract Object - } - - private static void AssertObject(EllipseObject actual, EllipseObject expected) - { - Assert.True(true); // An ellipse object is the same as the abstract Object - } - - private static void AssertObject(PointObject actual, PointObject expected) - { - Assert.True(true); // A point object is the same as the abstract Object - } - - private static void AssertObject(PolygonObject actual, PolygonObject expected) - { - Assert.Equal(expected.Points, actual.Points); - } - - private static void AssertObject(PolylineObject actual, PolylineObject expected) - { - Assert.Equal(expected.Points, actual.Points); - } - - private static void AssertObject(TextObject actual, TextObject expected) - { - // Attributes - Assert.Equal(expected.FontFamily, actual.FontFamily); - Assert.Equal(expected.PixelSize, actual.PixelSize); - Assert.Equal(expected.Wrap, actual.Wrap); - Assert.Equal(expected.Color, actual.Color); - Assert.Equal(expected.Bold, actual.Bold); - Assert.Equal(expected.Italic, actual.Italic); - Assert.Equal(expected.Underline, actual.Underline); - Assert.Equal(expected.Strikeout, actual.Strikeout); - Assert.Equal(expected.Kerning, actual.Kerning); - Assert.Equal(expected.HorizontalAlignment, actual.HorizontalAlignment); - Assert.Equal(expected.VerticalAlignment, actual.VerticalAlignment); - - Assert.Equal(expected.Text, actual.Text); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.PropertiesTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.PropertiesTests.cs deleted file mode 100644 index e22e5b9..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.PropertiesTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace DotTiled.Tests; - -public partial class TmxSerializerPropertiesTests -{ - public static void AssertProperties(Dictionary? actual, Dictionary? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - Assert.NotNull(actual); - Assert.Equal(expected.Count, actual.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 actual, IProperty expected) - { - Assert.Equal(expected.Type, actual.Type); - Assert.Equal(expected.Name, actual.Name); - AssertProperties((dynamic)actual, (dynamic)expected); - } - - private static void AssertProperty(StringProperty actual, StringProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(IntProperty actual, IntProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(FloatProperty actual, FloatProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(BoolProperty actual, BoolProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(ColorProperty actual, ColorProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(FileProperty actual, FileProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(ObjectProperty actual, ObjectProperty expected) - { - Assert.Equal(expected.Value, actual.Value); - } - - private static void AssertProperty(ClassProperty actual, ClassProperty expected) - { - Assert.Equal(expected.PropertyType, actual.PropertyType); - AssertProperties(actual.Properties, expected.Properties); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializer.TilesetTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializer.TilesetTests.cs deleted file mode 100644 index 04bb54b..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializer.TilesetTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -namespace DotTiled.Tests; - -public partial class TmxSerializerTilesetTests -{ - public static void AssertTileset(Tileset actual, Tileset expected) - { - // Attributes - Assert.Equal(expected.Version, actual.Version); - Assert.Equal(expected.TiledVersion, actual.TiledVersion); - Assert.Equal(expected.FirstGID, actual.FirstGID); - Assert.Equal(expected.Source, actual.Source); - Assert.Equal(expected.Name, actual.Name); - Assert.Equal(expected.Class, actual.Class); - Assert.Equal(expected.TileWidth, actual.TileWidth); - Assert.Equal(expected.TileHeight, actual.TileHeight); - Assert.Equal(expected.Spacing, actual.Spacing); - Assert.Equal(expected.Margin, actual.Margin); - Assert.Equal(expected.TileCount, actual.TileCount); - Assert.Equal(expected.Columns, actual.Columns); - Assert.Equal(expected.ObjectAlignment, actual.ObjectAlignment); - Assert.Equal(expected.RenderSize, actual.RenderSize); - 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); - // 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]); - } - AssertTransformations(actual.Transformations, expected.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]); - } - - private static void AssertTileOffset(TileOffset? actual, TileOffset? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - // Attributes - Assert.NotNull(actual); - Assert.Equal(expected.X, actual.X); - Assert.Equal(expected.Y, actual.Y); - } - - private static void AssertGrid(Grid? actual, Grid? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - // Attributes - Assert.NotNull(actual); - Assert.Equal(expected.Orientation, actual.Orientation); - Assert.Equal(expected.Width, actual.Width); - Assert.Equal(expected.Height, actual.Height); - } - - private static void AssertWangset(Wangset actual, Wangset expected) - { - // Attributes - Assert.Equal(expected.Name, actual.Name); - Assert.Equal(expected.Class, actual.Class); - Assert.Equal(expected.Tile, actual.Tile); - - // At most one of - TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.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]); - } - for (var i = 0; i < expected.WangTiles.Count; i++) - AssertWangTile(actual.WangTiles[i], expected.WangTiles[i]); - } - - private static void AssertWangColor(WangColor actual, WangColor expected) - { - // Attributes - Assert.Equal(expected.Name, actual.Name); - Assert.Equal(expected.Class, actual.Class); - Assert.Equal(expected.Color, actual.Color); - Assert.Equal(expected.Tile, actual.Tile); - Assert.Equal(expected.Probability, actual.Probability); - - TmxSerializerPropertiesTests.AssertProperties(actual.Properties, expected.Properties); - } - - private static void AssertWangTile(WangTile actual, WangTile expected) - { - // Attributes - Assert.Equal(expected.TileID, actual.TileID); - Assert.Equal(expected.WangID, actual.WangID); - } - - private static void AssertTransformations(Transformations? actual, Transformations? expected) - { - if (expected is null) - { - Assert.Null(actual); - return; - } - - // Attributes - Assert.NotNull(actual); - Assert.Equal(expected.HFlip, actual.HFlip); - Assert.Equal(expected.VFlip, actual.VFlip); - Assert.Equal(expected.Rotate, actual.Rotate); - Assert.Equal(expected.PreferUntransformed, actual.PreferUntransformed); - } - - private static void AssertTile(Tile actual, Tile expected) - { - // Attributes - Assert.Equal(expected.ID, actual.ID); - Assert.Equal(expected.Type, actual.Type); - Assert.Equal(expected.Probability, actual.Probability); - Assert.Equal(expected.X, actual.X); - Assert.Equal(expected.Y, actual.Y); - Assert.Equal(expected.Width, actual.Width); - 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); - 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]); - } - } - - private static void AssertFrame(Frame actual, Frame expected) - { - // Attributes - Assert.Equal(expected.TileID, actual.TileID); - Assert.Equal(expected.Duration, actual.Duration); - } -} diff --git a/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs b/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs deleted file mode 100644 index 3d1730e..0000000 --- a/DotTiled.Tests/TmxSerializer/TmxSerializerTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace DotTiled.Tests; - -public class TmxSerializerTests -{ - [Fact] - public void TmxSerializerConstructor_ExternalTilesetResolverIsNull_ThrowsArgumentNullException() - { - // Arrange - Func externalTilesetResolver = null!; - - // Act - Action act = () => _ = new TmxSerializer(externalTilesetResolver); - - // Assert - Assert.Throws(act); - } - - [Fact] - public void TmxSerializerConstructor_ExternalTilesetResolverIsNotNull_DoesNotThrow() - { - // Arrange - Func externalTilesetResolver = _ => new Tileset(); - - // Act - var tmxSerializer = new TmxSerializer(externalTilesetResolver); - - // Assert - Assert.NotNull(tmxSerializer); - } -} diff --git a/DotTiled.sln b/DotTiled.sln index 86b44df..421e996 100644 --- a/DotTiled.sln +++ b/DotTiled.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled", "DotTiled\DotTil EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Tests", "DotTiled.Tests\DotTiled.Tests.csproj", "{C1311A5A-5206-467C-B323-B131CA11FDB8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Benchmark", "DotTiled.Benchmark\DotTiled.Benchmark.csproj", "{510F3077-8EA4-47D1-8D01-E2D538F1B899}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {C1311A5A-5206-467C-B323-B131CA11FDB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C1311A5A-5206-467C-B323-B131CA11FDB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {C1311A5A-5206-467C-B323-B131CA11FDB8}.Release|Any CPU.Build.0 = Release|Any CPU + {510F3077-8EA4-47D1-8D01-E2D538F1B899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {510F3077-8EA4-47D1-8D01-E2D538F1B899}.Debug|Any CPU.Build.0 = Debug|Any CPU + {510F3077-8EA4-47D1-8D01-E2D538F1B899}.Release|Any CPU.ActiveCfg = Release|Any CPU + {510F3077-8EA4-47D1-8D01-E2D538F1B899}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/DotTiled/Model/Color.cs b/DotTiled/Model/Color.cs index 29bafe9..ae74d0d 100644 --- a/DotTiled/Model/Color.cs +++ b/DotTiled/Model/Color.cs @@ -66,4 +66,6 @@ public class Color : IParsable, IEquatable public override bool Equals(object? obj) => obj is Color other && Equals(other); public override int GetHashCode() => HashCode.Combine(R, G, B, A); + + public override string ToString() => $"#{A:x2}{R:x2}{G:x2}{B:x2}"; } diff --git a/DotTiled/Model/IProperty.cs b/DotTiled/Model/IProperty.cs deleted file mode 100644 index 9558ee2..0000000 --- a/DotTiled/Model/IProperty.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; - -namespace DotTiled; - -public enum PropertyType -{ - String, - Int, - Float, - Bool, - Color, - File, - Object, - Class -} - -public interface IProperty -{ - public string Name { get; set; } - public PropertyType Type { get; } -} - -public class StringProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.String; - public required string Value { get; set; } -} - -public class IntProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Int; - public required int Value { get; set; } -} - -public class FloatProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Float; - public required float Value { get; set; } -} - -public class BoolProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Bool; - public required bool Value { get; set; } -} - -public class ColorProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Color; - public required Color Value { get; set; } -} - -public class FileProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.File; - public required string Value { get; set; } -} - -public class ObjectProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Object; - public required uint Value { get; set; } -} - -public class ClassProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => DotTiled.PropertyType.Class; - public required string PropertyType { get; set; } - public required Dictionary Properties { get; set; } -} diff --git a/DotTiled/Model/Layers/BaseLayer.cs b/DotTiled/Model/Layers/BaseLayer.cs index 950a6f1..0f10a95 100644 --- a/DotTiled/Model/Layers/BaseLayer.cs +++ b/DotTiled/Model/Layers/BaseLayer.cs @@ -8,8 +8,6 @@ public abstract class BaseLayer public required uint ID { get; set; } public string Name { get; set; } = ""; public string Class { get; set; } = ""; - public uint X { get; set; } = 0; - public uint Y { get; set; } = 0; public float Opacity { get; set; } = 1.0f; public bool Visible { get; set; } = true; public Color? TintColor { get; set; } diff --git a/DotTiled/Model/Layers/Group.cs b/DotTiled/Model/Layers/Group.cs new file mode 100644 index 0000000..f58a83c --- /dev/null +++ b/DotTiled/Model/Layers/Group.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public class Group : BaseLayer +{ + // Uses same attributes as BaseLayer + + // Any number of + public List Layers { get; set; } = []; +} diff --git a/DotTiled/Model/Layers/ImageLayer.cs b/DotTiled/Model/Layers/ImageLayer.cs index 7d0b141..a140b0d 100644 --- a/DotTiled/Model/Layers/ImageLayer.cs +++ b/DotTiled/Model/Layers/ImageLayer.cs @@ -3,8 +3,10 @@ namespace DotTiled; public class ImageLayer : BaseLayer { // Attributes - public required bool RepeatX { get; set; } - public required bool RepeatY { get; set; } + public uint X { get; set; } = 0; + public uint Y { get; set; } = 0; + public bool RepeatX { get; set; } = false; + public bool RepeatY { get; set; } = false; // At most one of public Image? Image { get; set; } diff --git a/DotTiled/Model/Layers/ObjectLayer.cs b/DotTiled/Model/Layers/ObjectLayer.cs index acf96a5..ca3be60 100644 --- a/DotTiled/Model/Layers/ObjectLayer.cs +++ b/DotTiled/Model/Layers/ObjectLayer.cs @@ -11,10 +11,12 @@ public enum DrawOrder public class ObjectLayer : BaseLayer { // Attributes + public uint X { get; set; } = 0; + public uint Y { get; set; } = 0; public uint? Width { get; set; } public uint? Height { get; set; } - public required Color? Color { get; set; } - public required DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; + public Color? Color { get; set; } + public DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; // Elements public required List Objects { get; set; } diff --git a/DotTiled/Model/Layers/Objects/Object.cs b/DotTiled/Model/Layers/Objects/Object.cs index 2684188..765de69 100644 --- a/DotTiled/Model/Layers/Objects/Object.cs +++ b/DotTiled/Model/Layers/Objects/Object.cs @@ -5,7 +5,7 @@ namespace DotTiled; public abstract class Object { // Attributes - public required uint ID { get; set; } + public uint? ID { get; set; } public string Name { get; set; } = ""; public string Type { get; set; } = ""; public float X { get; set; } = 0f; @@ -13,7 +13,6 @@ public abstract class Object public float Width { get; set; } = 0f; public float Height { get; set; } = 0f; public float Rotation { get; set; } = 0f; - public uint? GID { get; set; } public bool Visible { get; set; } = true; public string? Template { get; set; } diff --git a/DotTiled/Model/Layers/Objects/TileObject.cs b/DotTiled/Model/Layers/Objects/TileObject.cs new file mode 100644 index 0000000..c066780 --- /dev/null +++ b/DotTiled/Model/Layers/Objects/TileObject.cs @@ -0,0 +1,6 @@ +namespace DotTiled; + +public class TileObject : Object +{ + public uint GID { get; set; } +} diff --git a/DotTiled/Model/Layers/TileLayer.cs b/DotTiled/Model/Layers/TileLayer.cs index de4b25d..7692266 100644 --- a/DotTiled/Model/Layers/TileLayer.cs +++ b/DotTiled/Model/Layers/TileLayer.cs @@ -3,6 +3,8 @@ namespace DotTiled; public class TileLayer : BaseLayer { // Attributes + public uint X { get; set; } = 0; + public uint Y { get; set; } = 0; public required uint Width { get; set; } public required uint Height { get; set; } diff --git a/DotTiled/Model/Map.cs b/DotTiled/Model/Map.cs index 99868b1..fdcdbd1 100644 --- a/DotTiled/Model/Map.cs +++ b/DotTiled/Model/Map.cs @@ -60,5 +60,4 @@ public class Map // Any number of public List Tilesets { get; set; } = []; public List Layers { get; set; } = []; - // public List Groups { get; set; } = []; } diff --git a/DotTiled/Model/Properties/BoolProperty.cs b/DotTiled/Model/Properties/BoolProperty.cs new file mode 100644 index 0000000..949858f --- /dev/null +++ b/DotTiled/Model/Properties/BoolProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class BoolProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Bool; + public required bool Value { get; set; } + + public IProperty Clone() => new BoolProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Properties/ClassProperty.cs b/DotTiled/Model/Properties/ClassProperty.cs new file mode 100644 index 0000000..0b1391d --- /dev/null +++ b/DotTiled/Model/Properties/ClassProperty.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotTiled; + +public class ClassProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => DotTiled.PropertyType.Class; + public required string PropertyType { get; set; } + public required Dictionary Properties { get; set; } + + public IProperty Clone() => new ClassProperty + { + Name = Name, + PropertyType = PropertyType, + Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) + }; +} diff --git a/DotTiled/Model/Properties/ColorProperty.cs b/DotTiled/Model/Properties/ColorProperty.cs new file mode 100644 index 0000000..07ca25e --- /dev/null +++ b/DotTiled/Model/Properties/ColorProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class ColorProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Color; + public required Color Value { get; set; } + + public IProperty Clone() => new ColorProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs b/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs new file mode 100644 index 0000000..ec92b3f --- /dev/null +++ b/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +[Flags] +public enum CustomClassUseAs +{ + Property, + Map, + Layer, + Object, + Tile, + Tileset, + WangColor, + Wangset, + Project, + All = Property | Map | Layer | Object | Tile | Tileset | WangColor | Wangset | Project +} + +public class CustomClassDefinition : CustomTypeDefinition +{ + public Color? Color { get; set; } + public bool DrawFill { get; set; } + public CustomClassUseAs UseAs { get; set; } + public List Members { get; set; } = []; +} diff --git a/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs b/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs new file mode 100644 index 0000000..d570442 --- /dev/null +++ b/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace DotTiled; + +public enum CustomEnumStorageType +{ + Int, + String +} + +public class CustomEnumDefinition : CustomTypeDefinition +{ + public CustomEnumStorageType StorageType { get; set; } + public List Values { get; set; } = []; + public bool ValueAsFlags { get; set; } +} diff --git a/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs b/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs new file mode 100644 index 0000000..1f50462 --- /dev/null +++ b/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs @@ -0,0 +1,7 @@ +namespace DotTiled; + +public abstract class CustomTypeDefinition +{ + public uint ID { get; set; } + public string Name { get; set; } = ""; +} diff --git a/DotTiled/Model/Properties/FileProperty.cs b/DotTiled/Model/Properties/FileProperty.cs new file mode 100644 index 0000000..edc939c --- /dev/null +++ b/DotTiled/Model/Properties/FileProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class FileProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.File; + public required string Value { get; set; } + + public IProperty Clone() => new FileProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Properties/FloatProperty.cs b/DotTiled/Model/Properties/FloatProperty.cs new file mode 100644 index 0000000..469cc45 --- /dev/null +++ b/DotTiled/Model/Properties/FloatProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class FloatProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Float; + public required float Value { get; set; } + + public IProperty Clone() => new FloatProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Properties/IProperty.cs b/DotTiled/Model/Properties/IProperty.cs new file mode 100644 index 0000000..f4294cd --- /dev/null +++ b/DotTiled/Model/Properties/IProperty.cs @@ -0,0 +1,9 @@ +namespace DotTiled; + +public interface IProperty +{ + public string Name { get; set; } + public PropertyType Type { get; } + + IProperty Clone(); +} diff --git a/DotTiled/Model/Properties/IntProperty.cs b/DotTiled/Model/Properties/IntProperty.cs new file mode 100644 index 0000000..b8fb02a --- /dev/null +++ b/DotTiled/Model/Properties/IntProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class IntProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Int; + public required int Value { get; set; } + + public IProperty Clone() => new IntProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Properties/ObjectProperty.cs b/DotTiled/Model/Properties/ObjectProperty.cs new file mode 100644 index 0000000..1591319 --- /dev/null +++ b/DotTiled/Model/Properties/ObjectProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class ObjectProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.Object; + public required uint Value { get; set; } + + public IProperty Clone() => new ObjectProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Properties/PropertyType.cs b/DotTiled/Model/Properties/PropertyType.cs new file mode 100644 index 0000000..79b05cb --- /dev/null +++ b/DotTiled/Model/Properties/PropertyType.cs @@ -0,0 +1,13 @@ +namespace DotTiled; + +public enum PropertyType +{ + String, + Int, + Float, + Bool, + Color, + File, + Object, + Class +} diff --git a/DotTiled/Model/Properties/StringProperty.cs b/DotTiled/Model/Properties/StringProperty.cs new file mode 100644 index 0000000..655b7b4 --- /dev/null +++ b/DotTiled/Model/Properties/StringProperty.cs @@ -0,0 +1,14 @@ +namespace DotTiled; + +public class StringProperty : IProperty +{ + public required string Name { get; set; } + public PropertyType Type => PropertyType.String; + public required string Value { get; set; } + + public IProperty Clone() => new StringProperty + { + Name = Name, + Value = Value + }; +} diff --git a/DotTiled/Model/Template.cs b/DotTiled/Model/Template.cs new file mode 100644 index 0000000..11ae128 --- /dev/null +++ b/DotTiled/Model/Template.cs @@ -0,0 +1,8 @@ +namespace DotTiled; + +public class Template +{ + // At most one of (if the template is a tile object) + public Tileset? Tileset { get; set; } + public required Object Object { get; set; } +} diff --git a/DotTiled/Model/Tileset/Tileset.cs b/DotTiled/Model/Tileset/Tileset.cs index ac1da0d..7b1a982 100644 --- a/DotTiled/Model/Tileset/Tileset.cs +++ b/DotTiled/Model/Tileset/Tileset.cs @@ -39,8 +39,8 @@ public class Tileset public string Class { get; set; } = ""; public uint? TileWidth { get; set; } public uint? TileHeight { get; set; } - public float? Spacing { get; set; } - public float? Margin { get; set; } + public float? Spacing { get; set; } = 0f; + public float? Margin { get; set; } = 0f; public uint? TileCount { get; set; } public uint? Columns { get; set; } public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; diff --git a/DotTiled/Model/Tileset/WangColor.cs b/DotTiled/Model/Tileset/WangColor.cs index e278bb9..9ac751c 100644 --- a/DotTiled/Model/Tileset/WangColor.cs +++ b/DotTiled/Model/Tileset/WangColor.cs @@ -8,7 +8,7 @@ public class WangColor public required string Name { get; set; } public string Class { get; set; } = ""; public required Color Color { get; set; } - public required uint Tile { get; set; } + public required int Tile { get; set; } public float Probability { get; set; } = 0f; // Elements diff --git a/DotTiled/Model/Tileset/Wangset.cs b/DotTiled/Model/Tileset/Wangset.cs index 8d4d1a5..61f8496 100644 --- a/DotTiled/Model/Tileset/Wangset.cs +++ b/DotTiled/Model/Tileset/Wangset.cs @@ -7,7 +7,7 @@ public class Wangset // Attributes public required string Name { get; set; } public string Class { get; set; } = ""; - public required uint Tile { get; set; } + public required int Tile { get; set; } // Elements // At most one of diff --git a/DotTiled/Serialization/Helpers.cs b/DotTiled/Serialization/Helpers.cs new file mode 100644 index 0000000..2e36124 --- /dev/null +++ b/DotTiled/Serialization/Helpers.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace DotTiled; + +internal static partial class Helpers +{ + internal static uint[] ReadMemoryStreamAsInt32Array(Stream stream) + { + var finalValues = new List(); + var int32Bytes = new byte[4]; + while (stream.Read(int32Bytes, 0, 4) == 4) + { + var value = BitConverter.ToUInt32(int32Bytes, 0); + finalValues.Add(value); + } + return [.. finalValues]; + } + + internal static uint[] DecompressGZip(MemoryStream stream) + { + using var decompressedStream = new GZipStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } + + internal static uint[] DecompressZLib(MemoryStream stream) + { + using var decompressedStream = new ZLibStream(stream, CompressionMode.Decompress); + return ReadMemoryStreamAsInt32Array(decompressedStream); + } + + internal static uint[] ReadBytesAsInt32Array(byte[] bytes) + { + var intArray = new uint[bytes.Length / 4]; + for (var i = 0; i < intArray.Length; i++) + { + intArray[i] = BitConverter.ToUInt32(bytes, i * 4); + } + + return intArray; + } + + internal static (uint[] GlobalTileIDs, FlippingFlags[] FlippingFlags) ReadAndClearFlippingFlagsFromGIDs(uint[] globalTileIDs) + { + var clearedGlobalTileIDs = new uint[globalTileIDs.Length]; + var flippingFlags = new FlippingFlags[globalTileIDs.Length]; + for (var i = 0; i < globalTileIDs.Length; i++) + { + var gid = globalTileIDs[i]; + var flags = gid & 0xF0000000u; + flippingFlags[i] = (FlippingFlags)flags; + clearedGlobalTileIDs[i] = gid & 0x0FFFFFFFu; + } + + return (clearedGlobalTileIDs, flippingFlags); + } + + internal static ImageFormat ParseImageFormatFromSource(string source) + { + var extension = Path.GetExtension(source).ToLowerInvariant(); + return extension switch + { + ".png" => ImageFormat.Png, + ".gif" => ImageFormat.Gif, + ".jpg" => ImageFormat.Jpg, + ".jpeg" => ImageFormat.Jpg, + ".bmp" => ImageFormat.Bmp, + _ => throw new NotSupportedException($"Unsupported image format '{extension}'") + }; + } + + internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary? overrideProperties) + { + if (baseProperties is null) + return overrideProperties ?? new Dictionary(); + + if (overrideProperties is null) + return baseProperties; + + var result = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); + foreach (var (key, value) in overrideProperties) + { + if (!result.TryGetValue(key, out var baseProp)) + { + result[key] = value; + continue; + } + else + { + if (value is ClassProperty classProp) + { + ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); + } + else + { + result[key] = value; + } + } + } + + return result; + } + + internal static void SetAtMostOnce(ref T? field, T value, string fieldName) + { + if (field is not null) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + } + + internal static void SetAtMostOnceUsingCounter(ref T? field, T value, string fieldName, ref int counter) + { + if (counter > 0) + throw new InvalidOperationException($"{fieldName} already set"); + + field = value; + counter++; + } +} diff --git a/DotTiled/Serialization/IMapReader.cs b/DotTiled/Serialization/IMapReader.cs new file mode 100644 index 0000000..97c1fd7 --- /dev/null +++ b/DotTiled/Serialization/IMapReader.cs @@ -0,0 +1,8 @@ +using System; + +namespace DotTiled; + +public interface IMapReader : IDisposable +{ + Map ReadMap(); +} diff --git a/DotTiled/Serialization/ITemplateReader.cs b/DotTiled/Serialization/ITemplateReader.cs new file mode 100644 index 0000000..8de77bc --- /dev/null +++ b/DotTiled/Serialization/ITemplateReader.cs @@ -0,0 +1,8 @@ +using System; + +namespace DotTiled; + +public interface ITemplateReader : IDisposable +{ + Template ReadTemplate(); +} diff --git a/DotTiled/Serialization/ITilesetReader.cs b/DotTiled/Serialization/ITilesetReader.cs new file mode 100644 index 0000000..37f5257 --- /dev/null +++ b/DotTiled/Serialization/ITilesetReader.cs @@ -0,0 +1,8 @@ +using System; + +namespace DotTiled; + +public interface ITilesetReader : IDisposable +{ + Tileset ReadTileset(); +} diff --git a/DotTiled/Serialization/Tmj/ExtensionsJsonElement.cs b/DotTiled/Serialization/Tmj/ExtensionsJsonElement.cs new file mode 100644 index 0000000..7462c56 --- /dev/null +++ b/DotTiled/Serialization/Tmj/ExtensionsJsonElement.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; + +namespace DotTiled; + +internal static class ExtensionsJsonElement +{ + internal static T GetRequiredProperty(this JsonElement element, string propertyName) + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return property.GetValueAs(); + } + + internal static T GetOptionalProperty(this JsonElement element, string propertyName, T defaultValue) + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + if (property.ValueKind == JsonValueKind.Null) + return defaultValue; + + return property.GetValueAs(); + } + + internal static T GetValueAs(this JsonElement element) + { + bool isNullable = Nullable.GetUnderlyingType(typeof(T)) != null; + + if (isNullable && element.ValueKind == JsonValueKind.Null) + return default!; + + var realType = isNullable ? Nullable.GetUnderlyingType(typeof(T))! : typeof(T); + + string val = realType switch + { + Type t when t == typeof(string) => element.GetString()!, + Type t when t == typeof(int) => element.GetInt32().ToString(CultureInfo.InvariantCulture), + Type t when t == typeof(uint) => element.GetUInt32().ToString(CultureInfo.InvariantCulture), + Type t when t == typeof(float) => element.GetSingle().ToString(CultureInfo.InvariantCulture), + Type t when t == typeof(bool) => element.GetBoolean().ToString(CultureInfo.InvariantCulture), + _ => throw new JsonException($"Unsupported type '{typeof(T)}'.") + }; + + return (T)Convert.ChangeType(val, realType, CultureInfo.InvariantCulture); + } + + internal static T GetRequiredPropertyParseable(this JsonElement element, string propertyName) where T : IParsable + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return T.Parse(property.GetString()!, CultureInfo.InvariantCulture); + } + + internal static T GetRequiredPropertyParseable(this JsonElement element, string propertyName, Func parser) + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return parser(property.GetString()!); + } + + internal static T GetOptionalPropertyParseable(this JsonElement element, string propertyName, T defaultValue) where T : IParsable + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + return T.Parse(property.GetString()!, CultureInfo.InvariantCulture); + } + + internal static T GetOptionalPropertyParseable(this JsonElement element, string propertyName, Func parser, T defaultValue) + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + return parser(property.GetString()!); + } + + internal static T GetRequiredPropertyCustom(this JsonElement element, string propertyName, Func parser) + { + if (!element.TryGetProperty(propertyName, out var property)) + throw new JsonException($"Missing required property '{propertyName}'."); + + return parser(property); + } + + internal static T GetOptionalPropertyCustom(this JsonElement element, string propertyName, Func parser, T defaultValue) + { + if (!element.TryGetProperty(propertyName, out var property)) + return defaultValue; + + return parser(property); + } + + internal static List GetValueAsList(this JsonElement element, Func parser) + { + var list = new List(); + + foreach (var item in element.EnumerateArray()) + list.Add(parser(item)); + + return list; + } +} diff --git a/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/DotTiled/Serialization/Tmj/TjTemplateReader.cs new file mode 100644 index 0000000..0dc1dd8 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +public class TjTemplateReader : ITemplateReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TjTemplateReader( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + } + + public Template ReadTemplate() + { + var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); + var rootElement = jsonDoc.RootElement; + return Tmj.ReadTemplate(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TjTemplateReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Data.cs b/DotTiled/Serialization/Tmj/Tmj.Data.cs new file mode 100644 index 0000000..0b05c01 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Data.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) + { + var chunks = element.GetValueAsList(e => ReadChunk(e, compression, encoding)).ToArray(); + return new Data + { + Chunks = chunks, + Compression = compression, + Encoding = encoding, + FlippingFlags = null, + GlobalTileIDs = null + }; + } + + internal static Chunk ReadChunk(JsonElement element, DataCompression? compression, DataEncoding encoding) + { + var data = ReadDataWithoutChunks(element, compression, encoding); + + var x = element.GetRequiredProperty("x"); + var y = element.GetRequiredProperty("y"); + var width = element.GetRequiredProperty("width"); + var height = element.GetRequiredProperty("height"); + + return new Chunk + { + X = x, + Y = y, + Width = width, + Height = height, + GlobalTileIDs = data.GlobalTileIDs!, + FlippingFlags = data.FlippingFlags! + }; + } + + internal static Data ReadDataWithoutChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) + { + if (encoding == DataEncoding.Csv) + { + // Array of uint + var data = element.GetValueAsList(e => e.GetValueAs()).ToArray(); + var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(data); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null }; + } + else if (encoding == DataEncoding.Base64) + { + var base64Data = element.GetBytesFromBase64(); + + if (compression == null) + { + var data = Helpers.ReadBytesAsInt32Array(base64Data); + var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(data); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null }; + } + + using var stream = new MemoryStream(base64Data); + var decompressed = compression switch + { + DataCompression.GZip => Helpers.DecompressGZip(stream), + DataCompression.ZLib => Helpers.DecompressZLib(stream), + _ => throw new JsonException($"Unsupported compression '{compression}'.") + }; + + { + var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(decompressed); + return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null }; + } + } + + throw new JsonException($"Unsupported encoding '{encoding}'."); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Group.cs b/DotTiled/Serialization/Tmj/Tmj.Group.cs new file mode 100644 index 0000000..44e8b4d --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Group.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Group ReadGroup( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); + var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + + return new Group + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Layers = layers + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs new file mode 100644 index 0000000..dbd75a1 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static ImageLayer ReadImageLayer( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); + var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + + var image = element.GetRequiredProperty("image"); + var repeatX = element.GetOptionalProperty("repeatx", false); + var repeatY = element.GetOptionalProperty("repeaty", false); + var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var x = element.GetOptionalProperty("x", 0); + var y = element.GetOptionalProperty("y", 0); + + var imgModel = new Image + { + Format = Helpers.ParseImageFormatFromSource(image), + Height = 0, + Width = 0, + Source = image, + TransparentColor = transparentColor + }; + + return new ImageLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Image = imgModel, + RepeatX = repeatX, + RepeatY = repeatY, + X = x, + Y = y + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/DotTiled/Serialization/Tmj/Tmj.Layer.cs new file mode 100644 index 0000000..f14d614 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Layer.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static BaseLayer ReadLayer( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var type = element.GetRequiredProperty("type"); + + return type switch + { + "tilelayer" => ReadTileLayer(element, customTypeDefinitions), + "objectgroup" => ReadObjectLayer(element, externalTemplateResolver, customTypeDefinitions), + "imagelayer" => ReadImageLayer(element, customTypeDefinitions), + "group" => ReadGroup(element, externalTemplateResolver, customTypeDefinitions), + _ => throw new JsonException($"Unsupported layer type '{type}'.") + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Map.cs b/DotTiled/Serialization/Tmj/Tmj.Map.cs new file mode 100644 index 0000000..ea7313f --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Map ReadMap( + JsonElement element, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var version = element.GetRequiredProperty("version"); + var tiledVersion = element.GetRequiredProperty("tiledversion"); + string @class = element.GetOptionalProperty("class", ""); + var orientation = element.GetRequiredPropertyParseable("orientation", s => s switch + { + "orthogonal" => MapOrientation.Orthogonal, + "isometric" => MapOrientation.Isometric, + "staggered" => MapOrientation.Staggered, + "hexagonal" => MapOrientation.Hexagonal, + _ => throw new JsonException($"Unknown orientation '{s}'") + }); + var renderOrder = element.GetOptionalPropertyParseable("renderorder", s => s switch + { + "right-down" => RenderOrder.RightDown, + "right-up" => RenderOrder.RightUp, + "left-down" => RenderOrder.LeftDown, + "left-up" => RenderOrder.LeftUp, + _ => throw new JsonException($"Unknown render order '{s}'") + }, RenderOrder.RightDown); + var compressionLevel = element.GetOptionalProperty("compressionlevel", -1); + var width = element.GetRequiredProperty("width"); + var height = element.GetRequiredProperty("height"); + var tileWidth = element.GetRequiredProperty("tilewidth"); + var tileHeight = element.GetRequiredProperty("tileheight"); + var hexSideLength = element.GetOptionalProperty("hexsidelength", null); + var staggerAxis = element.GetOptionalPropertyParseable("staggeraxis", s => s switch + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new JsonException($"Unknown stagger axis '{s}'") + }, null); + var staggerIndex = element.GetOptionalPropertyParseable("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new JsonException($"Unknown stagger index '{s}'") + }, null); + var parallaxOriginX = element.GetOptionalProperty("parallaxoriginx", 0.0f); + var parallaxOriginY = element.GetOptionalProperty("parallaxoriginy", 0.0f); + var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), Color.Parse("#00000000", CultureInfo.InvariantCulture)); + var nextLayerID = element.GetRequiredProperty("nextlayerid"); + var nextObjectID = element.GetRequiredProperty("nextobjectid"); + var infinite = element.GetOptionalProperty("infinite", false); + + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + + List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []); + + return new Map + { + Version = version, + TiledVersion = tiledVersion, + Class = @class, + Orientation = orientation, + RenderOrder = renderOrder, + CompressionLevel = compressionLevel, + Width = width, + Height = height, + TileWidth = tileWidth, + TileHeight = tileHeight, + HexSideLength = hexSideLength, + StaggerAxis = staggerAxis, + StaggerIndex = staggerIndex, + ParallaxOriginX = parallaxOriginX, + ParallaxOriginY = parallaxOriginY, + BackgroundColor = backgroundColor, + NextLayerID = nextLayerID, + NextObjectID = nextObjectID, + Infinite = infinite, + Properties = properties, + Tilesets = tilesets, + Layers = layers + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs new file mode 100644 index 0000000..564f2db --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static ObjectLayer ReadObjectLayer( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var @class = element.GetOptionalProperty("class", ""); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var visible = element.GetOptionalProperty("visible", true); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); + var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + + var x = element.GetOptionalProperty("x", 0); + var y = element.GetOptionalProperty("y", 0); + var width = element.GetOptionalProperty("width", null); + var height = element.GetOptionalProperty("height", null); + var color = element.GetOptionalPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var drawOrder = element.GetOptionalPropertyParseable("draworder", s => s switch + { + "topdown" => DrawOrder.TopDown, + "index" => DrawOrder.Index, + _ => throw new JsonException($"Unknown draw order '{s}'.") + }, DrawOrder.TopDown); + + var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); + + return new ObjectLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + X = x, + Y = y, + Width = width, + Height = height, + Color = color, + DrawOrder = drawOrder, + Objects = objects + }; + } + + internal static Object ReadObject( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + uint? idDefault = null; + string nameDefault = ""; + string typeDefault = ""; + float xDefault = 0f; + float yDefault = 0f; + float widthDefault = 0f; + float heightDefault = 0f; + float rotationDefault = 0f; + uint? gidDefault = null; + bool visibleDefault = true; + bool ellipseDefault = false; + bool pointDefault = false; + List? polygonDefault = null; + List? polylineDefault = null; + Dictionary? propertiesDefault = null; + + var template = element.GetOptionalProperty("template", null); + if (template is not null) + { + var resolvedTemplate = externalTemplateResolver(template); + var templObj = resolvedTemplate.Object; + + idDefault = templObj.ID; + nameDefault = templObj.Name; + typeDefault = templObj.Type; + xDefault = templObj.X; + yDefault = templObj.Y; + widthDefault = templObj.Width; + heightDefault = templObj.Height; + rotationDefault = templObj.Rotation; + visibleDefault = templObj.Visible; + propertiesDefault = templObj.Properties; + ellipseDefault = templObj is EllipseObject; + pointDefault = templObj is PointObject; + polygonDefault = (templObj is PolygonObject polygonObj) ? polygonObj.Points : null; + polylineDefault = (templObj is PolylineObject polylineObj) ? polylineObj.Points : null; + } + + var ellipse = element.GetOptionalProperty("ellipse", ellipseDefault); + var gid = element.GetOptionalProperty("gid", gidDefault); + var height = element.GetOptionalProperty("height", heightDefault); + var id = element.GetOptionalProperty("id", idDefault); + var name = element.GetOptionalProperty("name", nameDefault); + var point = element.GetOptionalProperty("point", pointDefault); + var polygon = element.GetOptionalPropertyCustom?>("polygon", e => ReadPoints(e), polygonDefault); + var polyline = element.GetOptionalPropertyCustom?>("polyline", e => ReadPoints(e), polylineDefault); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var rotation = element.GetOptionalProperty("rotation", rotationDefault); + var text = element.GetOptionalPropertyCustom("text", ReadText, null); + var type = element.GetOptionalProperty("type", typeDefault); + var visible = element.GetOptionalProperty("visible", visibleDefault); + var width = element.GetOptionalProperty("width", widthDefault); + var x = element.GetOptionalProperty("x", xDefault); + var y = element.GetOptionalProperty("y", yDefault); + + if (gid is not null) + { + return new TileObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + Visible = visible, + Template = template, + Properties = properties, + GID = gid.Value + }; + } + + if (ellipse) + { + return new EllipseObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + Visible = visible, + Template = template, + Properties = properties + }; + } + + if (point) + { + return new PointObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + Visible = visible, + Template = template, + Properties = properties + }; + } + + if (polygon is not null) + { + return new PolygonObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + Visible = visible, + Template = template, + Properties = properties, + Points = polygon + }; + } + + if (polyline is not null) + { + return new PolylineObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + Visible = visible, + Template = template, + Properties = properties, + Points = polyline + }; + } + + if (text is not null) + { + text.ID = id; + text.Name = name; + text.Type = type; + text.X = x; + text.Y = y; + text.Width = width; + text.Height = height; + text.Rotation = rotation; + text.Visible = visible; + text.Template = template; + text.Properties = properties; + return text; + } + + return new RectangleObject + { + ID = id, + Name = name, + Type = type, + X = x, + Y = y, + Width = width, + Height = height, + Rotation = rotation, + Visible = visible, + Template = template, + Properties = properties + }; + } + + internal static List ReadPoints(JsonElement element) => + element.GetValueAsList(e => + { + var x = e.GetRequiredProperty("x"); + var y = e.GetRequiredProperty("y"); + return new Vector2(x, y); + }); + + internal static TextObject ReadText(JsonElement element) + { + var bold = element.GetOptionalProperty("bold", false); + var color = element.GetOptionalPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture), Color.Parse("#00000000", CultureInfo.InvariantCulture)); + var fontfamily = element.GetOptionalProperty("fontfamily", "sans-serif"); + var halign = element.GetOptionalPropertyParseable("halign", s => s switch + { + "left" => TextHorizontalAlignment.Left, + "center" => TextHorizontalAlignment.Center, + "right" => TextHorizontalAlignment.Right, + _ => throw new JsonException($"Unknown horizontal alignment '{s}'.") + }, TextHorizontalAlignment.Left); + var italic = element.GetOptionalProperty("italic", false); + var kerning = element.GetOptionalProperty("kerning", true); + var pixelsize = element.GetOptionalProperty("pixelsize", 16); + var strikeout = element.GetOptionalProperty("strikeout", false); + var text = element.GetRequiredProperty("text"); + var underline = element.GetOptionalProperty("underline", false); + var valign = element.GetOptionalPropertyParseable("valign", s => s switch + { + "top" => TextVerticalAlignment.Top, + "center" => TextVerticalAlignment.Center, + "bottom" => TextVerticalAlignment.Bottom, + _ => throw new JsonException($"Unknown vertical alignment '{s}'.") + }, TextVerticalAlignment.Top); + var wrap = element.GetOptionalProperty("wrap", false); + + return new TextObject + { + Bold = bold, + Color = color, + FontFamily = fontfamily, + HorizontalAlignment = halign, + Italic = italic, + Kerning = kerning, + PixelSize = pixelsize, + Strikeout = strikeout, + Text = text, + Underline = underline, + VerticalAlignment = valign, + Wrap = wrap + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/DotTiled/Serialization/Tmj/Tmj.Properties.cs new file mode 100644 index 0000000..6981521 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Properties.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Dictionary ReadProperties( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) => + element.GetValueAsList(e => + { + var name = e.GetRequiredProperty("name"); + var type = e.GetOptionalPropertyParseable("type", s => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + "float" => PropertyType.Float, + "bool" => PropertyType.Bool, + "color" => PropertyType.Color, + "file" => PropertyType.File, + "object" => PropertyType.Object, + "class" => PropertyType.Class, + _ => throw new JsonException("Invalid property type") + }, PropertyType.String); + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable("value") }, + PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Class => ReadClassProperty(e, customTypeDefinitions), + _ => throw new JsonException("Invalid property type") + }; + + return property!; + }).ToDictionary(p => p.Name); + + internal static ClassProperty ReadClassProperty( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + + var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); + + if (customTypeDef is CustomClassDefinition ccd) + { + var propsInType = CreateInstanceOfCustomClass(ccd); + var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); + + var mergedProps = Helpers.MergeProperties(propsInType, props); + + return new ClassProperty + { + Name = name, + PropertyType = propertyType, + Properties = mergedProps + }; + } + + throw new JsonException($"Unknown custom class '{propertyType}'."); + } + + internal static Dictionary ReadCustomClassProperties( + JsonElement element, + CustomClassDefinition customClassDefinition, + IReadOnlyCollection customTypeDefinitions) + { + Dictionary resultingProps = []; + + foreach (var prop in customClassDefinition.Members) + { + if (!element.TryGetProperty(prop.Name, out var propElement)) + continue; // Property not present in element, therefore will use default value + + IProperty property = prop.Type switch + { + PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs(), CultureInfo.InvariantCulture) }, + PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Class => ReadClassProperty(propElement, customTypeDefinitions), + _ => throw new JsonException("Invalid property type") + }; + + resultingProps[prop.Name] = property; + } + + return resultingProps; + } + + internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) + { + return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Template.cs b/DotTiled/Serialization/Tmj/Tmj.Template.cs new file mode 100644 index 0000000..79c7860 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Template.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Template ReadTemplate( + JsonElement element, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var type = element.GetRequiredProperty("type"); + var tileset = element.GetOptionalPropertyCustom("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null); + var @object = element.GetRequiredPropertyCustom("object", el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)); + + return new Template + { + Tileset = tileset, + Object = @object + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs new file mode 100644 index 0000000..5528177 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + + internal static TileLayer ReadTileLayer( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var compression = element.GetOptionalPropertyParseable("compression", s => s switch + { + "zlib" => DataCompression.ZLib, + "gzip" => DataCompression.GZip, + "" => null, + _ => throw new JsonException($"Unsupported compression '{s}'.") + }, null); + var encoding = element.GetOptionalPropertyParseable("encoding", s => s switch + { + "csv" => DataEncoding.Csv, + "base64" => DataEncoding.Base64, + _ => throw new JsonException($"Unsupported encoding '{s}'.") + }, DataEncoding.Csv); + var chunks = element.GetOptionalPropertyCustom("chunks", e => ReadDataAsChunks(e, compression, encoding), null); + var @class = element.GetOptionalProperty("class", ""); + var data = element.GetOptionalPropertyCustom("data", e => ReadDataWithoutChunks(e, compression, encoding), null); + var height = element.GetRequiredProperty("height"); + var id = element.GetRequiredProperty("id"); + var name = element.GetRequiredProperty("name"); + var offsetX = element.GetOptionalProperty("offsetx", 0.0f); + var offsetY = element.GetOptionalProperty("offsety", 0.0f); + var opacity = element.GetOptionalProperty("opacity", 1.0f); + var parallaxx = element.GetOptionalProperty("parallaxx", 1.0f); + var parallaxy = element.GetOptionalProperty("parallaxy", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var repeatX = element.GetOptionalProperty("repeatx", false); + var repeatY = element.GetOptionalProperty("repeaty", false); + var startX = element.GetOptionalProperty("startx", 0); + var startY = element.GetOptionalProperty("starty", 0); + var tintColor = element.GetOptionalPropertyParseable("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var visible = element.GetOptionalProperty("visible", true); + var width = element.GetRequiredProperty("width"); + var x = element.GetRequiredProperty("x"); + var y = element.GetRequiredProperty("y"); + + if ((data ?? chunks) is null) + throw new JsonException("Tile layer does not contain data."); + + return new TileLayer + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxx, + ParallaxY = parallaxy, + Properties = properties, + X = x, + Y = y, + Width = width, + Height = height, + Data = data ?? chunks + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/DotTiled/Serialization/Tmj/Tmj.Tileset.cs new file mode 100644 index 0000000..d455b42 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Tileset.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Tileset ReadTileset( + JsonElement element, + Func? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var @class = element.GetOptionalProperty("class", ""); + var columns = element.GetOptionalProperty("columns", null); + var fillMode = element.GetOptionalPropertyParseable("fillmode", s => s switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new JsonException($"Unknown fill mode '{s}'") + }, FillMode.Stretch); + var firstGID = element.GetOptionalProperty("firstgid", null); + var grid = element.GetOptionalPropertyCustom("grid", ReadGrid, null); + var image = element.GetOptionalProperty("image", null); + var imageHeight = element.GetOptionalProperty("imageheight", null); + var imageWidth = element.GetOptionalProperty("imagewidth", null); + var margin = element.GetOptionalProperty("margin", null); + var name = element.GetOptionalProperty("name", null); + var objectAlignment = element.GetOptionalPropertyParseable("objectalignment", s => s switch + { + "unspecified" => ObjectAlignment.Unspecified, + "topleft" => ObjectAlignment.TopLeft, + "top" => ObjectAlignment.Top, + "topright" => ObjectAlignment.TopRight, + "left" => ObjectAlignment.Left, + "center" => ObjectAlignment.Center, + "right" => ObjectAlignment.Right, + "bottomleft" => ObjectAlignment.BottomLeft, + "bottom" => ObjectAlignment.Bottom, + "bottomright" => ObjectAlignment.BottomRight, + _ => throw new JsonException($"Unknown object alignment '{s}'") + }, ObjectAlignment.Unspecified); + var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var source = element.GetOptionalProperty("source", null); + var spacing = element.GetOptionalProperty("spacing", null); + var tileCount = element.GetOptionalProperty("tilecount", null); + var tiledVersion = element.GetOptionalProperty("tiledversion", null); + var tileHeight = element.GetOptionalProperty("tileheight", null); + var tileOffset = element.GetOptionalPropertyCustom("tileoffset", ReadTileOffset, null); + var tileRenderSize = element.GetOptionalPropertyParseable("tilerendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new JsonException($"Unknown tile render size '{s}'") + }, TileRenderSize.Tile); + var tiles = element.GetOptionalPropertyCustom>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []); + var tileWidth = element.GetOptionalProperty("tilewidth", null); + var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); + var type = element.GetOptionalProperty("type", null); + var version = element.GetOptionalProperty("version", null); + var transformations = element.GetOptionalPropertyCustom("transformations", ReadTransformations, null); + var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e, customTypeDefinitions)), null); + + if (source is not null) + { + if (externalTilesetResolver is null) + throw new JsonException("External tileset resolver is required to resolve external tilesets."); + + var resolvedTileset = externalTilesetResolver(source); + resolvedTileset.FirstGID = firstGID; + resolvedTileset.Source = source; + return resolvedTileset; + } + + var imageModel = image is not null ? new Image + { + Format = Helpers.ParseImageFormatFromSource(image), + Source = image, + Height = imageHeight, + Width = imageWidth, + TransparentColor = transparentColor + } : null; + + return new Tileset + { + Class = @class, + Columns = columns, + FillMode = fillMode, + FirstGID = firstGID, + Grid = grid, + Image = imageModel, + Margin = margin, + Name = name, + ObjectAlignment = objectAlignment, + Properties = properties, + Source = source, + Spacing = spacing, + TileCount = tileCount, + TiledVersion = tiledVersion, + TileHeight = tileHeight, + TileOffset = tileOffset, + RenderSize = tileRenderSize, + Tiles = tiles, + TileWidth = tileWidth, + Version = version, + Wangsets = wangsets, + Transformations = transformations + }; + } + + internal static Transformations ReadTransformations(JsonElement element) + { + var hFlip = element.GetOptionalProperty("hflip", false); + var vFlip = element.GetOptionalProperty("vflip", false); + var rotate = element.GetOptionalProperty("rotate", false); + var preferUntransformed = element.GetOptionalProperty("preferuntransformed", false); + + return new Transformations + { + HFlip = hFlip, + VFlip = vFlip, + Rotate = rotate, + PreferUntransformed = preferUntransformed + }; + } + + internal static Grid ReadGrid(JsonElement element) + { + var orientation = element.GetOptionalPropertyParseable("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new JsonException($"Unknown grid orientation '{s}'") + }, GridOrientation.Orthogonal); + var height = element.GetRequiredProperty("height"); + var width = element.GetRequiredProperty("width"); + + return new Grid + { + Orientation = orientation, + Height = height, + Width = width + }; + } + + internal static TileOffset ReadTileOffset(JsonElement element) + { + var x = element.GetRequiredProperty("x"); + var y = element.GetRequiredProperty("y"); + + return new TileOffset + { + X = x, + Y = y + }; + } + + internal static List ReadTiles( + JsonElement element, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) => + element.GetValueAsList(e => + { + var animation = e.GetOptionalPropertyCustom?>("animation", e => e.GetValueAsList(ReadFrame), null); + var id = e.GetRequiredProperty("id"); + var image = e.GetOptionalProperty("image", null); + var imageHeight = e.GetOptionalProperty("imageheight", null); + var imageWidth = e.GetOptionalProperty("imagewidth", null); + var x = e.GetOptionalProperty("x", 0); + var y = e.GetOptionalProperty("y", 0); + var width = e.GetOptionalProperty("width", imageWidth ?? 0); + var height = e.GetOptionalProperty("height", imageHeight ?? 0); + var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null); + var probability = e.GetOptionalProperty("probability", 0.0f); + var properties = e.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + // var terrain, replaced by wangsets + var type = e.GetOptionalProperty("type", ""); + + var imageModel = image != null ? new Image + { + Format = Helpers.ParseImageFormatFromSource(image), + Source = image, + Height = imageHeight ?? 0, + Width = imageWidth ?? 0 + } : null; + + return new Tile + { + Animation = animation, + ID = id, + Image = imageModel, + X = x, + Y = y, + Width = width, + Height = height, + ObjectLayer = objectGroup, + Probability = probability, + Properties = properties, + Type = type + }; + }); + + internal static Frame ReadFrame(JsonElement element) + { + var duration = element.GetRequiredProperty("duration"); + var tileID = element.GetRequiredProperty("tileid"); + + return new Frame + { + Duration = duration, + TileID = tileID + }; + } + + internal static Wangset ReadWangset( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var @clalss = element.GetOptionalProperty("class", ""); + var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); + var name = element.GetRequiredProperty("name"); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var tile = element.GetOptionalProperty("tile", 0); + var type = element.GetOptionalProperty("type", ""); + var wangTiles = element.GetOptionalPropertyCustom>("wangtiles", e => e.GetValueAsList(ReadWangTile), []); + + return new Wangset + { + Class = @clalss, + WangColors = colors, + Name = name, + Properties = properties, + Tile = tile, + WangTiles = wangTiles + }; + } + + internal static WangColor ReadWangColor( + JsonElement element, + IReadOnlyCollection customTypeDefinitions) + { + var @class = element.GetOptionalProperty("class", ""); + var color = element.GetRequiredPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); + var name = element.GetRequiredProperty("name"); + var probability = element.GetOptionalProperty("probability", 1.0f); + var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var tile = element.GetOptionalProperty("tile", 0); + + return new WangColor + { + Class = @class, + Color = color, + Name = name, + Probability = probability, + Properties = properties, + Tile = tile + }; + } + + internal static WangTile ReadWangTile(JsonElement element) + { + var tileID = element.GetRequiredProperty("tileid"); + var wangID = element.GetOptionalPropertyCustom>("wangid", e => e.GetValueAsList(el => (byte)el.GetUInt32()), []); + + return new WangTile + { + TileID = tileID, + WangID = [.. wangID] + }; + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Wangset.cs b/DotTiled/Serialization/Tmj/Tmj.Wangset.cs new file mode 100644 index 0000000..cf9f024 --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Wangset.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ +} diff --git a/DotTiled/Serialization/Tmj/TmjMapReader.cs b/DotTiled/Serialization/Tmj/TmjMapReader.cs new file mode 100644 index 0000000..260cd21 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace DotTiled; + +public class TmjMapReader : IMapReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TmjMapReader( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + } + + public Map ReadMap() + { + var jsonDoc = JsonDocument.Parse(_jsonString); + var rootElement = jsonDoc.RootElement; + return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TmjMapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/DotTiled/Serialization/Tmj/TsjTilesetReader.cs new file mode 100644 index 0000000..14e5323 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +public class TsjTilesetReader : ITilesetReader +{ + // External resolvers + private readonly Func _externalTemplateResolver; + + private readonly string _jsonString; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TsjTilesetReader( + string jsonString, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + } + + public Tileset ReadTileset() + { + var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); + var rootElement = jsonDoc.RootElement; + return Tmj.ReadTileset( + rootElement, + _ => throw new NotSupportedException("External tilesets cannot refer to other external tilesets."), + _externalTemplateResolver, + _customTypeDefinitions); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TsjTilesetReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/TmxSerializer/ExtensionsXmlReader.cs b/DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs similarity index 100% rename from DotTiled/TmxSerializer/ExtensionsXmlReader.cs rename to DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs diff --git a/DotTiled/TmxSerializer/TmxSerializer.Chunk.cs b/DotTiled/Serialization/Tmx/Tmx.Chunk.cs similarity index 89% rename from DotTiled/TmxSerializer/TmxSerializer.Chunk.cs rename to DotTiled/Serialization/Tmx/Tmx.Chunk.cs index 47a2cb2..9d06082 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Chunk.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Chunk.cs @@ -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("x"); var y = reader.GetRequiredAttributeParseable("y"); diff --git a/DotTiled/TmxSerializer/TmxSerializer.Data.cs b/DotTiled/Serialization/Tmx/Tmx.Data.cs similarity index 85% rename from DotTiled/TmxSerializer/TmxSerializer.Data.cs rename to DotTiled/Serialization/Tmx/Tmx.Data.cs index 4725614..85598e0 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Data.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Data.cs @@ -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("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("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(); 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); diff --git a/DotTiled/TmxSerializer/TmxSerializer.Map.cs b/DotTiled/Serialization/Tmx/Tmx.Map.cs similarity index 81% rename from DotTiled/TmxSerializer/TmxSerializer.Map.cs rename to DotTiled/Serialization/Tmx/Tmx.Map.cs index bf3432b..bb7f1ae 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Map.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Map.cs @@ -6,9 +6,13 @@ 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 externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var version = reader.GetRequiredAttribute("version"); @@ -64,11 +68,12 @@ public partial class TmxSerializer reader.ProcessChildren("map", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "tileset" => () => tilesets.Add(ReadTileset(r)), - "layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r)), - "imagelayer" => () => layers.Add(ReadImageLayer(r)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), + "layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)), + "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), + "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)), + "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)), _ => r.Skip }); diff --git a/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs new file mode 100644 index 0000000..4d70b91 --- /dev/null +++ b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Xml; + +namespace DotTiled; + +internal partial class Tmx +{ + internal static ObjectLayer ReadObjectLayer( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + // Attributes + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var @class = reader.GetOptionalAttribute("class") ?? ""; + var x = reader.GetOptionalAttributeParseable("x") ?? 0; + var y = reader.GetOptionalAttributeParseable("y") ?? 0; + var width = reader.GetOptionalAttributeParseable("width"); + var height = reader.GetOptionalAttributeParseable("height"); + var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var color = reader.GetOptionalAttributeClass("color"); + var drawOrder = reader.GetOptionalAttributeEnum("draworder", s => s switch + { + "topdown" => DrawOrder.TopDown, + "index" => DrawOrder.Index, + _ => throw new Exception($"Unknown draw order '{s}'") + }) ?? DrawOrder.TopDown; + + // Elements + Dictionary? properties = null; + List objects = []; + + reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), + _ => r.Skip + }); + + return new ObjectLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Width = width, + Height = height, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Color = color, + Properties = properties, + DrawOrder = drawOrder, + Objects = objects + }; + } + + internal static Object ReadObject( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + // Attributes + var template = reader.GetOptionalAttribute("template"); + Object? obj = null; + if (template is not null) + obj = externalTemplateResolver(template).Object; + + uint? idDefault = obj?.ID ?? null; + string nameDefault = obj?.Name ?? ""; + string typeDefault = obj?.Type ?? ""; + float xDefault = obj?.X ?? 0f; + float yDefault = obj?.Y ?? 0f; + float widthDefault = obj?.Width ?? 0f; + float heightDefault = obj?.Height ?? 0f; + float rotationDefault = obj?.Rotation ?? 0f; + uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null; + bool visibleDefault = obj?.Visible ?? true; + Dictionary? propertiesDefault = obj?.Properties ?? null; + + var id = reader.GetOptionalAttributeParseable("id") ?? idDefault; + var name = reader.GetOptionalAttribute("name") ?? nameDefault; + var type = reader.GetOptionalAttribute("type") ?? typeDefault; + var x = reader.GetOptionalAttributeParseable("x") ?? xDefault; + var y = reader.GetOptionalAttributeParseable("y") ?? yDefault; + var width = reader.GetOptionalAttributeParseable("width") ?? widthDefault; + var height = reader.GetOptionalAttributeParseable("height") ?? heightDefault; + var rotation = reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; + var gid = reader.GetOptionalAttributeParseable("gid") ?? gidDefault; + var visible = reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; + + // Elements + Object? foundObject = null; + int propertiesCounter = 0; + Dictionary? properties = propertiesDefault; + + reader.ProcessChildren("object", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(r), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(r), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(r), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(r), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(r), "Object marker"), + _ => throw new Exception($"Unknown object marker '{elementName}'") + }); + + if (foundObject is null) + { + if (gid is not null) + foundObject = new TileObject { ID = id, GID = gid.Value }; + else + foundObject = new RectangleObject { ID = id }; + } + + foundObject.ID = id; + foundObject.Name = name; + foundObject.Type = type; + foundObject.X = x; + foundObject.Y = y; + foundObject.Width = width; + foundObject.Height = height; + foundObject.Rotation = rotation; + foundObject.Visible = visible; + foundObject.Properties = properties; + foundObject.Template = template; + + return OverrideObject(obj, foundObject); + } + + internal static Object OverrideObject(Object? obj, Object foundObject) + { + if (obj is null) + return foundObject; + + if (obj.GetType() != foundObject.GetType()) + { + obj.ID = foundObject.ID; + obj.Name = foundObject.Name; + obj.Type = foundObject.Type; + obj.X = foundObject.X; + obj.Y = foundObject.Y; + obj.Width = foundObject.Width; + obj.Height = foundObject.Height; + obj.Rotation = foundObject.Rotation; + obj.Visible = foundObject.Visible; + obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties); + obj.Template = foundObject.Template; + return obj; + } + + return OverrideObject((dynamic)obj, (dynamic)foundObject); + } + + internal static EllipseObject ReadEllipseObject(XmlReader reader) + { + reader.Skip(); + return new EllipseObject { }; + } + + internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject foundObject) => obj; + + internal static PointObject ReadPointObject(XmlReader reader) + { + reader.Skip(); + return new PointObject { }; + } + + internal static PointObject OverrideObject(PointObject obj, PointObject foundObject) => obj; + + internal static PolygonObject ReadPolygonObject(XmlReader reader) + { + // Attributes + var points = reader.GetRequiredAttributeParseable>("points", s => + { + // Takes on format "x1,y1 x2,y2 x3,y3 ..." + var coords = s.Split(' '); + return coords.Select(c => + { + var xy = c.Split(','); + return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); + }).ToList(); + }); + + reader.ReadStartElement("polygon"); + return new PolygonObject { Points = points }; + } + + internal static PolygonObject OverrideObject(PolygonObject obj, PolygonObject foundObject) + { + obj.Points = foundObject.Points; + return obj; + } + + internal static PolylineObject ReadPolylineObject(XmlReader reader) + { + // Attributes + var points = reader.GetRequiredAttributeParseable>("points", s => + { + // Takes on format "x1,y1 x2,y2 x3,y3 ..." + var coords = s.Split(' '); + return coords.Select(c => + { + var xy = c.Split(','); + return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); + }).ToList(); + }); + + reader.ReadStartElement("polyline"); + return new PolylineObject { Points = points }; + } + + internal static PolylineObject OverrideObject(PolylineObject obj, PolylineObject foundObject) + { + obj.Points = foundObject.Points; + return obj; + } + + internal static TextObject ReadTextObject(XmlReader reader) + { + // Attributes + var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; + var pixelSize = reader.GetOptionalAttributeParseable("pixelsize") ?? 16; + var wrap = reader.GetOptionalAttributeParseable("wrap") ?? false; + var color = reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); + var bold = reader.GetOptionalAttributeParseable("bold") ?? false; + var italic = reader.GetOptionalAttributeParseable("italic") ?? false; + var underline = reader.GetOptionalAttributeParseable("underline") ?? false; + var strikeout = reader.GetOptionalAttributeParseable("strikeout") ?? false; + var kerning = reader.GetOptionalAttributeParseable("kerning") ?? true; + var hAlign = reader.GetOptionalAttributeEnum("halign", s => s switch + { + "left" => TextHorizontalAlignment.Left, + "center" => TextHorizontalAlignment.Center, + "right" => TextHorizontalAlignment.Right, + "justify" => TextHorizontalAlignment.Justify, + _ => throw new Exception($"Unknown horizontal alignment '{s}'") + }) ?? TextHorizontalAlignment.Left; + var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch + { + "top" => TextVerticalAlignment.Top, + "center" => TextVerticalAlignment.Center, + "bottom" => TextVerticalAlignment.Bottom, + _ => throw new Exception($"Unknown vertical alignment '{s}'") + }) ?? TextVerticalAlignment.Top; + + // Elements + var text = reader.ReadElementContentAsString("text", ""); + + return new TextObject + { + FontFamily = fontFamily, + PixelSize = pixelSize, + Wrap = wrap, + Color = color, + Bold = bold, + Italic = italic, + Underline = underline, + Strikeout = strikeout, + Kerning = kerning, + HorizontalAlignment = hAlign, + VerticalAlignment = vAlign, + Text = text + }; + } + + internal static TextObject OverrideObject(TextObject obj, TextObject foundObject) + { + obj.FontFamily = foundObject.FontFamily; + obj.PixelSize = foundObject.PixelSize; + obj.Wrap = foundObject.Wrap; + obj.Color = foundObject.Color; + obj.Bold = foundObject.Bold; + obj.Italic = foundObject.Italic; + obj.Underline = foundObject.Underline; + obj.Strikeout = foundObject.Strikeout; + obj.Kerning = foundObject.Kerning; + obj.HorizontalAlignment = foundObject.HorizontalAlignment; + obj.VerticalAlignment = foundObject.VerticalAlignment; + obj.Text = foundObject.Text; + return obj; + } + + internal static TileObject OverrideObject(TileObject obj, TileObject foundObject) + { + obj.GID = foundObject.GID; + return obj; + } + + internal static Template ReadTemplate( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + // No attributes + + // At most one of + Tileset? tileset = null; + + // Should contain exactly one of + Object? obj = null; + + reader.ProcessChildren("template", (r, elementName) => elementName switch + { + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"), + _ => r.Skip + }); + + if (obj is null) + throw new NotSupportedException("Template must contain exactly one object"); + + return new Template + { + Tileset = tileset, + Object = obj + }; + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Properties.cs b/DotTiled/Serialization/Tmx/Tmx.Properties.cs similarity index 60% rename from DotTiled/TmxSerializer/TmxSerializer.Properties.cs rename to DotTiled/Serialization/Tmx/Tmx.Properties.cs index 1e7cd7e..6ea65e5 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Properties.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Properties.cs @@ -4,9 +4,11 @@ using System.Xml; namespace DotTiled; -public partial class TmxSerializer +internal partial class Tmx { - private Dictionary ReadProperties(XmlReader reader) + internal static Dictionary ReadProperties( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { return reader.ReadList("properties", "property", (r) => { @@ -33,22 +35,38 @@ public partial class TmxSerializer PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Class => ReadClassProperty(r), + PropertyType.Class => ReadClassProperty(r, customTypeDefinitions), _ => throw new XmlException("Invalid property type") }; return (name, property); }).ToDictionary(x => x.name, x => x.property); } - private ClassProperty ReadClassProperty(XmlReader reader) + internal static ClassProperty ReadClassProperty( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { var name = reader.GetRequiredAttribute("name"); var propertyType = reader.GetRequiredAttribute("propertytype"); - reader.ReadStartElement("property"); - var properties = ReadProperties(reader); - reader.ReadEndElement(); + var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); + if (customTypeDef is CustomClassDefinition ccd) + { + reader.ReadStartElement("property"); + var propsInType = CreateInstanceOfCustomClass(ccd); + var props = ReadProperties(reader, customTypeDefinitions); - return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties }; + var mergedProps = Helpers.MergeProperties(propsInType, props); + + reader.ReadEndElement(); + return new ClassProperty { Name = name, PropertyType = propertyType, Properties = mergedProps }; + } + + throw new XmlException($"Unkonwn custom class definition: {propertyType}"); + } + + internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) + { + return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); } } diff --git a/DotTiled/TmxSerializer/TmxSerializer.TileLayer.cs b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs similarity index 57% rename from DotTiled/TmxSerializer/TmxSerializer.TileLayer.cs rename to DotTiled/Serialization/Tmx/Tmx.TileLayer.cs index b6853c8..6fc64fb 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.TileLayer.cs +++ b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs @@ -5,9 +5,12 @@ 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, + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -30,7 +33,7 @@ public partial class TmxSerializer reader.ProcessChildren("layer", (r, elementName) => elementName switch { "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), _ => r.Skip }); @@ -55,7 +58,9 @@ public partial class TmxSerializer }; } - private ImageLayer ReadImageLayer(XmlReader reader) + internal static ImageLayer ReadImageLayer( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { var id = reader.GetRequiredAttributeParseable("id"); var name = reader.GetOptionalAttribute("name") ?? ""; @@ -69,8 +74,8 @@ public partial class TmxSerializer var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - var repeatX = reader.GetRequiredAttributeParseable("repeatx"); - var repeatY = reader.GetRequiredAttributeParseable("repeaty"); + var repeatX = (reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; + var repeatY = (reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; Dictionary? properties = null; Image? image = null; @@ -78,7 +83,7 @@ public partial class TmxSerializer reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch { "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), _ => r.Skip }); @@ -102,4 +107,50 @@ public partial class TmxSerializer RepeatY = repeatY }; } + + internal static Group ReadGroup( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + var id = reader.GetRequiredAttributeParseable("id"); + var name = reader.GetOptionalAttribute("name") ?? ""; + var @class = reader.GetOptionalAttribute("class") ?? ""; + var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + Dictionary? properties = null; + List layers = []; + + reader.ProcessChildren("group", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "layer" => () => layers.Add(ReadTileLayer(r, false, customTypeDefinitions)), + "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), + "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)), + "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)), + _ => r.Skip + }); + + return new Group + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties, + Layers = layers + }; + } } diff --git a/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs b/DotTiled/Serialization/Tmx/Tmx.Tileset.cs similarity index 75% rename from DotTiled/TmxSerializer/TmxSerializer.Tileset.cs rename to DotTiled/Serialization/Tmx/Tmx.Tileset.cs index 314b9e9..1912df2 100644 --- a/DotTiled/TmxSerializer/TmxSerializer.Tileset.cs +++ b/DotTiled/Serialization/Tmx/Tmx.Tileset.cs @@ -1,13 +1,18 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; 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? externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var version = reader.GetOptionalAttribute("version"); @@ -18,8 +23,8 @@ public partial class TmxSerializer var @class = reader.GetOptionalAttribute("class") ?? ""; var tileWidth = reader.GetOptionalAttributeParseable("tilewidth"); var tileHeight = reader.GetOptionalAttributeParseable("tileheight"); - var spacing = reader.GetOptionalAttributeParseable("spacing"); - var margin = reader.GetOptionalAttributeParseable("margin"); + var spacing = reader.GetOptionalAttributeParseable("spacing") ?? 0; + var margin = reader.GetOptionalAttributeParseable("margin") ?? 0; var tileCount = reader.GetOptionalAttributeParseable("tilecount"); var columns = reader.GetOptionalAttributeParseable("columns"); var objectAlignment = reader.GetOptionalAttributeEnum("objectalignment", s => s switch @@ -63,19 +68,22 @@ public partial class TmxSerializer "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(r), "TileOffset"), "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(r), "Grid"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r), "Wangsets"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r, customTypeDefinitions), "Wangsets"), "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"), - "tile" => () => tiles.Add(ReadTile(r)), + "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)), _ => r.Skip }); // Check if tileset is referring to external file if (source is not null) { - var resolvedTileset = _externalTilesetResolver(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; + resolvedTileset.Source = source; return resolvedTileset; } @@ -106,7 +114,7 @@ public partial class TmxSerializer }; } - private Image ReadImage(XmlReader reader) + internal static Image ReadImage(XmlReader reader) { // Attributes var format = reader.GetOptionalAttributeEnum("format", s => s switch @@ -128,6 +136,9 @@ public partial class TmxSerializer _ => r.Skip }); + if (format is null && source is not null) + format = ParseImageFormatFromSource(source); + return new Image { Format = format, @@ -138,7 +149,22 @@ public partial class TmxSerializer }; } - private TileOffset ReadTileOffset(XmlReader reader) + + private static ImageFormat ParseImageFormatFromSource(string source) + { + var extension = Path.GetExtension(source).ToLowerInvariant(); + return extension switch + { + ".png" => ImageFormat.Png, + ".gif" => ImageFormat.Gif, + ".jpg" => ImageFormat.Jpg, + ".jpeg" => ImageFormat.Jpg, + ".bmp" => ImageFormat.Bmp, + _ => throw new XmlException($"Unsupported image format '{extension}'") + }; + } + + internal static TileOffset ReadTileOffset(XmlReader reader) { // Attributes var x = reader.GetOptionalAttributeParseable("x") ?? 0f; @@ -148,7 +174,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("orientation", s => s switch @@ -164,19 +190,22 @@ 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("hflip") ?? false; - var vFlip = reader.GetOptionalAttributeParseable("vflip") ?? false; - var rotate = reader.GetOptionalAttributeParseable("rotate") ?? false; - var preferUntransformed = reader.GetOptionalAttributeParseable("preferuntransformed") ?? false; + var hFlip = (reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; + var vFlip = (reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; + var rotate = (reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; + var preferUntransformed = (reader.GetOptionalAttributeParseable("preferuntransformed") ?? 0) == 1; reader.ReadStartElement("transformations"); return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; } - private Tile ReadTile(XmlReader reader) + internal static Tile ReadTile( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) { // Attributes var id = reader.GetRequiredAttributeParseable("id"); @@ -195,9 +224,9 @@ public partial class TmxSerializer reader.ProcessChildren("tile", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r), "ObjectLayer"), + "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions), "ObjectLayer"), "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => { var tileID = ar.GetRequiredAttributeParseable("tileid"); @@ -223,17 +252,21 @@ public partial class TmxSerializer }; } - private List ReadWangsets(XmlReader reader) + internal static List ReadWangsets( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { - return reader.ReadList("wangsets", "wangset", ReadWangset); + return reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); } - private Wangset ReadWangset(XmlReader reader) + internal static Wangset ReadWangset( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { // Attributes var name = reader.GetRequiredAttribute("name"); var @class = reader.GetOptionalAttribute("class") ?? ""; - var tile = reader.GetRequiredAttributeParseable("tile"); + var tile = reader.GetRequiredAttributeParseable("tile"); // Elements Dictionary? properties = null; @@ -242,8 +275,8 @@ public partial class TmxSerializer reader.ProcessChildren("wangset", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "wangcolor" => () => wangColors.Add(ReadWangColor(r)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), + "wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)), "wangtile" => () => wangTiles.Add(ReadWangTile(r)), _ => r.Skip }); @@ -262,13 +295,15 @@ public partial class TmxSerializer }; } - private WangColor ReadWangColor(XmlReader reader) + internal static WangColor ReadWangColor( + XmlReader reader, + IReadOnlyCollection customTypeDefinitions) { // Attributes var name = reader.GetRequiredAttribute("name"); var @class = reader.GetOptionalAttribute("class") ?? ""; var color = reader.GetRequiredAttributeParseable("color"); - var tile = reader.GetRequiredAttributeParseable("tile"); + var tile = reader.GetRequiredAttributeParseable("tile"); var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; // Elements @@ -276,7 +311,7 @@ public partial class TmxSerializer reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), _ => r.Skip }); @@ -291,7 +326,7 @@ public partial class TmxSerializer }; } - private WangTile ReadWangTile(XmlReader reader) + internal static WangTile ReadWangTile(XmlReader reader) { // Attributes var tileID = reader.GetRequiredAttributeParseable("tileid"); @@ -304,6 +339,8 @@ public partial class TmxSerializer return indices; }); + reader.ReadStartElement("wangtile"); + return new WangTile { TileID = tileID, diff --git a/DotTiled/Serialization/Tmx/TmxMapReader.cs b/DotTiled/Serialization/Tmx/TmxMapReader.cs new file mode 100644 index 0000000..02388bb --- /dev/null +++ b/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace DotTiled; + +public class TmxMapReader : IMapReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TmxMapReader( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + + // Prepare reader + _reader.MoveToContent(); + } + + public Map ReadMap() + { + return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _reader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TmxTiledMapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } +} diff --git a/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/DotTiled/Serialization/Tmx/TsxTilesetReader.cs new file mode 100644 index 0000000..dba516b --- /dev/null +++ b/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace DotTiled; + +public class TsxTilesetReader : ITilesetReader +{ + // External resolvers + private readonly Func _externalTemplateResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TsxTilesetReader( + XmlReader reader, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + + // Prepare reader + _reader.MoveToContent(); + } + + public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions); + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~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); + } +} diff --git a/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/DotTiled/Serialization/Tmx/TxTemplateReader.cs new file mode 100644 index 0000000..eba6299 --- /dev/null +++ b/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace DotTiled; + +public class TxTemplateReader : ITemplateReader +{ + // Resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + private readonly IReadOnlyCollection _customTypeDefinitions; + + public TxTemplateReader( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + IReadOnlyCollection customTypeDefinitions) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + + // Prepare reader + _reader.MoveToContent(); + } + + public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~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); + } +} diff --git a/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs b/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs deleted file mode 100644 index 9676f4b..0000000 --- a/DotTiled/TmxSerializer/TmxSerializer.Helpers.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace DotTiled; - -public partial class TmxSerializer -{ - private static class Helpers - { - public static void SetAtMostOnce(ref T? field, T value, string fieldName) - { - if (field is not null) - throw new InvalidOperationException($"{fieldName} already set"); - - field = value; - } - } -} diff --git a/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs b/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs deleted file mode 100644 index 8772c04..0000000 --- a/DotTiled/TmxSerializer/TmxSerializer.ObjectLayer.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Numerics; -using System.Xml; - -namespace DotTiled; - -public partial class TmxSerializer -{ - private ObjectLayer ReadObjectLayer(XmlReader reader) - { - // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - var color = reader.GetOptionalAttributeClass("color"); - var drawOrder = reader.GetOptionalAttributeEnum("draworder", s => s switch - { - "topdown" => DrawOrder.TopDown, - "index" => DrawOrder.Index, - _ => throw new Exception($"Unknown draw order '{s}'") - }) ?? DrawOrder.TopDown; - - // Elements - Dictionary? properties = null; - List objects = []; - - reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "object" => () => objects.Add(ReadObject(r)), - _ => r.Skip - }); - - return new ObjectLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Width = width, - Height = height, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Color = color, - Properties = properties, - DrawOrder = drawOrder, - Objects = objects - }; - } - - private Object ReadObject(XmlReader reader) - { - // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var type = reader.GetOptionalAttribute("type") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0f; - var y = reader.GetOptionalAttributeParseable("y") ?? 0f; - var width = reader.GetOptionalAttributeParseable("width") ?? 0f; - var height = reader.GetOptionalAttributeParseable("height") ?? 0f; - var rotation = reader.GetOptionalAttributeParseable("rotation") ?? 0f; - var gid = reader.GetOptionalAttributeParseable("gid"); - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var template = reader.GetOptionalAttribute("template"); - - // Elements - Object? obj = null; - Dictionary? properties = null; - - reader.ProcessChildren("object", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), - "ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r, id), "Object marker"), - "point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r, id), "Object marker"), - "polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r, id), "Object marker"), - "polyline" => () => Helpers.SetAtMostOnce(ref obj, ReadPolylineObject(r, id), "Object marker"), - "text" => () => Helpers.SetAtMostOnce(ref obj, ReadTextObject(r, id), "Object marker"), - _ => throw new Exception($"Unknown object marker '{elementName}'") - }); - - if (obj is null) - { - obj = new RectangleObject { ID = id }; - reader.Skip(); - } - - obj.Name = name; - obj.Type = type; - obj.X = x; - obj.Y = y; - obj.Width = width; - obj.Height = height; - obj.Rotation = rotation; - obj.GID = gid; - obj.Visible = visible; - obj.Template = template; - obj.Properties = properties; - - return obj; - } - - private EllipseObject ReadEllipseObject(XmlReader reader, uint id) - { - reader.Skip(); - return new EllipseObject { ID = id }; - } - - private PointObject ReadPointObject(XmlReader reader, uint id) - { - reader.Skip(); - return new PointObject { ID = id }; - } - - private PolygonObject ReadPolygonObject(XmlReader reader, uint id) - { - // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => - { - // Takes on format "x1,y1 x2,y2 x3,y3 ..." - var coords = s.Split(' '); - return coords.Select(c => - { - var xy = c.Split(','); - return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); - }).ToList(); - }); - - reader.ReadStartElement("polygon"); - return new PolygonObject { ID = id, Points = points }; - } - - private PolylineObject ReadPolylineObject(XmlReader reader, uint id) - { - // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => - { - // Takes on format "x1,y1 x2,y2 x3,y3 ..." - var coords = s.Split(' '); - return coords.Select(c => - { - var xy = c.Split(','); - return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); - }).ToList(); - }); - - reader.ReadStartElement("polyline"); - return new PolylineObject { ID = id, Points = points }; - } - - private TextObject ReadTextObject(XmlReader reader, uint id) - { - // Attributes - var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; - var pixelSize = reader.GetOptionalAttributeParseable("pixelsize") ?? 16; - var wrap = reader.GetOptionalAttributeParseable("wrap") ?? false; - var color = reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); - var bold = reader.GetOptionalAttributeParseable("bold") ?? false; - var italic = reader.GetOptionalAttributeParseable("italic") ?? false; - var underline = reader.GetOptionalAttributeParseable("underline") ?? false; - var strikeout = reader.GetOptionalAttributeParseable("strikeout") ?? false; - var kerning = reader.GetOptionalAttributeParseable("kerning") ?? true; - var hAlign = reader.GetOptionalAttributeEnum("halign", s => s switch - { - "left" => TextHorizontalAlignment.Left, - "center" => TextHorizontalAlignment.Center, - "right" => TextHorizontalAlignment.Right, - "justify" => TextHorizontalAlignment.Justify, - _ => throw new Exception($"Unknown horizontal alignment '{s}'") - }) ?? TextHorizontalAlignment.Left; - var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch - { - "top" => TextVerticalAlignment.Top, - "center" => TextVerticalAlignment.Center, - "bottom" => TextVerticalAlignment.Bottom, - _ => throw new Exception($"Unknown vertical alignment '{s}'") - }) ?? TextVerticalAlignment.Top; - - // Elements - var text = reader.ReadElementContentAsString("text", ""); - - return new TextObject - { - ID = id, - FontFamily = fontFamily, - PixelSize = pixelSize, - Wrap = wrap, - Color = color, - Bold = bold, - Italic = italic, - Underline = underline, - Strikeout = strikeout, - Kerning = kerning, - HorizontalAlignment = hAlign, - VerticalAlignment = vAlign, - Text = text - }; - } -} diff --git a/DotTiled/TmxSerializer/TmxSerializer.cs b/DotTiled/TmxSerializer/TmxSerializer.cs deleted file mode 100644 index 9ced612..0000000 --- a/DotTiled/TmxSerializer/TmxSerializer.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Xml; - -namespace DotTiled; - -public partial class TmxSerializer -{ - private readonly Func _externalTilesetResolver; - - public TmxSerializer(Func externalTilesetResolver) - { - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - } - - public Map DeserializeMap(XmlReader reader) - { - reader.ReadToFollowing("map"); - return ReadMap(reader); - } -} diff --git a/Makefile b/Makefile index e69de29..e7dc7a5 100644 --- a/Makefile +++ b/Makefile @@ -0,0 +1,10 @@ +test: + dotnet test + +BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj +BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts +.PHONY: benchmark +benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md + +$(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES) + dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file diff --git a/README.md b/README.md index 81fe9e5..27ee68c 100644 --- a/README.md +++ b/README.md @@ -1 +1,85 @@ -# DotTiled \ No newline at end of file +# 📚 DotTiled + + + +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. + +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) +- [Feature coverage comparison](#feature-coverage-comparison) +- [Quickstart](#quickstart) + - [Installing DotTiled](#installing-dottiled) + +# Alternative libraries and comparison + +Other similar libraries exist, and you may want to consider them for your project as well: + +|**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.83 | 2.16 | - | - | - | +| Benchmark (memory)* | 1.00 | 1.43 | 2.03 | - | - | - | +| .NET Targets | `net8.0` |`net6.0`
`net7.0`|`netstandard2.1`|`netstandard2.0`|`netstandard2.0`|`net45`| +| Docs |Usage,
XML Docs|Usage|Usage, API,
XML Docs|Usage, API|Usage, XML Docs|Usage, XML Docs| +| License | MIT | MIT | MIT | Apache-2.0 | MIT | BSD 3-Clause | + +> [!NOTE] +> *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. + +[MonoGame](https://www.monogame.net) users may also want to consider using [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended) for loading Tiled maps and tilesets. Like MonoGame.Extended, DotTiled also provides a way to properly import Tiled maps and tilesets with the MonoGame content pipeline (with the DotTiled.MonoGame.Pipeline NuGet). However, unlike MonoGame.Extended, DotTiled does *not* include any kind of rendering capabilities, and it is up to you as a developer to implement any kind of rendering for your maps when using DotTiled. The feature coverage by MonoGame.Extended is less than that of DotTiled, so you may want to consider using DotTiled if you need access to more Tiled features and flexibility. + +
+ +Benchmark details + + +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 | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------------ |------------------------- |---------:|------:|-------:|-------:|----------:|------------:| +| DotTiled | MapFromInMemoryTmjString | 4.431 Ξs | 1.00 | 0.4349 | - | 5.58 KB | 1.00 | +| TiledLib | MapFromInMemoryTmjString | 6.369 Ξs | 1.44 | 0.7019 | 0.0153 | 9.01 KB | 1.61 | +| | | | | | | | | +| DotTiled | MapFromInMemoryTmxString | 3.125 Ξs | 1.00 | 1.2817 | 0.0610 | 16.36 KB | 1.00 | +| TiledLib | MapFromInMemoryTmxString | 5.709 Ξs | 1.83 | 1.8005 | 0.0916 | 23.32 KB | 1.43 | +| TiledCSPlus | MapFromInMemoryTmxString | 6.757 Ξs | 2.16 | 2.5940 | 0.1831 | 33.16 KB | 2.03 | + +It is important to note that the above benchmark results come from loading a very small map with a single tile layer as I had to find a common denominator between the libraries so that they all could load the same map. The results aim to be indicative of the performance of the libraries, but should be taken with a grain of salt. Only the actively maintained libraries are included in the benchmark results. TiledCSPlus does not support the `.tmj` format, so it was not included for that benchmark category. + +
+ +# Feature coverage comparison + +Below is a comparison of the feature coverage of DotTiled and other similar libraries. This comparison is based on the features provided by the Tiled map editor and the support for those features in each library. The comparison is not exhaustive, and you may want to refer to the respective library's documentation or implementation for details. Due to some libraries not having obvious documentation or feature lists, some features may be incorrectly marked as not supported. If you find any inaccuracies, please let me know. + +| **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)| +|---------------------------------|:-:|:-:|:-:|:-:|:-:|:-:| +| XML format `.tmx` |✅ |⚠ïļ|⚠ïļ|⚠ïļ|⚠ïļ|⚠ïļ| +| JSON format `.tmj` |✅ |⚠ïļ|❌|❌|❌|❌| +| External tileset callback |✅ |✅|❌|✅|❌|❌| +| Object templates |✅ |❌|❌|❌|❌|❌| +| Custom types (properties) |✅ |❌|❌|❌|❌|❌| +| Hierarchical layers (groups) |✅ |❌|❌|✅|❌|✅| +| Infinite maps |✅ |❌|✅|✅|✅|❌| +| Wangsets |✅ |❌|⚠ïļ|⚠ïļ|❌|⚠ïļ| + +> [!NOTE] +> ✅ Full support. ⚠ïļ Partial support, see respective library for details about supported features. ❌ No support. + +# Quickstart + +### Installing DotTiled + +DotTiled is available as a NuGet package. You can install it by using the NuGet Package Manager UI in Visual Studio, or equivalent, or using the following command for the .NET CLI: + +```pwsh +dotnet add package DotTiled +```