fec2cd8d24
- serialization - chunks - parralelfor test
346 lines
16 KiB
C#
346 lines
16 KiB
C#
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)}.");
|
||
}
|
||
}
|