From 0b2224ecca54f582c396ea57c8e92dc981e6581f Mon Sep 17 00:00:00 2001 From: Ullrich Praetz Date: Thu, 28 Nov 2024 09:20:55 +0100 Subject: [PATCH] Serialize: read relations as JSON array --- src/ECS/Base/Types/ComponentType.cs | 9 ++ src/ECS/Serialize/ComponentReader.cs | 144 ++++++++++++++---- .../ECS/Test_ComponentReader.cs | 2 +- .../ECS/Serialize/Test_ComponentReader.cs | 4 +- .../ECS/Serialize/Test_SerializeRelations.cs | 23 ++- 5 files changed, 142 insertions(+), 40 deletions(-) diff --git a/src/ECS/Base/Types/ComponentType.cs b/src/ECS/Base/Types/ComponentType.cs index 436cd7bdc..81b78916d 100644 --- a/src/ECS/Base/Types/ComponentType.cs +++ b/src/ECS/Base/Types/ComponentType.cs @@ -3,7 +3,9 @@ using System; using System.Runtime.CompilerServices; +using Friflo.Engine.ECS.Relations; using Friflo.Engine.ECS.Serialize; +using Friflo.Json.Fliox; using static Friflo.Engine.ECS.SchemaTypeKind; // ReSharper disable ConvertToPrimaryConstructor @@ -40,6 +42,7 @@ public abstract class ComponentType : SchemaType internal abstract bool AddEntityComponentValue(Entity entity, object value); internal virtual void WriteRelations(ComponentWriter writer, Entity entity) => throw new InvalidOperationException(); + internal virtual void ReadRelation(ComponentReader reader, Entity entity, JsonValue json) => throw new InvalidOperationException(); internal abstract BatchComponent CreateBatchComponent(); @@ -160,6 +163,12 @@ internal override void WriteRelations(ComponentWriter writer, Entity entity) writer.writer.SetPretty(pretty); } + internal override void ReadRelation(ComponentReader reader, Entity entity, JsonValue json) + { + var relation = reader.componentReader.Read(json); + EntityRelations.AddRelation(entity.store, entity.Id, relation); + } + internal override StructHeap CreateHeap() { return new StructHeap(StructIndex); } diff --git a/src/ECS/Serialize/ComponentReader.cs b/src/ECS/Serialize/ComponentReader.cs index 4f1a870e3..f0f91fd8a 100644 --- a/src/ECS/Serialize/ComponentReader.cs +++ b/src/ECS/Serialize/ComponentReader.cs @@ -18,7 +18,7 @@ namespace Friflo.Engine.ECS.Serialize; /// internal sealed class ComponentReader { - private readonly ObjectReader componentReader; + internal readonly ObjectReader componentReader; private readonly MapperContextEntityStore mapperContextStore; private readonly Dictionary schemaTypeByKey; private readonly Dictionary scriptTypeByType; @@ -32,14 +32,15 @@ internal sealed class ComponentReader private readonly Dictionary unresolvedComponentMap; private readonly Dictionary rawKeyCache; private Utf8JsonParser parser; - private Bytes buffer; private RawComponent[] components; private int componentCount; + private RawRelation[] relations; + private int relationCount; internal ComponentReader(TypeStore typeStore) { - buffer = new Bytes(128); components = new RawComponent[1]; + relations = Array.Empty(); componentReader = new ObjectReader(typeStore) { ErrorHandler = ObjectReader.NoThrow }; mapperContextStore = new MapperContextEntityStore(); componentReader.SetMapperContext(mapperContextStore); @@ -61,6 +62,7 @@ internal string Read(DataEntity dataEntity, Entity entity, EntityStoreBase store { mapperContextStore.store = (EntityStore)store; componentCount = 0; + relationCount = 0; var error = ReadRaw(dataEntity, entity); if (error != null) { return error; @@ -82,9 +84,10 @@ private string ReadRaw (DataEntity dataEntity, Entity entity) break; case JsonEvent.ObjectStart: ev = ReadRawComponents(); - if (ev != JsonEvent.ObjectEnd) { - // could support also scalar types in future: string, number or boolean - return $"'components' element must be an object. was {ev}. id: {entity.Id}, component: '{parser.key}'"; + if (ev != JsonEvent.ObjectEnd && + ev != JsonEvent.ArrayEnd) { + // could support also scalar types in the future: string, number or boolean + return $"'components' member must be object or array. was {ev}. id: {entity.Id}, component: '{parser.key}'"; } break; default: @@ -103,30 +106,18 @@ private string ReadComponents(Entity entity) } for (int n = 0; n < componentCount; n++) { - var component = components[n]; - buffer.Clear(); - var json = new JsonValue(parser.GetInputBytes(component.start - 1, component.end)); - var schemaType = component.rawKey.schemaType; - if (schemaType == unresolvedType) { - unresolvedComponentList.Add(new UnresolvedComponent(component.rawKey.key, json)); - continue; - } - switch (schemaType.Kind) { - case SchemaTypeKind.Script: - // --- read script - var scriptType = (ScriptType)schemaType; - scriptTypes.Remove(scriptType); - scriptType.ReadScript(componentReader, json, entity); + ref var component = ref components[n]; + string error = null; + switch (component.type) { + case RawComponentType.Object: + error = ReadComponent(entity, component); break; - case SchemaTypeKind.Component: - var componentType = (ComponentType)schemaType; - var heap = entity.archetype.heapMap[componentType.StructIndex]; // no range or null check required - // --- read & change component - heap.Read(componentReader, entity.compIndex, json); + case RawComponentType.Array: + error = ReadRelations(entity, component); break; } - if (componentReader.Error.ErrSet) { - return $"'components[{component.rawKey.key}]' - {componentReader.Error.GetMessage()}"; + if (error != null) { + return error; } } // --- remove missing scripts from entity @@ -140,6 +131,45 @@ private string ReadComponents(Entity entity) return null; } + private string ReadComponent(Entity entity, in RawComponent component) + { + var json = new JsonValue(parser.GetInputBytes(component.start - 1, component.end)); + var schemaType = component.rawKey.schemaType; + if (schemaType == unresolvedType) { + unresolvedComponentList.Add(new UnresolvedComponent(component.rawKey.key, json)); + return null; + } + switch (schemaType.Kind) { + case SchemaTypeKind.Script: + // --- read script + var scriptType = (ScriptType)schemaType; + scriptTypes.Remove(scriptType); + scriptType.ReadScript(componentReader, json, entity); + break; + case SchemaTypeKind.Component: + var componentType = (ComponentType)schemaType; + var heap = entity.archetype.heapMap[componentType.StructIndex]; // no range or null check required + // --- read & change component + heap.Read(componentReader, entity.compIndex, json); + break; + } + if (componentReader.Error.ErrSet) { + return $"'components[{component.rawKey.key}]' - {componentReader.Error.GetMessage()}"; + } + return null; + } + + private string ReadRelations(Entity entity, in RawComponent component) + { + var relationType = (ComponentType)component.rawKey.schemaType; + for (int index = component.start; index < component.end; index++) { + var relation = relations[index]; + var json = new JsonValue(parser.GetInputBytes(relation.start - 1, relation.end)); + relationType.ReadRelation(this, entity, json); + } + return null; + } + private void AddUnresolvedComponents(Entity entity) { ref var unresolved = ref entity.GetComponent(); @@ -280,7 +310,7 @@ private JsonEvent ReadRawComponents() if (componentCount == components.Length) { ArrayUtils.Resize(ref components, 2 * componentCount); } - components[componentCount++] = new RawComponent(rawKey, start, parser.Position); + components[componentCount++] = new RawComponent(RawComponentType.Object, rawKey, start, parser.Position); ev = parser.NextEvent(); if (ev == JsonEvent.ObjectEnd) { return JsonEvent.ObjectEnd; @@ -288,6 +318,37 @@ private JsonEvent ReadRawComponents() break; case JsonEvent.ObjectEnd: return JsonEvent.ObjectEnd; + case JsonEvent.ArrayStart: + return ReadRawRelations(); + default: + return ev; + } + } + } + + private JsonEvent ReadRawRelations() + { + var rawKey = ToRawKey(parser.key); + var startRelation = relationCount; + var ev = parser.NextEvent(); + while (true) { + switch (ev) { + case JsonEvent.ObjectStart: + var start = parser.Position; + parser.SkipTree(); + if (relationCount == relations.Length) { + ArrayUtils.Resize(ref relations, Math.Max(4, 2 * relationCount)); + } + relations[relationCount++] = new RawRelation(start, parser.Position); + ev = parser.NextEvent(); + if (ev == JsonEvent.ArrayEnd) { + if (componentCount == components.Length) { + ArrayUtils.Resize(ref components, 2 * componentCount); + } + components[componentCount++] = new RawComponent(RawComponentType.Array, rawKey, startRelation, relationCount); + return JsonEvent.ArrayEnd; + } + break; default: return ev; } @@ -337,17 +398,36 @@ internal RawKey(string key, SchemaType schemaType) { } } +internal enum RawComponentType +{ + Object, + Array +} + internal readonly struct RawComponent { - internal readonly RawKey rawKey; - internal readonly int start; - internal readonly int end; + internal readonly RawComponentType type; + internal readonly RawKey rawKey; + internal readonly int start; + internal readonly int end; public override string ToString() => rawKey.ToString(); - internal RawComponent(in RawKey rawKey, int start, int end) { + internal RawComponent(RawComponentType type, in RawKey rawKey, int start, int end) { + this.type = type; this.rawKey = rawKey; this.start = start; this.end = end; } +} + +internal readonly struct RawRelation +{ + internal readonly int start; + internal readonly int end; + + internal RawRelation(int start, int end) { + this.start = start; + this.end = end; + } } \ No newline at end of file diff --git a/src/Tests-internal/ECS/Test_ComponentReader.cs b/src/Tests-internal/ECS/Test_ComponentReader.cs index e57286de3..38022c36d 100644 --- a/src/Tests-internal/ECS/Test_ComponentReader.cs +++ b/src/Tests-internal/ECS/Test_ComponentReader.cs @@ -12,7 +12,7 @@ public static class Test_ComponentReader public static void Test_ComponentReader_RawComponent() { var unresolved = EntityStore.GetEntitySchema().unresolvedType; var rawKey = new RawKey("test", unresolved); - var rawComponent = new RawComponent(rawKey, 0, 0); + var rawComponent = new RawComponent(RawComponentType.Object, rawKey, 0, 0); Assert.AreEqual("test - Unresolved", rawComponent.ToString()); } } diff --git a/src/Tests/ECS/Serialize/Test_ComponentReader.cs b/src/Tests/ECS/Serialize/Test_ComponentReader.cs index 71c61d083..2adb65112 100644 --- a/src/Tests/ECS/Serialize/Test_ComponentReader.cs +++ b/src/Tests/ECS/Serialize/Test_ComponentReader.cs @@ -228,11 +228,11 @@ public static void Test_ComponentReader_read_invalid_component() var store = new EntityStore(PidType.UsePidAsId); var converter = EntityConverter.Default; - var json = new JsonValue("{ \"pos\": [] }"); + var json = new JsonValue("{ \"pos\": 123 }"); var node = new DataEntity { pid = 10, components = json }; var entity = converter.DataEntityToEntity(node, store, out var error); NotNull(entity); - AreEqual("'components' element must be an object. was ArrayStart. id: 10, component: 'pos'", error); + AreEqual("'components' member must be object or array. was ValueNumber. id: 10, component: 'pos'", error); } [Test] diff --git a/src/Tests/ECS/Serialize/Test_SerializeRelations.cs b/src/Tests/ECS/Serialize/Test_SerializeRelations.cs index f08d7ece1..1f46acc3e 100644 --- a/src/Tests/ECS/Serialize/Test_SerializeRelations.cs +++ b/src/Tests/ECS/Serialize/Test_SerializeRelations.cs @@ -6,14 +6,11 @@ using static NUnit.Framework.Assert; -// ReSharper disable MethodHasAsyncOverload -// ReSharper disable HeuristicUnreachableCode // ReSharper disable InconsistentNaming namespace Tests.ECS.Serialize { public static class Test_SerializeRelations { - /// referenced entity is loaded before entity reference private const string Json = @"[{ ""id"": 1, @@ -22,9 +19,26 @@ public static class Test_SerializeRelations } }]"; + +#region read relations + [Test] + public static void Test_SerializeRelations_read() + { + var store = new EntityStore(); + var serializer = new EntitySerializer(); + var stream = Test_Serializer.StringAsStream(Json); + + var result = serializer.ReadIntoStore(store, stream); + IsNull(result.error); + + var entity1 = store.GetEntityById(1); + var relations = entity1.GetRelations(); + AreEqual(2, relations.Length); + } + #endregion -#region write Entity +#region write relations [Test] public static void Test_SerializeRelations_write() { @@ -40,7 +54,6 @@ public static void Test_SerializeRelations_write() var str = Test_Serializer.MemoryStreamAsString(writeStream); AreEqual(Json, str); } - #endregion }