From c205e710bc910042b43eb1fbfe6594fb159a98a9 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Thu, 17 Apr 2025 22:19:39 +0300 Subject: [PATCH] chore: some experimentations with DotNetYaml --- .../Engine.Serialization.csproj | 18 ++ Engine.Serialization/EntityFieldConverter.cs | 172 ++++++++++++++++++ Engine.Serialization/EntityReference.cs | 6 + .../IgnoreSerializationAttribute.cs | 5 + Engine.Serialization/Serializer.cs | 30 +++ Engine.sln | 6 + Engine/Engine.csproj | 1 + 7 files changed, 238 insertions(+) create mode 100644 Engine.Serialization/Engine.Serialization.csproj create mode 100644 Engine.Serialization/EntityFieldConverter.cs create mode 100644 Engine.Serialization/EntityReference.cs create mode 100644 Engine.Serialization/IgnoreSerializationAttribute.cs create mode 100644 Engine.Serialization/Serializer.cs diff --git a/Engine.Serialization/Engine.Serialization.csproj b/Engine.Serialization/Engine.Serialization.csproj new file mode 100644 index 0000000..9198433 --- /dev/null +++ b/Engine.Serialization/Engine.Serialization.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + Syntriax.Engine.Serialization + + + + + + + + + + + diff --git a/Engine.Serialization/EntityFieldConverter.cs b/Engine.Serialization/EntityFieldConverter.cs new file mode 100644 index 0000000..028edb5 --- /dev/null +++ b/Engine.Serialization/EntityFieldConverter.cs @@ -0,0 +1,172 @@ +using System.Collections; +using System.Reflection; +using Syntriax.Engine.Core; + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Syntriax.Engine.Serialization; + +public class Vector2DConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) + { + return type == typeof(Vector2D); + } + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + parser.Consume(); + + float x = 0f; + float y = 0f; + + while (!parser.TryConsume(out _)) + { + string key = parser.Consume().Value; + string value = parser.Consume().Value; + + if (key.CompareTo(nameof(Vector2D.X)) == 0) x = float.Parse(value); + if (key.CompareTo(nameof(Vector2D.Y)) == 0) y = float.Parse(value); + } + + return new Vector2D(x, y); + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + Vector2D vector2D = (Vector2D)value!; + + emitter.Emit(new MappingStart()); + emitter.Emit(new Scalar(nameof(Vector2D.X))); + emitter.Emit(new Scalar(vector2D.X.ToString())); + emitter.Emit(new Scalar(nameof(Vector2D.Y))); + emitter.Emit(new Scalar(vector2D.Y.ToString())); + emitter.Emit(new MappingEnd()); + } +} + +public class EntityConverter : IYamlTypeConverter +{ + private static readonly ISerializer serializer = new SerializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .DisableAliases() + .Build(); + + private static readonly IDeserializer deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .Build(); + + public bool Accepts(Type type) + { + bool v = typeof(IEntity).IsAssignableFrom(type); + return v; + } + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + string? id = null; + + parser.Consume(); + + while (!parser.TryConsume(out _)) + { + string key = parser.Consume().Value; + string value = parser.Consume().Value; + + if (key.CompareTo(nameof(EntityReference.Id)) == 0) + id = value; + } + + return new EntityReference() { Id = id }; + } + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + if (value is not IEntity entity) + return; + + var typeData = GetTypeData(type); + emitter.Emit(new MappingStart()); + + foreach (var fieldInfo in typeData.Fields) + { + if (fieldInfo.GetCustomAttribute() != null) + continue; + + emitter.Emit(new Scalar(fieldInfo.Name)); + var fieldValue = fieldInfo.GetValue(entity); + + EmitValue(fieldValue, fieldInfo.FieldType, emitter, serializer); + } + + foreach (var propertyInfo in typeData.Properties) + { + emitter.Emit(new Scalar(propertyInfo.Name)); + var propValue = propertyInfo.GetValue(entity); + + EmitValue(propValue, propertyInfo.PropertyType, emitter, serializer); + } + + emitter.Emit(new MappingEnd()); + } + private void EmitValue(object? value, Type declaredType, IEmitter emitter, ObjectSerializer serializer) + { + if (value is null) + { + emitter.Emit(new Scalar("null")); + return; + } + + if (value is IEntity singleEntity && !IsEnumerable(declaredType)) + { + emitter.Emit(new Scalar(singleEntity.Id)); + return; + } + + if (IsEnumerable(declaredType) && value is System.Collections.IEnumerable list) + { + emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block)); + foreach (var item in list) + { + if (item is IEntity itemEntity) + emitter.Emit(new Scalar(itemEntity.Id)); + else + serializer(item); // fallback for non-entity items + } + emitter.Emit(new SequenceEnd()); + return; + } + + // fallback + serializer(value); + } + private static TypeData GetTypeData(Type objectType) + { + IEnumerable eventInfos = objectType.GetEvents(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + .OrderBy(ei => ei.Name) + .AsEnumerable(); + IEnumerable fieldInfos = objectType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + .Where(fi => !eventInfos.Any(ei => fi.Name.CompareTo(ei.Name) == 0)) + .OrderBy(ei => ei.Name) //ei => ei.FieldType.IsPrimitive || ei.FieldType == typeof(string)) + // .ThenByDescending(ei => ei.Name) + .AsEnumerable(); + IEnumerable propertyInfos = objectType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + .Where(pi => pi.SetMethod is not null) + .OrderBy(ei => ei.Name)// ei => ei.PropertyType.IsPrimitive || ei.PropertyType == typeof(string)) + // .ThenByDescending(ei => ei.Name) + .AsEnumerable(); + + return new TypeData(eventInfos, fieldInfos, propertyInfos); + } + private static bool IsEnumerable(Type type) + { + return typeof(System.Collections.IEnumerable).IsAssignableFrom(type) && type != typeof(string); + } + private record struct TypeData(IEnumerable Events, IEnumerable Fields, IEnumerable Properties) + { + public static implicit operator (IEnumerable events, IEnumerable fields, IEnumerable properties)(TypeData value) => (value.Events, value.Fields, value.Properties); + public static implicit operator TypeData((IEnumerable events, IEnumerable fields, IEnumerable properties) value) => new TypeData(value.events, value.fields, value.properties); + } +} diff --git a/Engine.Serialization/EntityReference.cs b/Engine.Serialization/EntityReference.cs new file mode 100644 index 0000000..218847d --- /dev/null +++ b/Engine.Serialization/EntityReference.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Serialization; + +public class EntityReference +{ + public string Id { get; set; } +} diff --git a/Engine.Serialization/IgnoreSerializationAttribute.cs b/Engine.Serialization/IgnoreSerializationAttribute.cs new file mode 100644 index 0000000..6604941 --- /dev/null +++ b/Engine.Serialization/IgnoreSerializationAttribute.cs @@ -0,0 +1,5 @@ +namespace Syntriax.Engine.Serialization; + +public class IgnoreSerializationAttribute : Attribute +{ +} diff --git a/Engine.Serialization/Serializer.cs b/Engine.Serialization/Serializer.cs new file mode 100644 index 0000000..c96647f --- /dev/null +++ b/Engine.Serialization/Serializer.cs @@ -0,0 +1,30 @@ +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Syntriax.Engine.Serialization; + +public static class Serializer +{ + private static readonly ISerializer serializer = new SerializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .DisableAliases() + .WithTypeConverter(new EntityConverter()) + .WithTypeConverter(new Vector2DConverter()) + .Build(); + + private static readonly IDeserializer deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .WithTypeConverter(new EntityConverter()) + .WithTypeConverter(new Vector2DConverter()) + .Build(); + + public static string Serialize(object instance) + { + return serializer.Serialize(instance); + } + + public static T Deserialize(string yaml) + { + return deserializer.Deserialize(yaml); + } +} diff --git a/Engine.sln b/Engine.sln index 4bc06af..218717c 100644 --- a/Engine.sln +++ b/Engine.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Systems", "Engine.Sy EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine", "Engine\Engine.csproj", "{58AE79C1-9203-44AE-8022-AA180F0A71DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Serialization", "Engine.Serialization\Engine.Serialization.csproj", "{9B46BE51-DD6B-4EDD-AAA8-41E993D7422C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +38,9 @@ Global {58AE79C1-9203-44AE-8022-AA180F0A71DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {58AE79C1-9203-44AE-8022-AA180F0A71DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {58AE79C1-9203-44AE-8022-AA180F0A71DC}.Release|Any CPU.Build.0 = Release|Any CPU + {9B46BE51-DD6B-4EDD-AAA8-41E993D7422C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B46BE51-DD6B-4EDD-AAA8-41E993D7422C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B46BE51-DD6B-4EDD-AAA8-41E993D7422C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B46BE51-DD6B-4EDD-AAA8-41E993D7422C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index c7b03c3..9d32971 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -8,6 +8,7 @@ +