Merge pull request #2 from dcronqvist/json-support

Add JSON map parsing support
This commit is contained in:
dcronqvist 2024-08-11 17:16:24 +02:00 committed by GitHub
commit 33f1f4dcb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2599 additions and 210 deletions

View file

@ -15,68 +15,54 @@ namespace MyBenchmarks
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn] [CategoriesColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)] [Orderer(SummaryOrderPolicy.FastestToSlowest)]
[HideColumns(["StdDev", "Error", "RatioSD"])]
public class MapLoading public class MapLoading
{ {
private string _tmxPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\Tmx\TestData\Map\empty-map-csv.tmx"; private string _tmxPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\TestData\Map\empty-map-csv.tmx";
private string _tmxContents = ""; private string _tmxContents = "";
private string _tmjPath = @"C:\Users\Daniel\winrepos\DotTiled\DotTiled.Tests\Serialization\TestData\Map\empty-map-csv.tmj";
private string _tmjContents = "";
public MapLoading() public MapLoading()
{ {
_tmxContents = System.IO.File.ReadAllText(_tmxPath); _tmxContents = System.IO.File.ReadAllText(_tmxPath);
_tmjContents = System.IO.File.ReadAllText(_tmjPath);
} }
[BenchmarkCategory("MapFromInMemoryTmxString")] [BenchmarkCategory("MapFromInMemoryTmxString")]
[Benchmark(Baseline = true, Description = "DotTiled")] [Benchmark(Baseline = true, Description = "DotTiled")]
public DotTiled.Map LoadWithDotTiledFromInMemoryString() public DotTiled.Map LoadWithDotTiledFromInMemoryTmxString()
{ {
using var stringReader = new StringReader(_tmxContents); using var stringReader = new StringReader(_tmxContents);
using var xmlReader = XmlReader.Create(stringReader); using var xmlReader = XmlReader.Create(stringReader);
using var mapReader = new DotTiled.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception()); using var mapReader = new DotTiled.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception(), []);
return mapReader.ReadMap(); return mapReader.ReadMap();
} }
[BenchmarkCategory("MapFromTmxFile")] [BenchmarkCategory("MapFromInMemoryTmjString")]
[Benchmark(Baseline = true, Description = "DotTiled")] [Benchmark(Baseline = true, Description = "DotTiled")]
public DotTiled.Map LoadWithDotTiledFromFile() public DotTiled.Map LoadWithDotTiledFromInMemoryTmjString()
{ {
using var fileStream = System.IO.File.OpenRead(_tmxPath); using var mapReader = new DotTiled.TmjMapReader(_tmjContents, _ => throw new Exception(), _ => throw new Exception(), []);
using var xmlReader = XmlReader.Create(fileStream);
using var mapReader = new DotTiled.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception());
return mapReader.ReadMap(); return mapReader.ReadMap();
} }
[BenchmarkCategory("MapFromInMemoryTmxString")] [BenchmarkCategory("MapFromInMemoryTmxString")]
[Benchmark(Description = "TiledLib")] [Benchmark(Description = "TiledLib")]
public TiledLib.Map LoadWithTiledLibFromInMemoryString() public TiledLib.Map LoadWithTiledLibFromInMemoryTmxString()
{ {
using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents)); using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents));
return TiledLib.Map.FromStream(memStream); return TiledLib.Map.FromStream(memStream);
} }
[BenchmarkCategory("MapFromTmxFile")]
[Benchmark(Description = "TiledLib")]
public TiledLib.Map LoadWithTiledLibFromFile()
{
using var fileStream = System.IO.File.OpenRead(_tmxPath);
var map = TiledLib.Map.FromStream(fileStream);
return map;
}
[BenchmarkCategory("MapFromInMemoryTmxString")] [BenchmarkCategory("MapFromInMemoryTmxString")]
[Benchmark(Description = "TiledCSPlus")] [Benchmark(Description = "TiledCSPlus")]
public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromInMemoryString() public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromInMemoryTmxString()
{ {
using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents)); using var memStream = new MemoryStream(Encoding.UTF8.GetBytes(_tmxContents));
return new TiledCSPlus.TiledMap(memStream); return new TiledCSPlus.TiledMap(memStream);
} }
[BenchmarkCategory("MapFromTmxFile")]
[Benchmark(Description = "TiledCSPlus")]
public TiledCSPlus.TiledMap LoadWithTiledCSPlusFromFile()
{
using var fileStream = System.IO.File.OpenRead(_tmxPath);
return new TiledCSPlus.TiledMap(fileStream);
}
} }
public class Program public class Program
@ -84,6 +70,7 @@ namespace MyBenchmarks
public static void Main(string[] args) public static void Main(string[] args)
{ {
var config = BenchmarkDotNet.Configs.DefaultConfig.Instance var config = BenchmarkDotNet.Configs.DefaultConfig.Instance
.WithArtifactsPath(args[0])
.WithOptions(ConfigOptions.DisableOptimizationsValidator) .WithOptions(ConfigOptions.DisableOptimizationsValidator)
.AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default); .AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default);
var summary = BenchmarkRunner.Run<MapLoading>(config); var summary = BenchmarkRunner.Run<MapLoading>(config);

View file

@ -30,11 +30,11 @@ public static partial class DotTiledAssert
Assert.NotNull(actual.Tilesets); Assert.NotNull(actual.Tilesets);
Assert.Equal(expected.Tilesets.Count, actual.Tilesets.Count); Assert.Equal(expected.Tilesets.Count, actual.Tilesets.Count);
for (var i = 0; i < expected.Tilesets.Count; i++) for (var i = 0; i < expected.Tilesets.Count; i++)
AssertTileset(actual.Tilesets[i], expected.Tilesets[i]); AssertTileset(expected.Tilesets[i], actual.Tilesets[i]);
Assert.NotNull(actual.Layers); Assert.NotNull(actual.Layers);
Assert.Equal(expected.Layers.Count, actual.Layers.Count); Assert.Equal(expected.Layers.Count, actual.Layers.Count);
for (var i = 0; i < expected.Layers.Count; i++) for (var i = 0; i < expected.Layers.Count; i++)
AssertLayer(actual.Layers[i], expected.Layers[i]); AssertLayer(expected.Layers[i], actual.Layers[i]);
} }
} }

View file

@ -17,7 +17,7 @@ public static partial class DotTiledAssert
Assert.Equal(expected.Visible, actual.Visible); Assert.Equal(expected.Visible, actual.Visible);
Assert.Equal(expected.Template, actual.Template); Assert.Equal(expected.Template, actual.Template);
AssertProperties(actual.Properties, expected.Properties); AssertProperties(expected.Properties, actual.Properties);
AssertObject((dynamic)expected, (dynamic)actual); AssertObject((dynamic)expected, (dynamic)actual);
} }

View file

@ -19,51 +19,51 @@ public static partial class DotTiledAssert
} }
} }
private static void AssertProperty(IProperty actual, IProperty expected) private static void AssertProperty(IProperty expected, IProperty actual)
{ {
Assert.Equal(expected.Type, actual.Type); Assert.Equal(expected.Type, actual.Type);
Assert.Equal(expected.Name, actual.Name); Assert.Equal(expected.Name, actual.Name);
AssertProperties((dynamic)actual, (dynamic)expected); AssertProperties((dynamic)actual, (dynamic)expected);
} }
private static void AssertProperty(StringProperty actual, StringProperty expected) private static void AssertProperty(StringProperty expected, StringProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(IntProperty actual, IntProperty expected) private static void AssertProperty(IntProperty expected, IntProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(FloatProperty actual, FloatProperty expected) private static void AssertProperty(FloatProperty expected, FloatProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(BoolProperty actual, BoolProperty expected) private static void AssertProperty(BoolProperty expected, BoolProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(ColorProperty actual, ColorProperty expected) private static void AssertProperty(ColorProperty expected, ColorProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(FileProperty actual, FileProperty expected) private static void AssertProperty(FileProperty expected, FileProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(ObjectProperty actual, ObjectProperty expected) private static void AssertProperty(ObjectProperty expected, ObjectProperty actual)
{ {
Assert.Equal(expected.Value, actual.Value); Assert.Equal(expected.Value, actual.Value);
} }
private static void AssertProperty(ClassProperty actual, ClassProperty expected) private static void AssertProperty(ClassProperty expected, ClassProperty actual)
{ {
Assert.Equal(expected.PropertyType, actual.PropertyType); Assert.Equal(expected.PropertyType, actual.PropertyType);
AssertProperties(actual.Properties, expected.Properties); AssertProperties(expected.Properties, actual.Properties);
} }
} }

View file

@ -25,8 +25,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- TmxSerializer test data --> <!-- Test data -->
<EmbeddedResource Include="Serialization/Tmx/TestData/**/*" /> <EmbeddedResource Include="Serialization/TestData/**/*" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,12 +2,12 @@ using System.Xml;
namespace DotTiled.Tests; namespace DotTiled.Tests;
public static class TmxMapReaderTestData public static partial class TestData
{ {
public static XmlReader GetXmlReaderFor(string testDataFile) public static XmlReader GetXmlReaderFor(string testDataFile)
{ {
var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}"; var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}";
using var stream = typeof(TmxMapReaderTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) using var stream = typeof(TestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found"); ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found");
using var stringReader = new StreamReader(stream); using var stringReader = new StreamReader(stream);
@ -19,7 +19,7 @@ public static class TmxMapReaderTestData
public static string GetRawStringFor(string testDataFile) public static string GetRawStringFor(string testDataFile)
{ {
var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}"; var fullyQualifiedTestDataFile = $"DotTiled.Tests.{testDataFile}";
using var stream = typeof(TmxMapReaderTestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile) using var stream = typeof(TestData).Assembly.GetManifestResourceStream(fullyQualifiedTestDataFile)
?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found"); ?? throw new ArgumentException($"Test data file '{fullyQualifiedTestDataFile}' not found");
using var stringReader = new StreamReader(stream); using var stringReader = new StreamReader(stream);

View file

@ -0,0 +1,103 @@
[
{
"id": 4,
"name": "Enum0String",
"storageType": "string",
"type": "enum",
"values": [
"Enum0_1",
"Enum0_2",
"Enum0_3"
],
"valuesAsFlags": false
},
{
"id": 5,
"name": "Enum1Num",
"storageType": "int",
"type": "enum",
"values": [
"Enum1Num_1",
"Enum1Num_2",
"Enum1Num_3",
"Enum1Num_4"
],
"valuesAsFlags": false
},
{
"id": 6,
"name": "Enum2StringFlags",
"storageType": "string",
"type": "enum",
"values": [
"Enum2StringFlags_1",
"Enum2StringFlags_2",
"Enum2StringFlags_3",
"Enum2StringFlags_4"
],
"valuesAsFlags": true
},
{
"id": 7,
"name": "Enum3NumFlags",
"storageType": "int",
"type": "enum",
"values": [
"Enum3NumFlags_1",
"Enum3NumFlags_2",
"Enum3NumFlags_3",
"Enum3NumFlags_4",
"Enum3NumFlags_5"
],
"valuesAsFlags": true
},
{
"color": "#ffa0a0a4",
"drawFill": true,
"id": 2,
"members": [
{
"name": "Yep",
"propertyType": "TestClass",
"type": "class",
"value": {
}
}
],
"name": "Test",
"type": "class",
"useAs": [
"property",
"map",
"layer",
"object",
"tile",
"tileset",
"wangcolor",
"wangset",
"project"
]
},
{
"color": "#ffa0a0a4",
"drawFill": true,
"id": 1,
"members": [
{
"name": "Amount",
"type": "float",
"value": 0
},
{
"name": "Name",
"type": "string",
"value": ""
}
],
"name": "TestClass",
"type": "class",
"useAs": [
"property"
]
}
]

View file

@ -0,0 +1,24 @@
[
{
"color": "#ffa0a0a4",
"drawFill": true,
"id": 1,
"members": [
{
"name": "Amount",
"type": "float",
"value": 0
},
{
"name": "Name",
"type": "string",
"value": ""
}
],
"name": "TestClass",
"type": "class",
"useAs": [
"property"
]
}
]

View file

@ -0,0 +1,30 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"compression":"gzip",
"data":"H4sIAAAAAAAACmNgoD0AAMrGiJlkAAAA",
"encoding":"base64",
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,30 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"compression":"zlib",
"data":"eJxjYKA9AAAAZAAB",
"encoding":"base64",
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,30 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"compression":"",
"data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"encoding":"base64",
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,32 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests; namespace DotTiled.Tests;
public partial class TmxMapReaderTests public partial class TestData
{ {
private static Map EmptyMapWithProperties() => new Map public static Map EmptyMapWithProperties() => new Map
{ {
Version = "1.10", Version = "1.10",
TiledVersion = "1.11.0", TiledVersion = "1.11.0",

View file

@ -0,0 +1,69 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"properties":[
{
"name":"MapBool",
"type":"bool",
"value":true
},
{
"name":"MapColor",
"type":"color",
"value":"#ffff0000"
},
{
"name":"MapFile",
"type":"file",
"value":"file.png"
},
{
"name":"MapFloat",
"type":"float",
"value":5.2
},
{
"name":"MapInt",
"type":"int",
"value":42
},
{
"name":"MapObject",
"type":"object",
"value":5
},
{
"name":"MapString",
"type":"string",
"value":"string in map"
}],
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests; namespace DotTiled.Tests;
public partial class TmxMapReaderTests public partial class TestData
{ {
private static Map EmptyMapWithEncodingAndCompression(DataEncoding dataEncoding, DataCompression? compression) => new Map public static Map EmptyMapWithEncodingAndCompression(DataEncoding dataEncoding, DataCompression? compression) => new Map
{ {
Version = "1.10", Version = "1.10",
TiledVersion = "1.11.0", TiledVersion = "1.11.0",

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests; namespace DotTiled.Tests;
public partial class TmxMapReaderTests public partial class TestData
{ {
private static Map MapWithGroup() => new Map public static Map MapWithGroup() => new Map
{ {
Version = "1.10", Version = "1.10",
TiledVersion = "1.11.0", TiledVersion = "1.11.0",

View file

@ -0,0 +1,80 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":4,
"name":"Tile Layer 2",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
},
{
"id":3,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"Object Layer 1",
"objects":[
{
"height":64.5,
"id":1,
"name":"Name",
"rotation":0,
"type":"",
"visible":true,
"width":64.5,
"x":35.5,
"y":26
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"name":"Group 1",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests; namespace DotTiled.Tests;
public partial class TmxMapReaderTests public partial class TestData
{ {
private static Map MapWithObjectTemplate() => new Map public static Map MapWithObjectTemplate(string templateExtension) => new Map
{ {
Version = "1.10", Version = "1.10",
TiledVersion = "1.11.0", TiledVersion = "1.11.0",
@ -49,7 +49,7 @@ public partial class TmxMapReaderTests
new RectangleObject new RectangleObject
{ {
ID = 1, ID = 1,
Template = "map-with-object-template.tx", Template = $"map-with-object-template.{templateExtension}",
Name = "Thingy 2", Name = "Thingy 2",
X = 94.5749f, X = 94.5749f,
Y = 33.6842f, Y = 33.6842f,
@ -73,7 +73,7 @@ public partial class TmxMapReaderTests
new RectangleObject new RectangleObject
{ {
ID = 2, ID = 2,
Template = "map-with-object-template.tx", Template = $"map-with-object-template.{templateExtension}",
Name = "Thingy", Name = "Thingy",
X = 29.7976f, X = 29.7976f,
Y = 33.8693f, Y = 33.8693f,
@ -97,7 +97,7 @@ public partial class TmxMapReaderTests
new RectangleObject new RectangleObject
{ {
ID = 3, ID = 3,
Template = "map-with-object-template.tx", Template = $"map-with-object-template.{templateExtension}",
Name = "Thingy 3", Name = "Thingy 3",
X = 5, X = 5,
Y = 5, Y = 5,
@ -112,7 +112,7 @@ public partial class TmxMapReaderTests
PropertyType = "TestClass", PropertyType = "TestClass",
Properties = new Dictionary<string, IProperty> Properties = new Dictionary<string, IProperty>
{ {
["Amount"] = new FloatProperty { Name = "Amount", Value = 4.2f }, ["Amount"] = new FloatProperty { Name = "Amount", Value = 0.0f },
["Name"] = new StringProperty { Name = "Name", Value = "I am here 3" } ["Name"] = new StringProperty { Name = "Name", Value = "I am here 3" }
} }
} }

View file

@ -0,0 +1,104 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"Object Layer 1",
"objects":[
{
"height":37.0156,
"id":1,
"template":"map-with-object-template.tj",
"name":"Thingy 2",
"properties":[
{
"name":"Bool",
"type":"bool",
"value":true
},
{
"name":"TestClassInTemplate",
"propertytype":"TestClass",
"type":"class",
"value":
{
"Amount":37,
"Name":"I am here"
}
}],
"rotation":0,
"type":"",
"visible":true,
"width":37.0156,
"x":94.5749,
"y":33.6842
},
{
"id":2,
"template":"map-with-object-template.tj",
"x":29.7976,
"y":33.8693
},
{
"height":37.0156,
"id":3,
"template":"map-with-object-template.tj",
"name":"Thingy 3",
"properties":[
{
"name":"Bool",
"type":"bool",
"value":true
},
{
"name":"TestClassInTemplate",
"propertytype":"TestClass",
"type":"class",
"value":
{
"Name":"I am here 3"
}
}],
"rotation":0,
"type":"",
"visible":true,
"width":37.0156,
"x":5,
"y":5
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":3,
"nextobjectid":3,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -1,8 +1,8 @@
namespace DotTiled.Tests; namespace DotTiled.Tests;
public partial class TmxMapReaderTests public partial class TestData
{ {
private static Map SimpleMapWithEmbeddedTileset() => new Map public static Map SimpleMapWithEmbeddedTileset() => new Map
{ {
Version = "1.10", Version = "1.10",
TiledVersion = "1.11.0", TiledVersion = "1.11.0",
@ -26,6 +26,7 @@ public partial class TmxMapReaderTests
Columns = 4, Columns = 4,
Image = new Image Image = new Image
{ {
Format = ImageFormat.Png,
Source = "tiles.png", Source = "tiles.png",
Width = 128, Width = 128,
Height = 64 Height = 64

View file

@ -0,0 +1,45 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
2, 2, 2, 2, 2,
2, 2, 2, 2, 2],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[
{
"columns":4,
"firstgid":1,
"image":"tiles.png",
"imageheight":64,
"imagewidth":128,
"margin":0,
"name":"Tileset 1",
"spacing":0,
"tilecount":8,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,28 @@
{ "object":
{
"height":37.0156,
"id":2,
"name":"Thingy",
"properties":[
{
"name":"Bool",
"type":"bool",
"value":true
},
{
"name":"TestClassInTemplate",
"propertytype":"TestClass",
"type":"class",
"value":
{
"Amount":4.2,
"Name":"Hello there"
}
}],
"rotation":0,
"type":"",
"visible":true,
"width":37.0156
},
"type":"template"
}

View file

@ -0,0 +1,107 @@
namespace DotTiled.Tests;
public partial class TmjMapReaderTests
{
public static IEnumerable<object[]> DeserializeMap_ValidTmjNoExternalTilesets_ReturnsMapWithoutThrowing_Data =>
[
["Serialization.TestData.Map.empty-map-csv.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)],
["Serialization.TestData.Map.empty-map-base64.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)],
["Serialization.TestData.Map.empty-map-base64-gzip.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)],
["Serialization.TestData.Map.empty-map-base64-zlib.tmj", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)],
["Serialization.TestData.Map.simple-tileset-embed.tmj", TestData.SimpleMapWithEmbeddedTileset()],
["Serialization.TestData.Map.empty-map-properties.tmj", TestData.EmptyMapWithProperties()],
];
[Theory]
[MemberData(nameof(DeserializeMap_ValidTmjNoExternalTilesets_ReturnsMapWithoutThrowing_Data))]
public void TmxMapReaderReadMap_ValidTmjNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
var json = TestData.GetRawStringFor(testDataFile);
static Template ResolveTemplate(string source)
{
throw new NotSupportedException("External templates are not supported in this test.");
}
static Tileset ResolveTileset(string source)
{
throw new NotSupportedException("External tilesets are not supported in this test.");
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, []);
// Act
var map = mapReader.ReadMap();
// Assert
Assert.NotNull(map);
DotTiledAssert.AssertMap(expectedMap, map);
}
public static IEnumerable<object[]> DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data =>
[
["Serialization.TestData.Map.map-with-object-template.tmj", TestData.MapWithObjectTemplate("tj")],
["Serialization.TestData.Map.map-with-group.tmj", TestData.MapWithGroup()],
];
[Theory]
[MemberData(nameof(DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data))]
public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{
// Arrange
CustomTypeDefinition[] customTypeDefinitions = [
new CustomClassDefinition
{
Name = "TestClass",
ID = 1,
UseAs = CustomClassUseAs.Property,
Members = [
new StringProperty
{
Name = "Name",
Value = ""
},
new FloatProperty
{
Name = "Amount",
Value = 0f
}
]
},
new CustomClassDefinition
{
Name = "Test",
ID = 2,
UseAs = CustomClassUseAs.All,
Members = [
new ClassProperty
{
Name = "Yep",
PropertyType = "TestClass",
Properties = []
}
]
}
];
var json = TestData.GetRawStringFor(testDataFile);
Template ResolveTemplate(string source)
{
var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}");
using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions);
return templateReader.ReadTemplate();
}
Tileset ResolveTileset(string source)
{
var tilesetJson = TestData.GetRawStringFor($"Serialization.TestData.Tileset.{source}");
using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions);
return tilesetReader.ReadTileset();
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions);
// Act
var map = mapReader.ReadMap();
// Assert
Assert.NotNull(map);
DotTiledAssert.AssertMap(expectedMap, map);
}
}

View file

@ -15,7 +15,7 @@ public partial class TmxMapReaderTests
// Act // Act
Action act = () => Action act = () =>
{ {
using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []);
}; };
// Assert // Assert
@ -34,7 +34,7 @@ public partial class TmxMapReaderTests
// Act // Act
Action act = () => Action act = () =>
{ {
using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []);
}; };
// Assert // Assert
@ -53,7 +53,7 @@ public partial class TmxMapReaderTests
// Act // Act
Action act = () => Action act = () =>
{ {
using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); using var _ = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []);
}; };
// Assert // Assert
@ -70,7 +70,7 @@ public partial class TmxMapReaderTests
Func<string, Template> externalTemplateResolver = (_) => new Template { Object = new RectangleObject { } }; Func<string, Template> externalTemplateResolver = (_) => new Template { Object = new RectangleObject { } };
// Act // Act
using var tmxMapReader = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver); using var tmxMapReader = new TmxMapReader(xmlReader, externalTilesetResolver, externalTemplateResolver, []);
// Assert // Assert
Assert.NotNull(tmxMapReader); Assert.NotNull(tmxMapReader);
@ -78,12 +78,12 @@ public partial class TmxMapReaderTests
public static IEnumerable<object[]> DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data => public static IEnumerable<object[]> DeserializeMap_ValidXmlNoExternalTilesets_ReturnsMapWithoutThrowing_Data =>
[ [
["Serialization.Tmx.TestData.Map.empty-map-csv.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)], ["Serialization.TestData.Map.empty-map-csv.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Csv, null)],
["Serialization.Tmx.TestData.Map.empty-map-base64.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)], ["Serialization.TestData.Map.empty-map-base64.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, null)],
["Serialization.Tmx.TestData.Map.empty-map-base64-gzip.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)], ["Serialization.TestData.Map.empty-map-base64-gzip.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.GZip)],
["Serialization.Tmx.TestData.Map.empty-map-base64-zlib.tmx", EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)], ["Serialization.TestData.Map.empty-map-base64-zlib.tmx", TestData.EmptyMapWithEncodingAndCompression(DataEncoding.Base64, DataCompression.ZLib)],
["Serialization.Tmx.TestData.Map.simple-tileset-embed.tmx", SimpleMapWithEmbeddedTileset()], ["Serialization.TestData.Map.simple-tileset-embed.tmx", TestData.SimpleMapWithEmbeddedTileset()],
["Serialization.Tmx.TestData.Map.empty-map-properties.tmx", EmptyMapWithProperties()], ["Serialization.TestData.Map.empty-map-properties.tmx", TestData.EmptyMapWithProperties()],
]; ];
[Theory] [Theory]
@ -91,20 +91,55 @@ public partial class TmxMapReaderTests
public void TmxMapReaderReadMap_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) public void TmxMapReaderReadMap_ValidXmlNoExternalTilesets_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{ {
// Arrange // Arrange
using var reader = TmxMapReaderTestData.GetXmlReaderFor(testDataFile); CustomTypeDefinition[] customTypeDefinitions = [
static Template ResolveTemplate(string source) new CustomClassDefinition
{ {
using var xmlTemplateReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Template.{source}"); Name = "TestClass",
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate); ID = 1,
UseAs = CustomClassUseAs.Property,
Members = [
new StringProperty
{
Name = "Name",
Value = ""
},
new FloatProperty
{
Name = "Amount",
Value = 0f
}
]
},
new CustomClassDefinition
{
Name = "Test",
ID = 2,
UseAs = CustomClassUseAs.All,
Members = [
new ClassProperty
{
Name = "Yep",
PropertyType = "TestClass",
Properties = []
}
]
}
];
using var reader = TestData.GetXmlReaderFor(testDataFile);
Template ResolveTemplate(string source)
{
using var xmlTemplateReader = TestData.GetXmlReaderFor($"Serialization.TestData.Template.{source}");
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions);
return templateReader.ReadTemplate(); return templateReader.ReadTemplate();
} }
static Tileset ResolveTileset(string source) Tileset ResolveTileset(string source)
{ {
using var xmlTilesetReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Tileset.{source}"); using var xmlTilesetReader = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}");
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate); using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions);
return tilesetReader.ReadTileset(); return tilesetReader.ReadTileset();
} }
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate); using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions);
// Act // Act
var map = mapReader.ReadMap(); var map = mapReader.ReadMap();
@ -116,8 +151,8 @@ public partial class TmxMapReaderTests
public static IEnumerable<object[]> DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => public static IEnumerable<object[]> DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data =>
[ [
["Serialization.Tmx.TestData.Map.map-with-object-template.tmx", MapWithObjectTemplate()], ["Serialization.TestData.Map.map-with-object-template.tmx", TestData.MapWithObjectTemplate("tx")],
["Serialization.Tmx.TestData.Map.map-with-group.tmx", MapWithGroup()], ["Serialization.TestData.Map.map-with-group.tmx", TestData.MapWithGroup()],
]; ];
[Theory] [Theory]
@ -125,20 +160,54 @@ public partial class TmxMapReaderTests
public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{ {
// Arrange // Arrange
using var reader = TmxMapReaderTestData.GetXmlReaderFor(testDataFile); CustomTypeDefinition[] customTypeDefinitions = [
static Template ResolveTemplate(string source) new CustomClassDefinition
{ {
using var xmlTemplateReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Template.{source}"); Name = "TestClass",
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate); ID = 1,
UseAs = CustomClassUseAs.Property,
Members = [
new StringProperty
{
Name = "Name",
Value = ""
},
new FloatProperty
{
Name = "Amount",
Value = 0f
}
]
},
new CustomClassDefinition
{
Name = "Test",
ID = 2,
UseAs = CustomClassUseAs.All,
Members = [
new ClassProperty
{
Name = "Yep",
PropertyType = "TestClass",
Properties = []
}
]
}
];
using var reader = TestData.GetXmlReaderFor(testDataFile);
Template ResolveTemplate(string source)
{
using var xmlTemplateReader = TestData.GetXmlReaderFor($"Serialization.TestData.Template.{source}");
using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions);
return templateReader.ReadTemplate(); return templateReader.ReadTemplate();
} }
static Tileset ResolveTileset(string source) Tileset ResolveTileset(string source)
{ {
using var xmlTilesetReader = TmxMapReaderTestData.GetXmlReaderFor($"Serialization.Tmx.TestData.Tileset.{source}"); using var xmlTilesetReader = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}");
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate); using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions);
return tilesetReader.ReadTileset(); return tilesetReader.ReadTileset();
} }
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate); using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions);
// Act // Act
var map = mapReader.ReadMap(); var map = mapReader.ReadMap();

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace DotTiled; namespace DotTiled;
@ -18,6 +20,8 @@ public interface IProperty
{ {
public string Name { get; set; } public string Name { get; set; }
public PropertyType Type { get; } public PropertyType Type { get; }
IProperty Clone();
} }
public class StringProperty : IProperty public class StringProperty : IProperty
@ -25,6 +29,12 @@ public class StringProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.String; public PropertyType Type => PropertyType.String;
public required string Value { get; set; } public required string Value { get; set; }
public IProperty Clone() => new StringProperty
{
Name = Name,
Value = Value
};
} }
public class IntProperty : IProperty public class IntProperty : IProperty
@ -32,6 +42,12 @@ public class IntProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.Int; public PropertyType Type => PropertyType.Int;
public required int Value { get; set; } public required int Value { get; set; }
public IProperty Clone() => new IntProperty
{
Name = Name,
Value = Value
};
} }
public class FloatProperty : IProperty public class FloatProperty : IProperty
@ -39,6 +55,12 @@ public class FloatProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.Float; public PropertyType Type => PropertyType.Float;
public required float Value { get; set; } public required float Value { get; set; }
public IProperty Clone() => new FloatProperty
{
Name = Name,
Value = Value
};
} }
public class BoolProperty : IProperty public class BoolProperty : IProperty
@ -46,6 +68,12 @@ public class BoolProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.Bool; public PropertyType Type => PropertyType.Bool;
public required bool Value { get; set; } public required bool Value { get; set; }
public IProperty Clone() => new BoolProperty
{
Name = Name,
Value = Value
};
} }
public class ColorProperty : IProperty public class ColorProperty : IProperty
@ -53,6 +81,12 @@ public class ColorProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.Color; public PropertyType Type => PropertyType.Color;
public required Color Value { get; set; } public required Color Value { get; set; }
public IProperty Clone() => new ColorProperty
{
Name = Name,
Value = Value
};
} }
public class FileProperty : IProperty public class FileProperty : IProperty
@ -60,6 +94,12 @@ public class FileProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.File; public PropertyType Type => PropertyType.File;
public required string Value { get; set; } public required string Value { get; set; }
public IProperty Clone() => new FileProperty
{
Name = Name,
Value = Value
};
} }
public class ObjectProperty : IProperty public class ObjectProperty : IProperty
@ -67,6 +107,12 @@ public class ObjectProperty : IProperty
public required string Name { get; set; } public required string Name { get; set; }
public PropertyType Type => PropertyType.Object; public PropertyType Type => PropertyType.Object;
public required uint Value { get; set; } public required uint Value { get; set; }
public IProperty Clone() => new ObjectProperty
{
Name = Name,
Value = Value
};
} }
public class ClassProperty : IProperty public class ClassProperty : IProperty
@ -75,4 +121,53 @@ public class ClassProperty : IProperty
public PropertyType Type => DotTiled.PropertyType.Class; public PropertyType Type => DotTiled.PropertyType.Class;
public required string PropertyType { get; set; } public required string PropertyType { get; set; }
public required Dictionary<string, IProperty> Properties { get; set; } public required Dictionary<string, IProperty> Properties { get; set; }
public IProperty Clone() => new ClassProperty
{
Name = Name,
PropertyType = PropertyType,
Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone())
};
}
public abstract class CustomTypeDefinition
{
public uint ID { get; set; }
public string Name { get; set; } = "";
}
[Flags]
public enum CustomClassUseAs
{
Property,
Map,
Layer,
Object,
Tile,
Tileset,
WangColor,
Wangset,
Project,
All = Property | Map | Layer | Object | Tile | Tileset | WangColor | Wangset | Project
}
public class CustomClassDefinition : CustomTypeDefinition
{
public Color Color { get; set; }
public bool DrawFill { get; set; }
public CustomClassUseAs UseAs { get; set; }
public List<IProperty> Members { get; set; }
}
public enum CustomEnumStorageType
{
Int,
String
}
public class CustomEnumDefinition : CustomTypeDefinition
{
public CustomEnumStorageType StorageType { get; set; }
public List<string> Values { get; set; } = [];
public bool ValueAsFlags { get; set; }
} }

View file

@ -39,8 +39,8 @@ public class Tileset
public string Class { get; set; } = ""; public string Class { get; set; } = "";
public uint? TileWidth { get; set; } public uint? TileWidth { get; set; }
public uint? TileHeight { get; set; } public uint? TileHeight { get; set; }
public float? Spacing { get; set; } public float? Spacing { get; set; } = 0f;
public float? Margin { get; set; } public float? Margin { get; set; } = 0f;
public uint? TileCount { get; set; } public uint? TileCount { get; set; }
public uint? Columns { get; set; } public uint? Columns { get; set; }
public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified;

View file

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace DotTiled;
internal static partial class Helpers
{
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];
}
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);
}
internal static uint[] ReadBytesAsInt32Array(byte[] bytes)
{
var intArray = new uint[bytes.Length / 4];
for (var i = 0; i < intArray.Length; i++)
{
intArray[i] = BitConverter.ToUInt32(bytes, i * 4);
}
return intArray;
}
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 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 NotSupportedException($"Unsupported image format '{extension}'")
};
}
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 = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone());
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 void SetAtMostOnce<T>(ref T? field, T value, string fieldName)
{
if (field is not null)
throw new InvalidOperationException($"{fieldName} already set");
field = value;
}
internal 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,108 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
namespace DotTiled;
internal static class ExtensionsJsonElement
{
internal static T GetRequiredProperty<T>(this JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out var property))
throw new JsonException($"Missing required property '{propertyName}'.");
return property.GetValueAs<T>();
}
internal static T GetOptionalProperty<T>(this JsonElement element, string propertyName, T defaultValue)
{
if (!element.TryGetProperty(propertyName, out var property))
return defaultValue;
if (property.ValueKind == JsonValueKind.Null)
return defaultValue;
return property.GetValueAs<T>();
}
internal static T GetValueAs<T>(this JsonElement element)
{
bool isNullable = Nullable.GetUnderlyingType(typeof(T)) != null;
if (isNullable && element.ValueKind == JsonValueKind.Null)
return default!;
var realType = isNullable ? Nullable.GetUnderlyingType(typeof(T))! : typeof(T);
string val = realType switch
{
Type t when t == typeof(string) => element.GetString()!,
Type t when t == typeof(int) => element.GetInt32().ToString(CultureInfo.InvariantCulture),
Type t when t == typeof(uint) => element.GetUInt32().ToString(CultureInfo.InvariantCulture),
Type t when t == typeof(float) => element.GetSingle().ToString(CultureInfo.InvariantCulture),
Type t when t == typeof(bool) => element.GetBoolean().ToString(CultureInfo.InvariantCulture),
_ => throw new JsonException($"Unsupported type '{typeof(T)}'.")
};
return (T)Convert.ChangeType(val, realType, CultureInfo.InvariantCulture);
}
internal static T GetRequiredPropertyParseable<T>(this JsonElement element, string propertyName) where T : IParsable<T>
{
if (!element.TryGetProperty(propertyName, out var property))
throw new JsonException($"Missing required property '{propertyName}'.");
return T.Parse(property.GetString()!, CultureInfo.InvariantCulture);
}
internal static T GetRequiredPropertyParseable<T>(this JsonElement element, string propertyName, Func<string, T> parser)
{
if (!element.TryGetProperty(propertyName, out var property))
throw new JsonException($"Missing required property '{propertyName}'.");
return parser(property.GetString()!);
}
internal static T GetOptionalPropertyParseable<T>(this JsonElement element, string propertyName, T defaultValue) where T : IParsable<T>
{
if (!element.TryGetProperty(propertyName, out var property))
return defaultValue;
return T.Parse(property.GetString()!, CultureInfo.InvariantCulture);
}
internal static T GetOptionalPropertyParseable<T>(this JsonElement element, string propertyName, Func<string, T> parser, T defaultValue)
{
if (!element.TryGetProperty(propertyName, out var property))
return defaultValue;
return parser(property.GetString()!);
}
internal static T GetRequiredPropertyCustom<T>(this JsonElement element, string propertyName, Func<JsonElement, T> parser)
{
if (!element.TryGetProperty(propertyName, out var property))
throw new JsonException($"Missing required property '{propertyName}'.");
return parser(property);
}
internal static T GetOptionalPropertyCustom<T>(this JsonElement element, string propertyName, Func<JsonElement, T> parser, T defaultValue)
{
if (!element.TryGetProperty(propertyName, out var property))
return defaultValue;
return parser(property);
}
internal static List<T> GetValueAsList<T>(this JsonElement element, Func<JsonElement, T> parser)
{
var list = new List<T>();
foreach (var item in element.EnumerateArray())
list.Add(parser(item));
return list;
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
namespace DotTiled;
public class TjTemplateReader : ITemplateReader
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly string _jsonString;
private bool disposedValue;
private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
public TjTemplateReader(
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
public Template ReadTemplate()
{
var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement;
return Tmj.ReadTemplate(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
}
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
// ~TjTemplateReader()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

View file

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding)
{
var chunks = element.GetValueAsList<Chunk>(e => ReadChunk(e, compression, encoding)).ToArray();
return new Data
{
Chunks = chunks,
Compression = compression,
Encoding = encoding,
FlippingFlags = null,
GlobalTileIDs = null
};
}
internal static Chunk ReadChunk(JsonElement element, DataCompression? compression, DataEncoding encoding)
{
var data = ReadDataWithoutChunks(element, compression, encoding);
var x = element.GetRequiredProperty<int>("x");
var y = element.GetRequiredProperty<int>("y");
var width = element.GetRequiredProperty<uint>("width");
var height = element.GetRequiredProperty<uint>("height");
return new Chunk
{
X = x,
Y = y,
Width = width,
Height = height,
GlobalTileIDs = data.GlobalTileIDs!,
FlippingFlags = data.FlippingFlags!
};
}
internal static Data ReadDataWithoutChunks(JsonElement element, DataCompression? compression, DataEncoding encoding)
{
if (encoding == DataEncoding.Csv)
{
// Array of uint
var data = element.GetValueAsList<uint>(e => e.GetValueAs<uint>()).ToArray();
var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(data);
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null };
}
else if (encoding == DataEncoding.Base64)
{
var base64Data = element.GetBytesFromBase64();
if (compression == null)
{
var data = Helpers.ReadBytesAsInt32Array(base64Data);
var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(data);
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null };
}
using var stream = new MemoryStream(base64Data);
var decompressed = compression switch
{
DataCompression.GZip => Helpers.DecompressGZip(stream),
DataCompression.ZLib => Helpers.DecompressZLib(stream),
_ => throw new JsonException($"Unsupported compression '{compression}'.")
};
{
var (globalTileIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs(decompressed);
return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags, Chunks = null };
}
}
throw new JsonException($"Unsupported encoding '{encoding}'.");
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static Group ReadGroup(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name");
var @class = element.GetOptionalProperty<string>("class", "");
var opacity = element.GetOptionalProperty<float>("opacity", 1.0f);
var visible = element.GetOptionalProperty<bool>("visible", true);
var tintColor = element.GetOptionalPropertyParseable<Color?>("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var offsetX = element.GetOptionalProperty<float>("offsetx", 0.0f);
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var layers = element.GetOptionalPropertyCustom<List<BaseLayer>>("layers", e => e.GetValueAsList<BaseLayer>(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []);
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,63 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static ImageLayer ReadImageLayer(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name");
var @class = element.GetOptionalProperty<string>("class", "");
var opacity = element.GetOptionalProperty<float>("opacity", 1.0f);
var visible = element.GetOptionalProperty<bool>("visible", true);
var tintColor = element.GetOptionalPropertyParseable<Color?>("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var offsetX = element.GetOptionalProperty<float>("offsetx", 0.0f);
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var image = element.GetRequiredProperty<string>("image");
var repeatX = element.GetRequiredProperty<bool>("repeatx");
var repeatY = element.GetRequiredProperty<bool>("repeaty");
var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var x = element.GetOptionalProperty<uint>("x", 0);
var y = element.GetOptionalProperty<uint>("y", 0);
var imgModel = new Image
{
Format = Helpers.ParseImageFormatFromSource(image),
Height = 0,
Width = 0,
Source = image,
TransparentColor = transparentColor
};
return new ImageLayer
{
ID = id,
Name = name,
Class = @class,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Properties = properties,
Image = imgModel,
RepeatX = repeatX,
RepeatY = repeatY,
X = x,
Y = y
};
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static BaseLayer ReadLayer(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var type = element.GetRequiredProperty<string>("type");
return type switch
{
"tilelayer" => ReadTileLayer(element, customTypeDefinitions),
"objectgroup" => ReadObjectLayer(element, externalTemplateResolver, customTypeDefinitions),
"imagelayer" => ReadImageLayer(element, customTypeDefinitions),
"group" => ReadGroup(element, externalTemplateResolver, customTypeDefinitions),
_ => throw new JsonException($"Unsupported layer type '{type}'.")
};
}
}

View file

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static Map ReadMap(
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var version = element.GetRequiredProperty<string>("version");
var tiledVersion = element.GetRequiredProperty<string>("tiledversion");
string @class = element.GetOptionalProperty<string>("class", "");
var orientation = element.GetRequiredPropertyParseable<MapOrientation>("orientation", s => s switch
{
"orthogonal" => MapOrientation.Orthogonal,
"isometric" => MapOrientation.Isometric,
"staggered" => MapOrientation.Staggered,
"hexagonal" => MapOrientation.Hexagonal,
_ => throw new JsonException($"Unknown orientation '{s}'")
});
var renderOrder = element.GetOptionalPropertyParseable<RenderOrder>("renderorder", s => s switch
{
"right-down" => RenderOrder.RightDown,
"right-up" => RenderOrder.RightUp,
"left-down" => RenderOrder.LeftDown,
"left-up" => RenderOrder.LeftUp,
_ => throw new JsonException($"Unknown render order '{s}'")
}, RenderOrder.RightDown);
var compressionLevel = element.GetOptionalProperty<int>("compressionlevel", -1);
var width = element.GetRequiredProperty<uint>("width");
var height = element.GetRequiredProperty<uint>("height");
var tileWidth = element.GetRequiredProperty<uint>("tilewidth");
var tileHeight = element.GetRequiredProperty<uint>("tileheight");
var hexSideLength = element.GetOptionalProperty<uint?>("hexsidelength", null);
var staggerAxis = element.GetOptionalPropertyParseable<StaggerAxis?>("staggeraxis", s => s switch
{
"x" => StaggerAxis.X,
"y" => StaggerAxis.Y,
_ => throw new JsonException($"Unknown stagger axis '{s}'")
}, null);
var staggerIndex = element.GetOptionalPropertyParseable<StaggerIndex?>("staggerindex", s => s switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new JsonException($"Unknown stagger index '{s}'")
}, null);
var parallaxOriginX = element.GetOptionalProperty<float>("parallaxoriginx", 0.0f);
var parallaxOriginY = element.GetOptionalProperty<float>("parallaxoriginy", 0.0f);
var backgroundColor = element.GetOptionalPropertyParseable<Color>("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), Color.Parse("#00000000", CultureInfo.InvariantCulture));
var nextLayerID = element.GetRequiredProperty<uint>("nextlayerid");
var nextObjectID = element.GetRequiredProperty<uint>("nextobjectid");
var infinite = element.GetOptionalProperty<bool>("infinite", false);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null);
List<BaseLayer> layers = element.GetOptionalPropertyCustom<List<BaseLayer>>("layers", e => e.GetValueAsList<BaseLayer>(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []);
List<Tileset> tilesets = element.GetOptionalPropertyCustom<List<Tileset>>("tilesets", e => e.GetValueAsList<Tileset>(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []);
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,289 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static ObjectLayer ReadObjectLayer(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name");
var @class = element.GetOptionalProperty<string>("class", "");
var opacity = element.GetOptionalProperty<float>("opacity", 1.0f);
var visible = element.GetOptionalProperty<bool>("visible", true);
var tintColor = element.GetOptionalPropertyParseable<Color?>("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var offsetX = element.GetOptionalProperty<float>("offsetx", 0.0f);
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var parallaxX = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxY = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var x = element.GetOptionalProperty<uint>("x", 0);
var y = element.GetOptionalProperty<uint>("y", 0);
var width = element.GetOptionalProperty<uint?>("width", null);
var height = element.GetOptionalProperty<uint?>("height", null);
var color = element.GetOptionalPropertyParseable<Color?>("color", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var drawOrder = element.GetOptionalPropertyParseable<DrawOrder>("draworder", s => s switch
{
"topdown" => DrawOrder.TopDown,
"index" => DrawOrder.Index,
_ => throw new JsonException($"Unknown draw order '{s}'.")
}, DrawOrder.TopDown);
var objects = element.GetOptionalPropertyCustom<List<Object>>("objects", e => e.GetValueAsList<Object>(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []);
return new ObjectLayer
{
ID = id,
Name = name,
Class = @class,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Properties = properties,
X = x,
Y = y,
Width = width,
Height = height,
Color = color,
DrawOrder = drawOrder,
Objects = objects
};
}
internal static Object ReadObject(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
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;
bool ellipseDefault = false;
bool pointDefault = false;
List<Vector2>? polygonDefault = null;
List<Vector2>? polylineDefault = null;
Dictionary<string, IProperty>? propertiesDefault = null;
var template = element.GetOptionalProperty<string?>("template", null);
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;
ellipseDefault = templObj is EllipseObject;
pointDefault = templObj is PointObject;
polygonDefault = (templObj is PolygonObject polygonObj) ? polygonObj.Points : null;
polylineDefault = (templObj is PolylineObject polylineObj) ? polylineObj.Points : null;
}
var ellipse = element.GetOptionalProperty<bool>("ellipse", ellipseDefault);
var gid = element.GetOptionalProperty<uint?>("gid", gidDefault);
var height = element.GetOptionalProperty<float>("height", heightDefault);
var id = element.GetOptionalProperty<uint?>("id", idDefault);
var name = element.GetOptionalProperty<string>("name", nameDefault);
var point = element.GetOptionalProperty<bool>("point", pointDefault);
var polygon = element.GetOptionalPropertyCustom<List<Vector2>?>("polygon", e => ReadPoints(e), polygonDefault);
var polyline = element.GetOptionalPropertyCustom<List<Vector2>?>("polyline", e => ReadPoints(e), polylineDefault);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault);
var rotation = element.GetOptionalProperty<float>("rotation", rotationDefault);
var text = element.GetOptionalPropertyCustom<TextObject?>("text", ReadText, null);
var type = element.GetOptionalProperty<string>("type", typeDefault);
var visible = element.GetOptionalProperty<bool>("visible", visibleDefault);
var width = element.GetOptionalProperty<float>("width", widthDefault);
var x = element.GetOptionalProperty<float>("x", xDefault);
var y = element.GetOptionalProperty<float>("y", yDefault);
if (ellipse)
{
return new EllipseObject
{
ID = id,
Name = name,
Type = type,
X = x,
Y = y,
Width = width,
Height = height,
Rotation = rotation,
GID = gid,
Visible = visible,
Template = template,
Properties = properties
};
}
if (point)
{
return new PointObject
{
ID = id,
Name = name,
Type = type,
X = x,
Y = y,
Width = width,
Height = height,
Rotation = rotation,
GID = gid,
Visible = visible,
Template = template,
Properties = properties
};
}
if (polygon is not null)
{
return new PolygonObject
{
ID = id,
Name = name,
Type = type,
X = x,
Y = y,
Width = width,
Height = height,
Rotation = rotation,
GID = gid,
Visible = visible,
Template = template,
Properties = properties,
Points = polygon
};
}
if (polyline is not null)
{
return new PolylineObject
{
ID = id,
Name = name,
Type = type,
X = x,
Y = y,
Width = width,
Height = height,
Rotation = rotation,
GID = gid,
Visible = visible,
Template = template,
Properties = properties,
Points = polyline
};
}
if (text is not null)
{
text.ID = id;
text.Name = name;
text.Type = type;
text.X = x;
text.Y = y;
text.Width = width;
text.Height = height;
text.Rotation = rotation;
text.GID = gid;
text.Visible = visible;
text.Template = template;
text.Properties = properties;
return text;
}
return new RectangleObject
{
ID = id,
Name = name,
Type = type,
X = x,
Y = y,
Width = width,
Height = height,
Rotation = rotation,
GID = gid,
Visible = visible,
Template = template,
Properties = properties
};
}
internal static List<Vector2> ReadPoints(JsonElement element) =>
element.GetValueAsList<Vector2>(e =>
{
var x = e.GetRequiredProperty<float>("x");
var y = e.GetRequiredProperty<float>("y");
return new Vector2(x, y);
});
internal static TextObject ReadText(JsonElement element)
{
var bold = element.GetOptionalProperty<bool>("bold", false);
var color = element.GetOptionalPropertyParseable<Color>("color", s => Color.Parse(s, CultureInfo.InvariantCulture), Color.Parse("#00000000", CultureInfo.InvariantCulture));
var fontfamily = element.GetOptionalProperty<string>("fontfamily", "sans-serif");
var halign = element.GetOptionalPropertyParseable<TextHorizontalAlignment>("halign", s => s switch
{
"left" => TextHorizontalAlignment.Left,
"center" => TextHorizontalAlignment.Center,
"right" => TextHorizontalAlignment.Right,
_ => throw new JsonException($"Unknown horizontal alignment '{s}'.")
}, TextHorizontalAlignment.Left);
var italic = element.GetOptionalProperty<bool>("italic", false);
var kerning = element.GetOptionalProperty<bool>("kerning", true);
var pixelsize = element.GetOptionalProperty<int>("pixelsize", 16);
var strikeout = element.GetOptionalProperty<bool>("strikeout", false);
var text = element.GetRequiredProperty<string>("text");
var underline = element.GetOptionalProperty<bool>("underline", false);
var valign = element.GetOptionalPropertyParseable<TextVerticalAlignment>("valign", s => s switch
{
"top" => TextVerticalAlignment.Top,
"center" => TextVerticalAlignment.Center,
"bottom" => TextVerticalAlignment.Bottom,
_ => throw new JsonException($"Unknown vertical alignment '{s}'.")
}, TextVerticalAlignment.Top);
var wrap = element.GetOptionalProperty<bool>("wrap", false);
return new TextObject
{
Bold = bold,
Color = color,
FontFamily = fontfamily,
HorizontalAlignment = halign,
Italic = italic,
Kerning = kerning,
PixelSize = pixelsize,
Strikeout = strikeout,
Text = text,
Underline = underline,
VerticalAlignment = valign,
Wrap = wrap
};
}
}

View file

@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static Dictionary<string, IProperty> ReadProperties(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) =>
element.GetValueAsList<IProperty>(e =>
{
var name = e.GetRequiredProperty<string>("name");
var type = e.GetOptionalPropertyParseable<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 JsonException("Invalid property type")
}, PropertyType.String);
IProperty property = type switch
{
PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty<uint>("value") },
PropertyType.Class => ReadClassProperty(e, customTypeDefinitions),
_ => throw new JsonException("Invalid property type")
};
return property!;
}).ToDictionary(p => p.Name);
internal static ClassProperty ReadClassProperty(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype");
var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType);
if (customTypeDef is CustomClassDefinition ccd)
{
var propsInType = CreateInstanceOfCustomClass(ccd);
var props = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []);
var mergedProps = Helpers.MergeProperties(propsInType, props);
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Properties = mergedProps
};
}
throw new JsonException($"Unknown custom class '{propertyType}'.");
}
internal static Dictionary<string, IProperty> ReadCustomClassProperties(
JsonElement element,
CustomClassDefinition customClassDefinition,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
Dictionary<string, IProperty> resultingProps = [];
foreach (var prop in customClassDefinition.Members)
{
if (!element.TryGetProperty(prop.Name, out var propElement))
continue; // Property not present in element, therefore will use default value
IProperty property = prop.Type switch
{
PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs<int>() },
PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs<float>() },
PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs<bool>() },
PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs<string>(), CultureInfo.InvariantCulture) },
PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs<uint>() },
PropertyType.Class => ReadClassProperty(propElement, customTypeDefinitions),
_ => throw new JsonException("Invalid property type")
};
resultingProps[prop.Name] = property;
}
return resultingProps;
}
internal static Dictionary<string, IProperty> CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition)
{
return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone());
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static Template ReadTemplate(
JsonElement element,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var type = element.GetRequiredProperty<string>("type");
var tileset = element.GetOptionalPropertyCustom<Tileset?>("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null);
var @object = element.GetRequiredPropertyCustom<Object>("object", el => ReadObject(el, externalTemplateResolver, customTypeDefinitions));
return new Template
{
Tileset = tileset,
Object = @object
};
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static TileLayer ReadTileLayer(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var compression = element.GetOptionalPropertyParseable<DataCompression?>("compression", s => s switch
{
"zlib" => DataCompression.ZLib,
"gzip" => DataCompression.GZip,
"" => null,
_ => throw new JsonException($"Unsupported compression '{s}'.")
}, null);
var encoding = element.GetOptionalPropertyParseable<DataEncoding>("encoding", s => s switch
{
"csv" => DataEncoding.Csv,
"base64" => DataEncoding.Base64,
_ => throw new JsonException($"Unsupported encoding '{s}'.")
}, DataEncoding.Csv);
var chunks = element.GetOptionalPropertyCustom<Data?>("chunks", e => ReadDataAsChunks(e, compression, encoding), null);
var @class = element.GetOptionalProperty<string>("class", "");
var data = element.GetOptionalPropertyCustom<Data?>("data", e => ReadDataWithoutChunks(e, compression, encoding), null);
var height = element.GetRequiredProperty<uint>("height");
var id = element.GetRequiredProperty<uint>("id");
var name = element.GetRequiredProperty<string>("name");
var offsetX = element.GetOptionalProperty<float>("offsetx", 0.0f);
var offsetY = element.GetOptionalProperty<float>("offsety", 0.0f);
var opacity = element.GetOptionalProperty<float>("opacity", 1.0f);
var parallaxx = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxy = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var repeatX = element.GetOptionalProperty<bool>("repeatx", false);
var repeatY = element.GetOptionalProperty<bool>("repeaty", false);
var startX = element.GetOptionalProperty<int>("startx", 0);
var startY = element.GetOptionalProperty<int>("starty", 0);
var tintColor = element.GetOptionalPropertyParseable<Color?>("tintcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var visible = element.GetOptionalProperty<bool>("visible", true);
var width = element.GetRequiredProperty<uint>("width");
var x = element.GetRequiredProperty<uint>("x");
var y = element.GetRequiredProperty<uint>("y");
if ((data ?? chunks) is null)
throw new JsonException("Tile layer does not contain data.");
return new TileLayer
{
ID = id,
Name = name,
Class = @class,
Opacity = opacity,
Visible = visible,
TintColor = tintColor,
OffsetX = offsetX,
OffsetY = offsetY,
ParallaxX = parallaxx,
ParallaxY = parallaxy,
Properties = properties,
X = x,
Y = y,
Width = width,
Height = height,
Data = data ?? chunks
};
}
}

View file

@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
internal static Tileset ReadTileset(
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var backgroundColor = element.GetOptionalPropertyParseable<Color?>("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var @class = element.GetOptionalProperty<string>("class", "");
var columns = element.GetOptionalProperty<uint?>("columns", null);
var fillMode = element.GetOptionalPropertyParseable<FillMode>("fillmode", s => s switch
{
"stretch" => FillMode.Stretch,
"preserve-aspect-fit" => FillMode.PreserveAspectFit,
_ => throw new JsonException($"Unknown fill mode '{s}'")
}, FillMode.Stretch);
var firstGID = element.GetOptionalProperty<uint?>("firstgid", null);
var grid = element.GetOptionalPropertyCustom<Grid?>("grid", ReadGrid, null);
var image = element.GetOptionalProperty<string?>("image", null);
var imageHeight = element.GetOptionalProperty<uint?>("imageheight", null);
var imageWidth = element.GetOptionalProperty<uint?>("imagewidth", null);
var margin = element.GetOptionalProperty<uint?>("margin", null);
var name = element.GetOptionalProperty<string?>("name", null);
var objectAlignment = element.GetOptionalPropertyParseable<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 JsonException($"Unknown object alignment '{s}'")
}, ObjectAlignment.Unspecified);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null);
var source = element.GetOptionalProperty<string?>("source", null);
var spacing = element.GetOptionalProperty<uint?>("spacing", null);
var tileCount = element.GetOptionalProperty<uint?>("tilecount", null);
var tiledVersion = element.GetOptionalProperty<string?>("tiledversion", null);
var tileHeight = element.GetOptionalProperty<uint?>("tileheight", null);
var tileOffset = element.GetOptionalPropertyCustom<TileOffset?>("tileoffset", ReadTileOffset, null);
var tileRenderSize = element.GetOptionalPropertyParseable<TileRenderSize>("tilerendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,
_ => throw new JsonException($"Unknown tile render size '{s}'")
}, TileRenderSize.Tile);
var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []);
var tileWidth = element.GetOptionalProperty<uint?>("tilewidth", null);
var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var type = element.GetOptionalProperty<string?>("type", null);
var version = element.GetOptionalProperty<string?>("version", null);
//var wangsets = element.GetOptionalPropertyCustom<List<Wangset>?>("wangsets", ReadWangSets, null);
if (source is not null)
{
if (externalTilesetResolver is null)
throw new JsonException("External tileset resolver is required to resolve external tilesets.");
var resolvedTileset = externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source;
return resolvedTileset;
}
var imageModel = new Image
{
Format = Helpers.ParseImageFormatFromSource(image!),
Source = image,
Height = imageHeight,
Width = imageWidth,
TransparentColor = transparentColor
};
return new Tileset
{
Class = @class,
Columns = columns,
FillMode = fillMode,
FirstGID = firstGID,
Grid = grid,
Image = imageModel,
Margin = margin,
Name = name,
ObjectAlignment = objectAlignment,
Properties = properties,
Source = source,
Spacing = spacing,
TileCount = tileCount,
TiledVersion = tiledVersion,
TileHeight = tileHeight,
TileOffset = tileOffset,
RenderSize = tileRenderSize,
Tiles = tiles,
TileWidth = tileWidth,
Version = version,
//Wangsets = wangsets
};
}
internal static Grid ReadGrid(JsonElement element)
{
var orientation = element.GetOptionalPropertyParseable<GridOrientation>("orientation", s => s switch
{
"orthogonal" => GridOrientation.Orthogonal,
"isometric" => GridOrientation.Isometric,
_ => throw new JsonException($"Unknown grid orientation '{s}'")
}, GridOrientation.Orthogonal);
var height = element.GetRequiredProperty<uint>("height");
var width = element.GetRequiredProperty<uint>("width");
return new Grid
{
Orientation = orientation,
Height = height,
Width = width
};
}
internal static TileOffset ReadTileOffset(JsonElement element)
{
var x = element.GetRequiredProperty<int>("x");
var y = element.GetRequiredProperty<int>("y");
return new TileOffset
{
X = x,
Y = y
};
}
internal static List<Tile> ReadTiles(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) =>
element.GetValueAsList<Tile>(e =>
{
var animation = e.GetOptionalPropertyCustom<List<Frame>?>("animation", e => e.GetValueAsList<Frame>(ReadFrame), null);
var id = e.GetRequiredProperty<uint>("id");
var image = e.GetOptionalProperty<string?>("image", null);
var imageHeight = e.GetOptionalProperty<uint?>("imageheight", null);
var imageWidth = e.GetOptionalProperty<uint?>("imagewidth", null);
var x = e.GetOptionalProperty<uint>("x", 0);
var y = e.GetOptionalProperty<uint>("y", 0);
var width = e.GetOptionalProperty<uint>("width", imageWidth ?? 0);
var height = e.GetOptionalProperty<uint>("height", imageHeight ?? 0);
var objectGroup = e.GetOptionalPropertyCustom<ObjectLayer?>("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null);
var probability = e.GetOptionalProperty<float>("probability", 1.0f);
var properties = e.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null);
// var terrain, replaced by wangsets
var type = e.GetOptionalProperty<string>("type", "");
var imageModel = image != null ? new Image
{
Format = Helpers.ParseImageFormatFromSource(image),
Source = image,
Height = imageHeight ?? 0,
Width = imageWidth ?? 0
} : null;
return new Tile
{
Animation = animation,
ID = id,
Image = imageModel,
X = x,
Y = y,
Width = width,
Height = height,
ObjectLayer = objectGroup,
Probability = probability,
Properties = properties,
Type = type
};
});
internal static Frame ReadFrame(JsonElement element)
{
var duration = element.GetRequiredProperty<uint>("duration");
var tileID = element.GetRequiredProperty<uint>("tileid");
return new Frame
{
Duration = duration,
TileID = tileID
};
}
internal static Wangset ReadWangset(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var @clalss = element.GetOptionalProperty<string>("class", "");
var colors = element.GetOptionalPropertyCustom<List<WangColor>>("colors", e => e.GetValueAsList<WangColor>(el => ReadWangColor(el, customTypeDefinitions)), []);
var name = element.GetRequiredProperty<string>("name");
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var tile = element.GetOptionalProperty<uint>("tile", 0);
var type = element.GetOptionalProperty<string>("type", "");
var wangTiles = element.GetOptionalPropertyCustom<List<WangTile>>("wangtiles", e => e.GetValueAsList<WangTile>(ReadWangTile), []);
return new Wangset
{
Class = @clalss,
WangColors = colors,
Name = name,
Properties = properties,
Tile = tile,
WangTiles = wangTiles
};
}
internal static WangColor ReadWangColor(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
var @class = element.GetOptionalProperty<string>("class", "");
var color = element.GetRequiredPropertyParseable<Color>("color", s => Color.Parse(s, CultureInfo.InvariantCulture));
var name = element.GetRequiredProperty<string>("name");
var probability = element.GetOptionalProperty<float>("probability", 1.0f);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var tile = element.GetOptionalProperty<uint>("tile", 0);
return new WangColor
{
Class = @class,
Color = color,
Name = name,
Probability = probability,
Properties = properties,
Tile = tile
};
}
internal static WangTile ReadWangTile(JsonElement element)
{
var tileID = element.GetRequiredProperty<uint>("tileid");
var wangID = element.GetOptionalPropertyCustom<List<byte>>("wangid", e => e.GetValueAsList<byte>(el => (byte)el.GetUInt32()), []);
return new WangTile
{
TileID = tileID,
WangID = [.. wangID]
};
}
}

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
namespace DotTiled;
internal partial class Tmj
{
}

View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
namespace DotTiled;
public class TmjMapReader : IMapReader
{
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private string _jsonString;
private bool disposedValue;
private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
public TmjMapReader(
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
public Map ReadMap()
{
var jsonDoc = JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement;
return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
}
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
// ~TmjMapReader()
// {
// // 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,65 @@
using System;
using System.Collections.Generic;
namespace DotTiled;
public class TsjTilesetReader : ITilesetReader
{
// External resolvers
private readonly Func<string, Template> _externalTemplateResolver;
private readonly string _jsonString;
private bool disposedValue;
private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
public TsjTilesetReader(
string jsonString,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{
_jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
}
public Tileset ReadTileset()
{
var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement;
return Tmj.ReadTileset(
rootElement,
_ => throw new NotSupportedException("External tilesets cannot refer to other external tilesets."),
_externalTemplateResolver,
_customTypeDefinitions);
}
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
// ~TsjTilesetReader()
// {
// // 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

@ -1,26 +0,0 @@
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

@ -8,7 +8,11 @@ namespace DotTiled;
internal partial class Tmx internal partial class Tmx
{ {
internal static Map ReadMap(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver) internal static Map ReadMap(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var version = reader.GetRequiredAttribute("version"); var version = reader.GetRequiredAttribute("version");
@ -64,12 +68,12 @@ internal partial class Tmx
reader.ProcessChildren("map", (r, elementName) => elementName switch reader.ProcessChildren("map", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver)), "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)),
"layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: infinite)), "layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)), "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)),
"imagelayer" => () => layers.Add(ReadImageLayer(r)), "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)), "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip _ => r.Skip
}); });

View file

@ -9,7 +9,10 @@ namespace DotTiled;
internal partial class Tmx internal partial class Tmx
{ {
internal static ObjectLayer ReadObjectLayer(XmlReader reader, Func<string, Template> externalTemplateResolver) internal static ObjectLayer ReadObjectLayer(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id"); var id = reader.GetRequiredAttributeParseable<uint>("id");
@ -40,8 +43,8 @@ internal partial class Tmx
reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"object" => () => objects.Add(ReadObject(r, externalTemplateResolver)), "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip _ => r.Skip
}); });
@ -68,7 +71,10 @@ internal partial class Tmx
}; };
} }
internal static Object ReadObject(XmlReader reader, Func<string, Template> externalTemplateResolver) internal static Object ReadObject(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var template = reader.GetOptionalAttribute("template"); var template = reader.GetOptionalAttribute("template");
@ -122,7 +128,7 @@ internal partial class Tmx
reader.ProcessChildren("object", (r, elementName) => elementName switch reader.ProcessChildren("object", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, MergeProperties(properties, ReadProperties(r)), "Properties", ref propertiesCounter), "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)), "Properties", ref propertiesCounter),
"ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r), "Object marker"), "ellipse" => () => Helpers.SetAtMostOnce(ref obj, ReadEllipseObject(r), "Object marker"),
"point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r), "Object marker"), "point" => () => Helpers.SetAtMostOnce(ref obj, ReadPointObject(r), "Object marker"),
"polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r), "Object marker"), "polygon" => () => Helpers.SetAtMostOnce(ref obj, ReadPolygonObject(r), "Object marker"),
@ -152,38 +158,6 @@ internal partial class Tmx
return obj; 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) internal static EllipseObject ReadEllipseObject(XmlReader reader)
{ {
reader.Skip(); reader.Skip();
@ -280,7 +254,11 @@ internal partial class Tmx
}; };
} }
internal static Template ReadTemplate(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver) internal static Template ReadTemplate(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// No attributes // No attributes
@ -292,8 +270,8 @@ internal partial class Tmx
reader.ProcessChildren("template", (r, elementName) => elementName switch reader.ProcessChildren("template", (r, elementName) => elementName switch
{ {
"tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver), "Tileset"), "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), "Tileset"),
"object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver), "Object"), "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"),
_ => r.Skip _ => r.Skip
}); });

View file

@ -6,7 +6,9 @@ namespace DotTiled;
internal partial class Tmx internal partial class Tmx
{ {
internal static Dictionary<string, IProperty> ReadProperties(XmlReader reader) internal static Dictionary<string, IProperty> ReadProperties(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
return reader.ReadList("properties", "property", (r) => return reader.ReadList("properties", "property", (r) =>
{ {
@ -33,22 +35,38 @@ internal partial class Tmx
PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") }, PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("value") }, PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("value") },
PropertyType.Class => ReadClassProperty(r), PropertyType.Class => ReadClassProperty(r, customTypeDefinitions),
_ => throw new XmlException("Invalid property type") _ => throw new XmlException("Invalid property type")
}; };
return (name, property); return (name, property);
}).ToDictionary(x => x.name, x => x.property); }).ToDictionary(x => x.name, x => x.property);
} }
internal static ClassProperty ReadClassProperty(XmlReader reader) internal static ClassProperty ReadClassProperty(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var name = reader.GetRequiredAttribute("name"); var name = reader.GetRequiredAttribute("name");
var propertyType = reader.GetRequiredAttribute("propertytype"); var propertyType = reader.GetRequiredAttribute("propertytype");
var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType);
if (customTypeDef is CustomClassDefinition ccd)
{
reader.ReadStartElement("property"); reader.ReadStartElement("property");
var properties = ReadProperties(reader); var propsInType = CreateInstanceOfCustomClass(ccd);
reader.ReadEndElement(); var props = ReadProperties(reader, customTypeDefinitions);
return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties }; var mergedProps = Helpers.MergeProperties(propsInType, props);
reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Properties = mergedProps };
}
throw new XmlException($"Unkonwn custom class definition: {propertyType}");
}
internal static Dictionary<string, IProperty> CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition)
{
return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone());
} }
} }

View file

@ -7,7 +7,10 @@ namespace DotTiled;
internal partial class Tmx internal partial class Tmx
{ {
internal static TileLayer ReadTileLayer(XmlReader reader, bool dataUsesChunks) internal static TileLayer ReadTileLayer(
XmlReader reader,
bool dataUsesChunks,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var id = reader.GetRequiredAttributeParseable<uint>("id"); var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? ""; var name = reader.GetOptionalAttribute("name") ?? "";
@ -30,7 +33,7 @@ internal partial class Tmx
reader.ProcessChildren("layer", (r, elementName) => elementName switch reader.ProcessChildren("layer", (r, elementName) => elementName switch
{ {
"data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"), "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
_ => r.Skip _ => r.Skip
}); });
@ -55,7 +58,9 @@ internal partial class Tmx
}; };
} }
internal static ImageLayer ReadImageLayer(XmlReader reader) internal static ImageLayer ReadImageLayer(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var id = reader.GetRequiredAttributeParseable<uint>("id"); var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? ""; var name = reader.GetOptionalAttribute("name") ?? "";
@ -78,7 +83,7 @@ internal partial class Tmx
reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch
{ {
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
_ => r.Skip _ => r.Skip
}); });
@ -103,7 +108,10 @@ internal partial class Tmx
}; };
} }
internal static Group ReadGroup(XmlReader reader, Func<string, Template> externalTemplateResolver) internal static Group ReadGroup(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var id = reader.GetRequiredAttributeParseable<uint>("id"); var id = reader.GetRequiredAttributeParseable<uint>("id");
var name = reader.GetOptionalAttribute("name") ?? ""; var name = reader.GetOptionalAttribute("name") ?? "";
@ -121,11 +129,11 @@ internal partial class Tmx
reader.ProcessChildren("group", (r, elementName) => elementName switch reader.ProcessChildren("group", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"layer" => () => layers.Add(ReadTileLayer(r, dataUsesChunks: false)), "layer" => () => layers.Add(ReadTileLayer(r, false, customTypeDefinitions)),
"objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver)), "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)),
"imagelayer" => () => layers.Add(ReadImageLayer(r)), "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)),
"group" => () => layers.Add(ReadGroup(r, externalTemplateResolver)), "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip _ => r.Skip
}); });

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
@ -7,7 +8,11 @@ namespace DotTiled;
internal partial class Tmx internal partial class Tmx
{ {
internal static Tileset ReadTileset(XmlReader reader, Func<string, Tileset>? externalTilesetResolver, Func<string, Template> externalTemplateResolver) internal static Tileset ReadTileset(
XmlReader reader,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var version = reader.GetOptionalAttribute("version"); var version = reader.GetOptionalAttribute("version");
@ -18,8 +23,8 @@ internal partial class Tmx
var @class = reader.GetOptionalAttribute("class") ?? ""; var @class = reader.GetOptionalAttribute("class") ?? "";
var tileWidth = reader.GetOptionalAttributeParseable<uint>("tilewidth"); var tileWidth = reader.GetOptionalAttributeParseable<uint>("tilewidth");
var tileHeight = reader.GetOptionalAttributeParseable<uint>("tileheight"); var tileHeight = reader.GetOptionalAttributeParseable<uint>("tileheight");
var spacing = reader.GetOptionalAttributeParseable<uint>("spacing"); var spacing = reader.GetOptionalAttributeParseable<uint>("spacing") ?? 0;
var margin = reader.GetOptionalAttributeParseable<uint>("margin"); var margin = reader.GetOptionalAttributeParseable<uint>("margin") ?? 0;
var tileCount = reader.GetOptionalAttributeParseable<uint>("tilecount"); var tileCount = reader.GetOptionalAttributeParseable<uint>("tilecount");
var columns = reader.GetOptionalAttributeParseable<uint>("columns"); var columns = reader.GetOptionalAttributeParseable<uint>("columns");
var objectAlignment = reader.GetOptionalAttributeEnum<ObjectAlignment>("objectalignment", s => s switch var objectAlignment = reader.GetOptionalAttributeEnum<ObjectAlignment>("objectalignment", s => s switch
@ -63,10 +68,10 @@ internal partial class Tmx
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(r), "TileOffset"), "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(r), "TileOffset"),
"grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(r), "Grid"), "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(r), "Grid"),
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r), "Wangsets"), "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r, customTypeDefinitions), "Wangsets"),
"transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"), "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"),
"tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver)), "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)),
_ => r.Skip _ => r.Skip
}); });
@ -131,6 +136,9 @@ internal partial class Tmx
_ => r.Skip _ => r.Skip
}); });
if (format is null && source is not null)
format = ParseImageFormatFromSource(source);
return new Image return new Image
{ {
Format = format, Format = format,
@ -141,6 +149,21 @@ internal partial class Tmx
}; };
} }
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) internal static TileOffset ReadTileOffset(XmlReader reader)
{ {
// Attributes // Attributes
@ -179,7 +202,10 @@ internal partial class Tmx
return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed };
} }
internal static Tile ReadTile(XmlReader reader, Func<string, Template> externalTemplateResolver) internal static Tile ReadTile(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var id = reader.GetRequiredAttributeParseable<uint>("id"); var id = reader.GetRequiredAttributeParseable<uint>("id");
@ -198,9 +224,9 @@ internal partial class Tmx
reader.ProcessChildren("tile", (r, elementName) => elementName switch reader.ProcessChildren("tile", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"),
"objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver), "ObjectLayer"), "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions), "ObjectLayer"),
"animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList<Frame>("animation", "frame", (ar) => "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList<Frame>("animation", "frame", (ar) =>
{ {
var tileID = ar.GetRequiredAttributeParseable<uint>("tileid"); var tileID = ar.GetRequiredAttributeParseable<uint>("tileid");
@ -226,12 +252,16 @@ internal partial class Tmx
}; };
} }
internal static List<Wangset> ReadWangsets(XmlReader reader) internal static List<Wangset> ReadWangsets(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
return reader.ReadList<Wangset>("wangsets", "wangset", ReadWangset); return reader.ReadList<Wangset>("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions));
} }
internal static Wangset ReadWangset(XmlReader reader) internal static Wangset ReadWangset(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var name = reader.GetRequiredAttribute("name"); var name = reader.GetRequiredAttribute("name");
@ -245,8 +275,8 @@ internal partial class Tmx
reader.ProcessChildren("wangset", (r, elementName) => elementName switch reader.ProcessChildren("wangset", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
"wangcolor" => () => wangColors.Add(ReadWangColor(r)), "wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)),
"wangtile" => () => wangTiles.Add(ReadWangTile(r)), "wangtile" => () => wangTiles.Add(ReadWangTile(r)),
_ => r.Skip _ => r.Skip
}); });
@ -265,7 +295,9 @@ internal partial class Tmx
}; };
} }
internal static WangColor ReadWangColor(XmlReader reader) internal static WangColor ReadWangColor(
XmlReader reader,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
// Attributes // Attributes
var name = reader.GetRequiredAttribute("name"); var name = reader.GetRequiredAttribute("name");
@ -279,7 +311,7 @@ internal partial class Tmx
reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch
{ {
"properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r), "Properties"), "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"),
_ => r.Skip _ => r.Skip
}); });

View file

@ -13,11 +13,18 @@ public class TmxMapReader : IMapReader
private readonly XmlReader _reader; private readonly XmlReader _reader;
private bool disposedValue; private bool disposedValue;
public TmxMapReader(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver) private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
public TmxMapReader(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); _reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
// Prepare reader // Prepare reader
_reader.MoveToContent(); _reader.MoveToContent();
@ -25,7 +32,7 @@ public class TmxMapReader : IMapReader
public Map ReadMap() public Map ReadMap()
{ {
return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver); return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
} }
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Xml; using System.Xml;
namespace DotTiled; namespace DotTiled;
@ -11,13 +12,22 @@ public class TsxTilesetReader : ITilesetReader
private readonly XmlReader _reader; private readonly XmlReader _reader;
private bool disposedValue; private bool disposedValue;
public TsxTilesetReader(XmlReader reader, Func<string, Template> externalTemplateResolver) private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
public TsxTilesetReader(
XmlReader reader,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); _reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
// Prepare reader
_reader.MoveToContent();
} }
public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver); public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions);
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Xml; using System.Xml;
namespace DotTiled; namespace DotTiled;
@ -12,17 +13,24 @@ public class TxTemplateReader : ITemplateReader
private readonly XmlReader _reader; private readonly XmlReader _reader;
private bool disposedValue; private bool disposedValue;
public TxTemplateReader(XmlReader reader, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver) private readonly IReadOnlyCollection<CustomTypeDefinition> _customTypeDefinitions;
public TxTemplateReader(
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
_reader = reader ?? throw new ArgumentNullException(nameof(reader)); _reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));
_customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions));
// Prepare reader // Prepare reader
_reader.MoveToContent(); _reader.MoveToContent();
} }
public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver); public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {

View file

@ -0,0 +1,10 @@
test:
dotnet test
BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj
BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts
.PHONY: benchmark
benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md
$(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES)
dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR)

View file

@ -60,15 +60,13 @@ BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4651/22H2/2022Update)
[Host] : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2 [Host] : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2 DefaultJob : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2
``` ```
| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | | Method | Categories | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------ |------------------------- |----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:| |------------ |------------------------- |---------:|------:|-------:|-------:|----------:|------------:|
| DotTiled | MapFromInMemoryTmxString | 2.991 μs | 0.0266 μs | 0.0236 μs | 1.00 | 0.00 | 1.2817 | 0.0610 | 16.37 KB | 1.00 | | DotTiled | MapFromInMemoryTmjString | 4.292 μs | 1.00 | 0.4349 | - | 5.62 KB | 1.00 |
| TiledLib | MapFromInMemoryTmxString | 5.405 μs | 0.0466 μs | 0.0413 μs | 1.81 | 0.02 | 1.8158 | 0.1068 | 23.32 KB | 1.42 | | | | | | | | | |
| TiledCSPlus | MapFromInMemoryTmxString | 6.354 μs | 0.0703 μs | 0.0587 μs | 2.12 | 0.03 | 2.5940 | 0.1831 | 33.23 KB | 2.03 | | DotTiled | MapFromInMemoryTmxString | 3.075 μs | 1.00 | 1.2817 | 0.0610 | 16.4 KB | 1.00 |
| | | | | | | | | | | | | TiledLib | MapFromInMemoryTmxString | 5.574 μs | 1.81 | 1.8005 | 0.0916 | 23.32 KB | 1.42 |
| DotTiled | MapFromTmxFile | 28.570 μs | 0.1216 μs | 0.1137 μs | 1.00 | 0.00 | 1.0376 | - | 13.88 KB | 1.00 | | TiledCSPlus | MapFromInMemoryTmxString | 6.546 μs | 2.13 | 2.5940 | 0.1831 | 33.16 KB | 2.02 |
| TiledCSPlus | MapFromTmxFile | 33.377 μs | 0.1086 μs | 0.1016 μs | 1.17 | 0.01 | 2.8076 | 0.1221 | 36.93 KB | 2.66 |
| TiledLib | MapFromTmxFile | 36.077 μs | 0.1900 μs | 0.1777 μs | 1.26 | 0.01 | 2.0752 | 0.1221 | 27.1 KB | 1.95 |
</details> </details>