238 lines
7.4 KiB
C#
238 lines
7.4 KiB
C#
|
|
using FluentAssertions;
|
||
|
|
using MessagePack;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace OECS.Tests;
|
||
|
|
|
||
|
|
// ── Test components for serialization ─────────────────────────────────
|
||
|
|
|
||
|
|
[MessagePackObject]
|
||
|
|
public struct SerialPos : IMessagePackSerializationCallbackReceiver
|
||
|
|
{
|
||
|
|
[Key(0)] public float X { get; set; }
|
||
|
|
[Key(1)] public float Y { get; set; }
|
||
|
|
|
||
|
|
public void OnBeforeSerialize() { }
|
||
|
|
public void OnAfterDeserialize() { }
|
||
|
|
}
|
||
|
|
|
||
|
|
[MessagePackObject]
|
||
|
|
public struct SerialHealth : IMessagePackSerializationCallbackReceiver
|
||
|
|
{
|
||
|
|
[Key(0)] public int Value { get; set; }
|
||
|
|
|
||
|
|
public void OnBeforeSerialize() { }
|
||
|
|
public void OnAfterDeserialize() { }
|
||
|
|
}
|
||
|
|
|
||
|
|
[MessagePackObject]
|
||
|
|
public struct SerialConfig : IMessagePackSerializationCallbackReceiver
|
||
|
|
{
|
||
|
|
[Key(0)] public float Gravity { get; set; }
|
||
|
|
[Key(1)] public int MaxPlayers { get; set; }
|
||
|
|
|
||
|
|
public void OnBeforeSerialize() { }
|
||
|
|
public void OnAfterDeserialize() { }
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Phantom types for relationship serialization ──────────────────────
|
||
|
|
|
||
|
|
public struct SerialChildOf { }
|
||
|
|
public struct SerialParentOf { }
|
||
|
|
|
||
|
|
// ── Tests ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public class SerializationTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_RoundTrip_PreservesComponents()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
var entity = world.CreateEntity();
|
||
|
|
world.AddComponent(entity, new SerialPos { X = 1.5f, Y = 2.5f });
|
||
|
|
world.AddComponent(entity, new SerialHealth { Value = 100 });
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
// Load into a fresh world.
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
// Find the entity in the loaded world.
|
||
|
|
var found = world2.FindEntity<SerialPos, SerialHealth>();
|
||
|
|
found.Should().NotBe(Entity.Null);
|
||
|
|
|
||
|
|
ref var pos = ref world2.GetComponent<SerialPos>(found);
|
||
|
|
pos.X.Should().Be(1.5f);
|
||
|
|
pos.Y.Should().Be(2.5f);
|
||
|
|
|
||
|
|
ref var hp = ref world2.GetComponent<SerialHealth>(found);
|
||
|
|
hp.Value.Should().Be(100);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_RoundTrip_PreservesMultipleEntities()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
var a = world.CreateEntity();
|
||
|
|
var b = world.CreateEntity();
|
||
|
|
world.AddComponent(a, new SerialPos { X = 1, Y = 2 });
|
||
|
|
world.AddComponent(a, new SerialHealth { Value = 10 });
|
||
|
|
world.AddComponent(b, new SerialPos { X = 3, Y = 4 });
|
||
|
|
world.AddComponent(b, new SerialHealth { Value = 20 });
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
var positions = new List<(float X, float Y, int Health)>();
|
||
|
|
foreach (var it in world2.Select<SerialPos, SerialHealth>())
|
||
|
|
{
|
||
|
|
positions.Add((it.Val1.X, it.Val1.Y, it.Val2.Value));
|
||
|
|
}
|
||
|
|
|
||
|
|
positions.Should().BeEquivalentTo([(1, 2, 10), (3, 4, 20)]);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_RoundTrip_PreservesEntityIds()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
var entity = world.CreateEntity();
|
||
|
|
world.AddComponent(entity, new SerialPos { X = 1, Y = 2 });
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
var found = world2.FindEntity<SerialPos>();
|
||
|
|
found.Id.Should().Be(entity.Id);
|
||
|
|
found.Version.Should().Be(entity.Version);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_RoundTrip_PreservesSingletons()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
world.SetSingleton(new SerialConfig { Gravity = 9.8f, MaxPlayers = 4 });
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
world2.HasSingleton<SerialConfig>().Should().BeTrue();
|
||
|
|
ref var config = ref world2.GetSingleton<SerialConfig>();
|
||
|
|
config.Gravity.Should().Be(9.8f);
|
||
|
|
config.MaxPlayers.Should().Be(4);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_RoundTrip_PreservesRelationships()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
var child = world.CreateEntity();
|
||
|
|
var parent = world.CreateEntity();
|
||
|
|
world.AddComponent(child, new Relationship<SerialChildOf, SerialParentOf>
|
||
|
|
{
|
||
|
|
Target = parent
|
||
|
|
});
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
// Find the relationship.
|
||
|
|
var found = world2.FindEntity<Relationship<SerialChildOf, SerialParentOf>>();
|
||
|
|
found.Should().NotBe(Entity.Null);
|
||
|
|
|
||
|
|
ref var rel = ref world2.GetComponent<Relationship<SerialChildOf, SerialParentOf>>(found);
|
||
|
|
rel.Target.Should().NotBe(Entity.Null);
|
||
|
|
|
||
|
|
// The target should be alive and have the same ID as the original parent.
|
||
|
|
world2.IsAlive(rel.Target).Should().BeTrue();
|
||
|
|
rel.Target.Id.Should().Be(parent.Id);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_RoundTrip_PreservesRelationshipTargetIntegrity()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
var child = world.CreateEntity();
|
||
|
|
var parent = world.CreateEntity();
|
||
|
|
world.AddComponent(parent, new SerialPos { X = 10, Y = 20 });
|
||
|
|
world.AddComponent(child, new Relationship<SerialChildOf, SerialParentOf>
|
||
|
|
{
|
||
|
|
Target = parent
|
||
|
|
});
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
var foundChild = world2.FindEntity<Relationship<SerialChildOf, SerialParentOf>>();
|
||
|
|
ref var rel = ref world2.GetComponent<Relationship<SerialChildOf, SerialParentOf>>(foundChild);
|
||
|
|
|
||
|
|
// The target entity should still have its component.
|
||
|
|
world2.HasComponent<SerialPos>(rel.Target).Should().BeTrue();
|
||
|
|
ref var pos = ref world2.GetComponent<SerialPos>(rel.Target);
|
||
|
|
pos.X.Should().Be(10);
|
||
|
|
pos.Y.Should().Be(20);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_EmptyWorld_Works()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
// Loading an empty world should not throw.
|
||
|
|
var count = 0;
|
||
|
|
foreach (var it in world2.Select<SerialPos>())
|
||
|
|
count++;
|
||
|
|
count.Should().Be(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SaveLoad_EntityWithNoComponents_IsNotSerialized()
|
||
|
|
{
|
||
|
|
var world = new World();
|
||
|
|
world.CreateEntity(); // Entity with no components.
|
||
|
|
|
||
|
|
var stream = new MemoryStream();
|
||
|
|
WorldSerializer.Save(world, stream);
|
||
|
|
|
||
|
|
stream.Position = 0;
|
||
|
|
var world2 = new World();
|
||
|
|
WorldSerializer.Load(world2, stream);
|
||
|
|
|
||
|
|
// No entities should be loaded since the empty entity wasn't serialized.
|
||
|
|
var count = 0;
|
||
|
|
foreach (var it in world2.Select<SerialPos>())
|
||
|
|
count++;
|
||
|
|
count.Should().Be(0);
|
||
|
|
}
|
||
|
|
}
|