diff --git a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index 2462fcd..7e15bac 100644 --- a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -14,12 +14,45 @@ public partial class TmjMapReaderTests "nextobjectid":1, "nextlayerid":1, "orientation":"orthogonal", + "properties": [ + { + "name":"mapProperty1", + "type":"string", + "value":"one" + }, + { + "name":"mapProperty3", + "type":"string", + "value":"twoeee" + } + ], "renderorder":"right-down", "tileheight":32, "tilewidth":32, "version":"1", "tiledversion":"1.0.3", - "width":4 + "width":4, + "tilesets": [ + { + "columns":19, + "firstgid":1, + "image":"image/fishbaddie_parts.png", + "imageheight":480, + "imagewidth":640, + "margin":3, + "name":"", + "properties":[ + { + "name":"myProperty1", + "type":"string", + "value":"myProperty1_value" + }], + "spacing":1, + "tilecount":266, + "tileheight":32, + "tilewidth":32 + } + ] } """; diff --git a/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs b/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs index a0cd7a6..1c98bc6 100644 --- a/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs +++ b/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs @@ -14,40 +14,26 @@ internal partial class Tmj internal string PropertyName { get; } = propertyName; } - internal class RequiredProperty(string propertyName, Action withValue) : JsonProperty(propertyName) + internal delegate void UseReader(ref Utf8JsonReader reader); + + internal class RequiredProperty(string propertyName, UseReader useReader) : JsonProperty(propertyName) { - internal Action WithValue { get; } = withValue; + internal UseReader UseReader { get; } = useReader; } - internal class OptionalProperty(string propertyName, Action withValue, bool allowNull = false) : JsonProperty(propertyName) + internal class OptionalProperty(string propertyName, UseReader useReader, bool allowNull = true) : JsonProperty(propertyName) { - internal Action WithValue { get; } = withValue; + internal UseReader UseReader { get; } = useReader; internal bool AllowNull { get; } = allowNull; } } internal static class ExtensionsUtf8JsonReader { - private static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) + internal static T Progress(ref this Utf8JsonReader reader, T value) { - while (toCheck != typeof(object)) - { - var cur = toCheck!.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; - if (generic == cur) - return true; - - toCheck = toCheck.BaseType!; - } - - return false; - } - - internal static void Require(this ref Utf8JsonReader reader, ProcessProperty process) - { - if (reader.TokenType == JsonTokenType.Null) - throw new JsonException("Value is required."); - - process(ref reader); + reader.Read(); + return value; } internal static void MoveToContent(this ref Utf8JsonReader reader) @@ -59,16 +45,15 @@ internal static class ExtensionsUtf8JsonReader internal delegate void ProcessProperty(ref Utf8JsonReader reader); - internal static void ProcessJsonObject(this Utf8JsonReader reader, (string PropertyName, ProcessProperty Processor)[] processors) + private static void ProcessJsonObject(this ref Utf8JsonReader reader, (string PropertyName, ProcessProperty Processor)[] processors) { if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected start of object."); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - return; + reader.Read(); + while (reader.TokenType != JsonTokenType.EndObject) + { if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected property name."); @@ -77,7 +62,11 @@ internal static class ExtensionsUtf8JsonReader if (!processors.Any(x => x.PropertyName == propertyName)) { - reader.Skip(); + var depthBefore = reader.CurrentDepth; + + while (reader.TokenType != JsonTokenType.PropertyName || reader.CurrentDepth > depthBefore) + reader.Read(); + continue; } @@ -85,74 +74,66 @@ internal static class ExtensionsUtf8JsonReader processor(ref reader); } - throw new JsonException("Expected end of object."); + if (reader.TokenType != JsonTokenType.EndObject) + throw new JsonException("Expected end of object."); + + reader.Read(); } - delegate T UseReader(ref Utf8JsonReader reader); - - internal static void ProcessJsonObject(this Utf8JsonReader reader, Tmj.JsonProperty[] properties) + internal static void ProcessJsonObject(this ref Utf8JsonReader reader, Tmj.JsonProperty[] properties, string objectTypeName) { List processedProperties = []; - bool CheckType(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader useReader) + ProcessJsonObject(ref reader, properties.Select(x => (x.PropertyName, (ref Utf8JsonReader reader) => { - return CheckRequire(ref reader, prop, (ref Utf8JsonReader r) => useReader(ref r)!) || CheckOptional(ref reader, prop, useReader); - } + if (processedProperties.Contains(x.PropertyName)) + throw new JsonException($"Property '{x.PropertyName}' was already processed."); - bool CheckRequire(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader useReader) - { - if (prop is Tmj.RequiredProperty requiredProp) + processedProperties.Add(x.PropertyName); + + if (x is Tmj.RequiredProperty req) { - reader.Require((ref Utf8JsonReader r) => - { - requiredProp.WithValue(useReader(ref r)); - }); - return true; - } - return false; - } + if (reader.TokenType == JsonTokenType.Null) + throw new JsonException($"Required property '{req.PropertyName}' cannot be null when reading {objectTypeName}."); - bool CheckOptional(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader useReader) - { - if (prop is Tmj.OptionalProperty optionalProp) + req.UseReader(ref reader); + } + else if (x is Tmj.OptionalProperty opt) { - if (reader.TokenType == JsonTokenType.Null && !optionalProp.AllowNull) - throw new JsonException("Value cannot be null for optional property."); - else if (reader.TokenType == JsonTokenType.Null && optionalProp.AllowNull) - optionalProp.WithValue(default); - else - optionalProp.WithValue(useReader(ref reader)); - return true; + if (reader.TokenType == JsonTokenType.Null && !opt.AllowNull) + throw new JsonException($"Value cannot be null for optional property '{opt.PropertyName}' when reading {objectTypeName}."); + else if (reader.TokenType == JsonTokenType.Null && opt.AllowNull) + return; + + opt.UseReader(ref reader); } - return false; - } - - ProcessJsonObject(reader, properties.Select(x => (x.PropertyName.ToLowerInvariant(), (ref Utf8JsonReader reader) => - { - var lowerInvariant = x.PropertyName.ToLowerInvariant(); - - if (processedProperties.Contains(lowerInvariant)) - throw new JsonException($"Property '{lowerInvariant}' was already processed."); - - processedProperties.Add(lowerInvariant); - - if (CheckType(ref reader, x, (ref Utf8JsonReader r) => r.GetString()!)) - return; - if (CheckType(ref reader, x, (ref Utf8JsonReader r) => r.GetInt32())) - return; - if (CheckType(ref reader, x, (ref Utf8JsonReader r) => r.GetUInt32())) - return; - if (CheckType(ref reader, x, (ref Utf8JsonReader r) => r.GetSingle())) - return; - - throw new NotSupportedException($"Unsupported property type '{x.GetType().GenericTypeArguments.First()}'."); } )).ToArray()); foreach (var property in properties) { - if (IsSubclassOfRawGeneric(typeof(Tmj.RequiredProperty<>), property.GetType()) && !processedProperties.Contains(property.PropertyName.ToLowerInvariant())) - throw new JsonException($"Required property '{property.PropertyName}' was not found."); + if (property is Tmj.RequiredProperty && !processedProperties.Contains(property.PropertyName)) + throw new JsonException($"Required property '{property.PropertyName}' was not found when reading {objectTypeName}."); } } + + internal delegate void UseReader(ref Utf8JsonReader reader); + + internal static void ProcessJsonArray(this ref Utf8JsonReader reader, UseReader useReader) + { + if (reader.TokenType != JsonTokenType.StartArray) + throw new JsonException("Expected start of array."); + + reader.Read(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + useReader(ref reader); + } + + if (reader.TokenType != JsonTokenType.EndArray) + throw new JsonException("Expected end of array."); + + reader.Read(); + } } diff --git a/DotTiled/Serialization/Tmj/Tmj.Map.cs b/DotTiled/Serialization/Tmj/Tmj.Map.cs index ce3ab2a..c76c391 100644 --- a/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Text.Json; namespace DotTiled; @@ -27,11 +29,18 @@ internal partial class Tmj uint nextObjectID = 0; bool infinite = false; + // At most one of + Dictionary? properties = null; + + // Any number of + List layers = []; + List tilesets = []; + reader.ProcessJsonObject([ - new RequiredProperty("version", value => version = value), - new RequiredProperty("tiledVersion", value => tiledVersion = value), - new OptionalProperty("class", value => @class = value ?? ""), - new RequiredProperty("orientation", value => orientation = value switch + new RequiredProperty("version", (ref Utf8JsonReader reader) => version = reader.Progress(reader.GetString()!)), + new RequiredProperty("tiledversion", (ref Utf8JsonReader reader) => tiledVersion = reader.Progress(reader.GetString()!)), + new OptionalProperty("class", (ref Utf8JsonReader reader) => @class = reader.Progress(reader.GetString() ?? ""), allowNull: true), + new RequiredProperty("orientation", (ref Utf8JsonReader reader) => orientation = reader.Progress(reader.GetString()) switch { "orthogonal" => MapOrientation.Orthogonal, "isometric" => MapOrientation.Isometric, @@ -39,7 +48,7 @@ internal partial class Tmj "hexagonal" => MapOrientation.Hexagonal, _ => throw new JsonException("Invalid orientation.") }), - new OptionalProperty("renderOrder", value => renderOrder = value switch + new OptionalProperty("renderorder", (ref Utf8JsonReader reader) => renderOrder = reader.Progress(reader.GetString()) switch { "right-down" => RenderOrder.RightDown, "right-up" => RenderOrder.RightUp, @@ -47,31 +56,34 @@ internal partial class Tmj "left-up" => RenderOrder.LeftUp, _ => throw new JsonException("Invalid render order.") }), - new OptionalProperty("compressionLevel", value => compressionLevel = value), - new RequiredProperty("width", value => width = value), - new RequiredProperty("height", value => height = value), - new RequiredProperty("tileWidth", value => tileWidth = value), - new RequiredProperty("tileHeight", value => tileHeight = value), - new OptionalProperty("hexSideLength", value => hexSideLength = value), - new OptionalProperty("staggerAxis", value => staggerAxis = value switch + new OptionalProperty("compressionlevel", (ref Utf8JsonReader reader) => compressionLevel = reader.Progress(reader.GetInt32())), + new RequiredProperty("width", (ref Utf8JsonReader reader) => width = reader.Progress(reader.GetUInt32())), + new RequiredProperty("height", (ref Utf8JsonReader reader) => height = reader.Progress(reader.GetUInt32())), + new RequiredProperty("tilewidth", (ref Utf8JsonReader reader) => tileWidth = reader.Progress(reader.GetUInt32())), + new RequiredProperty("tileheight", (ref Utf8JsonReader reader) => tileHeight = reader.Progress(reader.GetUInt32())), + new OptionalProperty("hexsidelength", (ref Utf8JsonReader reader) => hexSideLength = reader.Progress(reader.GetUInt32())), + new OptionalProperty("staggeraxis", (ref Utf8JsonReader reader) => staggerAxis = reader.Progress(reader.GetString()) switch { "x" => StaggerAxis.X, "y" => StaggerAxis.Y, _ => throw new JsonException("Invalid stagger axis.") }), - new OptionalProperty("staggerIndex", value => staggerIndex = value switch + new OptionalProperty("staggerindex", (ref Utf8JsonReader reader) => staggerIndex = reader.Progress(reader.GetString()) switch { "odd" => StaggerIndex.Odd, "even" => StaggerIndex.Even, _ => throw new JsonException("Invalid stagger index.") }), - new OptionalProperty("parallaxOriginX", value => parallaxOriginX = value), - new OptionalProperty("parallaxOriginY", value => parallaxOriginY = value), - new OptionalProperty("backgroundColor", value => backgroundColor = Color.Parse(value!, CultureInfo.InvariantCulture)), - new RequiredProperty("nextLayerID", value => nextLayerID = value), - new RequiredProperty("nextObjectID", value => nextObjectID = value), - new OptionalProperty("infinite", value => infinite = value == 1) - ]); + new OptionalProperty("parallaxoriginx", (ref Utf8JsonReader reader) => parallaxOriginX = reader.Progress(reader.GetSingle())), + new OptionalProperty("parallaxoriginy", (ref Utf8JsonReader reader) => parallaxOriginY = reader.Progress(reader.GetSingle())), + new OptionalProperty("backgroundcolor", (ref Utf8JsonReader reader) => backgroundColor = Color.Parse(reader.Progress(reader.GetString()!), CultureInfo.InvariantCulture)), + new RequiredProperty("nextlayerid", (ref Utf8JsonReader reader) => nextLayerID = reader.Progress(reader.GetUInt32())), + new RequiredProperty("nextobjectid", (ref Utf8JsonReader reader) => nextObjectID = reader.Progress(reader.GetUInt32())), + new OptionalProperty("infinite", (ref Utf8JsonReader reader) => infinite = reader.Progress(reader.GetUInt32()) == 1), + + new OptionalProperty("properties", (ref Utf8JsonReader reader) => properties = ReadProperties(ref reader)), + new OptionalProperty("tilesets", (ref Utf8JsonReader reader) => tilesets = ReadTilesets(ref reader)) + ], "map"); return new Map { @@ -94,9 +106,174 @@ internal partial class Tmj NextLayerID = nextLayerID, NextObjectID = nextObjectID, Infinite = infinite, - //Properties = properties, - //Tilesets = tilesets, - //Layers = layers + Properties = properties, + Tilesets = tilesets, + Layers = layers + }; + } + + internal static Dictionary ReadProperties(ref Utf8JsonReader reader) + { + var properties = new Dictionary(); + + reader.ProcessJsonArray((ref Utf8JsonReader reader) => + { + var property = ReadProperty(ref reader); + properties.Add(property.Name, property); + }); + + return properties; + } + + internal static IProperty ReadProperty(ref Utf8JsonReader reader) + { + string name = default!; + string type = default!; + IProperty property = null; + + reader.ProcessJsonObject([ + new RequiredProperty("name", (ref Utf8JsonReader reader) => name = reader.Progress(reader.GetString()!)), + new RequiredProperty("type", (ref Utf8JsonReader reader) => type = reader.Progress(reader.GetString()!)), + new RequiredProperty("value", (ref Utf8JsonReader reader) => + { + property = type switch + { + "string" => new StringProperty { Name = name, Value = reader.Progress(reader.GetString()!) }, + "int" => new IntProperty { Name = name, Value = reader.Progress(reader.GetInt32()) }, + "float" => new FloatProperty { Name = name, Value = reader.Progress(reader.GetSingle()) }, + "bool" => new BoolProperty { Name = name, Value = reader.Progress(reader.GetBoolean()) }, + "color" => new ColorProperty { Name = name, Value = Color.Parse(reader.Progress(reader.GetString()!), CultureInfo.InvariantCulture) }, + "file" => new FileProperty { Name = name, Value = reader.Progress(reader.GetString()!) }, + "object" => new ObjectProperty { Name = name, Value = reader.Progress(reader.GetUInt32()) }, + // "class" => ReadClassProperty(ref reader), + _ => throw new JsonException("Invalid property type.") + }; + }), + ], "property"); + + return property!; + } + + internal static List ReadTilesets(ref Utf8JsonReader reader) + { + var tilesets = new List(); + + reader.ProcessJsonArray((ref Utf8JsonReader reader) => + { + var tileset = ReadTileset(ref reader); + tilesets.Add(tileset); + }); + + return tilesets; + } + + internal static Tileset ReadTileset(ref Utf8JsonReader reader) + { + string? version = null; + string? tiledVersion = null; + uint? firstGID = null; + string? source = null; + string? name = null; + string @class = ""; + uint? tileWidth = null; + uint? tileHeight = null; + uint? spacing = null; + uint? margin = null; + uint? tileCount = null; + uint? columns = null; + ObjectAlignment objectAlignment = ObjectAlignment.Unspecified; + FillMode fillMode = FillMode.Stretch; + + string? image = null; + uint? imageWidth = null; + uint? imageHeight = null; + + Dictionary? properties = null; + + reader.ProcessJsonObject([ + new OptionalProperty("version", (ref Utf8JsonReader reader) => version = reader.Progress(reader.GetString())), + new OptionalProperty("tiledversion", (ref Utf8JsonReader reader) => tiledVersion = reader.Progress(reader.GetString())), + new OptionalProperty("firstgid", (ref Utf8JsonReader reader) => firstGID = reader.Progress(reader.GetUInt32())), + new OptionalProperty("source", (ref Utf8JsonReader reader) => source = reader.Progress(reader.GetString())), + new OptionalProperty("name", (ref Utf8JsonReader reader) => name = reader.Progress(reader.GetString())), + new OptionalProperty("class", (ref Utf8JsonReader reader) => @class = reader.Progress(reader.GetString() ?? ""), allowNull: true), + new OptionalProperty("tilewidth", (ref Utf8JsonReader reader) => tileWidth = reader.Progress(reader.GetUInt32())), + new OptionalProperty("tileheight", (ref Utf8JsonReader reader) => tileHeight = reader.Progress(reader.GetUInt32())), + new OptionalProperty("spacing", (ref Utf8JsonReader reader) => spacing = reader.Progress(reader.GetUInt32())), + new OptionalProperty("margin", (ref Utf8JsonReader reader) => margin = reader.Progress(reader.GetUInt32())), + new OptionalProperty("tilecount", (ref Utf8JsonReader reader) => tileCount = reader.Progress(reader.GetUInt32())), + new OptionalProperty("columns", (ref Utf8JsonReader reader) => columns = reader.Progress(reader.GetUInt32())), + new OptionalProperty("objectalignment", (ref Utf8JsonReader reader) => objectAlignment = reader.Progress(reader.GetString()) 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("Invalid object alignment.") + }), + new OptionalProperty("fillmode", (ref Utf8JsonReader reader) => fillMode = reader.Progress(reader.GetString()) switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new JsonException("Invalid fill mode.") + }), + + new OptionalProperty("image", (ref Utf8JsonReader reader) => image = reader.Progress(reader.GetString())), + new OptionalProperty("imagewidth", (ref Utf8JsonReader reader) => imageWidth = reader.Progress(reader.GetUInt32())), + new OptionalProperty("imageheight", (ref Utf8JsonReader reader) => imageHeight = reader.Progress(reader.GetUInt32())), + + new OptionalProperty("properties", (ref Utf8JsonReader reader) => properties = ReadProperties(ref reader)) + ], "tileset"); + + Image? imageInstance = image is not null ? new Image + { + Format = ParseImageFormatFromSource(image), + Width = imageWidth, + Height = imageHeight, + Source = image + } : null; + + return new Tileset + { + Version = version, + TiledVersion = tiledVersion, + FirstGID = firstGID, + Source = source, + Name = name, + Class = @class, + TileWidth = tileWidth, + TileHeight = tileHeight, + Spacing = spacing, + Margin = margin, + TileCount = tileCount, + Columns = columns, + ObjectAlignment = objectAlignment, + FillMode = fillMode, + Image = imageInstance, + Properties = properties + }; + } + + private static ImageFormat ParseImageFormatFromSource(string? source) + { + if (source is null) + throw new JsonException("Image source is required to determine image format."); + + var extension = Path.GetExtension(source); + return extension switch + { + ".png" => ImageFormat.Png, + ".jpg" => ImageFormat.Jpg, + ".jpeg" => ImageFormat.Jpg, + ".gif" => ImageFormat.Gif, + ".bmp" => ImageFormat.Bmp, + _ => throw new JsonException("Invalid image format.") }; } }