Files
Nerfed/Nerfed.Runtime/Scene/SceneManager.cs
T
max fec2cd8d24 testing building some core systems
- serialization
- chunks
- parralelfor test
2026-04-24 19:21:03 +02:00

346 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Reflection;
using MoonTools.ECS;
using Nerfed.Runtime.Components;
namespace Nerfed.Runtime.Scene;
/// <summary>
/// Central hub for scene serialization and deserialization.
///
/// On first use the static constructor scans all loaded assemblies for:
/// • Structs annotated with <see cref="SceneComponentAttribute"/> — serialized as per-entity component data.
/// • Structs annotated with <see cref="SceneRelationAttribute"/> — serialized as cross-entity relation data.
///
/// The <see cref="Components.ChildParentRelation"/> hierarchy is handled separately via
/// <see cref="SceneEntityData.ParentId"/> and does NOT need a <see cref="SceneRelationAttribute"/>.
///
/// Usage:
/// <code>
/// var serializer = new JsonSceneSerializer();
/// SceneManager.Save(world, "Assets/level1.scene", serializer, "Level 1");
/// SceneManager.Load(world, "Assets/level1.scene", serializer);
/// </code>
/// </summary>
public static class SceneManager
{
// Full CLR type name → Type
private static readonly Dictionary<string, Type> ComponentRegistry = new();
private static readonly Dictionary<string, Type> RelationRegistry = new();
// Reflection cache so we only build the delegates once per type.
private static readonly Dictionary<Type, Func<World, Entity, bool>> HasComponentCache = new();
private static readonly Dictionary<Type, Func<World, Entity, ValueType>> GetComponentCache = new();
private static readonly Dictionary<Type, Action<World, Entity, ValueType>> SetComponentCache = new();
private static readonly Dictionary<Type, Func<World, Entity, bool>> HasOutRelationCache = new();
private static readonly Dictionary<Type, Func<World, Entity, Entity[]>> OutRelationsCache = new();
private static readonly Dictionary<Type, Func<World, Entity, Entity, ValueType>> GetRelationDataCache = new();
private static readonly Dictionary<Type, Action<World, Entity, Entity, ValueType>> 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<SceneComponentAttribute>() is not null)
ComponentRegistry[type.FullName] = type;
if(type.GetCustomAttribute<SceneRelationAttribute>() 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<string, Type> RegisteredComponentTypes => ComponentRegistry;
public static IReadOnlyDictionary<string, Type> 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<Guid, Entity> 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<uint, Guid> 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<ChildParentRelation>()) {
EnsureEntity(child);
EnsureEntity(parent);
}
// ── 2. Build entity records (parents must be known before children so
// we sort parents-before-children for readable output) ───────────
List<SceneEntityData> 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<Guid, Entity> Instantiate(World world, SceneData scene) {
Dictionary<Guid, Entity> 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<SceneEntityData> BuildSortedEntityList(
World world,
Dictionary<uint, Guid> entityToGuid) {
// Build per-entity data (unsorted first).
Dictionary<Guid, SceneEntityData> byGuid = new(entityToGuid.Count);
foreach((uint entityId, Guid guid) in entityToGuid) {
Entity entity = new(entityId);
Guid? parentId = null;
if(world.HasOutRelation<ChildParentRelation>(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<ChildParentRelation>(entity)) {
if(entityToGuid.TryGetValue(parent.ID, out Guid parentGuid)) {
parentId = parentGuid;
break;
}
}
}
List<SceneComponentData> 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<SceneEntityData> sorted = new(byGuid.Count);
HashSet<Guid> 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<World, Entity, bool>? 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<World, Entity, ValueType>? 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<World, Entity, ValueType>? 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<T>() 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<World, Entity, Entity, ValueType>? 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<World, Entity, Entity, ValueType>? 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)}.");
}
}