First initial modelling, going to move serialization away from model

This commit is contained in:
Daniel Cronqvist 2024-07-26 00:36:57 +02:00
parent f0548060c0
commit d2be83972d
10 changed files with 1302 additions and 0 deletions

8
.editorconfig Normal file
View file

@ -0,0 +1,8 @@
root = true
[*.cs]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true

1
.gitignore vendored
View file

@ -9,6 +9,7 @@
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
.vscode/
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DotTiled\DotTiled.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

259
DotTiled.Tests/MapTests.cs Normal file
View file

@ -0,0 +1,259 @@
using System.Text;
using System.Xml.Serialization;
using DotTiled;
namespace DotTiled.Tests;
public class MapTests
{
[Fact]
public void ReadXml_Always_SetsRequiredAttributes()
{
// Arrange
var xml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<map
version="1.2"
class="class"
orientation="orthogonal"
renderorder="right-down"
compressionlevel="5"
width="10"
height="10"
tilewidth="32"
tileheight="32"
parallaxoriginx="0.5"
parallaxoriginy="0.5"
nextlayerid="1"
nextobjectid="1"
infinite="1"
>
</map>
""";
var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
// Act
var map = Map.LoadFromStream(xmlStream);
// Assert
// Assert all required properties are set
Assert.Equal("1.2", map.Version);
Assert.Equal("class", map.Class);
Assert.Equal(Orientation.Orthogonal, map.Orientation);
Assert.Equal(RenderOrder.RightDown, map.RenderOrder);
Assert.Equal(5, map.CompressionLevel);
Assert.Equal(10u, map.Width);
Assert.Equal(10u, map.Height);
Assert.Equal(32u, map.TileWidth);
Assert.Equal(32u, map.TileHeight);
Assert.Equal(0.5f, map.ParallaxOriginX);
Assert.Equal(0.5f, map.ParallaxOriginY);
Assert.Equal(1u, map.NextLayerId);
Assert.Equal(1u, map.NextObjectId);
Assert.True(map.Infinite);
// Assert all optional properties are set to their default values
Assert.Null(map.TiledVersion);
Assert.Null(map.HexSideLength);
Assert.Null(map.StaggerAxis);
Assert.Null(map.StaggerIndex);
Assert.Null(map.BackgroundColor);
}
public static IEnumerable<object[]> ColorData =>
new List<object[]>
{
new object[] { "#ff0000", new TiledColor { R = 255, G = 0, B = 0, A = 255 } },
new object[] { "#00ff00", new TiledColor { R = 0, G = 255, B = 0, A = 255 } },
new object[] { "#0000ff", new TiledColor { R = 0, G = 0, B = 255, A = 255 } },
new object[] { "#ffffff", new TiledColor { R = 255, G = 255, B = 255, A = 255 } },
new object[] { "#000000", new TiledColor { R = 0, G = 0, B = 0, A = 255 } },
new object[] { "#ff000000", new TiledColor { R = 0, G = 0, B = 0, A = 255 } },
new object[] { "#fe000000", new TiledColor { R = 0, G = 0, B = 0, A = 254 } },
new object[] { "#fe00ff00", new TiledColor { R = 0, G = 255, B = 0, A = 254 } },
};
[Theory]
[MemberData(nameof(ColorData))]
public void ReadXml_WhenPresent_SetsOptionalAttributes(string color, TiledColor expectedColor)
{
// Arrange
var xml =
$"""
<?xml version="1.0" encoding="UTF-8"?>
<map
version="1.2"
class="class"
orientation="orthogonal"
renderorder="right-down"
compressionlevel="5"
width="10"
height="10"
tilewidth="32"
tileheight="32"
hexsidelength="10"
staggeraxis="y"
staggerindex="odd"
parallaxoriginx="0.5"
parallaxoriginy="0.5"
backgroundcolor="{color}"
nextlayerid="1"
nextobjectid="1"
infinite="1"
>
</map>
""";
var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
// Act
var map = Map.LoadFromStream(xmlStream);
// Assert
// Assert all required properties are set
Assert.Equal("1.2", map.Version);
Assert.Equal("class", map.Class);
Assert.Equal(Orientation.Orthogonal, map.Orientation);
Assert.Equal(RenderOrder.RightDown, map.RenderOrder);
Assert.Equal(5, map.CompressionLevel);
Assert.Equal(10u, map.Width);
Assert.Equal(10u, map.Height);
Assert.Equal(32u, map.TileWidth);
Assert.Equal(32u, map.TileHeight);
Assert.Equal(10u, map.HexSideLength);
Assert.Equal(StaggerAxis.Y, map.StaggerAxis);
Assert.Equal(StaggerIndex.Odd, map.StaggerIndex);
Assert.Equal(0.5f, map.ParallaxOriginX);
Assert.Equal(0.5f, map.ParallaxOriginY);
Assert.Equal(expectedColor, map.BackgroundColor);
Assert.Equal(1u, map.NextLayerId);
Assert.Equal(1u, map.NextObjectId);
Assert.True(map.Infinite);
}
[Fact]
public void ReadXml_Always_ReadsPropertiesCorrectly()
{
// Arrange
var xml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<map
version="1.2"
class="class"
orientation="orthogonal"
renderorder="right-down"
compressionlevel="5"
width="10"
height="10"
tilewidth="32"
tileheight="32"
parallaxoriginx="0.5"
parallaxoriginy="0.5"
nextlayerid="1"
nextobjectid="1"
infinite="1"
>
<properties>
<property name="string" type="string" value="string"/>
<property name="int" type="int" value="42"/>
<property name="float" type="float" value="42.42"/>
<property name="bool" type="bool" value="true"/>
<property name="color" type="color" value="#ff0000"/>
<property name="file" type="file" value="file"/>
<property name="object" type="object" value="5"/>
<property name="class" type="class" propertytype="TestClass">
<properties>
<property name="TestClassString" type="string" value="string"/>
<property name="TestClassInt" type="int" value="43"/>
</properties>
</property>
</properties>
</map>
""";
var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
// Act
var map = Map.LoadFromStream(xmlStream);
// Assert
Assert.NotNull(map.Properties);
Assert.Equal(8, map.Properties.Count);
Assert.Equal(PropertyType.String, map.Properties["string"].Type);
Assert.Equal("string", map.GetProperty<StringProperty>("string").Value);
Assert.Equal(PropertyType.Int, map.Properties["int"].Type);
Assert.Equal(42, map.GetProperty<IntProperty>("int").Value);
Assert.Equal(PropertyType.Float, map.Properties["float"].Type);
Assert.Equal(42.42f, map.GetProperty<FloatProperty>("float").Value);
Assert.Equal(PropertyType.Bool, map.Properties["bool"].Type);
Assert.True(map.GetProperty<BooleanProperty>("bool").Value);
Assert.Equal(PropertyType.Color, map.Properties["color"].Type);
Assert.Equal(new TiledColor { R = 255, G = 0, B = 0, A = 255 }, map.GetProperty<ColorProperty>("color").Value);
Assert.Equal(PropertyType.File, map.Properties["file"].Type);
Assert.Equal("file", map.GetProperty<FileProperty>("file").Value);
Assert.Equal(PropertyType.Object, map.Properties["object"].Type);
Assert.Equal(5, map.GetProperty<ObjectProperty>("object").Value);
Assert.Equal(PropertyType.Class, map.Properties["class"].Type);
var classProperty = map.GetProperty<ClassProperty>("class");
Assert.Equal("TestClass", classProperty.PropertyType);
Assert.Equal(2, classProperty.Value.Count);
Assert.Equal("string", classProperty.GetProperty<StringProperty>("TestClassString").Value);
Assert.Equal(43, classProperty.GetProperty<IntProperty>("TestClassInt").Value);
}
[Fact]
public void ReadXml_Always_1()
{
// Arrange
var xml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<map
version="1.2"
class="class"
orientation="orthogonal"
renderorder="right-down"
compressionlevel="5"
width="10"
height="10"
tilewidth="32"
tileheight="32"
parallaxoriginx="0.5"
parallaxoriginy="0.5"
nextlayerid="1"
nextobjectid="1"
infinite="1"
>
<properties>
<property name="string" type="string" value="string"/>
<property name="int" type="int" value="42"/>
<property name="float" type="float" value="42.42"/>
<property name="bool" type="bool" value="true"/>
<property name="color" type="color" value="#ff0000"/>
<property name="file" type="file" value="file"/>
<property name="object" type="object" value="5"/>
<property name="class" type="class" propertytype="TestClass">
<properties>
<property name="TestClassString" type="string" value="string"/>
<property name="TestClassInt" type="int" value="43"/>
</properties>
</property>
</properties>
<tileset firstgid="1" source="textures/tiles.tsx"/>
</map>
""";
var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
// Act
var map = Map.LoadFromStream(xmlStream);
}
}

28
DotTiled.sln Normal file
View file

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled", "DotTiled\DotTiled.csproj", "{80A60DE7-D6AE-4CC7-825F-75308D83F36D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Tests", "DotTiled.Tests\DotTiled.Tests.csproj", "{C1311A5A-5206-467C-B323-B131CA11FDB8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{80A60DE7-D6AE-4CC7-825F-75308D83F36D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80A60DE7-D6AE-4CC7-825F-75308D83F36D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80A60DE7-D6AE-4CC7-825F-75308D83F36D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80A60DE7-D6AE-4CC7-825F-75308D83F36D}.Release|Any CPU.Build.0 = Release|Any CPU
{C1311A5A-5206-467C-B323-B131CA11FDB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1311A5A-5206-467C-B323-B131CA11FDB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1311A5A-5206-467C-B323-B131CA11FDB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1311A5A-5206-467C-B323-B131CA11FDB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

9
DotTiled/DotTiled.csproj Normal file
View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

809
DotTiled/Map.cs Normal file
View file

@ -0,0 +1,809 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace DotTiled;
public static class Helpers
{
public static void SetAtMostOnce<T>(ref T? field, T value, string fieldName)
{
if (field is not null)
throw new XmlException($"{fieldName} already set");
field = value;
}
}
public enum Orientation
{
[XmlEnum(Name = "orthogonal")]
Orthogonal,
[XmlEnum(Name = "isometric")]
Isometric,
[XmlEnum(Name = "staggered")]
Staggered,
[XmlEnum(Name = "hexagonal")]
Hexagonal
}
public enum RenderOrder
{
[XmlEnum(Name = "right-down")]
RightDown,
[XmlEnum(Name = "right-up")]
RightUp,
[XmlEnum(Name = "left-down")]
LeftDown,
[XmlEnum(Name = "left-up")]
LeftUp
}
public enum StaggerAxis
{
[XmlEnum(Name = "x")]
X,
[XmlEnum(Name = "y")]
Y
}
public enum StaggerIndex
{
[XmlEnum(Name = "even")]
Even,
[XmlEnum(Name = "odd")]
Odd
}
public class TiledColor : IParsable<TiledColor>, IEquatable<TiledColor>
{
public required byte R { get; set; }
public required byte G { get; set; }
public required byte B { get; set; }
public byte A { get; set; } = 255;
public static TiledColor Parse(string s, IFormatProvider? provider)
{
TryParse(s, provider, out var result);
return result ?? throw new FormatException($"Invalid format for TiledColor: {s}");
}
public static bool TryParse(
[NotNullWhen(true)] string? s,
IFormatProvider? provider,
[MaybeNullWhen(false)] out TiledColor result)
{
// Format: #RRGGBB or #AARRGGBB
if (s is null || s.Length != 7 && s.Length != 9 || s[0] != '#')
{
result = default;
return false;
}
if (s.Length == 7)
{
result = new TiledColor
{
R = byte.Parse(s[1..3], NumberStyles.HexNumber, provider),
G = byte.Parse(s[3..5], NumberStyles.HexNumber, provider),
B = byte.Parse(s[5..7], NumberStyles.HexNumber, provider)
};
}
else
{
result = new TiledColor
{
A = byte.Parse(s[1..3], NumberStyles.HexNumber, provider),
R = byte.Parse(s[3..5], NumberStyles.HexNumber, provider),
G = byte.Parse(s[5..7], NumberStyles.HexNumber, provider),
B = byte.Parse(s[7..9], NumberStyles.HexNumber, provider)
};
}
return true;
}
public bool Equals(TiledColor? other)
{
if (other is null)
return false;
return R == other.R && G == other.G && B == other.B && A == other.A;
}
public override bool Equals(object? obj) => obj is TiledColor other && Equals(other);
public override int GetHashCode() => HashCode.Combine(R, G, B, A);
}
public enum PropertyType
{
[XmlEnum(Name = "string")]
String,
[XmlEnum(Name = "int")]
Int,
[XmlEnum(Name = "float")]
Float,
[XmlEnum(Name = "bool")]
Bool,
[XmlEnum(Name = "color")]
Color,
[XmlEnum(Name = "file")]
File,
[XmlEnum(Name = "object")]
Object,
[XmlEnum(Name = "class")]
Class
}
[XmlRoot(ElementName = "property")]
public interface IProperty : IXmlSerializable
{
public string Name { get; set; }
public PropertyType Type { get; set; }
}
[XmlRoot(ElementName = "property")]
public class BooleanProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required bool Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute<bool>("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class ColorProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required TiledColor Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute<TiledColor>("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class FileProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required string Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class FloatProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required float Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute<float>("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class IntProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required int Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute<int>("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class ObjectProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required int Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute<int>("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class StringProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required string Value { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
Value = reader.GetRequiredAttribute("value");
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "property")]
public class ClassProperty : IProperty
{
public required string Name { get; set; }
public required PropertyType Type { get; set; }
public required string PropertyType { get; set; }
public required Dictionary<string, IProperty> Value { get; set; }
public T GetProperty<T>(string propertyName) where T : IProperty =>
(T)Value[propertyName];
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
Name = reader.GetRequiredAttribute("name");
Type = reader.GetRequiredAttributeEnum<PropertyType>("type");
PropertyType = reader.GetRequiredAttribute("propertytype");
// First read the start element
reader.ReadStartElement("property");
// Then read the properties
Value = XmlHelpers.ReadProperties(reader);
// Finally read the end element
reader.ReadEndElement();
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
public enum ObjectAlignment
{
[XmlEnum(Name = "unspecified")]
Unspecified,
[XmlEnum(Name = "topleft")]
TopLeft,
[XmlEnum(Name = "top")]
Top,
[XmlEnum(Name = "topright")]
TopRight,
[XmlEnum(Name = "left")]
Left,
[XmlEnum(Name = "center")]
Center,
[XmlEnum(Name = "right")]
Right,
[XmlEnum(Name = "bottomleft")]
BottomLeft,
[XmlEnum(Name = "bottom")]
Bottom,
[XmlEnum(Name = "bottomright")]
BottomRight
}
public enum TileRenderSize
{
[XmlEnum(Name = "tile")]
Tile,
[XmlEnum(Name = "grid")]
Grid
}
public enum FillMode
{
[XmlEnum(Name = "stretch")]
Stretch,
[XmlEnum(Name = "preserve-aspect-fit")]
PreserveAspectFit
}
public enum ImageFormat
{
[XmlEnum(Name = "png")]
Png,
[XmlEnum(Name = "gif")]
Gif,
[XmlEnum(Name = "jpg")]
Jpg,
[XmlEnum(Name = "bmp")]
Bmp
}
public enum TiledDataEncoding
{
[XmlEnum(Name = "csv")]
Csv,
[XmlEnum(Name = "base64")]
Base64
}
public enum TiledDataCompression
{
[XmlEnum(Name = "gzip")]
GZip,
[XmlEnum(Name = "zlib")]
ZLib,
[XmlEnum(Name = "zstd")]
ZStd
}
[XmlRoot(ElementName = "data")]
public class TiledData : IXmlSerializable
{
public TiledDataEncoding? Encoding { get; set; }
public TiledDataCompression? Compression { get; set; }
public required int[] Data { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
ReadXmlAttributes(reader);
ReadXmlElements(reader);
}
private void ReadXmlAttributes(XmlReader reader)
{
Encoding = reader.GetOptionalAttributeEnum<TiledDataEncoding>("encoding");
Compression = reader.GetOptionalAttributeEnum<TiledDataCompression>("compression");
}
private void ReadXmlElements(XmlReader reader)
{
if (Encoding is null && Compression is null)
{
// Plain csv
reader.ReadStartElement("data");
var dataAsCsvStringFromFile = reader.ReadContentAsString();
var data = dataAsCsvStringFromFile
.Split((char[])['\n', '\r', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(int.Parse)
.ToArray();
Data = data;
reader.ReadEndElement();
}
throw new NotImplementedException();
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "image")]
public class Image : IXmlSerializable
{
public ImageFormat? Format { get; set; }
public string? ID { get; set; } = null; // Deprecated and unsupported
public string? Source { get; set; }
public TiledColor? TransparentColor { get; set; }
public uint? Width { get; set; }
public uint? Height { get; set; }
private TiledData? _data = null;
public TiledData? Data
{
get => _data;
set => _data = value;
}
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
ReadXmlAttributes(reader);
ReadXmlElements(reader);
}
private void ReadXmlAttributes(XmlReader reader)
{
Format = reader.GetOptionalAttributeEnum<ImageFormat>("format");
ID = reader.GetOptionalAttribute("id");
Source = reader.GetOptionalAttribute("source");
TransparentColor = reader.GetOptionalAttributeClass<TiledColor>("trans");
Width = reader.GetOptionalAttribute<uint>("width");
Height = reader.GetOptionalAttribute<uint>("height");
}
private void ReadXmlElements(XmlReader reader)
{
reader.ReadStartElement("image");
while (reader.IsStartElement())
{
var name = reader.Name;
Action action = name switch
{
"data" => () => Helpers.SetAtMostOnce(ref _data, reader.ReadElementAs<TiledData>(), "Data"),
_ => reader.Skip
};
action();
if (reader.NodeType == XmlNodeType.EndElement)
return;
}
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
public abstract class BaseTileset : IXmlSerializable
{
public required string? FirstGID { get; set; } // Not set in tsx
public required string? Source { get; set; } // Not set in tsx
public required string Name { get; set; }
public required string Class { get; set; }
public required uint TileWidth { get; set; }
public required uint TileHeight { get; set; }
public required uint? Spacing { get; set; }
public required uint? Margin { get; set; }
public required uint TileCount { get; set; }
public required uint Columns { get; set; }
public required ObjectAlignment ObjectAlignment { get; set; }
public required TileRenderSize TileRenderSize { get; set; }
public required FillMode FillMode { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
ReadXmlAttributes(reader);
ReadXmlElements(reader);
}
private void ReadXmlAttributes(XmlReader reader)
{
FirstGID = reader.GetOptionalAttribute("firstgid");
Source = reader.GetOptionalAttribute("source");
Name = reader.GetRequiredAttribute("name");
Class = reader.GetOptionalAttribute("class") ?? ""; // default value
TileWidth = reader.GetRequiredAttribute<uint>("tilewidth");
TileHeight = reader.GetRequiredAttribute<uint>("tileheight");
Spacing = reader.GetOptionalAttribute<uint>("spacing");
Margin = reader.GetOptionalAttribute<uint>("margin");
TileCount = reader.GetRequiredAttribute<uint>("tilecount");
Columns = reader.GetRequiredAttribute<uint>("columns");
ObjectAlignment = reader.GetOptionalAttributeEnum<ObjectAlignment>("objectalignment") ?? ObjectAlignment.Unspecified;
TileRenderSize = reader.GetOptionalAttributeEnum<TileRenderSize>("tilerendersize") ?? TileRenderSize.Tile;
FillMode = reader.GetOptionalAttributeEnum<FillMode>("fillmode") ?? FillMode.Stretch;
}
protected abstract void ReadXmlElements(XmlReader reader);
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "tileset")]
public class ImageTileset : BaseTileset
{
private Image? _image = null;
public required Image Image
{
get => _image ?? throw new InvalidOperationException("Image not set"); // Should not be able to happen
set => _image = value;
}
protected override void ReadXmlElements(XmlReader reader)
{
// Different types of tilesets
reader.ReadStartElement("tileset");
while (reader.IsStartElement())
{
var name = reader.Name;
Action action = name switch
{
"image" => () => Helpers.SetAtMostOnce(ref _image, reader.ReadElementAs<Image>(), "Image"),
"tileoffset" => reader.Skip,
"tile" => reader.Skip,
"terraintypes" => reader.Skip,
"wangsets" => reader.Skip,
_ => reader.Skip
};
action();
if (reader.NodeType == XmlNodeType.EndElement)
return;
}
}
}
[XmlRoot(ElementName = "layer")]
public class Layer : IXmlSerializable
{
public required string ID { get; set; }
public required string Name { get; set; }
public required string Class { get; set; }
public required uint X { get; set; }
public required uint Y { get; set; }
public required uint Width { get; set; }
public required uint Height { get; set; }
public required float Opacity { get; set; }
public required bool Visible { get; set; }
public required TiledColor? TintColor { get; set; }
public required float OffsetX { get; set; }
public required float OffsetY { get; set; }
public required float ParallaxX { get; set; }
public required float ParallaxY { get; set; }
private Dictionary<string, IProperty>? _properties = null;
public required Dictionary<string, IProperty>? Properties
{
get => _properties;
set => _properties = value;
}
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
ReadXmlAttributes(reader);
ReadXmlElements(reader);
}
private void ReadXmlAttributes(XmlReader reader)
{
ID = reader.GetRequiredAttribute("id");
Name = reader.GetRequiredAttribute("name");
Class = reader.GetOptionalAttribute("class") ?? ""; // default value
X = reader.GetRequiredAttribute<uint>("x");
Y = reader.GetRequiredAttribute<uint>("y");
Width = reader.GetRequiredAttribute<uint>("width");
Height = reader.GetRequiredAttribute<uint>("height");
Opacity = reader.GetRequiredAttribute<float>("opacity");
Visible = reader.GetRequiredAttribute<uint>("visible") == 1;
TintColor = reader.GetOptionalAttributeClass<TiledColor>("tintcolor");
OffsetX = reader.GetRequiredAttribute<float>("offsetx");
OffsetY = reader.GetRequiredAttribute<float>("offsety");
ParallaxX = reader.GetRequiredAttribute<float>("parallaxx");
ParallaxY = reader.GetRequiredAttribute<float>("parallaxy");
}
private void ReadXmlElements(XmlReader reader)
{
reader.ReadStartElement("layer");
while (reader.IsStartElement())
{
var name = reader.Name;
Action action = name switch
{
"properties" => () => Helpers.SetAtMostOnce(ref _properties, XmlHelpers.ReadProperties(reader), "Properties"),
"data" => reader.Skip,
_ => reader.Skip
};
action();
}
reader.ReadEndElement();
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}
[XmlRoot(ElementName = "map")]
public class Map : IXmlSerializable
{
public required string Version { get; set; }
public string? TiledVersion { get; set; }
public required string Class { get; set; }
public required Orientation Orientation { get; set; }
public required RenderOrder RenderOrder { get; set; }
public required int CompressionLevel { get; set; }
public required uint Width { get; set; }
public required uint Height { get; set; }
public required uint TileWidth { get; set; }
public required uint TileHeight { get; set; }
public uint? HexSideLength { get; set; }
public StaggerAxis? StaggerAxis { get; set; }
public StaggerIndex? StaggerIndex { get; set; }
public required float ParallaxOriginX { get; set; }
public required float ParallaxOriginY { get; set; }
public TiledColor? BackgroundColor { get; set; }
public required uint NextLayerId { get; set; }
public required uint NextObjectId { get; set; }
public required bool Infinite { get; set; }
private Dictionary<string, IProperty>? _properties = null;
public required Dictionary<string, IProperty>? Properties
{
get => _properties;
set => _properties = value;
}
public required List<BaseTileset> Tilesets { get; set; } = [];
public T GetProperty<T>(string propertyName) where T : IProperty
{
if (Properties is null)
throw new InvalidOperationException("Properties not set");
return (T)Properties[propertyName];
}
public static Map LoadFromStream(Stream stream)
{
using var reader = new StreamReader(stream, Encoding.UTF8);
var serializer = new XmlSerializer(typeof(Map));
return (Map)serializer.Deserialize(reader)!;
}
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
ReadXmlAttributes(reader);
ReadXmlElements(reader, (s) => null);
}
private void ReadXmlAttributes(XmlReader reader)
{
Version = reader.GetRequiredAttribute("version");
TiledVersion = reader.GetOptionalAttribute("tiledversion");
Class = reader.GetOptionalAttribute("class") ?? ""; // default value
Orientation = reader.GetRequiredAttributeEnum<Orientation>("orientation");
RenderOrder = reader.GetRequiredAttributeEnum<RenderOrder>("renderorder");
CompressionLevel = reader.GetRequiredAttribute<int>("compressionlevel");
Width = reader.GetRequiredAttribute<uint>("width");
Height = reader.GetRequiredAttribute<uint>("height");
TileWidth = reader.GetRequiredAttribute<uint>("tilewidth");
TileHeight = reader.GetRequiredAttribute<uint>("tileheight");
HexSideLength = reader.GetOptionalAttribute<uint>("hexsidelength");
StaggerAxis = reader.GetOptionalAttributeEnum<StaggerAxis>("staggeraxis");
StaggerIndex = reader.GetOptionalAttributeEnum<StaggerIndex>("staggerindex");
ParallaxOriginX = reader.GetRequiredAttribute<float>("parallaxoriginx");
ParallaxOriginY = reader.GetRequiredAttribute<float>("parallaxoriginy");
BackgroundColor = reader.GetOptionalAttributeClass<TiledColor>("backgroundcolor");
NextLayerId = reader.GetRequiredAttribute<uint>("nextlayerid");
NextObjectId = reader.GetRequiredAttribute<uint>("nextobjectid");
Infinite = reader.GetRequiredAttribute<uint>("infinite") == 1;
}
private void ReadXmlElements(XmlReader reader, Func<string, BaseTileset> tilesetResolver)
{
reader.ReadStartElement("map");
while (reader.IsStartElement())
{
var name = reader.Name;
Action action = name switch
{
"properties" => () => Helpers.SetAtMostOnce(ref _properties, XmlHelpers.ReadProperties(reader), "Properties"),
"editorsettings" => reader.Skip,
"tileset" => () => Tilesets.Add(XmlHelpers.ReadTileset(reader, tilesetResolver)),
_ => reader.Skip
};
action();
}
reader.ReadEndElement();
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,109 @@
using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
namespace DotTiled;
internal static class ExtensionsXmlReader
{
internal static string GetRequiredAttribute(this XmlReader reader, string attribute)
{
return reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required"); ;
}
internal static T GetRequiredAttribute<T>(this XmlReader reader, string attribute) where T : IParsable<T>
{
var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required");
return T.Parse(value, CultureInfo.InvariantCulture);
}
internal static T GetRequiredAttributeEnum<T>(this XmlReader reader, string attribute) where T : Enum
{
var value = reader.GetAttribute(attribute) ?? throw new XmlException($"{attribute} attribute is required");
return ParseEnumUsingXmlEnumAttribute<T>(value);
}
internal static string? GetOptionalAttribute(this XmlReader reader, string attribute, string? defaultValue = default)
{
return reader.GetAttribute(attribute) ?? defaultValue;
}
internal static T? GetOptionalAttribute<T>(this XmlReader reader, string attribute) where T : struct, IParsable<T>
{
var value = reader.GetAttribute(attribute);
if (value is null)
return null;
return T.Parse(value, CultureInfo.InvariantCulture);
}
internal static T? GetOptionalAttributeClass<T>(this XmlReader reader, string attribute) where T : class, IParsable<T>
{
var value = reader.GetAttribute(attribute);
if (value is null)
return null;
return T.Parse(value, CultureInfo.InvariantCulture);
}
internal static T? GetOptionalAttributeEnum<T>(this XmlReader reader, string attribute) where T : struct, Enum
{
var value = reader.GetAttribute(attribute);
return value != null ? ParseEnumUsingXmlEnumAttribute<T>(value) : null;
}
internal static T ParseEnumUsingXmlEnumAttribute<T>(string value) where T : Enum
{
var enumType = typeof(T);
var enumValues = Enum.GetValues(enumType);
foreach (var enumValue in enumValues)
{
var enumMember = enumType.GetMember(enumValue.ToString()!)[0];
var xmlEnumAttribute = enumMember.GetCustomAttributes(typeof(XmlEnumAttribute), false).FirstOrDefault() as XmlEnumAttribute;
if (xmlEnumAttribute?.Name == value)
return (T)enumValue;
}
throw new XmlException($"Failed to parse enum value {value}");
}
internal static List<T> ReadList<T>(this XmlReader reader, string wrapper, string elementName, Func<XmlReader, T> readElement)
{
var list = new List<T>();
if (reader.IsEmptyElement)
return list;
reader.ReadStartElement(wrapper);
while (reader.IsStartElement(elementName))
{
list.Add(readElement(reader));
if (reader.NodeType == XmlNodeType.EndElement)
continue; // At end of list, no need to read again
reader.Read();
}
reader.ReadEndElement();
return list;
}
public static T ReadElementAs<T>(this XmlReader reader) where T : IXmlSerializable
{
var serializer = new XmlSerializer(typeof(T));
return (T)serializer.Deserialize(reader)!;
}
public static int CountDirectChildrenWithName(this XmlReader reader, string name)
{
var subTree = reader.ReadSubtree();
int count = 0;
while (subTree.Read())
{
if (subTree.NodeType == XmlNodeType.Element && subTree.Name == name)
count++;
}
return count;
}
}

View file

@ -0,0 +1,52 @@
using System.Xml;
using System.Xml.Serialization;
namespace DotTiled;
public static class XmlHelpers
{
public static Dictionary<string, IProperty> ReadProperties(XmlReader reader)
{
return reader.ReadList<(string PropName, IProperty Prop)>("properties", "property",
reader =>
{
var type = reader.GetRequiredAttributeEnum<PropertyType>("type");
var propertyRuntimeType = type switch
{
PropertyType.String => typeof(StringProperty),
PropertyType.Int => typeof(IntProperty),
PropertyType.Float => typeof(FloatProperty),
PropertyType.Bool => typeof(BooleanProperty),
PropertyType.Color => typeof(ColorProperty),
PropertyType.File => typeof(FileProperty),
PropertyType.Object => typeof(ObjectProperty),
PropertyType.Class => typeof(ClassProperty),
_ => throw new XmlException("Invalid property type")
};
var serializer = new XmlSerializer(propertyRuntimeType);
var deserializedProperty = (IProperty)serializer.Deserialize(reader)!;
return (deserializedProperty.Name, deserializedProperty);
}
).ToDictionary(x => x.PropName, x => x.Prop);
}
public static BaseTileset ReadTileset(XmlReader reader, Func<string, BaseTileset> tilesetResolver)
{
var imageChildren = reader.CountDirectChildrenWithName("image");
var tileChildren = reader.CountDirectChildrenWithName("tile");
if (imageChildren == 0 && tileChildren == 0)
{
// This is a tileset that must have "source" set
var source = reader.GetRequiredAttribute("source");
return tilesetResolver(source);
}
if (imageChildren == 1)
{
// This is a single image tileset
return reader.ReadElementAs<ImageTileset>();
}
throw new XmlException("Invalid tileset");
}
}

0
Makefile Normal file
View file