Start json reader

This commit is contained in:
Daniel Cronqvist 2024-08-06 22:39:50 +02:00
parent 34dcca7f35
commit 0f05bd10aa
4 changed files with 352 additions and 0 deletions

View file

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

View file

@ -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<T>(string propertyName, Action<T> withValue) : JsonProperty(propertyName)
{
internal Action<T> WithValue { get; } = withValue;
}
internal class OptionalProperty<T>(string propertyName, Action<T?> withValue, bool allowNull = false) : JsonProperty(propertyName)
{
internal Action<T?> 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<T>(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<T>(ref Utf8JsonReader reader);
internal static void ProcessJsonObject(this Utf8JsonReader reader, Tmj.JsonProperty[] properties)
{
List<string> processedProperties = [];
bool CheckType<T>(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader<T?> useReader)
{
return CheckRequire<T>(ref reader, prop, (ref Utf8JsonReader r) => useReader(ref r)!) || CheckOptional<T>(ref reader, prop, useReader);
}
bool CheckRequire<T>(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader<T> useReader)
{
if (prop is Tmj.RequiredProperty<T> requiredProp)
{
reader.Require<string>((ref Utf8JsonReader r) =>
{
requiredProp.WithValue(useReader(ref r));
});
return true;
}
return false;
}
bool CheckOptional<T>(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader<T?> useReader)
{
if (prop is Tmj.OptionalProperty<T> 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<Tmj.JsonProperty, (string, ProcessProperty)>(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<string>(ref reader, x, (ref Utf8JsonReader r) => r.GetString()!))
return;
if (CheckType<int>(ref reader, x, (ref Utf8JsonReader r) => r.GetInt32()))
return;
if (CheckType<uint>(ref reader, x, (ref Utf8JsonReader r) => r.GetUInt32()))
return;
if (CheckType<float>(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.");
}
}
}

View file

@ -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<string>("version", value => version = value),
new RequiredProperty<string>("tiledVersion", value => tiledVersion = value),
new OptionalProperty<string>("class", value => @class = value ?? ""),
new RequiredProperty<string>("orientation", value => orientation = value switch
{
"orthogonal" => MapOrientation.Orthogonal,
"isometric" => MapOrientation.Isometric,
"staggered" => MapOrientation.Staggered,
"hexagonal" => MapOrientation.Hexagonal,
_ => throw new JsonException("Invalid orientation.")
}),
new OptionalProperty<string>("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<int>("compressionLevel", value => compressionLevel = value),
new RequiredProperty<uint>("width", value => width = value),
new RequiredProperty<uint>("height", value => height = value),
new RequiredProperty<uint>("tileWidth", value => tileWidth = value),
new RequiredProperty<uint>("tileHeight", value => tileHeight = value),
new OptionalProperty<uint>("hexSideLength", value => hexSideLength = value),
new OptionalProperty<string>("staggerAxis", value => staggerAxis = value switch
{
"x" => StaggerAxis.X,
"y" => StaggerAxis.Y,
_ => throw new JsonException("Invalid stagger axis.")
}),
new OptionalProperty<string>("staggerIndex", value => staggerIndex = value switch
{
"odd" => StaggerIndex.Odd,
"even" => StaggerIndex.Even,
_ => throw new JsonException("Invalid stagger index.")
}),
new OptionalProperty<float>("parallaxOriginX", value => parallaxOriginX = value),
new OptionalProperty<float>("parallaxOriginY", value => parallaxOriginY = value),
new OptionalProperty<string>("backgroundColor", value => backgroundColor = Color.Parse(value!, CultureInfo.InvariantCulture)),
new RequiredProperty<uint>("nextLayerID", value => nextLayerID = value),
new RequiredProperty<uint>("nextObjectID", value => nextObjectID = value),
new OptionalProperty<uint>("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
};
}
}

View file

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