oecs-sharp/OECS/WorldSerializer.cs

137 lines
4.9 KiB
C#
Raw Normal View History

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>>();
// Collect relationship target entities that may have no components.
// They must be serialized so that Relationship.Target references remain
// valid after deserialization (bare entity handles round-trip correctly
// thanks to the EntityFormatter which reads/writes the raw uint).
var relationshipTargets = new HashSet<Entity>();
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)
});
// Track relationship targets.
if (component is IRelationship rel && rel.Target != Entity.Null)
relationshipTargets.Add(rel.Target);
}
}
// Ensure bare relationship target entities appear in the snapshot.
foreach (var target in relationshipTargets)
{
if (!entityComponents.ContainsKey(target))
entityComponents[target] = new List<ComponentEntry>();
}
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;
// First pass: identify which entities are singleton backings.
// Singletons are registered in ComponentRegistry with a known marker.
var singletonTypes = new HashSet<Type>();
foreach (var desc in ComponentRegistry.Descriptors)
{
if (desc.IsSingleton)
singletonTypes.Add(desc.Type);
}
foreach (var es in snapshot.Entities)
{
var entity = world.CreateEntity(es.Entity);
bool isSingleton = false;
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.");
if (desc.IsSingleton)
isSingleton = true;
// Deserialize and add via the typed internal method.
var component = desc.Deserialize(ce.Data);
world.AddComponentBoxed(entity, component, desc.Type);
}
// If the entity has a singleton component, register it.
if (isSingleton)
{
foreach (var ce in es.Components)
{
if (lookup.TryGetValue(ce.TypeName, out var desc) && desc.IsSingleton)
{
world.RegisterSingletonEntity(desc.Type, entity);
}
}
}
}
}
}