e27a495530
- 10 EditMode files: every bare trailing world.Dispose() now inside using(world){} (single-world) or a [TearDown]+List<World> (the multi-world reject-matrix tests in BoonApplyTests/RouteSelectSystemTests) — an assertion failure can no longer leak the World and mask the true first failure. No test logic changed.
- HudSystem: collapse the blank-line run before the class close; trim the extracted route-map clause from the READY-panel comment.
466/466 EditMode tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
6.3 KiB
C#
141 lines
6.3 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>
|
|
/// Regression pins for the two post-impl-review fixes on <see cref="RunDirectorSystem"/>:
|
|
/// (1) a dead-respawned (base-region) player's stale <see cref="BoonOffer.Pending"/> neither holds the
|
|
/// RoomReward exit gate nor survives it (the wedge that stalled every reward 30 s and left the modal
|
|
/// pickable through the next fight); (2) room advances teleport ONLY <see cref="RunParticipant"/>s —
|
|
/// a dead-respawned participant is re-conscripted (operator-locked default) while a mid-run late joiner
|
|
/// stays at base (spec §2.2 closed party).
|
|
/// </summary>
|
|
public class RunRosterRegressionTests
|
|
{
|
|
const uint Seed = 777u;
|
|
const uint T0 = 2000;
|
|
|
|
static (World world, SimulationSystemGroup group, Entity dir) MakeWorld(out RunMap map)
|
|
{
|
|
var world = new World("RosterTest");
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
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) });
|
|
|
|
map = RunMapMath.Generate(Seed);
|
|
var dir = em.CreateEntity(typeof(RunInfo), typeof(RunRuntime), typeof(ExpeditionObjective),
|
|
typeof(RouteCommand), typeof(MetaCounters), typeof(GoalProgress), typeof(ThreatState), typeof(SaveRequest));
|
|
return (world, group, dir);
|
|
}
|
|
|
|
static Entity MakePlayer(EntityManager em, byte region, bool participant, byte pending)
|
|
{
|
|
var player = participant
|
|
? em.CreateEntity(typeof(PlayerTag), typeof(PlayerReady), typeof(RegionTag),
|
|
typeof(LocalTransform), typeof(BoonOffer), typeof(RunParticipant))
|
|
: em.CreateEntity(typeof(PlayerTag), typeof(PlayerReady), typeof(RegionTag),
|
|
typeof(LocalTransform), typeof(BoonOffer));
|
|
em.SetComponentData(player, new RegionTag { Region = region });
|
|
em.SetComponentData(player, LocalTransform.Identity);
|
|
em.SetComponentData(player, new BoonOffer { Pending = pending, Option0 = 1, Option1 = 2, Option2 = 3 });
|
|
return player;
|
|
}
|
|
|
|
[Test]
|
|
public void StalePendingAtBase_DoesNotHoldGate_AndIsStrippedOnExit()
|
|
{
|
|
var (world, group, dir) = MakeWorld(out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
em.SetComponentData(dir, new RunInfo
|
|
{
|
|
Lifecycle = RunLifecycle.RoomReward,
|
|
CurrentRoom = 1,
|
|
CurrentCol = 0,
|
|
RoomCount = map.LayerCount,
|
|
RunSeed = Seed,
|
|
});
|
|
em.SetComponentData(dir, new RunRuntime
|
|
{
|
|
RunSeed = Seed,
|
|
RunEpoch = 1,
|
|
RoomEpoch = 2,
|
|
// Grace far in the future: ONLY the all-picked path can advance this tick.
|
|
RewardGraceTick = TickUtil.NonZero(T0 + 100000),
|
|
});
|
|
|
|
var alive = MakePlayer(em, RegionId.Expedition, participant: true, pending: 0); // picked already
|
|
var deadAtBase = MakePlayer(em, RegionId.Base, participant: true, pending: 1); // the wedge
|
|
|
|
group.Update();
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreNotEqual(RunLifecycle.RoomReward, info.Lifecycle,
|
|
"a base-region player's stale Pending must not hold the reward gate");
|
|
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(deadAtBase).Pending,
|
|
"the stale offer is stripped on the gate exit");
|
|
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(alive).Pending,
|
|
"no offer survives the gate");
|
|
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Advance_TeleportsOnlyParticipants_LateJoinerStaysAtBase()
|
|
{
|
|
var (world, group, dir) = MakeWorld(out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
em.SetComponentData(dir, new RunInfo
|
|
{
|
|
Lifecycle = RunLifecycle.RouteSelect,
|
|
CurrentRoom = 0,
|
|
CurrentCol = 0,
|
|
RoomCount = map.LayerCount,
|
|
RunSeed = Seed,
|
|
RouteOptionCount = 1,
|
|
RouteOpt0Col = 0,
|
|
});
|
|
em.SetComponentData(dir, new RunRuntime
|
|
{
|
|
RunSeed = Seed,
|
|
RunEpoch = 1,
|
|
RoomEpoch = 1,
|
|
RouteGraceTick = TickUtil.NonZero(T0 + 100000),
|
|
});
|
|
em.SetComponentData(dir, new RouteCommand { HasPick = 1, OptionIndex = 0, ForRunEpoch = 1, ForLayer = 0 });
|
|
|
|
var inRoom = MakePlayer(em, RegionId.Expedition, participant: true, pending: 0); // fighting on
|
|
var deadAtBase = MakePlayer(em, RegionId.Base, participant: true, pending: 0); // re-conscripted
|
|
var lateJoiner = MakePlayer(em, RegionId.Base, participant: false, pending: 0); // stays home
|
|
|
|
group.Update();
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.InRoom, info.Lifecycle, "the committed pick advances the run");
|
|
Assert.GreaterOrEqual(em.GetComponentData<LocalTransform>(inRoom).Position.x, 1000f,
|
|
"the in-room participant rides the advance");
|
|
Assert.GreaterOrEqual(em.GetComponentData<LocalTransform>(deadAtBase).Position.x, 1000f,
|
|
"a dead-respawned participant is re-conscripted onto the new room");
|
|
Assert.AreEqual(0f, em.GetComponentData<LocalTransform>(lateJoiner).Position.x,
|
|
"a non-participant late joiner is never yanked into the fight");
|
|
Assert.AreEqual(RegionId.Base, em.GetComponentData<RegionTag>(lateJoiner).Region,
|
|
"the late joiner stays in the Base relevancy bucket");
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|