oecs-sharp/OECS/WorldSerializer.cs

125 lines
4.4 KiB
C#

using System.Reflection;
using MessagePack;
namespace OECS;
/// <summary>
/// Saves and loads a <see cref="World"/> to/from a MessagePack stream.
///
/// Uses the compile-time generated <see cref="ComponentRegistry"/> 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.
/// </summary>
public static class WorldSerializer
{
/// <summary>
/// Saves the world state to a stream.
/// </summary>
public static void Save(World world, Stream stream)
{
var entityComponents = new Dictionary<Entity, List<ComponentEntry>>();
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<ComponentEntry>();
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);
}
/// <summary>
/// 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.
/// </summary>
public static void Load(World world, Stream stream)
{
var snapshot = MessagePackSerializer.Deserialize<WorldSnapshot>(stream);
// Build a lookup by type name for O(1) descriptor resolution.
var lookup = new Dictionary<string, ComponentDescriptor>();
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
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<Type, PropertyInfo?> _sourcePropCache = new();
}