diff --git a/DotTiled.Tests/Assert/AssertMap.cs b/DotTiled.Tests/Assert/AssertMap.cs index 6596ee8..e9ad8be 100644 --- a/DotTiled.Tests/Assert/AssertMap.cs +++ b/DotTiled.Tests/Assert/AssertMap.cs @@ -1,3 +1,6 @@ +using System.Collections; +using System.Numerics; + namespace DotTiled.Tests; public static partial class DotTiledAssert @@ -10,6 +13,29 @@ public static partial class DotTiledAssert 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; @@ -24,6 +50,20 @@ public static partial class DotTiledAssert 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}'"); } diff --git a/DotTiled.Tests/Assert/AssertObject.cs b/DotTiled.Tests/Assert/AssertObject.cs index c49b6e7..bd303f9 100644 --- a/DotTiled.Tests/Assert/AssertObject.cs +++ b/DotTiled.Tests/Assert/AssertObject.cs @@ -13,7 +13,6 @@ public static partial class DotTiledAssert 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.GID, actual.GID, nameof(Object.GID)); AssertEqual(expected.Visible, actual.Visible, nameof(Object.Visible)); AssertEqual(expected.Template, actual.Template, nameof(Object.Template)); @@ -63,4 +62,10 @@ public static partial class DotTiledAssert 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/Serialization/TestData.cs b/DotTiled.Tests/Serialization/TestData.cs index d2d3316..c3d52f8 100644 --- a/DotTiled.Tests/Serialization/TestData.cs +++ b/DotTiled.Tests/Serialization/TestData.cs @@ -39,6 +39,7 @@ public static partial class TestData ["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 = [ 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..8ef6ce5 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs @@ -0,0 +1,214 @@ +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) + ] + }, + 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..16561f4 --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmj @@ -0,0 +1,181 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "id":2, + "layers":[ + { + "draworder":"topdown", + "id":3, + "name":"Objects", + "objects":[ + { + "height":31.3333333333333, + "id":1, + "name":"Object 1", + "rotation":0, + "type":"", + "visible":true, + "width":31.3333333333333, + "x":25.6666666666667, + "y":28.6666666666667 + }, + { + "height":0, + "id":3, + "name":"P1", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":117.666666666667, + "y":48.6666666666667 + }, + { + "ellipse":true, + "height":34.6666666666667, + "id":4, + "name":"Circle1", + "rotation":0, + "type":"", + "visible":true, + "width":34.6666666666667, + "x":77, + "y":72.3333333333333 + }, + { + "height":0, + "id":5, + "name":"Poly", + "polygon":[ + { + "x":0, + "y":0 + }, + { + "x":104, + "y":20 + }, + { + "x":35.6666666666667, + "y":32.3333333333333 + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":20.6666666666667, + "y":114.666666666667 + }, + { + "gid":7, + "height":146, + "id":6, + "name":"TileObj", + "rotation":0, + "type":"", + "visible":true, + "width":64, + "x":-35, + "y":110.333333333333 + }], + "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..34cd91c --- /dev/null +++ b/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.tmx @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + +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/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/Model/Layers/ImageLayer.cs b/DotTiled/Model/Layers/ImageLayer.cs index 6489c22..a140b0d 100644 --- a/DotTiled/Model/Layers/ImageLayer.cs +++ b/DotTiled/Model/Layers/ImageLayer.cs @@ -5,8 +5,8 @@ public class ImageLayer : BaseLayer // Attributes public uint X { get; set; } = 0; public uint Y { get; set; } = 0; - public required bool RepeatX { get; set; } - public required bool RepeatY { get; set; } + 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/Objects/Object.cs b/DotTiled/Model/Layers/Objects/Object.cs index b3313d7..765de69 100644 --- a/DotTiled/Model/Layers/Objects/Object.cs +++ b/DotTiled/Model/Layers/Objects/Object.cs @@ -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/Serialization/Tmj/Tmj.ImageLayer.cs b/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs index d315891..dbd75a1 100644 --- a/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs +++ b/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs @@ -25,8 +25,8 @@ internal partial class Tmj var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); var image = element.GetRequiredProperty("image"); - var repeatX = element.GetRequiredProperty("repeatx"); - var repeatY = element.GetRequiredProperty("repeaty"); + var 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); diff --git a/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs index 2fdf3c9..564f2db 100644 --- a/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs +++ b/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs @@ -97,7 +97,6 @@ internal partial class Tmj widthDefault = templObj.Width; heightDefault = templObj.Height; rotationDefault = templObj.Rotation; - gidDefault = templObj.GID; visibleDefault = templObj.Visible; propertiesDefault = templObj.Properties; ellipseDefault = templObj is EllipseObject; @@ -123,6 +122,25 @@ internal partial class Tmj 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 @@ -135,7 +153,6 @@ internal partial class Tmj Width = width, Height = height, Rotation = rotation, - GID = gid, Visible = visible, Template = template, Properties = properties @@ -154,7 +171,6 @@ internal partial class Tmj Width = width, Height = height, Rotation = rotation, - GID = gid, Visible = visible, Template = template, Properties = properties @@ -173,7 +189,6 @@ internal partial class Tmj Width = width, Height = height, Rotation = rotation, - GID = gid, Visible = visible, Template = template, Properties = properties, @@ -193,7 +208,6 @@ internal partial class Tmj Width = width, Height = height, Rotation = rotation, - GID = gid, Visible = visible, Template = template, Properties = properties, @@ -211,7 +225,6 @@ internal partial class Tmj text.Width = width; text.Height = height; text.Rotation = rotation; - text.GID = gid; text.Visible = visible; text.Template = template; text.Properties = properties; @@ -228,7 +241,6 @@ internal partial class Tmj Width = width, Height = height, Rotation = rotation, - GID = gid, Visible = visible, Template = template, Properties = properties diff --git a/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs index 2367974..fa80805 100644 --- a/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs @@ -105,7 +105,6 @@ internal partial class Tmx widthDefault = templObj.Width; heightDefault = templObj.Height; rotationDefault = templObj.Rotation; - gidDefault = templObj.GID; visibleDefault = templObj.Visible; propertiesDefault = templObj.Properties; } @@ -137,12 +136,19 @@ internal partial class Tmx _ => throw new Exception($"Unknown object marker '{elementName}'") }); + if (gid is not null) + { + obj = new TileObject { ID = id, GID = gid.Value }; + reader.Skip(); + } + if (obj is null) { obj = new RectangleObject { ID = id }; reader.Skip(); } + obj.ID = id; obj.Name = name; obj.Type = type; obj.X = x; @@ -150,7 +156,6 @@ internal partial class Tmx obj.Width = width; obj.Height = height; obj.Rotation = rotation; - obj.GID = gid; obj.Visible = visible; obj.Template = template; obj.Properties = properties; diff --git a/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs index 78096e3..6fc64fb 100644 --- a/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ b/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs @@ -74,8 +74,8 @@ internal partial class Tmx 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;