mirror of
https://github.com/dcronqvist/DotTiled.git
synced 2025-02-05 08:52:50 +02:00
Start json reader
This commit is contained in:
parent
34dcca7f35
commit
0f05bd10aa
4 changed files with 352 additions and 0 deletions
32
DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs
Normal file
32
DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs
Normal 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();
|
||||
}
|
||||
}
|
158
DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs
Normal file
158
DotTiled/Serialization/Tmj/ExtensionsUtf8JsonReader.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
102
DotTiled/Serialization/Tmj/Tmj.Map.cs
Normal file
102
DotTiled/Serialization/Tmj/Tmj.Map.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
60
DotTiled/Serialization/Tmj/TmjMapReader.cs
Normal file
60
DotTiled/Serialization/Tmj/TmjMapReader.cs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue