Stuck on how to handle class properties

This commit is contained in:
Daniel Cronqvist 2024-08-08 23:04:05 +02:00
parent 0f05bd10aa
commit 2a10ebb496
3 changed files with 296 additions and 105 deletions

View file

@ -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
}
]
}
""";

View file

@ -14,40 +14,26 @@ internal partial class Tmj
internal string PropertyName { get; } = propertyName;
}
internal class RequiredProperty<T>(string propertyName, Action<T> withValue) : JsonProperty(propertyName)
internal delegate void UseReader(ref Utf8JsonReader reader);
internal class RequiredProperty(string propertyName, UseReader useReader) : JsonProperty(propertyName)
{
internal Action<T> WithValue { get; } = withValue;
internal UseReader UseReader { get; } = useReader;
}
internal class OptionalProperty<T>(string propertyName, Action<T?> withValue, bool allowNull = false) : JsonProperty(propertyName)
internal class OptionalProperty(string propertyName, UseReader useReader, bool allowNull = true) : JsonProperty(propertyName)
{
internal Action<T?> 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<T>(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<T>(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);
}
if (reader.TokenType != JsonTokenType.EndObject)
throw new JsonException("Expected end of object.");
reader.Read();
}
delegate T UseReader<T>(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<string> processedProperties = [];
bool CheckType<T>(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader<T?> useReader)
ProcessJsonObject(ref reader, properties.Select<Tmj.JsonProperty, (string, ProcessProperty)>(x => (x.PropertyName, (ref Utf8JsonReader reader) =>
{
return CheckRequire<T>(ref reader, prop, (ref Utf8JsonReader r) => useReader(ref r)!) || CheckOptional<T>(ref reader, prop, useReader);
if (processedProperties.Contains(x.PropertyName))
throw new JsonException($"Property '{x.PropertyName}' was already processed.");
processedProperties.Add(x.PropertyName);
if (x is Tmj.RequiredProperty req)
{
if (reader.TokenType == JsonTokenType.Null)
throw new JsonException($"Required property '{req.PropertyName}' cannot be null when reading {objectTypeName}.");
req.UseReader(ref reader);
}
bool CheckRequire<T>(ref Utf8JsonReader reader, Tmj.JsonProperty prop, UseReader<T> useReader)
else if (x is Tmj.OptionalProperty opt)
{
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()))
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;
throw new NotSupportedException($"Unsupported property type '{x.GetType().GenericTypeArguments.First()}'.");
opt.UseReader(ref reader);
}
}
)).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();
}
}

View file

@ -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<string, IProperty>? properties = null;
// Any number of
List<BaseLayer> layers = [];
List<Tileset> tilesets = [];
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
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<string>("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<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
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<string>("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<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)
]);
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<string, IProperty> ReadProperties(ref Utf8JsonReader reader)
{
var properties = new Dictionary<string, IProperty>();
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<Tileset> ReadTilesets(ref Utf8JsonReader reader)
{
var tilesets = new List<Tileset>();
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<string, IProperty>? 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.")
};
}
}