Tmx reader is now a base abstract class

This commit is contained in:
Daniel Cronqvist 2024-08-25 17:43:13 +02:00
parent eda3fbe308
commit 11f1ef783e
25 changed files with 1059 additions and 960 deletions

View file

@ -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();
}

View file

@ -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");

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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<string> { "CustomEnumString_2" }
},
new EnumProperty
{
Name = "customenumstringflagsprop",
PropertyType = "CustomEnumStringFlags",
Value = new HashSet<string> { "CustomEnumStringFlags_1", "CustomEnumStringFlags_2" }
},
new EnumProperty
{
Name = "customenumintprop",
PropertyType = "CustomEnumInt",
Value = new HashSet<string> { "CustomEnumInt_4" }
},
new EnumProperty
{
Name = "customenumintflagsprop",
PropertyType = "CustomEnumIntFlags",
Value = new HashSet<string> { "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"
]
}
];
}

View file

@ -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",

View file

@ -8,6 +8,10 @@
<property name="stringinclass" value="This is a set string"/>
</properties>
</property>
<property name="customenumintflagsprop" type="int" propertytype="CustomEnumIntFlags" value="6"/>
<property name="customenumintprop" type="int" propertytype="CustomEnumInt" value="3"/>
<property name="customenumstringflagsprop" propertytype="CustomEnumStringFlags" value="CustomEnumStringFlags_1,CustomEnumStringFlags_2"/>
<property name="customenumstringprop" propertytype="CustomEnumString" value="CustomEnumString_2"/>
</properties>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">

View file

@ -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();

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
namespace DotTiled.Model;
/// <summary>
/// Represents an enum property.
/// </summary>
public class EnumProperty : IProperty<ISet<string>>
{
/// <inheritdoc/>
public required string Name { get; set; }
/// <inheritdoc/>
public PropertyType Type => Model.PropertyType.Enum;
/// <summary>
/// The type of the class property. This will be the name of a custom defined
/// type in Tiled.
/// </summary>
public required string PropertyType { get; set; }
/// <summary>
/// The value of the enum property.
/// </summary>
public required ISet<string> Value { get; set; }
/// <inheritdoc/>
public IProperty Clone() => new EnumProperty
{
Name = Name,
PropertyType = PropertyType,
Value = Value.ToHashSet()
};
/// <summary>
/// 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.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the enum property is equal to the specified value; otherwise, false.</returns>
public bool IsValue(string value) => Value.Contains(value) && Value.Count == 1;
/// <summary>
/// Determines whether the enum property has the specified value. This method is very similar to the common <see cref="System.Enum.HasFlag" /> method.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the enum property has the specified value as one of its values; otherwise, false.</returns>
public bool HasValue(string value) => Value.Contains(value);
}

View file

@ -43,5 +43,10 @@ public enum PropertyType
/// <summary>
/// A class property.
/// </summary>
Class
Class,
/// <summary>
/// An enum property.
/// </summary>
Enum
}

View file

@ -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<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var version = reader.GetRequiredAttribute("version");
var tiledVersion = reader.GetRequiredAttribute("tiledversion");
var @class = reader.GetOptionalAttribute("class") ?? "";
var orientation = reader.GetRequiredAttributeEnum<MapOrientation>("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>("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<int>("compressionlevel") ?? -1;
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
var tileWidth = reader.GetRequiredAttributeParseable<uint>("tilewidth");
var tileHeight = reader.GetRequiredAttributeParseable<uint>("tileheight");
var hexSideLength = reader.GetOptionalAttributeParseable<uint>("hexsidelength");
var staggerAxis = reader.GetOptionalAttributeEnum<StaggerAxis>("staggeraxis", s => s switch
{
"x" => StaggerAxis.X,
"y" => StaggerAxis.Y,
_ => throw new InvalidOperationException($"Unknown stagger axis '{s}'")
});
var staggerIndex = reader.GetOptionalAttributeEnum<StaggerIndex>("staggerindex", s => s switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new InvalidOperationException($"Unknown stagger index '{s}'")
});
var parallaxOriginX = reader.GetOptionalAttributeParseable<float>("parallaxoriginx") ?? 0.0f;
var parallaxOriginY = reader.GetOptionalAttributeParseable<float>("parallaxoriginy") ?? 0.0f;
var backgroundColor = reader.GetOptionalAttributeClass<Color>("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture);
var nextLayerID = reader.GetRequiredAttributeParseable<uint>("nextlayerid");
var nextObjectID = reader.GetRequiredAttributeParseable<uint>("nextobjectid");
var infinite = (reader.GetOptionalAttributeParseable<uint>("infinite") ?? 0) == 1;
// At most one of
List<IProperty>? properties = null;
// Any number of
List<BaseLayer> layers = [];
List<Tileset> 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
};
}
}

View file

@ -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<IProperty> ReadProperties(
XmlReader reader,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
return reader.ReadList("properties", "property", (r) =>
{
var name = r.GetRequiredAttribute("name");
var type = r.GetOptionalAttributeEnum<PropertyType>("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<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("value") },
PropertyType.Class => ReadClassProperty(r, customTypeDefinitions),
_ => throw new XmlException("Invalid property type")
};
return property;
});
}
internal static ClassProperty ReadClassProperty(
XmlReader reader,
IReadOnlyCollection<ICustomTypeDefinition> 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}");
}
}

View file

@ -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<ICustomTypeDefinition> customTypeDefinitions)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
List<IProperty>? 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<ICustomTypeDefinition> customTypeDefinitions)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var repeatX = (reader.GetOptionalAttributeParseable<uint>("repeatx") ?? 0) == 1;
var repeatY = (reader.GetOptionalAttributeParseable<uint>("repeaty") ?? 0) == 1;
List<IProperty>? 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<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
List<IProperty>? properties = null;
List<BaseLayer> 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
};
}
}

View file

@ -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<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var version = reader.GetOptionalAttribute("version");
var tiledVersion = reader.GetOptionalAttribute("tiledversion");
var firstGID = reader.GetOptionalAttributeParseable<uint>("firstgid");
var source = reader.GetOptionalAttribute("source");
var name = reader.GetOptionalAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var tileWidth = reader.GetOptionalAttributeParseable<uint>("tilewidth");
var tileHeight = reader.GetOptionalAttributeParseable<uint>("tileheight");
var spacing = reader.GetOptionalAttributeParseable<uint>("spacing") ?? 0;
var margin = reader.GetOptionalAttributeParseable<uint>("margin") ?? 0;
var tileCount = reader.GetOptionalAttributeParseable<uint>("tilecount");
var columns = reader.GetOptionalAttributeParseable<uint>("columns");
var objectAlignment = reader.GetOptionalAttributeEnum<ObjectAlignment>("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<TileRenderSize>("rendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,
_ => throw new InvalidOperationException($"Unknown render size '{s}'")
}) ?? TileRenderSize.Tile;
var fillMode = reader.GetOptionalAttributeEnum<FillMode>("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<IProperty>? properties = null;
List<Wangset>? wangsets = null;
Transformations? transformations = null;
List<Tile> 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<ImageFormat>("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<Color>("trans");
var width = reader.GetOptionalAttributeParseable<uint>("width");
var height = reader.GetOptionalAttributeParseable<uint>("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<float>("x") ?? 0f;
var y = reader.GetOptionalAttributeParseable<float>("y") ?? 0f;
reader.ReadStartElement("tileoffset");
return new TileOffset { X = x, Y = y };
}
internal static Grid ReadGrid(XmlReader reader)
{
// Attributes
var orientation = reader.GetOptionalAttributeEnum<GridOrientation>("orientation", s => s switch
{
"orthogonal" => GridOrientation.Orthogonal,
"isometric" => GridOrientation.Isometric,
_ => throw new InvalidOperationException($"Unknown orientation '{s}'")
}) ?? GridOrientation.Orthogonal;
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
reader.ReadStartElement("grid");
return new Grid { Orientation = orientation, Width = width, Height = height };
}
internal static Transformations ReadTransformations(XmlReader reader)
{
// Attributes
var hFlip = (reader.GetOptionalAttributeParseable<uint>("hflip") ?? 0) == 1;
var vFlip = (reader.GetOptionalAttributeParseable<uint>("vflip") ?? 0) == 1;
var rotate = (reader.GetOptionalAttributeParseable<uint>("rotate") ?? 0) == 1;
var preferUntransformed = (reader.GetOptionalAttributeParseable<uint>("preferuntransformed") ?? 0) == 1;
reader.ReadStartElement("transformations");
return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed };
}
internal static Tile ReadTile(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id");
var type = reader.GetOptionalAttribute("type") ?? "";
var probability = reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = reader.GetOptionalAttributeParseable<uint>("width");
var height = reader.GetOptionalAttributeParseable<uint>("height");
// Elements
List<IProperty>? properties = null;
Image? image = null;
ObjectLayer? objectLayer = null;
List<Frame>? 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<Frame>("animation", "frame", (ar) =>
{
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
var duration = ar.GetRequiredAttributeParseable<uint>("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<Wangset> ReadWangsets(
XmlReader reader,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions) =>
reader.ReadList<Wangset>("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions));
internal static Wangset ReadWangset(
XmlReader reader,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var tile = reader.GetRequiredAttributeParseable<int>("tile");
// Elements
List<IProperty>? properties = null;
List<WangColor> wangColors = [];
List<WangTile> 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<ICustomTypeDefinition> customTypeDefinitions)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var color = reader.GetRequiredAttributeParseable<Color>("color");
var tile = reader.GetRequiredAttributeParseable<int>("tile");
var probability = reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
// Elements
List<IProperty>? 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<uint>("tileid");
var wangID = reader.GetRequiredAttributeParseable<byte[]>("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
};
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Xml;
using DotTiled.Model;
@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx;
/// <summary>
/// A map reader for the Tiled XML format.
/// </summary>
public class TmxMapReader : IMapReader
public class TmxMapReader : TmxReaderBase, IMapReader
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
private readonly IReadOnlyCollection<ICustomTypeDefinition> _customTypeDefinitions;
/// <summary>
/// Constructs a new <see cref="TmxMapReader"/>.
/// </summary>
/// <param name="reader">An XML reader for reading a Tiled map in the Tiled XML format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeDefinitions">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
/// <inheritdoc />
public TmxMapReader(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> 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<string, ICustomTypeDefinition> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }
/// <inheritdoc/>
public Map ReadMap() => Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
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();
}

View file

@ -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<int>("x");
var y = reader.GetRequiredAttributeParseable<int>("y");
var width = reader.GetRequiredAttributeParseable<uint>("width");
var height = reader.GetRequiredAttributeParseable<uint>("height");
var x = _reader.GetRequiredAttributeParseable<int>("x");
var y = _reader.GetRequiredAttributeParseable<int>("y");
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("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 };
}

View file

@ -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<DataEncoding>("encoding", e => e switch
var encoding = _reader.GetOptionalAttributeEnum<DataEncoding>("encoding", e => e switch
{
"csv" => DataEncoding.Csv,
"base64" => DataEncoding.Base64,
_ => throw new XmlException("Invalid encoding")
});
var compression = reader.GetOptionalAttributeEnum<DataCompression>("compression", c => c switch
var compression = _reader.GetOptionalAttributeEnum<DataCompression>("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 };
}

View file

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
/// <summary>
/// Base class for Tiled XML format readers.
/// </summary>
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<MapOrientation>("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>("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<int>("compressionlevel") ?? -1;
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("height");
var tileWidth = _reader.GetRequiredAttributeParseable<uint>("tilewidth");
var tileHeight = _reader.GetRequiredAttributeParseable<uint>("tileheight");
var hexSideLength = _reader.GetOptionalAttributeParseable<uint>("hexsidelength");
var staggerAxis = _reader.GetOptionalAttributeEnum<StaggerAxis>("staggeraxis", s => s switch
{
"x" => StaggerAxis.X,
"y" => StaggerAxis.Y,
_ => throw new InvalidOperationException($"Unknown stagger axis '{s}'")
});
var staggerIndex = _reader.GetOptionalAttributeEnum<StaggerIndex>("staggerindex", s => s switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new InvalidOperationException($"Unknown stagger index '{s}'")
});
var parallaxOriginX = _reader.GetOptionalAttributeParseable<float>("parallaxoriginx") ?? 0.0f;
var parallaxOriginY = _reader.GetOptionalAttributeParseable<float>("parallaxoriginy") ?? 0.0f;
var backgroundColor = _reader.GetOptionalAttributeClass<Color>("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture);
var nextLayerID = _reader.GetRequiredAttributeParseable<uint>("nextlayerid");
var nextObjectID = _reader.GetRequiredAttributeParseable<uint>("nextobjectid");
var infinite = (_reader.GetOptionalAttributeParseable<uint>("infinite") ?? 0) == 1;
// At most one of
List<IProperty>? properties = null;
// Any number of
List<BaseLayer> layers = [];
List<Tileset> 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
};
}
}

View file

@ -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<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> customTypeDefinitions)
internal ObjectLayer ReadObjectLayer()
{
// Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? "";
var @class = reader.GetOptionalAttribute("class") ?? "";
var x = reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = reader.GetOptionalAttributeParseable<uint>("width");
var height = reader.GetOptionalAttributeParseable<uint>("height");
var opacity = reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var color = reader.GetOptionalAttributeClass<Color>("color");
var drawOrder = reader.GetOptionalAttributeEnum<DrawOrder>("draworder", s => s switch
var id = _reader.GetRequiredAttributeParseable<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = _reader.GetOptionalAttributeParseable<uint>("width");
var height = _reader.GetOptionalAttributeParseable<uint>("height");
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var color = _reader.GetOptionalAttributeClass<Color>("color");
var drawOrder = _reader.GetOptionalAttributeEnum<DrawOrder>("draworder", s => s switch
{
"topdown" => DrawOrder.TopDown,
"index" => DrawOrder.Index,
@ -42,10 +38,10 @@ internal partial class Tmx
List<IProperty>? properties = null;
List<Model.Object> 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<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> 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<IProperty>? propertiesDefault = obj?.Properties ?? null;
var id = reader.GetOptionalAttributeParseable<uint>("id") ?? idDefault;
var name = reader.GetOptionalAttribute("name") ?? nameDefault;
var type = reader.GetOptionalAttribute("type") ?? typeDefault;
var x = reader.GetOptionalAttributeParseable<float>("x") ?? xDefault;
var y = reader.GetOptionalAttributeParseable<float>("y") ?? yDefault;
var width = reader.GetOptionalAttributeParseable<float>("width") ?? widthDefault;
var height = reader.GetOptionalAttributeParseable<float>("height") ?? heightDefault;
var rotation = reader.GetOptionalAttributeParseable<float>("rotation") ?? rotationDefault;
var gid = reader.GetOptionalAttributeParseable<uint>("gid") ?? gidDefault;
var visible = reader.GetOptionalAttributeParseable<bool>("visible") ?? visibleDefault;
var id = _reader.GetOptionalAttributeParseable<uint>("id") ?? idDefault;
var name = _reader.GetOptionalAttribute("name") ?? nameDefault;
var type = _reader.GetOptionalAttribute("type") ?? typeDefault;
var x = _reader.GetOptionalAttributeParseable<float>("x") ?? xDefault;
var y = _reader.GetOptionalAttributeParseable<float>("y") ?? yDefault;
var width = _reader.GetOptionalAttributeParseable<float>("width") ?? widthDefault;
var height = _reader.GetOptionalAttributeParseable<float>("height") ?? heightDefault;
var rotation = _reader.GetOptionalAttributeParseable<float>("rotation") ?? rotationDefault;
var gid = _reader.GetOptionalAttributeParseable<uint>("gid") ?? gidDefault;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? visibleDefault;
// Elements
Model.Object? foundObject = null;
int propertiesCounter = 0;
List<IProperty>? 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<List<Vector2>>("points", s =>
var points = _reader.GetRequiredAttributeParseable<List<Vector2>>("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<List<Vector2>>("points", s =>
var points = _reader.GetRequiredAttributeParseable<List<Vector2>>("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<int>("pixelsize") ?? 16;
var wrap = reader.GetOptionalAttributeParseable<bool>("wrap") ?? false;
var color = reader.GetOptionalAttributeClass<Color>("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture);
var bold = reader.GetOptionalAttributeParseable<bool>("bold") ?? false;
var italic = reader.GetOptionalAttributeParseable<bool>("italic") ?? false;
var underline = reader.GetOptionalAttributeParseable<bool>("underline") ?? false;
var strikeout = reader.GetOptionalAttributeParseable<bool>("strikeout") ?? false;
var kerning = reader.GetOptionalAttributeParseable<bool>("kerning") ?? true;
var hAlign = reader.GetOptionalAttributeEnum<TextHorizontalAlignment>("halign", s => s switch
var fontFamily = _reader.GetOptionalAttribute("fontfamily") ?? "sans-serif";
var pixelSize = _reader.GetOptionalAttributeParseable<int>("pixelsize") ?? 16;
var wrap = _reader.GetOptionalAttributeParseable<bool>("wrap") ?? false;
var color = _reader.GetOptionalAttributeClass<Color>("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture);
var bold = _reader.GetOptionalAttributeParseable<bool>("bold") ?? false;
var italic = _reader.GetOptionalAttributeParseable<bool>("italic") ?? false;
var underline = _reader.GetOptionalAttributeParseable<bool>("underline") ?? false;
var strikeout = _reader.GetOptionalAttributeParseable<bool>("strikeout") ?? false;
var kerning = _reader.GetOptionalAttributeParseable<bool>("kerning") ?? true;
var hAlign = _reader.GetOptionalAttributeEnum<TextHorizontalAlignment>("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<TextVerticalAlignment>("valign", s => s switch
var vAlign = _reader.GetOptionalAttributeEnum<TextVerticalAlignment>("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<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> 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
});

View file

@ -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<IProperty> ReadProperties()
{
return _reader.ReadList("properties", "property", (r) =>
{
var name = r.GetRequiredAttribute("name");
var type = r.GetOptionalAttributeEnum<PropertyType>("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<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("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<PropertyType>("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<string> { value } };
}
}
else if (ced.StorageType == CustomEnumStorageType.Int)
{
var value = _reader.GetRequiredAttributeParseable<int>("value");
if (ced.ValueAsFlags)
{
var allValues = ced.Values;
var enumValues = new HashSet<string>();
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<string> { enumValue } };
}
}
throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}");
}
}

View file

@ -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<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("height");
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
List<IProperty>? 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<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
var repeatX = (_reader.GetOptionalAttributeParseable<uint>("repeatx") ?? 0) == 1;
var repeatY = (_reader.GetOptionalAttributeParseable<uint>("repeaty") ?? 0) == 1;
List<IProperty>? 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<uint>("id");
var name = _reader.GetOptionalAttribute("name") ?? "";
var @class = _reader.GetOptionalAttribute("class") ?? "";
var opacity = _reader.GetOptionalAttributeParseable<float>("opacity") ?? 1.0f;
var visible = _reader.GetOptionalAttributeParseable<bool>("visible") ?? true;
var tintColor = _reader.GetOptionalAttributeClass<Color>("tintcolor");
var offsetX = _reader.GetOptionalAttributeParseable<float>("offsetx") ?? 0.0f;
var offsetY = _reader.GetOptionalAttributeParseable<float>("offsety") ?? 0.0f;
var parallaxX = _reader.GetOptionalAttributeParseable<float>("parallaxx") ?? 1.0f;
var parallaxY = _reader.GetOptionalAttributeParseable<float>("parallaxy") ?? 1.0f;
List<IProperty>? properties = null;
List<BaseLayer> 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
};
}
}

View file

@ -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<uint>("firstgid");
var source = _reader.GetOptionalAttribute("source");
var name = _reader.GetOptionalAttribute("name");
var @class = _reader.GetOptionalAttribute("class") ?? "";
var tileWidth = _reader.GetOptionalAttributeParseable<uint>("tilewidth");
var tileHeight = _reader.GetOptionalAttributeParseable<uint>("tileheight");
var spacing = _reader.GetOptionalAttributeParseable<uint>("spacing") ?? 0;
var margin = _reader.GetOptionalAttributeParseable<uint>("margin") ?? 0;
var tileCount = _reader.GetOptionalAttributeParseable<uint>("tilecount");
var columns = _reader.GetOptionalAttributeParseable<uint>("columns");
var objectAlignment = _reader.GetOptionalAttributeEnum<ObjectAlignment>("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<TileRenderSize>("rendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,
_ => throw new InvalidOperationException($"Unknown render size '{s}'")
}) ?? TileRenderSize.Tile;
var fillMode = _reader.GetOptionalAttributeEnum<FillMode>("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<IProperty>? properties = null;
List<Wangset>? wangsets = null;
Transformations? transformations = null;
List<Tile> 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<ImageFormat>("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<Color>("trans");
var width = _reader.GetOptionalAttributeParseable<uint>("width");
var height = _reader.GetOptionalAttributeParseable<uint>("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<float>("x") ?? 0f;
var y = _reader.GetOptionalAttributeParseable<float>("y") ?? 0f;
_reader.ReadStartElement("tileoffset");
return new TileOffset { X = x, Y = y };
}
internal Grid ReadGrid()
{
// Attributes
var orientation = _reader.GetOptionalAttributeEnum<GridOrientation>("orientation", s => s switch
{
"orthogonal" => GridOrientation.Orthogonal,
"isometric" => GridOrientation.Isometric,
_ => throw new InvalidOperationException($"Unknown orientation '{s}'")
}) ?? GridOrientation.Orthogonal;
var width = _reader.GetRequiredAttributeParseable<uint>("width");
var height = _reader.GetRequiredAttributeParseable<uint>("height");
_reader.ReadStartElement("grid");
return new Grid { Orientation = orientation, Width = width, Height = height };
}
internal Transformations ReadTransformations()
{
// Attributes
var hFlip = (_reader.GetOptionalAttributeParseable<uint>("hflip") ?? 0) == 1;
var vFlip = (_reader.GetOptionalAttributeParseable<uint>("vflip") ?? 0) == 1;
var rotate = (_reader.GetOptionalAttributeParseable<uint>("rotate") ?? 0) == 1;
var preferUntransformed = (_reader.GetOptionalAttributeParseable<uint>("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<uint>("id");
var type = _reader.GetOptionalAttribute("type") ?? "";
var probability = _reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
var x = _reader.GetOptionalAttributeParseable<uint>("x") ?? 0;
var y = _reader.GetOptionalAttributeParseable<uint>("y") ?? 0;
var width = _reader.GetOptionalAttributeParseable<uint>("width");
var height = _reader.GetOptionalAttributeParseable<uint>("height");
// Elements
List<IProperty>? properties = null;
Image? image = null;
ObjectLayer? objectLayer = null;
List<Frame>? 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<Frame>("animation", "frame", (ar) =>
{
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
var duration = ar.GetRequiredAttributeParseable<uint>("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<Wangset> ReadWangsets() =>
_reader.ReadList<Wangset>("wangsets", "wangset", r => ReadWangset());
internal Wangset ReadWangset()
{
// Attributes
var name = _reader.GetRequiredAttribute("name");
var @class = _reader.GetOptionalAttribute("class") ?? "";
var tile = _reader.GetRequiredAttributeParseable<int>("tile");
// Elements
List<IProperty>? properties = null;
List<WangColor> wangColors = [];
List<WangTile> 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>("color");
var tile = _reader.GetRequiredAttributeParseable<int>("tile");
var probability = _reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
// Elements
List<IProperty>? 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<uint>("tileid");
var wangID = _reader.GetRequiredAttributeParseable<byte[]>("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
};
}
}

View file

@ -0,0 +1,74 @@
using System;
using System.Xml;
using DotTiled.Model;
namespace DotTiled.Serialization.Tmx;
/// <summary>
/// Base class for Tiled XML format readers.
/// </summary>
public abstract partial class TmxReaderBase : IDisposable
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly XmlReader _reader;
private bool disposedValue;
/// <summary>
/// Constructs a new <see cref="TmxReaderBase"/>, which is the base class for all Tiled XML format readers.
/// </summary>
/// <param name="reader">An XML reader for reading a Tiled map in the Tiled XML format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeResolver">A function that resolves custom types given their source.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
protected TmxReaderBase(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> 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();
}
/// <inheritdoc />
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);
// }
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Xml;
using DotTiled.Model;
@ -8,68 +7,20 @@ namespace DotTiled.Serialization.Tmx;
/// <summary>
/// A tileset reader for the Tiled XML format.
/// </summary>
public class TsxTilesetReader : ITilesetReader
public class TsxTilesetReader : TmxReaderBase, ITilesetReader
{
// External resolvers
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
private readonly IReadOnlyCollection<ICustomTypeDefinition> _customTypeDefinitions;
/// <summary>
/// Constructs a new <see cref="TsxTilesetReader"/>.
/// </summary>
/// <param name="reader">An XML reader for reading a Tiled tileset in the Tiled XML format.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeDefinitions">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
/// <inheritdoc />
public TsxTilesetReader(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> 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<string, ICustomTypeDefinition> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }
/// <inheritdoc/>
public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions);
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
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();
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Xml;
using DotTiled.Model;
@ -8,72 +7,20 @@ namespace DotTiled.Serialization.Tmx;
/// <summary>
/// A template reader for the Tiled XML format.
/// </summary>
public class TxTemplateReader : ITemplateReader
public class TxTemplateReader : TmxReaderBase, ITemplateReader
{
// Resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly XmlReader _reader;
private bool disposedValue;
private readonly IReadOnlyCollection<ICustomTypeDefinition> _customTypeDefinitions;
/// <summary>
/// Constructs a new <see cref="TxTemplateReader"/>.
/// </summary>
/// <param name="reader">An XML reader for reading a Tiled template in the Tiled XML format.</param>
/// <param name="externalTilesetResolver">A function that resolves external tilesets given their source.</param>
/// <param name="externalTemplateResolver">A function that resolves external templates given their source.</param>
/// <param name="customTypeDefinitions">A collection of custom type definitions that can be used to resolve custom types when encountering <see cref="ClassProperty"/>.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the arguments are null.</exception>
/// <inheritdoc />
public TxTemplateReader(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<ICustomTypeDefinition> 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<string, ICustomTypeDefinition> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }
/// <inheritdoc/>
public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
/// <inheritdoc/>
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);
// }
/// <inheritdoc/>
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();
}