diff --git a/src/DotTiled.Benchmark/Program.cs b/src/DotTiled.Benchmark/Program.cs index bf67fd0..3cd6e15 100644 --- a/src/DotTiled.Benchmark/Program.cs +++ b/src/DotTiled.Benchmark/Program.cs @@ -39,7 +39,7 @@ namespace DotTiled.Benchmark { using var stringReader = new StringReader(_tmxContents); using var xmlReader = XmlReader.Create(stringReader); - using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), []); + using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException()); return mapReader.ReadMap(); } diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index 6984b79..358029b 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -91,7 +91,7 @@ public static partial class DotTiledAssert AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); Assert.NotNull(actual.Tilesets); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); diff --git a/src/DotTiled.Tests/Assert/AssertProperties.cs b/src/DotTiled.Tests/Assert/AssertProperties.cs index 21fa639..84af365 100644 --- a/src/DotTiled.Tests/Assert/AssertProperties.cs +++ b/src/DotTiled.Tests/Assert/AssertProperties.cs @@ -45,4 +45,14 @@ public static partial class DotTiledAssert AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType"); AssertProperties(expected.Value, actual.Value); } + + private static void AssertProperty(EnumProperty expected, EnumProperty actual) + { + AssertEqual(expected.PropertyType, actual.PropertyType, "EnumProperty.PropertyType"); + AssertEqual(expected.Value.Count, actual.Value.Count, "EnumProperty.Value.Count"); + foreach (var value in expected.Value) + { + Assert.Contains(actual.Value, v => v == value); + } + } } diff --git a/src/DotTiled.Tests/Assert/AssertTileset.cs b/src/DotTiled.Tests/Assert/AssertTileset.cs index 134cc30..befc79a 100644 --- a/src/DotTiled.Tests/Assert/AssertTileset.cs +++ b/src/DotTiled.Tests/Assert/AssertTileset.cs @@ -141,9 +141,9 @@ public static partial class DotTiledAssert 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); + AssertProperties(expected.Properties, actual.Properties); + AssertImage(expected.Image, actual.Image); + AssertLayer((BaseLayer?)expected.ObjectLayer, (BaseLayer?)actual.ObjectLayer); if (expected.Animation is not null) { Assert.NotNull(actual.Animation); diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs index ea0575e..9a965c2 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -69,6 +69,30 @@ public partial class TestData new ObjectProperty { Name = "objectinclass", Value = 0 }, new StringProperty { Name = "stringinclass", Value = "This is a set string" } ] + }, + new EnumProperty + { + Name = "customenumstringprop", + PropertyType = "CustomEnumString", + Value = new HashSet { "CustomEnumString_2" } + }, + new EnumProperty + { + Name = "customenumstringflagsprop", + PropertyType = "CustomEnumStringFlags", + Value = new HashSet { "CustomEnumStringFlags_1", "CustomEnumStringFlags_2" } + }, + new EnumProperty + { + Name = "customenumintprop", + PropertyType = "CustomEnumInt", + Value = new HashSet { "CustomEnumInt_4" } + }, + new EnumProperty + { + Name = "customenumintflagsprop", + PropertyType = "CustomEnumIntFlags", + Value = new HashSet { "CustomEnumIntFlags_2", "CustomEnumIntFlags_3" } } ] }; @@ -116,6 +140,50 @@ public partial class TestData Value = "" } ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumString", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = false, + Values = [ + "CustomEnumString_1", + "CustomEnumString_2", + "CustomEnumString_3" + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumStringFlags", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = true, + Values = [ + "CustomEnumStringFlags_1", + "CustomEnumStringFlags_2" + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumInt", + StorageType = CustomEnumStorageType.Int, + ValueAsFlags = false, + Values = [ + "CustomEnumInt_1", + "CustomEnumInt_2", + "CustomEnumInt_3", + "CustomEnumInt_4", + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumIntFlags", + StorageType = CustomEnumStorageType.Int, + ValueAsFlags = true, + Values = [ + "CustomEnumIntFlags_1", + "CustomEnumIntFlags_2", + "CustomEnumIntFlags_3" + ] } ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj index a8c7f43..74f892b 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj @@ -32,6 +32,30 @@ "floatinclass":13.37, "stringinclass":"This is a set string" } + }, + { + "name":"customenumintflagsprop", + "propertytype":"CustomEnumIntFlags", + "type":"int", + "value":6 + }, + { + "name":"customenumintprop", + "propertytype":"CustomEnumInt", + "type":"int", + "value":3 + }, + { + "name":"customenumstringflagsprop", + "propertytype":"CustomEnumStringFlags", + "type":"string", + "value":"CustomEnumStringFlags_1,CustomEnumStringFlags_2" + }, + { + "name":"customenumstringprop", + "propertytype":"CustomEnumString", + "type":"string", + "value":"CustomEnumString_2" }], "renderorder":"right-down", "tiledversion":"1.11.0", diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx index c364577..cadc2fa 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx @@ -8,6 +8,10 @@ + + + + diff --git a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index fb825ed..748d4e3 100644 --- a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -20,16 +20,20 @@ public partial class TmxMapReaderTests Template ResolveTemplate(string source) { using var xmlTemplateReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); - using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return templateReader.ReadTemplate(); } Tileset ResolveTileset(string source) { using var xmlTilesetReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); - using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType); // Act var map = mapReader.ReadMap(); diff --git a/src/DotTiled/Model/Properties/EnumProperty.cs b/src/DotTiled/Model/Properties/EnumProperty.cs new file mode 100644 index 0000000..19e1b1f --- /dev/null +++ b/src/DotTiled/Model/Properties/EnumProperty.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotTiled.Model; + +/// +/// Represents an enum property. +/// +public class EnumProperty : IProperty> +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => Model.PropertyType.Enum; + + /// + /// The type of the class property. This will be the name of a custom defined + /// type in Tiled. + /// + public required string PropertyType { get; set; } + + /// + /// The value of the enum property. + /// + public required ISet Value { get; set; } + + /// + public IProperty Clone() => new EnumProperty + { + Name = Name, + PropertyType = PropertyType, + Value = Value.ToHashSet() + }; + + /// + /// Determines whether the enum property is equal to the specified value. + /// For enums which have multiple values (e.g. flag enums), this method will only return true if it is the only value. + /// + /// The value to check. + /// True if the enum property is equal to the specified value; otherwise, false. + public bool IsValue(string value) => Value.Contains(value) && Value.Count == 1; + + /// + /// Determines whether the enum property has the specified value. This method is very similar to the common method. + /// + /// The value to check. + /// True if the enum property has the specified value as one of its values; otherwise, false. + public bool HasValue(string value) => Value.Contains(value); +} diff --git a/src/DotTiled/Model/Properties/PropertyType.cs b/src/DotTiled/Model/Properties/PropertyType.cs index d6057cc..451ca5e 100644 --- a/src/DotTiled/Model/Properties/PropertyType.cs +++ b/src/DotTiled/Model/Properties/PropertyType.cs @@ -43,5 +43,10 @@ public enum PropertyType /// /// A class property. /// - Class + Class, + + /// + /// An enum property. + /// + Enum } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs deleted file mode 100644 index 4ce03a4..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Map ReadMap( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var version = reader.GetRequiredAttribute("version"); - var tiledVersion = reader.GetRequiredAttribute("tiledversion"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var orientation = reader.GetRequiredAttributeEnum("orientation", s => s switch - { - "orthogonal" => MapOrientation.Orthogonal, - "isometric" => MapOrientation.Isometric, - "staggered" => MapOrientation.Staggered, - "hexagonal" => MapOrientation.Hexagonal, - _ => throw new InvalidOperationException($"Unknown orientation '{s}'") - }); - var renderOrder = reader.GetOptionalAttributeEnum("renderorder", s => s switch - { - "right-down" => RenderOrder.RightDown, - "right-up" => RenderOrder.RightUp, - "left-down" => RenderOrder.LeftDown, - "left-up" => RenderOrder.LeftUp, - _ => throw new InvalidOperationException($"Unknown render order '{s}'") - }) ?? RenderOrder.RightDown; - var compressionLevel = reader.GetOptionalAttributeParseable("compressionlevel") ?? -1; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - var tileWidth = reader.GetRequiredAttributeParseable("tilewidth"); - var tileHeight = reader.GetRequiredAttributeParseable("tileheight"); - var hexSideLength = reader.GetOptionalAttributeParseable("hexsidelength"); - var staggerAxis = reader.GetOptionalAttributeEnum("staggeraxis", s => s switch - { - "x" => StaggerAxis.X, - "y" => StaggerAxis.Y, - _ => throw new InvalidOperationException($"Unknown stagger axis '{s}'") - }); - var staggerIndex = reader.GetOptionalAttributeEnum("staggerindex", s => s switch - { - "odd" => StaggerIndex.Odd, - "even" => StaggerIndex.Even, - _ => throw new InvalidOperationException($"Unknown stagger index '{s}'") - }); - var parallaxOriginX = reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; - var parallaxOriginY = reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; - var backgroundColor = reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); - var nextLayerID = reader.GetRequiredAttributeParseable("nextlayerid"); - var nextObjectID = reader.GetRequiredAttributeParseable("nextobjectid"); - var infinite = (reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; - - // At most one of - List? properties = null; - - // Any number of - List layers = []; - List tilesets = []; - - reader.ProcessChildren("map", (r, elementName) => elementName switch - { - "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 - }); - - 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/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs deleted file mode 100644 index 2e288a5..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static List ReadProperties( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - return reader.ReadList("properties", "property", (r) => - { - var name = r.GetRequiredAttribute("name"); - var type = r.GetOptionalAttributeEnum("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 XmlException("Invalid property type") - }) ?? PropertyType.String; - - IProperty property = type switch - { - PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") }, - PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - 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, customTypeDefinitions), - _ => throw new XmlException("Invalid property type") - }; - return property; - }); - } - - internal static ClassProperty ReadClassProperty( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - var name = reader.GetRequiredAttribute("name"); - var propertyType = reader.GetRequiredAttribute("propertytype"); - - var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - if (customTypeDef is CustomClassDefinition ccd) - { - reader.ReadStartElement("property"); - var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); - var props = ReadProperties(reader, customTypeDefinitions); - var mergedProps = Helpers.MergeProperties(propsInType, props); - - reader.ReadEndElement(); - return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; - } - - throw new XmlException($"Unkonwn custom class definition: {propertyType}"); - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs deleted file mode 100644 index 8605ade..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static TileLayer ReadTileLayer( - XmlReader reader, - bool dataUsesChunks, - IReadOnlyCollection customTypeDefinitions) - { - 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.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("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; - - List? properties = null; - Data? data = null; - - reader.ProcessChildren("layer", (r, elementName) => elementName switch - { - "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new TileLayer - { - 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, - Data = data, - Properties = properties ?? [] - }; - } - - internal static ImageLayer ReadImageLayer( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - 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 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 repeatX = (reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; - var repeatY = (reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; - - List? properties = null; - Image? image = null; - - reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch - { - "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new ImageLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Properties = properties ?? [], - Image = image, - RepeatX = repeatX, - 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; - - List? 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/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs deleted file mode 100644 index 84ccd24..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Xml; -using DotTiled.Model; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Tileset ReadTileset( - XmlReader reader, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var version = reader.GetOptionalAttribute("version"); - var tiledVersion = reader.GetOptionalAttribute("tiledversion"); - var firstGID = reader.GetOptionalAttributeParseable("firstgid"); - var source = reader.GetOptionalAttribute("source"); - var name = reader.GetOptionalAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tileWidth = reader.GetOptionalAttributeParseable("tilewidth"); - var tileHeight = reader.GetOptionalAttributeParseable("tileheight"); - 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 - { - "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 InvalidOperationException($"Unknown object alignment '{s}'") - }) ?? ObjectAlignment.Unspecified; - var renderSize = reader.GetOptionalAttributeEnum("rendersize", s => s switch - { - "tile" => TileRenderSize.Tile, - "grid" => TileRenderSize.Grid, - _ => throw new InvalidOperationException($"Unknown render size '{s}'") - }) ?? TileRenderSize.Tile; - var fillMode = reader.GetOptionalAttributeEnum("fillmode", s => s switch - { - "stretch" => FillMode.Stretch, - "preserve-aspect-fit" => FillMode.PreserveAspectFit, - _ => throw new InvalidOperationException($"Unknown fill mode '{s}'") - }) ?? FillMode.Stretch; - - // Elements - Image? image = null; - TileOffset? tileOffset = null; - Grid? grid = null; - List? properties = null; - List? wangsets = null; - Transformations? transformations = null; - List tiles = []; - - reader.ProcessChildren("tileset", (r, elementName) => elementName switch - { - "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, customTypeDefinitions), "Properties"), - "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r, customTypeDefinitions), "Wangsets"), - "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"), - "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - // Check if tileset is referring to external file - if (source is not null) - { - 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 = source; - return resolvedTileset; - } - - return new Tileset - { - Version = version, - TiledVersion = tiledVersion, - FirstGID = firstGID, - Source = source, - Name = name, - Class = @class, - TileWidth = tileWidth, - TileHeight = tileHeight, - Spacing = spacing, - Margin = margin, - TileCount = tileCount, - Columns = columns, - ObjectAlignment = objectAlignment, - RenderSize = renderSize, - FillMode = fillMode, - Image = image, - TileOffset = tileOffset, - Grid = grid, - Properties = properties ?? [], - Wangsets = wangsets, - Transformations = transformations, - Tiles = tiles - }; - } - - internal static Image ReadImage(XmlReader reader) - { - // Attributes - var format = reader.GetOptionalAttributeEnum("format", s => s switch - { - "png" => ImageFormat.Png, - "jpg" => ImageFormat.Jpg, - "bmp" => ImageFormat.Bmp, - "gif" => ImageFormat.Gif, - _ => throw new InvalidOperationException($"Unknown image format '{s}'") - }); - var source = reader.GetOptionalAttribute("source"); - var transparentColor = reader.GetOptionalAttributeClass("trans"); - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - - reader.ProcessChildren("image", (r, elementName) => elementName switch - { - "data" => throw new NotSupportedException("Embedded image data is not supported."), - _ => r.Skip - }); - - if (format is null && source is not null) - format = ParseImageFormatFromSource(source); - - return new Image - { - Format = format, - Source = source, - TransparentColor = transparentColor, - Width = width, - Height = height, - }; - } - - - 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; - var y = reader.GetOptionalAttributeParseable("y") ?? 0f; - - reader.ReadStartElement("tileoffset"); - return new TileOffset { X = x, Y = y }; - } - - internal static Grid ReadGrid(XmlReader reader) - { - // Attributes - var orientation = reader.GetOptionalAttributeEnum("orientation", s => s switch - { - "orthogonal" => GridOrientation.Orthogonal, - "isometric" => GridOrientation.Isometric, - _ => throw new InvalidOperationException($"Unknown orientation '{s}'") - }) ?? GridOrientation.Orthogonal; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - - reader.ReadStartElement("grid"); - return new Grid { Orientation = orientation, Width = width, Height = height }; - } - - internal static Transformations ReadTransformations(XmlReader reader) - { - // Attributes - 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 }; - } - - internal static Tile ReadTile( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var type = reader.GetOptionalAttribute("type") ?? ""; - var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - - // Elements - List? properties = null; - Image? image = null; - ObjectLayer? objectLayer = null; - List? animation = null; - - reader.ProcessChildren("tile", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions), "ObjectLayer"), - "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => - { - var tileID = ar.GetRequiredAttributeParseable("tileid"); - var duration = ar.GetRequiredAttributeParseable("duration"); - return new Frame { TileID = tileID, Duration = duration }; - }), "Animation"), - _ => r.Skip - }); - - return new Tile - { - ID = id, - Type = type, - Probability = probability, - X = x, - Y = y, - Width = width ?? image?.Width ?? 0, - Height = height ?? image?.Height ?? 0, - Properties = properties ?? [], - Image = image, - ObjectLayer = objectLayer, - Animation = animation - }; - } - - internal static List ReadWangsets( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) => - reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); - - internal static Wangset ReadWangset( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var name = reader.GetRequiredAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tile = reader.GetRequiredAttributeParseable("tile"); - - // Elements - List? properties = null; - List wangColors = []; - List wangTiles = []; - - reader.ProcessChildren("wangset", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)), - "wangtile" => () => wangTiles.Add(ReadWangTile(r)), - _ => r.Skip - }); - - if (wangColors.Count > 254) - throw new ArgumentException("Wangset can have at most 254 Wang colors."); - - return new Wangset - { - Name = name, - Class = @class, - Tile = tile, - Properties = properties ?? [], - WangColors = wangColors, - WangTiles = wangTiles - }; - } - - 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 probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; - - // Elements - List? properties = null; - - reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new WangColor - { - Name = name, - Class = @class, - Color = color, - Tile = tile, - Probability = probability, - Properties = properties ?? [] - }; - } - - internal static WangTile ReadWangTile(XmlReader reader) - { - // Attributes - var tileID = reader.GetRequiredAttributeParseable("tileid"); - var wangID = reader.GetRequiredAttributeParseable("wangid", s => - { - // Comma-separated list of indices (0-254) - var indices = s.Split(',').Select(i => byte.Parse(i, CultureInfo.InvariantCulture)).ToArray(); - if (indices.Length > 8) - throw new ArgumentException("Wang ID can have at most 8 indices."); - return indices; - }); - - reader.ReadStartElement("wangtile"); - - return new WangTile - { - TileID = tileID, - WangID = wangID - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs index 279c83d..e90caa1 100644 --- a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A map reader for the Tiled XML format. /// -public class TmxMapReader : IMapReader +public class TmxMapReader : TmxReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled map in the Tiled XML format. - /// A function that resolves external tilesets given their source. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// 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(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - public Map ReadMap() => 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); - GC.SuppressFinalize(this); - } + public new Map ReadMap() => base.ReadMap(); } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs similarity index 59% rename from src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs index b42ebb0..73c8052 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs @@ -1,27 +1,26 @@ -using System.Xml; using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression) + internal Chunk ReadChunk(DataEncoding? encoding, DataCompression? compression) { - var x = reader.GetRequiredAttributeParseable("x"); - var y = reader.GetRequiredAttributeParseable("y"); - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); + var x = _reader.GetRequiredAttributeParseable("x"); + var y = _reader.GetRequiredAttributeParseable("y"); + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); var usesTileChildrenInsteadOfRawData = encoding is null; if (usesTileChildrenInsteadOfRawData) { - var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", reader); + var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", _reader); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; } else { - var globalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var globalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs similarity index 88% rename from src/DotTiled/Serialization/Tmx/Tmx.Data.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs index e1b6111..254c1d3 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs @@ -8,17 +8,17 @@ using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static Data ReadData(XmlReader reader, bool usesChunks) + internal Data ReadData(bool usesChunks) { - var encoding = reader.GetOptionalAttributeEnum("encoding", e => e switch + var encoding = _reader.GetOptionalAttributeEnum("encoding", e => e switch { "csv" => DataEncoding.Csv, "base64" => DataEncoding.Base64, _ => throw new XmlException("Invalid encoding") }); - var compression = reader.GetOptionalAttributeEnum("compression", c => c switch + var compression = _reader.GetOptionalAttributeEnum("compression", c => c switch { "gzip" => DataCompression.GZip, "zlib" => DataCompression.ZLib, @@ -28,8 +28,8 @@ internal partial class Tmx if (usesChunks) { - var chunks = reader - .ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression)) + var chunks = _reader + .ReadList("data", "chunk", (r) => ReadChunk(encoding, compression)) .ToArray(); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks }; } @@ -37,12 +37,12 @@ internal partial class Tmx var usesTileChildrenInsteadOfRawData = encoding is null && compression is null; if (usesTileChildrenInsteadOfRawData) { - var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", reader); + var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", _reader); var (tileChildrenGlobalTileIDs, tileChildrenFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(tileChildrenGlobalTileIDsWithFlippingFlags); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = tileChildrenGlobalTileIDs, FlippingFlags = tileChildrenFlippingFlags, Chunks = null }; } - var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression); var (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null }; } diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs new file mode 100644 index 0000000..89f0c9b --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase +{ + internal Map ReadMap() + { + // Attributes + var version = _reader.GetRequiredAttribute("version"); + var tiledVersion = _reader.GetRequiredAttribute("tiledversion"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var orientation = _reader.GetRequiredAttributeEnum("orientation", s => s switch + { + "orthogonal" => MapOrientation.Orthogonal, + "isometric" => MapOrientation.Isometric, + "staggered" => MapOrientation.Staggered, + "hexagonal" => MapOrientation.Hexagonal, + _ => throw new InvalidOperationException($"Unknown orientation '{s}'") + }); + var renderOrder = _reader.GetOptionalAttributeEnum("renderorder", s => s switch + { + "right-down" => RenderOrder.RightDown, + "right-up" => RenderOrder.RightUp, + "left-down" => RenderOrder.LeftDown, + "left-up" => RenderOrder.LeftUp, + _ => throw new InvalidOperationException($"Unknown render order '{s}'") + }) ?? RenderOrder.RightDown; + var compressionLevel = _reader.GetOptionalAttributeParseable("compressionlevel") ?? -1; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + var tileWidth = _reader.GetRequiredAttributeParseable("tilewidth"); + var tileHeight = _reader.GetRequiredAttributeParseable("tileheight"); + var hexSideLength = _reader.GetOptionalAttributeParseable("hexsidelength"); + var staggerAxis = _reader.GetOptionalAttributeEnum("staggeraxis", s => s switch + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new InvalidOperationException($"Unknown stagger axis '{s}'") + }); + var staggerIndex = _reader.GetOptionalAttributeEnum("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new InvalidOperationException($"Unknown stagger index '{s}'") + }); + var parallaxOriginX = _reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; + var parallaxOriginY = _reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; + var backgroundColor = _reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); + var nextLayerID = _reader.GetRequiredAttributeParseable("nextlayerid"); + var nextObjectID = _reader.GetRequiredAttributeParseable("nextobjectid"); + var infinite = (_reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; + + // At most one of + List? properties = null; + + // Any number of + List layers = []; + List tilesets = []; + + _reader.ProcessChildren("map", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "tileset" => () => tilesets.Add(ReadTileset()), + "layer" => () => layers.Add(ReadTileLayer(infinite)), + "objectgroup" => () => layers.Add(ReadObjectLayer()), + "imagelayer" => () => layers.Add(ReadImageLayer()), + "group" => () => layers.Add(ReadGroup()), + _ => r.Skip + }); + + 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/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs similarity index 58% rename from src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs index 2ce3ca3..a680544 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs @@ -3,35 +3,31 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; -using System.Xml; using DotTiled.Model; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static ObjectLayer ReadObjectLayer( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal ObjectLayer ReadObjectLayer() { // 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 + 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, @@ -42,10 +38,10 @@ internal partial class Tmx List? properties = null; List objects = []; - reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch + _reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "object" => () => objects.Add(ReadObject()), _ => r.Skip }); @@ -72,16 +68,13 @@ internal partial class Tmx }; } - internal static Model.Object ReadObject( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Model.Object ReadObject() { // Attributes - var template = reader.GetOptionalAttribute("template"); + var template = _reader.GetOptionalAttribute("template"); Model.Object? obj = null; if (template is not null) - obj = externalTemplateResolver(template).Object; + obj = _externalTemplateResolver(template).Object; uint? idDefault = obj?.ID ?? null; string nameDefault = obj?.Name ?? ""; @@ -95,30 +88,30 @@ internal partial class Tmx bool visibleDefault = obj?.Visible ?? true; List? 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; + 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 Model.Object? foundObject = null; int propertiesCounter = 0; List? properties = propertiesDefault; - reader.ProcessChildren("object", (r, elementName) => elementName switch + _reader.ProcessChildren("object", (r, elementName) => elementName switch { - "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)).ToList(), "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"), + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties()).ToList(), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(), "Object marker"), _ => throw new InvalidOperationException($"Unknown object marker '{elementName}'") }); @@ -169,26 +162,26 @@ internal partial class Tmx return OverrideObject((dynamic)obj, (dynamic)foundObject); } - internal static EllipseObject ReadEllipseObject(XmlReader reader) + internal EllipseObject ReadEllipseObject() { - reader.Skip(); + _reader.Skip(); return new EllipseObject { }; } internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject _) => obj; - internal static PointObject ReadPointObject(XmlReader reader) + internal PointObject ReadPointObject() { - reader.Skip(); + _reader.Skip(); return new PointObject { }; } internal static PointObject OverrideObject(PointObject obj, PointObject _) => obj; - internal static PolygonObject ReadPolygonObject(XmlReader reader) + internal PolygonObject ReadPolygonObject() { // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => + var points = _reader.GetRequiredAttributeParseable>("points", s => { // Takes on format "x1,y1 x2,y2 x3,y3 ..." var coords = s.Split(' '); @@ -199,7 +192,7 @@ internal partial class Tmx }).ToList(); }); - reader.ReadStartElement("polygon"); + _reader.ReadStartElement("polygon"); return new PolygonObject { Points = points }; } @@ -209,10 +202,10 @@ internal partial class Tmx return obj; } - internal static PolylineObject ReadPolylineObject(XmlReader reader) + internal PolylineObject ReadPolylineObject() { // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => + var points = _reader.GetRequiredAttributeParseable>("points", s => { // Takes on format "x1,y1 x2,y2 x3,y3 ..." var coords = s.Split(' '); @@ -223,7 +216,7 @@ internal partial class Tmx }).ToList(); }); - reader.ReadStartElement("polyline"); + _reader.ReadStartElement("polyline"); return new PolylineObject { Points = points }; } @@ -233,19 +226,19 @@ internal partial class Tmx return obj; } - internal static TextObject ReadTextObject(XmlReader reader) + internal TextObject ReadTextObject() { // 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 + 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, @@ -253,7 +246,7 @@ internal partial class Tmx "justify" => TextHorizontalAlignment.Justify, _ => throw new InvalidOperationException($"Unknown horizontal alignment '{s}'") }) ?? TextHorizontalAlignment.Left; - var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch + var vAlign = _reader.GetOptionalAttributeEnum("valign", s => s switch { "top" => TextVerticalAlignment.Top, "center" => TextVerticalAlignment.Center, @@ -262,7 +255,7 @@ internal partial class Tmx }) ?? TextVerticalAlignment.Top; // Elements - var text = reader.ReadElementContentAsString("text", ""); + var text = _reader.ReadElementContentAsString("text", ""); return new TextObject { @@ -304,11 +297,7 @@ internal partial class Tmx return obj; } - internal static Template ReadTemplate( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Template ReadTemplate() { // No attributes @@ -318,10 +307,10 @@ internal partial class Tmx // Should contain exactly one of Model.Object? obj = null; - reader.ProcessChildren("template", (r, elementName) => elementName switch + _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"), + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(), "Object"), _ => r.Skip }); diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs new file mode 100644 index 0000000..1335d75 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal List ReadProperties() + { + return _reader.ReadList("properties", "property", (r) => + { + var name = r.GetRequiredAttribute("name"); + var type = r.GetOptionalAttributeEnum("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 XmlException("Invalid property type") + }) ?? PropertyType.String; + var propertyType = r.GetOptionalAttribute("propertytype"); + if (propertyType is not null) + { + return ReadPropertyWithCustomType(); + } + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + 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(), + _ => throw new XmlException("Invalid property type") + }; + return property; + }); + } + + internal IProperty ReadPropertyWithCustomType() + { + var isClass = _reader.GetOptionalAttribute("type") == "class"; + + if (isClass) + { + return ReadClassProperty(); + } + + return ReadEnumProperty(); + } + + internal ClassProperty ReadClassProperty() + { + var name = _reader.GetRequiredAttribute("name"); + var propertyType = _reader.GetRequiredAttribute("propertytype"); + + var customTypeDef = _customTypeResolver(propertyType); + if (customTypeDef is CustomClassDefinition ccd) + { + _reader.ReadStartElement("property"); + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd); + var props = ReadProperties(); + var mergedProps = Helpers.MergeProperties(propsInType, props); + + _reader.ReadEndElement(); + return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps }; + } + + throw new XmlException($"Unkonwn custom class definition: {propertyType}"); + } + + internal EnumProperty ReadEnumProperty() + { + var name = _reader.GetRequiredAttribute("name"); + var propertyType = _reader.GetRequiredAttribute("propertytype"); + var typeInXml = _reader.GetOptionalAttributeEnum("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + _ => throw new XmlException("Invalid property type") + }) ?? PropertyType.String; + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is not CustomEnumDefinition ced) + throw new XmlException($"Unknown custom enum definition: {propertyType}. Enums must be defined"); + + if (ced.StorageType == CustomEnumStorageType.String) + { + var value = _reader.GetRequiredAttribute("value"); + if (value.Contains(',') && !ced.ValueAsFlags) + throw new XmlException("Enum value must not contain ',' if not ValueAsFlags is set to true."); + + if (ced.ValueAsFlags) + { + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = _reader.GetRequiredAttributeParseable("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + for (var i = 0; i < allValues.Count; i++) + { + var mask = 1 << i; + if ((value & mask) == mask) + { + var enumValue = allValues[i]; + _ = enumValues.Add(enumValue); + } + } + return new EnumProperty { Name = name, PropertyType = propertyType, Value = enumValues }; + } + else + { + var allValues = ced.Values; + var enumValue = allValues[value]; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { enumValue } }; + } + } + + throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}"); + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs new file mode 100644 index 0000000..f69a739 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.Linq; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal TileLayer ReadTileLayer(bool dataUsesChunks) + { + 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.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("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; + + List? properties = null; + Data? data = null; + + _reader.ProcessChildren("layer", (r, elementName) => elementName switch + { + "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(dataUsesChunks), "Data"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new TileLayer + { + 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, + Data = data, + Properties = properties ?? [] + }; + } + + internal ImageLayer ReadImageLayer() + { + 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 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 repeatX = (_reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; + var repeatY = (_reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; + + List? properties = null; + Image? image = null; + + _reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new ImageLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties ?? [], + Image = image, + RepeatX = repeatX, + RepeatY = repeatY + }; + } + + internal Group ReadGroup() + { + 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; + + List? properties = null; + List layers = []; + + _reader.ProcessChildren("group", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "layer" => () => layers.Add(ReadTileLayer(false)), + "objectgroup" => () => layers.Add(ReadObjectLayer()), + "imagelayer" => () => layers.Add(ReadImageLayer()), + "group" => () => layers.Add(ReadGroup()), + _ => 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/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs new file mode 100644 index 0000000..72e66e9 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal Tileset ReadTileset() + { + // Attributes + var version = _reader.GetOptionalAttribute("version"); + var tiledVersion = _reader.GetOptionalAttribute("tiledversion"); + var firstGID = _reader.GetOptionalAttributeParseable("firstgid"); + var source = _reader.GetOptionalAttribute("source"); + var name = _reader.GetOptionalAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tileWidth = _reader.GetOptionalAttributeParseable("tilewidth"); + var tileHeight = _reader.GetOptionalAttributeParseable("tileheight"); + 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 + { + "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 InvalidOperationException($"Unknown object alignment '{s}'") + }) ?? ObjectAlignment.Unspecified; + var renderSize = _reader.GetOptionalAttributeEnum("rendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new InvalidOperationException($"Unknown render size '{s}'") + }) ?? TileRenderSize.Tile; + var fillMode = _reader.GetOptionalAttributeEnum("fillmode", s => s switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new InvalidOperationException($"Unknown fill mode '{s}'") + }) ?? FillMode.Stretch; + + // Elements + Image? image = null; + TileOffset? tileOffset = null; + Grid? grid = null; + List? properties = null; + List? wangsets = null; + Transformations? transformations = null; + List tiles = []; + + _reader.ProcessChildren("tileset", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(), "TileOffset"), + "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(), "Grid"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(), "Wangsets"), + "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(), "Transformations"), + "tile" => () => tiles.Add(ReadTile()), + _ => r.Skip + }); + + // Check if tileset is referring to external file + if (source is not null) + { + var resolvedTileset = _externalTilesetResolver(source); + resolvedTileset.FirstGID = firstGID; + resolvedTileset.Source = source; + return resolvedTileset; + } + + return new Tileset + { + Version = version, + TiledVersion = tiledVersion, + FirstGID = firstGID, + Source = source, + Name = name, + Class = @class, + TileWidth = tileWidth, + TileHeight = tileHeight, + Spacing = spacing, + Margin = margin, + TileCount = tileCount, + Columns = columns, + ObjectAlignment = objectAlignment, + RenderSize = renderSize, + FillMode = fillMode, + Image = image, + TileOffset = tileOffset, + Grid = grid, + Properties = properties ?? [], + Wangsets = wangsets, + Transformations = transformations, + Tiles = tiles + }; + } + + internal Image ReadImage() + { + // Attributes + var format = _reader.GetOptionalAttributeEnum("format", s => s switch + { + "png" => ImageFormat.Png, + "jpg" => ImageFormat.Jpg, + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, + _ => throw new InvalidOperationException($"Unknown image format '{s}'") + }); + var source = _reader.GetOptionalAttribute("source"); + var transparentColor = _reader.GetOptionalAttributeClass("trans"); + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + + _reader.ProcessChildren("image", (r, elementName) => elementName switch + { + "data" => throw new NotSupportedException("Embedded image data is not supported."), + _ => r.Skip + }); + + if (format is null && source is not null) + format = Helpers.ParseImageFormatFromSource(source); + + return new Image + { + Format = format, + Source = source, + TransparentColor = transparentColor, + Width = width, + Height = height, + }; + } + + internal TileOffset ReadTileOffset() + { + // Attributes + var x = _reader.GetOptionalAttributeParseable("x") ?? 0f; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0f; + + _reader.ReadStartElement("tileoffset"); + return new TileOffset { X = x, Y = y }; + } + + internal Grid ReadGrid() + { + // Attributes + var orientation = _reader.GetOptionalAttributeEnum("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new InvalidOperationException($"Unknown orientation '{s}'") + }) ?? GridOrientation.Orthogonal; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + + _reader.ReadStartElement("grid"); + return new Grid { Orientation = orientation, Width = width, Height = height }; + } + + internal Transformations ReadTransformations() + { + // Attributes + 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 }; + } + + internal Tile ReadTile() + { + // Attributes + var id = _reader.GetRequiredAttributeParseable("id"); + var type = _reader.GetOptionalAttribute("type") ?? ""; + var probability = _reader.GetOptionalAttributeParseable("probability") ?? 0f; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + + // Elements + List? properties = null; + Image? image = null; + ObjectLayer? objectLayer = null; + List? animation = null; + + _reader.ProcessChildren("tile", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(), "ObjectLayer"), + "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => + { + var tileID = ar.GetRequiredAttributeParseable("tileid"); + var duration = ar.GetRequiredAttributeParseable("duration"); + return new Frame { TileID = tileID, Duration = duration }; + }), "Animation"), + _ => r.Skip + }); + + return new Tile + { + ID = id, + Type = type, + Probability = probability, + X = x, + Y = y, + Width = width ?? image?.Width ?? 0, + Height = height ?? image?.Height ?? 0, + Properties = properties ?? [], + Image = image, + ObjectLayer = objectLayer, + Animation = animation + }; + } + + internal List ReadWangsets() => + _reader.ReadList("wangsets", "wangset", r => ReadWangset()); + + internal Wangset ReadWangset() + { + // Attributes + var name = _reader.GetRequiredAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tile = _reader.GetRequiredAttributeParseable("tile"); + + // Elements + List? properties = null; + List wangColors = []; + List wangTiles = []; + + _reader.ProcessChildren("wangset", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "wangcolor" => () => wangColors.Add(ReadWangColor()), + "wangtile" => () => wangTiles.Add(ReadWangTile()), + _ => r.Skip + }); + + if (wangColors.Count > 254) + throw new ArgumentException("Wangset can have at most 254 Wang colors."); + + return new Wangset + { + Name = name, + Class = @class, + Tile = tile, + Properties = properties ?? [], + WangColors = wangColors, + WangTiles = wangTiles + }; + } + + internal WangColor ReadWangColor() + { + // Attributes + var name = _reader.GetRequiredAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var color = _reader.GetRequiredAttributeParseable("color"); + var tile = _reader.GetRequiredAttributeParseable("tile"); + var probability = _reader.GetOptionalAttributeParseable("probability") ?? 0f; + + // Elements + List? properties = null; + + _reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new WangColor + { + Name = name, + Class = @class, + Color = color, + Tile = tile, + Probability = probability, + Properties = properties ?? [] + }; + } + + internal WangTile ReadWangTile() + { + // Attributes + var tileID = _reader.GetRequiredAttributeParseable("tileid"); + var wangID = _reader.GetRequiredAttributeParseable("wangid", s => + { + // Comma-separated list of indices (0-254) + var indices = s.Split(',').Select(i => byte.Parse(i, CultureInfo.InvariantCulture)).ToArray(); + if (indices.Length > 8) + throw new ArgumentException("Wang ID can have at most 8 indices."); + return indices; + }); + + _reader.ReadStartElement("wangtile"); + + return new WangTile + { + TileID = tileID, + WangID = wangID + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs new file mode 100644 index 0000000..5851d76 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml; +using DotTiled.Model; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + /// + /// Constructs a new , which is the base class for all Tiled XML format readers. + /// + /// An XML reader for reading a Tiled map in the Tiled XML format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + protected TmxReaderBase( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + + // Prepare reader + _ = _reader.MoveToContent(); + } + + /// + 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 + // ~TmxReaderBase() + // { + // // 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/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs index 0b69d4c..176872b 100644 --- a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,68 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A tileset reader for the Tiled XML format. /// -public class TsxTilesetReader : ITilesetReader +public class TsxTilesetReader : TmxReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled tileset in the Tiled XML format. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// public TsxTilesetReader( XmlReader reader, + Func externalTilesetResolver, 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(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - 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) - _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 - // ~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); - GC.SuppressFinalize(this); - } + public new Tileset ReadTileset() => base.ReadTileset(); } diff --git a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs index ed7ba8e..1ff8445 100644 --- a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Xml; using DotTiled.Model; @@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx; /// /// A template reader for the Tiled XML format. /// -public class TxTemplateReader : ITemplateReader +public class TxTemplateReader : TmxReaderBase, ITemplateReader { - // Resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - /// /// Constructs a new . /// - /// An XML reader for reading a Tiled template in the Tiled XML format. - /// A function that resolves external tilesets given their source. - /// A function that resolves external templates given their source. - /// A collection of custom type definitions that can be used to resolve custom types when encountering . - /// Thrown when any of the arguments are null. + /// 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(); - } + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } /// - 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) - _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 - // ~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); - GC.SuppressFinalize(this); - } + public new Template ReadTemplate() => base.ReadTemplate(); }