Big properties work

This commit is contained in:
Daniel Cronqvist 2024-08-10 19:18:33 +02:00
parent 1168917c23
commit 5e93716d2c
15 changed files with 820 additions and 55 deletions

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

@ -2,7 +2,7 @@ namespace DotTiled.Tests;
public partial class TestData public partial class TestData
{ {
public 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 TestData
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 TestData
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 TestData
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 TestData
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,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,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

@ -20,17 +20,13 @@ public partial class TmjMapReaderTests
var json = TestData.GetRawStringFor(testDataFile); var json = TestData.GetRawStringFor(testDataFile);
static Template ResolveTemplate(string source) static Template ResolveTemplate(string source)
{ {
var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}"); throw new NotSupportedException("External templates are not supported in this test.");
//var templateReader = new TmjTemplateReader(templateJson, ResolveTemplate);
return null;
} }
static Tileset ResolveTileset(string source) static Tileset ResolveTileset(string source)
{ {
var tilesetJson = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}"); throw new NotSupportedException("External tilesets are not supported in this test.");
//var tilesetReader = new TmjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate);
return null;
} }
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate); using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, []);
// Act // Act
var map = mapReader.ReadMap(); var map = mapReader.ReadMap();
@ -42,7 +38,7 @@ public partial class TmjMapReaderTests
public static IEnumerable<object[]> DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => public static IEnumerable<object[]> DeserializeMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data =>
[ [
["Serialization.TestData.Map.map-with-object-template.tmj", TestData.MapWithObjectTemplate()], ["Serialization.TestData.Map.map-with-object-template.tmj", TestData.MapWithObjectTemplate("tj")],
["Serialization.TestData.Map.map-with-group.tmj", TestData.MapWithGroup()], ["Serialization.TestData.Map.map-with-group.tmj", TestData.MapWithGroup()],
]; ];
@ -51,20 +47,55 @@ public partial class TmjMapReaderTests
public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap) public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected(string testDataFile, Map expectedMap)
{ {
// Arrange // 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); var json = TestData.GetRawStringFor(testDataFile);
static Template ResolveTemplate(string source) Template ResolveTemplate(string source)
{ {
var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}"); var templateJson = TestData.GetRawStringFor($"Serialization.TestData.Template.{source}");
//var templateReader = new TmjTemplateReader(templateJson, ResolveTemplate); using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions);
return null; return templateReader.ReadTemplate();
} }
static Tileset ResolveTileset(string source) Tileset ResolveTileset(string source)
{ {
var tilesetJson = TestData.GetXmlReaderFor($"Serialization.TestData.Tileset.{source}"); var tilesetJson = TestData.GetRawStringFor($"Serialization.TestData.Tileset.{source}");
//var tilesetReader = new TmjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate); using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions);
return null; return tilesetReader.ReadTileset();
} }
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate); using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions);
// Act // Act
var map = mapReader.ReadMap(); var map = mapReader.ReadMap();

View file

@ -116,7 +116,7 @@ public partial class TmxMapReaderTests
public static IEnumerable<object[]> DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data => public static IEnumerable<object[]> DeserializeMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected_Data =>
[ [
["Serialization.TestData.Map.map-with-object-template.tmx", TestData.MapWithObjectTemplate()], ["Serialization.TestData.Map.map-with-object-template.tmx", TestData.MapWithObjectTemplate("tx")],
["Serialization.TestData.Map.map-with-group.tmx", TestData.MapWithGroup()], ["Serialization.TestData.Map.map-with-group.tmx", TestData.MapWithGroup()],
]; ];

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

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

@ -4,27 +4,33 @@ using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Numerics;
using System.Text.Json; using System.Text.Json;
namespace DotTiled; namespace DotTiled;
internal partial class Tmj internal partial class Tmj
{ {
internal static BaseLayer ReadLayer(JsonElement element) internal static BaseLayer ReadLayer(
JsonElement element,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var type = element.GetRequiredProperty<string>("type"); var type = element.GetRequiredProperty<string>("type");
return type switch return type switch
{ {
"tilelayer" => ReadTileLayer(element), "tilelayer" => ReadTileLayer(element, customTypeDefinitions),
// "objectgroup" => ReadObjectGroup(element), "objectgroup" => ReadObjectLayer(element, externalTemplateResolver, customTypeDefinitions),
// "imagelayer" => ReadImageLayer(element), // "imagelayer" => ReadImageLayer(element),
// "group" => ReadGroup(element), "group" => ReadGroup(element, externalTemplateResolver, customTypeDefinitions),
_ => throw new JsonException($"Unsupported layer type '{type}'.") _ => throw new JsonException($"Unsupported layer type '{type}'.")
}; };
} }
internal static TileLayer ReadTileLayer(JsonElement element) internal static TileLayer ReadTileLayer(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var compression = element.GetOptionalPropertyParseable<DataCompression?>("compression", s => s switch var compression = element.GetOptionalPropertyParseable<DataCompression?>("compression", s => s switch
{ {
@ -50,7 +56,7 @@ internal partial class Tmj
var opacity = element.GetOptionalProperty<float>("opacity", 1.0f); var opacity = element.GetOptionalProperty<float>("opacity", 1.0f);
var parallaxx = element.GetOptionalProperty<float>("parallaxx", 1.0f); var parallaxx = element.GetOptionalProperty<float>("parallaxx", 1.0f);
var parallaxy = element.GetOptionalProperty<float>("parallaxy", 1.0f); var parallaxy = element.GetOptionalProperty<float>("parallaxy", 1.0f);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e), null); var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", e => ReadProperties(e, customTypeDefinitions), null);
var repeatX = element.GetOptionalProperty<bool>("repeatx", false); var repeatX = element.GetOptionalProperty<bool>("repeatx", false);
var repeatY = element.GetOptionalProperty<bool>("repeaty", false); var repeatY = element.GetOptionalProperty<bool>("repeaty", false);
var startX = element.GetOptionalProperty<int>("startx", 0); var startX = element.GetOptionalProperty<int>("startx", 0);
@ -85,4 +91,276 @@ internal partial class Tmj
Data = data ?? chunks Data = data ?? chunks
}; };
} }
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
};
}
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
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
};
}
// 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 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

@ -9,7 +9,11 @@ namespace DotTiled;
internal partial class Tmj internal partial class Tmj
{ {
internal static Map ReadMap(JsonElement element, Func<string, Tileset>? externalTilesetResolver, Func<string, Template> externalTemplateResolver) internal static Map ReadMap(
JsonElement element,
Func<string, Tileset>? externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var version = element.GetRequiredProperty<string>("version"); var version = element.GetRequiredProperty<string>("version");
var tiledVersion = element.GetRequiredProperty<string>("tiledversion"); var tiledVersion = element.GetRequiredProperty<string>("tiledversion");
@ -55,10 +59,10 @@ internal partial class Tmj
var nextObjectID = element.GetRequiredProperty<uint>("nextobjectid"); var nextObjectID = element.GetRequiredProperty<uint>("nextobjectid");
var infinite = element.GetOptionalProperty<bool>("infinite", false); var infinite = element.GetOptionalProperty<bool>("infinite", false);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", ReadProperties, null); 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>(ReadLayer), []); 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)), []); List<Tileset> tilesets = element.GetOptionalPropertyCustom<List<Tileset>>("tilesets", e => e.GetValueAsList<Tileset>(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []);
return new Map return new Map
{ {

View file

@ -8,7 +8,9 @@ namespace DotTiled;
internal partial class Tmj internal partial class Tmj
{ {
internal static Dictionary<string, IProperty> ReadProperties(JsonElement element) => internal static Dictionary<string, IProperty> ReadProperties(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) =>
element.GetValueAsList<IProperty>(e => element.GetValueAsList<IProperty>(e =>
{ {
var name = e.GetRequiredProperty<string>("name"); var name = e.GetRequiredProperty<string>("name");
@ -34,20 +36,105 @@ internal partial class Tmj
PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value") }, PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value") },
PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty<string>("value") }, PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty<uint>("value") }, PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty<uint>("value") },
PropertyType.Class => ReadClassProperty(e), PropertyType.Class => ReadClassProperty(e, customTypeDefinitions),
_ => throw new JsonException("Invalid property type") _ => throw new JsonException("Invalid property type")
}; };
return property!; return property!;
}).ToDictionary(p => p.Name); }).ToDictionary(p => p.Name);
internal static ClassProperty ReadClassProperty(JsonElement element) internal static ClassProperty ReadClassProperty(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions)
{ {
var name = element.GetRequiredProperty<string>("name"); var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype"); var propertyType = element.GetRequiredProperty<string>("propertytype");
var properties = element.GetRequiredPropertyCustom<Dictionary<string, IProperty>>("properties", ReadProperties); var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType);
return new ClassProperty { Name = name, PropertyType = propertyType, Properties = properties }; if (customTypeDef is CustomClassDefinition ccd)
{
var propsInType = CreateInstanceOfCustomClass(ccd);
var props = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []);
var mergedProps = 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());
}
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;
} }
} }

View file

@ -10,7 +10,11 @@ namespace DotTiled;
internal partial class Tmj internal partial class Tmj
{ {
internal static Tileset ReadTileset(JsonElement element, Func<string, Tileset>? externalTilesetResolver, Func<string, Template> externalTemplateResolver) 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 backgroundColor = element.GetOptionalPropertyParseable<Color?>("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var @class = element.GetOptionalProperty<string>("class", ""); var @class = element.GetOptionalProperty<string>("class", "");
@ -42,7 +46,7 @@ internal partial class Tmj
"bottomright" => ObjectAlignment.BottomRight, "bottomright" => ObjectAlignment.BottomRight,
_ => throw new JsonException($"Unknown object alignment '{s}'") _ => throw new JsonException($"Unknown object alignment '{s}'")
}, ObjectAlignment.Unspecified); }, ObjectAlignment.Unspecified);
var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", ReadProperties, null); var properties = element.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null);
var source = element.GetOptionalProperty<string?>("source", null); var source = element.GetOptionalProperty<string?>("source", null);
var spacing = element.GetOptionalProperty<uint?>("spacing", null); var spacing = element.GetOptionalProperty<uint?>("spacing", null);
var tileCount = element.GetOptionalProperty<uint?>("tilecount", null); var tileCount = element.GetOptionalProperty<uint?>("tilecount", null);
@ -55,7 +59,7 @@ internal partial class Tmj
"grid" => TileRenderSize.Grid, "grid" => TileRenderSize.Grid,
_ => throw new JsonException($"Unknown tile render size '{s}'") _ => throw new JsonException($"Unknown tile render size '{s}'")
}, TileRenderSize.Tile); }, TileRenderSize.Tile);
var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", ReadTiles, []); var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", el => ReadTiles(el, customTypeDefinitions), []);
var tileWidth = element.GetOptionalProperty<uint?>("tilewidth", null); var tileWidth = element.GetOptionalProperty<uint?>("tilewidth", null);
var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var transparentColor = element.GetOptionalPropertyParseable<Color?>("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null);
var type = element.GetOptionalProperty<string?>("type", null); var type = element.GetOptionalProperty<string?>("type", null);
@ -153,7 +157,9 @@ internal partial class Tmj
}; };
} }
internal static List<Tile> ReadTiles(JsonElement element) => internal static List<Tile> ReadTiles(
JsonElement element,
IReadOnlyCollection<CustomTypeDefinition> customTypeDefinitions) =>
element.GetValueAsList<Tile>(e => element.GetValueAsList<Tile>(e =>
{ {
//var animation = e.GetOptionalPropertyCustom<List<Frame>>("animation", ReadFrames, null); //var animation = e.GetOptionalPropertyCustom<List<Frame>>("animation", ReadFrames, null);
@ -167,7 +173,7 @@ internal partial class Tmj
var height = e.GetOptionalProperty<uint>("height", imageHeight ?? 0); var height = e.GetOptionalProperty<uint>("height", imageHeight ?? 0);
//var objectGroup = e.GetOptionalPropertyCustom<ObjectLayer?>("objectgroup", ReadObjectLayer, null); //var objectGroup = e.GetOptionalPropertyCustom<ObjectLayer?>("objectgroup", ReadObjectLayer, null);
var probability = e.GetOptionalProperty<float>("probability", 1.0f); var probability = e.GetOptionalProperty<float>("probability", 1.0f);
var properties = e.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", ReadProperties, null); var properties = e.GetOptionalPropertyCustom<Dictionary<string, IProperty>?>("properties", el => ReadProperties(el, customTypeDefinitions), null);
// var terrain, replaced by wangsets // var terrain, replaced by wangsets
var type = e.GetOptionalProperty<string>("type", ""); var type = e.GetOptionalProperty<string>("type", "");

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -14,18 +15,25 @@ public class TmjMapReader : IMapReader
private string _jsonString; private string _jsonString;
private bool disposedValue; private bool disposedValue;
public TmjMapReader(string jsonString, Func<string, Tileset> externalTilesetResolver, Func<string, Template> externalTemplateResolver) 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)); _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString));
_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));
} }
public Map ReadMap() public Map ReadMap()
{ {
var jsonDoc = JsonDocument.Parse(_jsonString); var jsonDoc = JsonDocument.Parse(_jsonString);
var rootElement = jsonDoc.RootElement; var rootElement = jsonDoc.RootElement;
return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver); return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions);
} }
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

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