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)}.");
}
}