From 0f05bd10aaa3620912dbe62e0d21db133036bbc0 Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Tue, 6 Aug 2024 22:39:50 +0200 Subject: [PATCH] Start json reader --- .../Serialization/Tmj/TmjMapReaderTests.cs | 32 ++++ .../Tmj/ExtensionsUtf8JsonReader.cs | 158 ++++++++++++++++++ DotTiled/Serialization/Tmj/Tmj.Map.cs | 102 +++++++++++ DotTiled/Serialization/Tmj/TmjMapReader.cs | 60 +++++++ 4 files changed, 352 insertions(+) create mode 100644 DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs create mode 100644 DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs create mode 100644 DotTiled/Serialization/Tmj/Tmj.Map.cs create mode 100644 DotTiled/Serialization/Tmj/TmjMapReader.cs diff --git a/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs new file mode 100644 index 0000000..2462fcd --- /dev/null +++ b/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -0,0 +1,32 @@ +namespace DotTiled.Tests; + +public partial class TmjMapReaderTests +{ + [Fact] + public void Test1() + { + // Arrange + var jsonString = + """ + { + "backgroundcolor":"#656667", + "height":4, + "nextobjectid":1, + "nextlayerid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tileheight":32, + "tilewidth":32, + "version":"1", + "tiledversion":"1.0.3", + "width":4 + } + """; + + // Act + using var tmjMapReader = new TmjMapReader(jsonString); + + // Assert + var map = tmjMapReader.ReadMap(); + } +} diff --git a/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs b/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs new file mode 100644 index 0000000..a0cd7a6 --- /dev/null +++ b/DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal abstract class JsonProperty(string propertyName) + { + internal string PropertyName { get; } = propertyName; + } + + internal class RequiredProperty(string propertyName, Action withValue) : JsonProperty(propertyName) + { + internal Action WithValue { get; } = withValue; + } + + internal class OptionalProperty(string propertyName, Action withValue, bool allowNull = false) : JsonProperty(propertyName) + { + internal Action WithValue { get; } = withValue; + internal bool AllowNull { get; } = allowNull; + } +} + +internal static class ExtensionsUtf8JsonReader +{ + private static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) + { + 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); + } + + internal static void MoveToContent(this ref Utf8JsonReader reader) + { + while (reader.Read() && reader.TokenType == JsonTokenType.Comment || + reader.TokenType == JsonTokenType.None) + ; + } + + internal delegate void ProcessProperty(ref Utf8JsonReader reader); + + internal static void ProcessJsonObject(this 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; + + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException("Expected property name."); + + var propertyName = reader.GetString(); + reader.Read(); + + if (!processors.Any(x => x.PropertyName == propertyName)) + { + reader.Skip(); + continue; + } + + var processor = processors.First(x => x.PropertyName == propertyName).Processor; + processor(ref reader); + } + + throw new JsonException("Expected end of object."); + } + + delegate T UseReader(ref Utf8JsonReader reader); + + internal static void ProcessJsonObject(this Utf8JsonReader reader, Tmj.JsonProperty[] properties) + { + List processedProperties = []; + + bool CheckType(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader useReader) + { + return CheckRequire(ref reader, prop, (ref Utf8JsonReader r) => useReader(ref r)!) || CheckOptional(ref reader, prop, useReader); + } + + bool CheckRequire(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader useReader) + { + if (prop is Tmj.RequiredProperty requiredProp) + { + reader.Require((ref Utf8JsonReader r) => + { + requiredProp.WithValue(useReader(ref r)); + }); + return true; + } + return false; + } + + bool CheckOptional(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader useReader) + { + if (prop is Tmj.OptionalProperty optionalProp) + { + 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; + } + 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."); + } + } +} diff --git a/DotTiled/Serialization/Tmj/Tmj.Map.cs b/DotTiled/Serialization/Tmj/Tmj.Map.cs new file mode 100644 index 0000000..ce3ab2a --- /dev/null +++ b/DotTiled/Serialization/Tmj/Tmj.Map.cs @@ -0,0 +1,102 @@ +using System.Globalization; +using System.Text.Json; + +namespace DotTiled; + +internal partial class Tmj +{ + internal static Map ReadMap(ref Utf8JsonReader reader) + { + string version = default!; + string tiledVersion = default!; + string @class = ""; + MapOrientation orientation = default; + RenderOrder renderOrder = RenderOrder.RightDown; + int compressionLevel = -1; + uint width = 0; + uint height = 0; + uint tileWidth = 0; + uint tileHeight = 0; + uint? hexSideLength = null; + StaggerAxis? staggerAxis = null; + StaggerIndex? staggerIndex = null; + float parallaxOriginX = 0.0f; + float parallaxOriginY = 0.0f; + Color backgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture); + uint nextLayerID = 0; + uint nextObjectID = 0; + bool infinite = false; + + 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 + { + "orthogonal" => MapOrientation.Orthogonal, + "isometric" => MapOrientation.Isometric, + "staggered" => MapOrientation.Staggered, + "hexagonal" => MapOrientation.Hexagonal, + _ => throw new JsonException("Invalid orientation.") + }), + new OptionalProperty("renderOrder", value => renderOrder = value switch + { + "right-down" => RenderOrder.RightDown, + "right-up" => RenderOrder.RightUp, + "left-down" => RenderOrder.LeftDown, + "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 + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new JsonException("Invalid stagger axis.") + }), + new OptionalProperty("staggerIndex", value => staggerIndex = value 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) + ]); + + 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 + }; + } +} diff --git a/DotTiled/Serialization/Tmj/TmjMapReader.cs b/DotTiled/Serialization/Tmj/TmjMapReader.cs new file mode 100644 index 0000000..42919d6 --- /dev/null +++ b/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace DotTiled; + +public class TmjMapReader : IMapReader +{ + private string _jsonString; + private bool disposedValue; + + public TmjMapReader(string jsonString) + { + _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); + } + + public Map ReadMap() + { + var bytes = Encoding.UTF8.GetBytes(_jsonString); + var options = new JsonReaderOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + var reader = new Utf8JsonReader(bytes, options); + reader.MoveToContent(); + + return Tmj.ReadMap(ref reader); + } + + 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); + } +}