270 lines
7.2 KiB
C#
270 lines
7.2 KiB
C#
using FluentAssertions;
|
|
using MessagePack;
|
|
using Xunit;
|
|
|
|
namespace OECS.Tests;
|
|
|
|
// ── Test components ──────────────────────────────────────────────────
|
|
|
|
[MessagePackObject]
|
|
public struct TestPosition : IMessagePackSerializationCallbackReceiver
|
|
{
|
|
[Key(0)] public float X { get; set; }
|
|
[Key(1)] public float Y { get; set; }
|
|
|
|
public void OnBeforeSerialize() { }
|
|
public void OnAfterDeserialize() { }
|
|
}
|
|
|
|
[MessagePackObject]
|
|
public struct TestHealth : IMessagePackSerializationCallbackReceiver
|
|
{
|
|
[Key(0)] public int Value { get; set; }
|
|
|
|
public void OnBeforeSerialize() { }
|
|
public void OnAfterDeserialize() { }
|
|
}
|
|
|
|
// ── Test commands ────────────────────────────────────────────────────
|
|
|
|
[MessagePackObject]
|
|
public struct SpawnCommand : ICommand
|
|
{
|
|
[Key(0)] public float X;
|
|
[Key(1)] public float Y;
|
|
[Key(2)] public int Health;
|
|
|
|
public void Execute(World world)
|
|
{
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new TestPosition { X = X, Y = Y });
|
|
world.AddComponent(entity, new TestHealth { Value = Health });
|
|
}
|
|
}
|
|
|
|
[MessagePackObject]
|
|
public struct MoveCommand : ICommand
|
|
{
|
|
[Key(0)] public Entity Entity;
|
|
[Key(1)] public float Dx;
|
|
[Key(2)] public float Dy;
|
|
|
|
public void Execute(World world)
|
|
{
|
|
ref var pos = ref world.GetComponent<TestPosition>(Entity);
|
|
pos.X += Dx;
|
|
pos.Y += Dy;
|
|
}
|
|
}
|
|
|
|
[MessagePackObject]
|
|
public struct DestroyCommand : ICommand
|
|
{
|
|
[Key(0)] public Entity Entity;
|
|
|
|
public void Execute(World world)
|
|
{
|
|
world.DestroyEntity(Entity);
|
|
}
|
|
}
|
|
|
|
[MessagePackObject]
|
|
public struct FailingCommand : ICommand
|
|
{
|
|
[Key(0)] public string Message;
|
|
|
|
public void Execute(World world)
|
|
{
|
|
throw new InvalidOperationException(Message);
|
|
}
|
|
}
|
|
|
|
[MessagePackObject]
|
|
public struct ChainedCommand : ICommand
|
|
{
|
|
[Key(0)] public float X;
|
|
[Key(1)] public float Y;
|
|
|
|
public void Execute(World world)
|
|
{
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new TestPosition { X = X, Y = Y });
|
|
|
|
// Enqueue a follow-up command during execution.
|
|
world.Commands.Enqueue(new MoveCommand
|
|
{
|
|
Entity = entity,
|
|
Dx = 10,
|
|
Dy = 20
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Test class ───────────────────────────────────────────────────────
|
|
|
|
public class CommandTests
|
|
{
|
|
[Fact]
|
|
public void Commands_ExecuteInFifoOrder()
|
|
{
|
|
var world = new World();
|
|
|
|
world.Commands.Enqueue(new SpawnCommand { X = 1, Y = 2, Health = 100 });
|
|
world.Commands.Enqueue(new SpawnCommand { X = 3, Y = 4, Health = 200 });
|
|
|
|
world.ExecuteCommands();
|
|
|
|
var positions = new List<(float X, float Y, int Health)>();
|
|
foreach (var it in world.Select<TestPosition, TestHealth>())
|
|
{
|
|
positions.Add((it.Item1.X, it.Item1.Y, it.Item2.Value));
|
|
}
|
|
|
|
positions.Should().BeEquivalentTo([
|
|
(1, 2, 100),
|
|
(3, 4, 200)
|
|
]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Command_CanReadWriteEcsState()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new TestPosition { X = 0, Y = 0 });
|
|
|
|
world.Commands.Enqueue(new MoveCommand { Entity = entity, Dx = 5, Dy = 10 });
|
|
world.ExecuteCommands();
|
|
|
|
ref var pos = ref world.GetComponent<TestPosition>(entity);
|
|
pos.X.Should().Be(5);
|
|
pos.Y.Should().Be(10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Command_EnqueuedDuringExecuteAll_RunsInSameDrainCycle()
|
|
{
|
|
var world = new World();
|
|
|
|
world.Commands.Enqueue(new ChainedCommand { X = 0, Y = 0 });
|
|
world.ExecuteCommands();
|
|
|
|
var positions = new List<(float X, float Y)>();
|
|
foreach (var it in world.Select<TestPosition>())
|
|
{
|
|
positions.Add((it.Item1.X, it.Item1.Y));
|
|
}
|
|
|
|
positions.Should().BeEquivalentTo([(10, 20)]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Exceptions_AreCollected_NotLost()
|
|
{
|
|
var world = new World();
|
|
|
|
world.Commands.Enqueue(new SpawnCommand { X = 1, Y = 2, Health = 100 });
|
|
world.Commands.Enqueue(new FailingCommand { Message = "boom" });
|
|
world.Commands.Enqueue(new SpawnCommand { X = 3, Y = 4, Health = 200 });
|
|
|
|
world.ExecuteCommands();
|
|
|
|
var count = 0;
|
|
foreach (var it in world.Select<TestPosition>())
|
|
count++;
|
|
count.Should().Be(2);
|
|
|
|
world.Commands.Errors.Should().HaveCount(1);
|
|
world.Commands.Errors[0].Should().BeOfType<InvalidOperationException>()
|
|
.Which.Message.Should().Be("boom");
|
|
}
|
|
|
|
[Fact]
|
|
public void Serialization_RoundTrip_PreservesCommandData()
|
|
{
|
|
var original = new SpawnCommand { X = 42, Y = 99, Health = 500 };
|
|
|
|
var bytes = MessagePackSerializer.Serialize(original);
|
|
var deserialized = MessagePackSerializer.Deserialize<SpawnCommand>(bytes);
|
|
|
|
deserialized.X.Should().Be(42);
|
|
deserialized.Y.Should().Be(99);
|
|
deserialized.Health.Should().Be(500);
|
|
}
|
|
|
|
[Fact]
|
|
public void Command_CanDestroyEntity()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new TestPosition { X = 1, Y = 2 });
|
|
|
|
world.Commands.Enqueue(new DestroyCommand { Entity = entity });
|
|
world.ExecuteCommands();
|
|
|
|
world.IsAlive(entity).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Clear_DiscardsPendingCommands()
|
|
{
|
|
var world = new World();
|
|
|
|
world.Commands.Enqueue(new SpawnCommand { X = 1, Y = 2, Health = 100 });
|
|
world.Commands.Count.Should().Be(1);
|
|
|
|
world.Commands.Clear();
|
|
world.Commands.Count.Should().Be(0);
|
|
|
|
world.ExecuteCommands();
|
|
|
|
var count = 0;
|
|
foreach (var it in world.Select<TestPosition>())
|
|
count++;
|
|
count.Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Commands_AreDrainedAfterEachSystem()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
group.Add(new CommandEnqueueSystem(world, new SpawnCommand { X = 1, Y = 2, Health = 100 }));
|
|
|
|
var observer = new EntityCountObserver();
|
|
group.Add(observer);
|
|
|
|
group.RunLogical();
|
|
|
|
observer.SeenCount.Should().Be(1);
|
|
}
|
|
|
|
private class CommandEnqueueSystem : ISystem
|
|
{
|
|
private readonly World _world;
|
|
private readonly SpawnCommand _command;
|
|
|
|
public CommandEnqueueSystem(World world, SpawnCommand command)
|
|
{
|
|
_world = world;
|
|
_command = command;
|
|
}
|
|
|
|
public void Run(World world)
|
|
{
|
|
_world.Commands.Enqueue(_command);
|
|
}
|
|
}
|
|
|
|
private class EntityCountObserver : ISystem
|
|
{
|
|
public int SeenCount { get; private set; }
|
|
|
|
public void Run(World world)
|
|
{
|
|
foreach (var it in world.Select<TestPosition>())
|
|
SeenCount++;
|
|
}
|
|
}
|
|
} |