diff --git a/examples/TicTacToe/test_load.txt b/examples/TicTacToe/test_load.txt new file mode 100644 index 0000000..1f5a4b4 --- /dev/null +++ b/examples/TicTacToe/test_load.txt @@ -0,0 +1,3 @@ +y +1 0 +0 2 \ No newline at end of file diff --git a/examples/TicTacToe/test_save.txt b/examples/TicTacToe/test_save.txt new file mode 100644 index 0000000..498f27a --- /dev/null +++ b/examples/TicTacToe/test_save.txt @@ -0,0 +1,3 @@ +0 0 +1 1 +0 1 \ No newline at end of file diff --git a/src/OECS/WorldSerializer.cs b/src/OECS/WorldSerializer.cs new file mode 100644 index 0000000..0a8ecec --- /dev/null +++ b/src/OECS/WorldSerializer.cs @@ -0,0 +1,91 @@ +using MessagePack; + +namespace OECS; + +/// +/// Saves and loads a to/from a MessagePack stream. +/// +/// Components are serialized by their runtime type. The caller must +/// ensure all component types used in the world are resolvable via +/// at load 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 ct in world.Components.ComponentTypes) + { + var set = world.Components.GetSet(ct); + 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 = ct.AssemblyQualifiedName!, + Data = MessagePackSerializer.Serialize(ct, 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); + + foreach (var es in snapshot.Entities) + { + var entity = world.CreateEntity(es.Entity); + + foreach (var ce in es.Components) + { + var type = Type.GetType(ce.TypeName) + ?? throw new InvalidOperationException( + $"Unknown component type '{ce.TypeName}'. " + + $"Ensure the assembly is loaded."); + + var component = MessagePackSerializer.Deserialize(type, ce.Data) + ?? throw new InvalidOperationException( + $"Failed to deserialize component of type '{ce.TypeName}'."); + + var method = typeof(World).GetMethod(nameof(World.AddComponent))! + .MakeGenericMethod(type); + method.Invoke(world, [entity, component]); + } + } + } +} \ No newline at end of file diff --git a/src/OECS/WorldSnapshot.cs b/src/OECS/WorldSnapshot.cs new file mode 100644 index 0000000..fd80802 --- /dev/null +++ b/src/OECS/WorldSnapshot.cs @@ -0,0 +1,39 @@ +using MessagePack; + +namespace OECS; + +/// +/// Serializable snapshot of the entire world state. +/// +[MessagePackObject] +public class WorldSnapshot +{ + [Key(0)] public EntitySnapshot[] Entities { get; set; } = []; +} + +/// +/// A single entity and all its components, serialized as name/data pairs. +/// Entity ID and Version are stored separately to avoid issues with +/// MessagePack deserialization of the opaque Entity struct. +/// +[MessagePackObject] +public class EntitySnapshot +{ + [Key(0)] public uint Id { get; set; } + [Key(1)] public uint Version { get; set; } + [Key(2)] public ComponentEntry[] Components { get; set; } = []; + + [IgnoreMember] + public Entity Entity => new(Id, Version); +} + +/// +/// A single component, identified by its fully-qualified type name and +/// serialized as a MessagePack byte blob. +/// +[MessagePackObject] +public class ComponentEntry +{ + [Key(0)] public string TypeName { get; set; } = ""; + [Key(1)] public byte[] Data { get; set; } = []; +} \ No newline at end of file