using System.Reflection; using MessagePack; namespace OECS; /// /// Saves and loads a to/from a MessagePack stream. /// /// Uses the compile-time generated to /// serialize and deserialize components without reflection. All component /// types used with the World API are discovered by the OECS.SourceGen /// incremental generator at build time. /// public static class WorldSerializer { /// /// Saves the world state to a stream. /// public static void Save(World world, Stream stream) { var entityComponents = new Dictionary>(); foreach (var desc in ComponentRegistry.Descriptors) { var set = world.Components.GetSet(desc.Type); if (set == null || set.Count == 0) continue; var entities = set.GetDenseEntities(); for (int i = 0; i < set.Count; i++) { var entity = entities[i]; var component = set.GetComponentAt(i); if (!entityComponents.TryGetValue(entity, out var list)) { list = new List(); entityComponents[entity] = list; } list.Add(new ComponentEntry { TypeName = desc.TypeName, Data = desc.Serialize(component) }); } } var snapshot = new WorldSnapshot { Entities = entityComponents .Select(kv => new EntitySnapshot { Id = kv.Key.Id, Version = kv.Key.Version, Components = kv.Value.ToArray() }) .ToArray() }; MessagePackSerializer.Serialize(stream, snapshot); } /// /// Loads the world state from a stream, adding entities and components /// to the given world. The world should be empty or the caller is /// responsible for managing duplicate entities. /// public static void Load(World world, Stream stream) { var snapshot = MessagePackSerializer.Deserialize(stream); // Build a lookup by type name for O(1) descriptor resolution. var lookup = new Dictionary(); foreach (var desc in ComponentRegistry.Descriptors) lookup[desc.TypeName] = desc; foreach (var es in snapshot.Entities) { var entity = world.CreateEntity(es.Entity); // If the loaded entity is the singleton entity, ensure the // singleton infrastructure is initialized so that subsequent // SetSingleton/GetSingleton calls work correctly. if (entity.Id == World.SingletonEntity.Id) { world.EnsureSingleton(); } foreach (var ce in es.Components) { if (!lookup.TryGetValue(ce.TypeName, out var desc)) throw new InvalidOperationException( $"Unknown component type '{ce.TypeName}'. " + $"Ensure the component type is used with the World " + $"API so the source generator can discover it."); // Deserialize and fix up IRelationship.Source before adding. var component = desc.Deserialize(ce.Data); if (component is IRelationship rel) FixupRelationshipSource(rel, entity); // Add via the typed internal method — no reflection for the add itself. world.AddComponentBoxed(entity, component, desc.Type); } } } /// /// Sets the Source field on a deserialized IRelationship to match /// the entity it's being restored to. Uses a small cache of PropertyInfo /// to avoid repeated reflection lookups. /// private static void FixupRelationshipSource(IRelationship rel, Entity entity) { var type = rel.GetType(); if (!_sourcePropCache.TryGetValue(type, out var prop)) { prop = type.GetProperty("Source"); _sourcePropCache[type] = prop; } prop?.SetValue(rel, entity); } private static readonly Dictionary _sourcePropCache = new(); }