211 lines
9.0 KiB
C#
211 lines
9.0 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Server;
|
|
using ProjectM.Simulation;
|
|
using Unity.Core;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// Plain-Entities EditMode tests for the ready-check spine: <see cref="ReadyToggleSystem"/> (RPC → PlayerReady,
|
|
/// Staging/Launching-only) ordered before <see cref="RunDirectorSystem"/> (the all-ready rising-edge launch, the
|
|
/// un-ready countdown abort, the F2 outcome guard, the sub-slot teleport out/home, and the Returning-edge
|
|
/// ready-flag clear). Ticks are driven manually through NetworkTime, so the countdown/dwell paths that Play-mode
|
|
/// polling races past are pinned deterministically here.
|
|
/// </summary>
|
|
public class ReadyCheckSystemTests
|
|
{
|
|
const uint T0 = 1000;
|
|
|
|
static (World world, SimulationSystemGroup group, Entity dir) MakeWorld()
|
|
{
|
|
var world = new World("ReadyCheckTest");
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<ReadyToggleSystem>());
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<RunDirectorSystem>());
|
|
group.SortSystems();
|
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
|
var em = world.EntityManager;
|
|
var nt = em.CreateEntity(typeof(NetworkTime));
|
|
em.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(T0) });
|
|
var dir = em.CreateEntity(typeof(RunInfo), typeof(RunRuntime));
|
|
em.SetComponentData(dir, new RunInfo { Lifecycle = RunLifecycle.Staging });
|
|
em.SetComponentData(dir, new RunRuntime { HostSalt = 1u });
|
|
return (world, group, dir);
|
|
}
|
|
|
|
static void SetTick(World world, uint tick)
|
|
{
|
|
var em = world.EntityManager;
|
|
using var q = em.CreateEntityQuery(typeof(NetworkTime));
|
|
em.SetComponentData(q.GetSingletonEntity(), new NetworkTime { ServerTick = new NetworkTick(tick) });
|
|
}
|
|
|
|
static Entity MakePlayer(EntityManager em, int networkId)
|
|
{
|
|
var e = em.CreateEntity(typeof(PlayerTag), typeof(PlayerReady), typeof(GhostOwner),
|
|
typeof(RegionTag), typeof(LocalTransform));
|
|
em.SetComponentData(e, new GhostOwner { NetworkId = networkId });
|
|
em.SetComponentData(e, new RegionTag { Region = RegionId.Base });
|
|
em.SetComponentData(e, LocalTransform.Identity);
|
|
return e;
|
|
}
|
|
|
|
static Entity MakeConnection(EntityManager em, int networkId)
|
|
{
|
|
var e = em.CreateEntity(typeof(NetworkId));
|
|
em.SetComponentData(e, new NetworkId { Value = networkId });
|
|
return e;
|
|
}
|
|
|
|
static void SendToggle(EntityManager em, Entity conn, byte ready)
|
|
{
|
|
var e = em.CreateEntity(typeof(ReadyToggleRequest), typeof(ReceiveRpcCommandRequest));
|
|
em.SetComponentData(e, new ReadyToggleRequest { Ready = ready });
|
|
em.SetComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = conn });
|
|
}
|
|
|
|
static int PendingRequests(EntityManager em)
|
|
{
|
|
using var q = em.CreateEntityQuery(typeof(ReadyToggleRequest));
|
|
return q.CalculateEntityCount();
|
|
}
|
|
|
|
[Test]
|
|
public void Toggle_SetsReady_AndSoloLaunches_SameTick()
|
|
{
|
|
var (world, group, dir) = MakeWorld();
|
|
var em = world.EntityManager;
|
|
var player = MakePlayer(em, 1);
|
|
var conn = MakeConnection(em, 1);
|
|
|
|
SendToggle(em, conn, 1);
|
|
group.Update();
|
|
|
|
Assert.AreEqual(1, em.GetComponentData<PlayerReady>(player).Value, "toggle landed");
|
|
Assert.AreEqual(0, PendingRequests(em), "request consumed");
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.Launching, info.Lifecycle, "1/1 ready -> rising edge -> Launching");
|
|
Assert.AreNotEqual(0u, info.LaunchTick, "countdown telegraph armed");
|
|
Assert.AreNotEqual(0u, info.RunSeed, "run seeded");
|
|
Assert.GreaterOrEqual(info.RoomCount, 6, "seed-varied length floor");
|
|
Assert.LessOrEqual(info.RoomCount, 10, "seed-varied length cap");
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void Toggle_Ignored_MidRun()
|
|
{
|
|
var (world, group, dir) = MakeWorld();
|
|
var em = world.EntityManager;
|
|
var player = MakePlayer(em, 1);
|
|
var conn = MakeConnection(em, 1);
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
info.Lifecycle = RunLifecycle.InRoom;
|
|
em.SetComponentData(dir, info);
|
|
|
|
SendToggle(em, conn, 1);
|
|
group.Update();
|
|
|
|
Assert.AreEqual(0, em.GetComponentData<PlayerReady>(player).Value, "mid-run toggle dropped");
|
|
Assert.AreEqual(0, PendingRequests(em), "request still consumed");
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void PartialReady_DoesNotLaunch()
|
|
{
|
|
var (world, group, dir) = MakeWorld();
|
|
var em = world.EntityManager;
|
|
MakePlayer(em, 1);
|
|
MakePlayer(em, 2);
|
|
var conn1 = MakeConnection(em, 1);
|
|
|
|
SendToggle(em, conn1, 1);
|
|
group.Update();
|
|
|
|
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle,
|
|
"1/2 ready must NOT launch");
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void UnReady_DuringCountdown_Aborts()
|
|
{
|
|
var (world, group, dir) = MakeWorld();
|
|
var em = world.EntityManager;
|
|
MakePlayer(em, 1);
|
|
var conn = MakeConnection(em, 1);
|
|
|
|
SendToggle(em, conn, 1);
|
|
group.Update();
|
|
Assert.AreEqual(RunLifecycle.Launching, em.GetComponentData<RunInfo>(dir).Lifecycle);
|
|
|
|
SendToggle(em, conn, 0); // change of heart during the 3-2-1
|
|
group.Update();
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.Staging, info.Lifecycle, "un-ready aborts the countdown");
|
|
Assert.AreEqual(0u, info.LaunchTick, "telegraph cleared");
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void Launch_TeleportsPartyOut_ThenHome_AndClearsReady()
|
|
{
|
|
var (world, group, dir) = MakeWorld();
|
|
var em = world.EntityManager;
|
|
var player = MakePlayer(em, 1);
|
|
var conn = MakeConnection(em, 1);
|
|
|
|
SendToggle(em, conn, 1);
|
|
group.Update(); // Staging -> Launching (countdown armed at T0)
|
|
|
|
SetTick(world, T0 + 200); // past the 180-tick countdown
|
|
group.Update(); // enter room 0 (the real traversal, Step 7)
|
|
|
|
Assert.AreEqual(RegionId.Expedition, em.GetComponentData<RegionTag>(player).Region,
|
|
"party region flipped to Expedition");
|
|
Assert.GreaterOrEqual(em.GetComponentData<LocalTransform>(player).Position.x, 999f,
|
|
"party teleported to the expedition room origin (sub-slot 0 at +1000)");
|
|
Assert.AreEqual(1f, em.GetComponentData<LocalTransform>(player).Scale, 1e-4f,
|
|
"Scale preserved through the teleport (never FromPosition)");
|
|
Assert.AreEqual(RunLifecycle.InRoom, em.GetComponentData<RunInfo>(dir).Lifecycle, "room 0 active");
|
|
|
|
// Simulate the all-left abort (disconnect edge): drop the player's region externally.
|
|
em.SetComponentData(player, new RegionTag { Region = RegionId.Base });
|
|
SetTick(world, T0 + 260);
|
|
group.Update(); // InRoom -> Returning (abort, no credit)
|
|
SetTick(world, T0 + 320);
|
|
group.Update(); // Returning: teleport home + clear ready flags -> StagingStaging
|
|
|
|
Assert.AreEqual(RegionId.Base, em.GetComponentData<RegionTag>(player).Region, "back home");
|
|
Assert.Less(em.GetComponentData<LocalTransform>(player).Position.x, 100f, "position restored to base");
|
|
Assert.AreEqual(0, em.GetComponentData<PlayerReady>(player).Value, "ready flag cleared on return");
|
|
var run = em.GetComponentData<RunRuntime>(dir);
|
|
Assert.AreEqual(run.RunEpoch, run.LastBankedRunEpoch, "terminal bank latch fired once");
|
|
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle);
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void LaunchGuard_BlocksWhenOutcomeLatched()
|
|
{
|
|
var (world, group, dir) = MakeWorld();
|
|
var em = world.EntityManager;
|
|
MakePlayer(em, 1);
|
|
var conn = MakeConnection(em, 1);
|
|
em.AddComponentData(dir, new RunOutcome { Value = RunOutcomeId.Victory });
|
|
|
|
SendToggle(em, conn, 1);
|
|
group.Update();
|
|
|
|
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle,
|
|
"a decided run must not launch (F2 guard)");
|
|
world.Dispose();
|
|
}
|
|
}
|
|
}
|