using System.Reflection; using MoonTools.ECS; using Nerfed.Runtime.Components; namespace Nerfed.Runtime.Scene; /// /// Central hub for scene serialization and deserialization. /// /// On first use the static constructor scans all loaded assemblies for: /// • Structs annotated with — serialized as per-entity component data. /// • Structs annotated with — serialized as cross-entity relation data. /// /// The hierarchy is handled separately via /// and does NOT need a . /// /// Usage: /// /// var serializer = new JsonSceneSerializer(); /// SceneManager.Save(world, "Assets/level1.scene", serializer, "Level 1"); /// SceneManager.Load(world, "Assets/level1.scene", serializer); /// /// public static class SceneManager { // Full CLR type name → Type private static readonly Dictionary ComponentRegistry = new(); private static readonly Dictionary RelationRegistry = new(); // Reflection cache so we only build the delegates once per type. private static readonly Dictionary> HasComponentCache = new(); private static readonly Dictionary> GetComponentCache = new(); private static readonly Dictionary> SetComponentCache = new(); private static readonly Dictionary> HasOutRelationCache = new(); private static readonly Dictionary> OutRelationsCache = new(); private static readonly Dictionary> GetRelationDataCache = new(); private static readonly Dictionary> RelateCache = new(); static SceneManager() { foreach(Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { Type[] types; try { types = assembly.GetTypes(); } catch(ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t is not null).ToArray()!; } foreach(Type type in types) { if(type.FullName is null) continue; if(type.GetCustomAttribute() is not null) ComponentRegistry[type.FullName] = type; if(type.GetCustomAttribute() is not null) RelationRegistry[type.FullName] = type; } } } // ------------------------------------------------------------------------- // Public registry accessors // ------------------------------------------------------------------------- public static Type? GetComponentType(string fullName) { ComponentRegistry.TryGetValue(fullName, out Type? type); return type; } public static Type? GetRelationType(string fullName) { RelationRegistry.TryGetValue(fullName, out Type? type); return type; } public static IReadOnlyDictionary RegisteredComponentTypes => ComponentRegistry; public static IReadOnlyDictionary RegisteredRelationTypes => RelationRegistry; // ------------------------------------------------------------------------- // High-level Save / Load // ------------------------------------------------------------------------- public static void Save(World world, string path, ISceneSerializer serializer, string sceneName = "") { SceneData scene = Extract(world, sceneName); string? directory = Path.GetDirectoryName(path); if(!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); using FileStream stream = File.Open(path, FileMode.Create, FileAccess.Write); serializer.Serialize(scene, stream); } public static Dictionary Load(World world, string path, ISceneSerializer serializer) { using FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read); SceneData scene = serializer.Deserialize(stream); return Instantiate(world, scene); } // ------------------------------------------------------------------------- // Extract (world → SceneData) // ------------------------------------------------------------------------- public static SceneData Extract(World world, string name = "") { SceneData scene = new() { Name = name }; // ── 1. Collect entities ────────────────────────────────────────────── // Include an entity if it has at least one scene component OR if it // appears as an endpoint of at least one scene relation. This ensures // pure grouping nodes and relation-only entities are not dropped. Dictionary entityToGuid = new(); void EnsureEntity(Entity e) { if(!entityToGuid.ContainsKey(e.ID)) entityToGuid[e.ID] = Guid.NewGuid(); } foreach(Entity entity in world.GetAllEntities()) { if(HasAnySceneComponent(world, entity)) EnsureEntity(entity); } // Walk all registered relation types and pull in both endpoints. foreach(Type relationType in RelationRegistry.Values) { foreach((Entity a, Entity b) in WorldAllRelations(world, relationType)) { EnsureEntity(a); EnsureEntity(b); } } // Also include entities that are part of the ChildParentRelation hierarchy // even if they carry no scene components and no user-defined relations. foreach((Entity child, Entity parent) in world.Relations()) { EnsureEntity(child); EnsureEntity(parent); } // ── 2. Build entity records (parents must be known before children so // we sort parents-before-children for readable output) ─────────── List ordered = BuildSortedEntityList(world, entityToGuid); scene.Entities.AddRange(ordered); // ── 3. Build relation records ──────────────────────────────────────── foreach((string typeName, Type relationType) in RelationRegistry) { foreach((Entity a, Entity b) in WorldAllRelations(world, relationType)) { if(!entityToGuid.TryGetValue(a.ID, out Guid guidA) || !entityToGuid.TryGetValue(b.ID, out Guid guidB)) continue; ValueType payload = WorldGetRelationData(world, a, b, relationType); scene.Relations.Add(new SceneRelationData { Type = typeName, EntityA = guidA, EntityB = guidB, Value = payload, }); } } return scene; } // ------------------------------------------------------------------------- // Instantiate (SceneData → world) // ------------------------------------------------------------------------- public static Dictionary Instantiate(World world, SceneData scene) { Dictionary guidToEntity = new(scene.Entities.Count); // Pass 1 – create all entities. foreach(SceneEntityData entityData in scene.Entities) { Entity entity = world.CreateEntity(entityData.Tag); guidToEntity[entityData.Id] = entity; } // Pass 2 – set components and wire up the ChildParentRelation hierarchy. foreach(SceneEntityData entityData in scene.Entities) { Entity entity = guidToEntity[entityData.Id]; foreach(SceneComponentData componentData in entityData.Components) WorldSetComponent(world, entity, componentData.Type, componentData.Value); if(entityData.ParentId is Guid parentGuid && guidToEntity.TryGetValue(parentGuid, out Entity parent)) { world.Set(entity, new Child()); world.Relate(entity, parent, new ChildParentRelation()); } else { world.Set(entity, new Root()); } } // Pass 3 – restore all user-defined relations. foreach(SceneRelationData relationData in scene.Relations) { if(!guidToEntity.TryGetValue(relationData.EntityA, out Entity entityA) || !guidToEntity.TryGetValue(relationData.EntityB, out Entity entityB)) continue; WorldRelate(world, entityA, entityB, relationData.Type, relationData.Value); } return guidToEntity; } // ------------------------------------------------------------------------- // Helpers – entity ordering // ------------------------------------------------------------------------- // Returns entities sorted so that a parent always appears before its children, // making the JSON file human-readable and easier to diff. private static List BuildSortedEntityList( World world, Dictionary entityToGuid) { // Build per-entity data (unsorted first). Dictionary byGuid = new(entityToGuid.Count); foreach((uint entityId, Guid guid) in entityToGuid) { Entity entity = new(entityId); Guid? parentId = null; if(world.HasOutRelation(entity)) { // Iterate all out-relations — an entity may have multiple parents // in theory, but ChildParentRelation is designed as singleton. // We capture the first valid one here. foreach(Entity parent in world.OutRelations(entity)) { if(entityToGuid.TryGetValue(parent.ID, out Guid parentGuid)) { parentId = parentGuid; break; } } } List components = new(); foreach((string typeName, Type componentType) in ComponentRegistry) { if(!WorldHasComponent(world, entity, componentType)) continue; ValueType value = WorldGetComponent(world, entity, componentType); components.Add(new SceneComponentData { Type = typeName, Value = value }); } byGuid[guid] = new SceneEntityData { Id = guid, Tag = world.GetTag(entity), ParentId = parentId, Components = components, }; } // Topological sort: parents before children. List sorted = new(byGuid.Count); HashSet visited = new(byGuid.Count); void Visit(Guid id) { if(!visited.Add(id)) return; SceneEntityData data = byGuid[id]; if(data.ParentId is Guid pid && byGuid.ContainsKey(pid)) Visit(pid); sorted.Add(data); } foreach(Guid id in byGuid.Keys) Visit(id); return sorted; } // ------------------------------------------------------------------------- // Reflection helpers – components // ------------------------------------------------------------------------- private static bool HasAnySceneComponent(World world, Entity entity) { foreach(Type componentType in ComponentRegistry.Values) { if(WorldHasComponent(world, entity, componentType)) return true; } return false; } private static bool WorldHasComponent(World world, Entity entity, Type componentType) { if(!HasComponentCache.TryGetValue(componentType, out Func? fn)) { MethodInfo method = FindGenericMethod(nameof(World.Has)).MakeGenericMethod(componentType); fn = (w, e) => (bool)method.Invoke(w, new object[] { e })!; HasComponentCache[componentType] = fn; } return fn(world, entity); } private static ValueType WorldGetComponent(World world, Entity entity, Type componentType) { if(!GetComponentCache.TryGetValue(componentType, out Func? fn)) { MethodInfo method = FindGenericMethod(nameof(World.Get)).MakeGenericMethod(componentType); fn = (w, e) => (ValueType)method.Invoke(w, new object[] { e })!; GetComponentCache[componentType] = fn; } return fn(world, entity); } private static void WorldSetComponent(World world, Entity entity, string typeName, ValueType value) { if(!ComponentRegistry.TryGetValue(typeName, out Type? componentType)) return; if(!SetComponentCache.TryGetValue(componentType, out Action? fn)) { MethodInfo method = FindGenericMethod(nameof(World.Set)).MakeGenericMethod(componentType); fn = (w, e, v) => method.Invoke(w, new object[] { e, v }); SetComponentCache[componentType] = fn; } fn(world, entity, value); } // ------------------------------------------------------------------------- // Reflection helpers – relations // ------------------------------------------------------------------------- private static IEnumerable<(Entity, Entity)> WorldAllRelations(World world, Type relationType) { // World.Relations() returns ReverseSpanEnumerator<(Entity,Entity)>. // We materialise it into a list so the caller can iterate freely. MethodInfo method = FindGenericMethod(nameof(World.Relations)).MakeGenericMethod(relationType); // Returns a boxed ReverseSpanEnumerator; invoke MoveNext/Current via dynamic. // Easiest: call via dynamic to avoid unsafe span-from-box issues. dynamic enumerator = method.Invoke(world, null)!; List<(Entity, Entity)> results = new(); while(enumerator.MoveNext()) results.Add(enumerator.Current); return results; } private static ValueType WorldGetRelationData(World world, Entity a, Entity b, Type relationType) { if(!GetRelationDataCache.TryGetValue(relationType, out Func? fn)) { MethodInfo method = FindGenericMethod(nameof(World.GetRelationData)).MakeGenericMethod(relationType); fn = (w, ea, eb) => (ValueType)method.Invoke(w, new object[] { ea, eb })!; GetRelationDataCache[relationType] = fn; } return fn(world, a, b); } private static void WorldRelate(World world, Entity a, Entity b, string typeName, ValueType value) { if(!RelationRegistry.TryGetValue(typeName, out Type? relationType)) return; if(!RelateCache.TryGetValue(relationType, out Action? fn)) { MethodInfo method = FindGenericMethod(nameof(World.Relate)).MakeGenericMethod(relationType); fn = (w, ea, eb, v) => method.Invoke(w, new object[] { ea, eb, v }); RelateCache[relationType] = fn; } fn(world, a, b, value); } // ------------------------------------------------------------------------- // Utility // ------------------------------------------------------------------------- private static MethodInfo FindGenericMethod(string name) { foreach(MethodInfo m in typeof(World).GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if(m.Name == name && m.IsGenericMethodDefinition) return m; } throw new InvalidOperationException($"Could not find generic method '{name}' on {nameof(World)}."); } }