Move to readers instead

This commit is contained in:
Daniel Cronqvist 2024-08-03 13:51:38 +02:00
parent 7e325ea95d
commit 453200bbb2
17 changed files with 1313 additions and 2 deletions

View file

@ -64,7 +64,24 @@ public partial class TmxSerializerMapTests
externalTemplateResolver); externalTemplateResolver);
// Act // Act
var map = tmxSerializer.DeserializeMap(reader);
static Template ResolveTemplate(string source)
{
using var xmlTemplateReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Template.{source}");
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate);
return templateReader.ReadTemplate();
}
static Tileset ResolveTileset(string source)
{
using var xmlTilesetReader = TmxSerializerTestData.GetReaderFor($"TmxSerializer.TestData.Tileset.{source}");
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate);
return tilesetReader.ReadTileset();
}
var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate);
var map = mapReader.ReadMap();
var raw = tmxSerializer.DeserializeMap(testDataFileText); var raw = tmxSerializer.DeserializeMap(testDataFileText);
// Assert // Assert

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static Chunk ReadChunk(XmlReader reader, 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 usesTileChildrenInsteadOfRawData = encoding is null;
if (usesTileChildrenInsteadOfRawData)
{
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 (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags);
return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags };
}
}
}

View file

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static Data ReadData(XmlReader reader, bool usesChunks)
{
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
{
"gzip" => DataCompression.GZip,
"zlib" => DataCompression.ZLib,
"zstd" => DataCompression.ZStd,
_ => throw new XmlException("Invalid compression")
});
if (usesChunks)
{
var chunks = reader
.ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression))
.ToArray();
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks };
}
var usesTileChildrenInsteadOfRawData = encoding is null && compression is null;
if (usesTileChildrenInsteadOfRawData)
{
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 (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags);
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null };
}
internal static (uint[] GlobalTileIDs, FlippingFlags[] FlippingFlags) ReadAndClearFlippingFlagsFromGIDs(uint[] globalTileIDs)
{
var clearedGlobalTileIDs = new uint[globalTileIDs.Length];
var flippingFlags = new FlippingFlags[globalTileIDs.Length];
for (var i = 0; i < globalTileIDs.Length; i++)
{
var gid = globalTileIDs[i];
var flags = gid & 0xF0000000u;
flippingFlags[i] = (FlippingFlags)flags;
clearedGlobalTileIDs[i] = gid & 0x0FFFFFFFu;
}
return (clearedGlobalTileIDs, flippingFlags);
}
internal static uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader)
{
return reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable<uint>("gid") ?? 0).ToArray();
}
internal static uint[] ReadRawData(XmlReader reader, DataEncoding encoding, DataCompression? compression)
{
var data = reader.ReadElementContentAsString();
if (encoding == DataEncoding.Csv)
return ParseCsvData(data);
using var bytes = new MemoryStream(Convert.FromBase64String(data));
if (compression is null)
return ReadMemoryStreamAsInt32Array(bytes);
var decompressed = compression switch
{
DataCompression.GZip => DecompressGZip(bytes),
DataCompression.ZLib => DecompressZLib(bytes),
DataCompression.ZStd => throw new NotSupportedException("ZStd compression is not supported."),
_ => throw new XmlException("Invalid compression")
};
return decompressed;
}
internal static uint[] ParseCsvData(string data)
{
var values = data
.Split((char[])['\n', '\r', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(uint.Parse)
.ToArray();
return values;
}
internal static uint[] ReadMemoryStreamAsInt32Array(Stream stream)
{
var finalValues = new List<uint>();
var int32Bytes = new byte[4];
while (stream.Read(int32Bytes, 0, 4) == 4)
{
var value = BitConverter.ToUInt32(int32Bytes, 0);
finalValues.Add(value);
}
return finalValues.ToArray();
}
internal static uint[] DecompressGZip(MemoryStream stream)
{
using var decompressedStream = new GZipStream(stream, CompressionMode.Decompress);
return ReadMemoryStreamAsInt32Array(decompressedStream);
}
internal static uint[] DecompressZLib(MemoryStream stream)
{
using var decompressedStream = new ZLibStream(stream, CompressionMode.Decompress);
return ReadMemoryStreamAsInt32Array(decompressedStream);
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace DotTiled;
internal partial class Tmx
{
private static class Helpers
{
public static void SetAtMostOnce<T>(ref T? field, T value, string fieldName)
{
if (field is not null)
throw new InvalidOperationException($"{fieldName} already set");
field = value;
}
public static void SetAtMostOnceUsingCounter<T>(ref T? field, T value, string fieldName, ref int counter)
{
if (counter > 0)
throw new InvalidOperationException($"{fieldName} already set");
field = value;
counter++;
}
}
}

View file

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static Map ReadMap(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
// 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 Exception($"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 Exception($"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 Exception($"Unknown stagger axis '{s}'")
});
var staggerIndex = reader.GetOptionalAttributeEnum<StaggerIndex>("staggerindex", s => s switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new Exception($"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
Dictionary<string, 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), "Properties"),
"tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver)),
"layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)),
"imagelayer" => () => layers.Add(ReadImageLayer(r)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)),
_ => 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

@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static ObjectLayer ReadObjectLayer(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
// 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
{
"topdown" => DrawOrder.TopDown,
"index" => DrawOrder.Index,
_ => throw new Exception($"Unknown draw order '{s}'")
}) ?? DrawOrder.TopDown;
// Elements
Dictionary<string, IProperty>? properties = null;
List<Object> objects = [];
reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"object" => () => objects.Add(ReadObject(r, externalTemplateResolver)),
_ => r.Skip
});
return new ObjectLayer
{
ID = id,
Name = name,
Class = @class,
X = x,
Y = y,
Width = width,
Height = height,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Color = color,
Properties = properties,
DrawOrder = drawOrder,
Objects = objects
};
}
internal static Object ReadObject(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
// Attributes
var template = reader.GetOptionalAttribute("template");
uint? idDefault = null;
string nameDefault = "";
string typeDefault = "";
float xDefault = 0f;
float yDefault = 0f;
float widthDefault = 0f;
float heightDefault = 0f;
float rotationDefault = 0f;
uint? gidDefault = null;
bool visibleDefault = true;
Dictionary<string, IProperty>? propertiesDefault = null;
// Perform template copy first
if (template is not null)
{
var resolvedTemplate = externalTemplateResolver(template);
var templObj = resolvedTemplate.Object;
idDefault = templObj.ID;
nameDefault = templObj.Name;
typeDefault = templObj.Type;
xDefault = templObj.X;
yDefault = templObj.Y;
widthDefault = templObj.Width;
heightDefault = templObj.Height;
rotationDefault = templObj.Rotation;
gidDefault = templObj.GID;
visibleDefault = templObj.Visible;
propertiesDefault = templObj.Properties;
}
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
Object? obj = null;
int propertiesCounter = 0;
Dictionary<string, IProperty>? properties = propertiesDefault;
reader.ProcessChildren("object", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, MergeProperties(properties, ReadProperties(r)), "Properties", ref propertiesCounter),
"ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r), "Object marker"),
"point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r), "Object marker"),
"polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r), "Object marker"),
"polyline" => () => Helpers.SetAtMostOnce(ref obj, ReadPolylineObject(r), "Object marker"),
"text" => () => Helpers.SetAtMostOnce(ref obj, ReadTextObject(r), "Object marker"),
_ => throw new Exception($"Unknown object marker '{elementName}'")
});
if (obj is null)
{
obj = new RectangleObject { ID = id };
reader.Skip();
}
obj.Name = name;
obj.Type = type;
obj.X = x;
obj.Y = y;
obj.Width = width;
obj.Height = height;
obj.Rotation = rotation;
obj.GID = gid;
obj.Visible = visible;
obj.Template = template;
obj.Properties = properties;
return obj;
}
internal static Dictionary<string, IProperty> MergeProperties(Dictionary<string, IProperty>? baseProperties, Dictionary<string, IProperty> overrideProperties)
{
if (baseProperties is null)
return overrideProperties ?? new Dictionary<string, IProperty>();
if (overrideProperties is null)
return baseProperties;
var result = new Dictionary<string, IProperty>(baseProperties);
foreach (var (key, value) in overrideProperties)
{
if (!result.TryGetValue(key, out var baseProp))
{
result[key] = value;
continue;
}
else
{
if (value is ClassProperty classProp)
{
((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties);
}
else
{
result[key] = value;
}
}
}
return result;
}
internal static EllipseObject ReadEllipseObject(XmlReader reader)
{
reader.Skip();
return new EllipseObject { };
}
internal static PointObject ReadPointObject(XmlReader reader)
{
reader.Skip();
return new PointObject { };
}
internal static PolygonObject ReadPolygonObject(XmlReader reader)
{
// Attributes
var points = reader.GetRequiredAttributeParseable<List<Vector2>>("points", s =>
{
// Takes on format "x1,y1 x2,y2 x3,y3 ..."
var coords = s.Split(' ');
return coords.Select(c =>
{
var xy = c.Split(',');
return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture));
}).ToList();
});
reader.ReadStartElement("polygon");
return new PolygonObject { Points = points };
}
internal static PolylineObject ReadPolylineObject(XmlReader reader)
{
// Attributes
var points = reader.GetRequiredAttributeParseable<List<Vector2>>("points", s =>
{
// Takes on format "x1,y1 x2,y2 x3,y3 ..."
var coords = s.Split(' ');
return coords.Select(c =>
{
var xy = c.Split(',');
return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture));
}).ToList();
});
reader.ReadStartElement("polyline");
return new PolylineObject { Points = points };
}
internal static TextObject ReadTextObject(XmlReader reader)
{
// 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
{
"left" => TextHorizontalAlignment.Left,
"center" => TextHorizontalAlignment.Center,
"right" => TextHorizontalAlignment.Right,
"justify" => TextHorizontalAlignment.Justify,
_ => throw new Exception($"Unknown horizontal alignment '{s}'")
}) ?? TextHorizontalAlignment.Left;
var vAlign = reader.GetOptionalAttributeEnum<TextVerticalAlignment>("valign", s => s switch
{
"top" => TextVerticalAlignment.Top,
"center" => TextVerticalAlignment.Center,
"bottom" => TextVerticalAlignment.Bottom,
_ => throw new Exception($"Unknown vertical alignment '{s}'")
}) ?? TextVerticalAlignment.Top;
// Elements
var text = reader.ReadElementContentAsString("text", "");
return new TextObject
{
FontFamily = fontFamily,
PixelSize = pixelSize,
Wrap = wrap,
Color = color,
Bold = bold,
Italic = italic,
Underline = underline,
Strikeout = strikeout,
Kerning = kerning,
HorizontalAlignment = hAlign,
VerticalAlignment = vAlign,
Text = text
};
}
internal static Template ReadTemplate(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
// No attributes
// At most one of
Tileset? tileset = null;
// Should contain exactly one of
Object? obj = null;
reader.ProcessChildren("template", (r, elementName) => elementName switch
{
"tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver), "Tileset"),
"object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver), "Object"),
_ => r.Skip
});
if (obj is null)
throw new NotSupportedException("Template must contain exactly one object");
return new Template
{
Tileset = tileset,
Object = obj
};
}
}

View file

@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static Dictionary<string, IProperty> ReadProperties(XmlReader reader)
{
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),
_ => throw new XmlException("Invalid property type")
};
return (name, property);
}).ToDictionary(x => x.name, x => x.property);
}
internal static ClassProperty ReadClassProperty(XmlReader reader)
{
var name = reader.GetRequiredAttribute("name");
var propertyType = reader.GetRequiredAttribute("propertytype");
reader.ReadStartElement("property");
var properties = ReadProperties(reader);
reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties };
}
}

View file

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static TileLayer ReadTileLayer(XmlReader reader, 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;
Dictionary<string, 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), "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)
{
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.GetRequiredAttributeParseable<bool>("repeatx");
var repeatY = reader.GetRequiredAttributeParseable<bool>("repeaty");
Dictionary<string, 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), "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)
{
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;
Dictionary<string, IProperty>? properties = null;
List<BaseLayer> layers = [];
reader.ProcessChildren("group", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: false)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)),
"imagelayer" => () => layers.Add(ReadImageLayer(r)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)),
_ => 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,316 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace DotTiled;
internal partial class Tmx
{
internal static Tileset ReadTileset(XmlReader reader, Func<string, Tileset>? externalTilesetResolver, Func<string, Template> externalTemplateResolver)
{
// 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");
var margin = reader.GetOptionalAttributeParseable<uint>("margin");
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 Exception($"Unknown object alignment '{s}'")
}) ?? ObjectAlignment.Unspecified;
var renderSize = reader.GetOptionalAttributeEnum<TileRenderSize>("rendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,
_ => throw new Exception($"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 Exception($"Unknown fill mode '{s}'")
}) ?? FillMode.Stretch;
// Elements
Image? image = null;
TileOffset? tileOffset = null;
Grid? grid = null;
Dictionary<string, 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), "Properties"),
"wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r), "Wangsets"),
"transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"),
"tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver)),
_ => 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 = null;
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 Exception($"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
});
return new Image
{
Format = format,
Source = source,
TransparentColor = transparentColor,
Width = width,
Height = height,
};
}
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 Exception($"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<bool>("hflip") ?? false;
var vFlip = reader.GetOptionalAttributeParseable<bool>("vflip") ?? false;
var rotate = reader.GetOptionalAttributeParseable<bool>("rotate") ?? false;
var preferUntransformed = reader.GetOptionalAttributeParseable<bool>("preferuntransformed") ?? false;
reader.ReadStartElement("transformations");
return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed };
}
internal static Tile ReadTile(XmlReader reader, Func<string, Template> externalTemplateResolver)
{
// 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
Dictionary<string, 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), "Properties"),
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver), "ObjectLayer"),
"animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList<Frame>("animation", "frame", (ar) =>
{
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
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)
{
return reader.ReadList<Wangset>("wangsets", "wangset", ReadWangset);
}
internal static Wangset ReadWangset(XmlReader reader)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var tile = reader.GetRequiredAttributeParseable<uint>("tile");
// Elements
Dictionary<string, IProperty>? properties = null;
List<WangColor> wangColors = [];
List<WangTile> wangTiles = [];
reader.ProcessChildren("wangset", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"),
"wangcolor" => () => wangColors.Add(ReadWangColor(r)),
"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)
{
// Attributes
var name = reader.GetRequiredAttribute("name");
var @class = reader.GetOptionalAttribute("class") ?? "";
var color = reader.GetRequiredAttributeParseable<Color>("color");
var tile = reader.GetRequiredAttributeParseable<uint>("tile");
var probability = reader.GetOptionalAttributeParseable<float>("probability") ?? 0f;
// Elements
Dictionary<string, IProperty>? properties = null;
reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch
{
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "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)).ToArray();
if (indices.Length > 8)
throw new ArgumentException("Wang ID can have at most 8 indices.");
return indices;
});
return new WangTile
{
TileID = tileID,
WangID = wangID
};
}
}

View file

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

View file

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

View file

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