Run Re-Do

This commit is contained in:
2026-07-02 20:41:43 -07:00
parent 86575dd5bc
commit 16e396841e
188 changed files with 8291 additions and 2429 deletions
@@ -0,0 +1,96 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Server receiver for <see cref="RouteSelectRequest"/> — the co-op route choice (any-player-first-commits, the
/// operator's locked authority model). Validates each pick server-authoritatively:
/// <c>Lifecycle == RouteSelect</c> · run identity <c>(uint)ForRunEpoch == RunRuntime.RunSeed</c> (the Step-8
/// review re-mean: the replicated seed IS the run token; the server-only RunEpoch is not client-knowable) ·
/// <c>ForLayer == RunInfo.CurrentRoom</c> (the gate's un-incremented cleared layer) · <c>OptionIndex</c> within
/// the replicated <c>RouteOptionCount</c> · the SENDER's <see cref="RegionTag"/> is Expedition (N3 — a
/// base-bound joiner cannot commit the party's route) · nothing accepted yet this gate.
///
/// FIRST-COMMIT LATCH: the accepted pick is written to the server-only <see cref="RouteCommand"/> via an
/// IMMEDIATE in-place <c>SystemAPI.SetComponent</c> INSIDE the drain loop plus a local accepted flag (the DR-014
/// atomicity idiom) — two same-tick picks can never both observe an open gate; a hoisted read would re-create
/// the exact N1 race the design review killed. <see cref="RouteCommand.ForRunEpoch"/> is stamped from the TRUE
/// server-only epoch (never the client-echoed value). Requests are ALWAYS destroyed. This system writes ONLY
/// RouteCommand — RunDirectorSystem stays the sole RunInfo/RunRuntime writer and consumes the latch
/// (abort → pick → grace, in that order). Ordered before it so a pick can land the same tick it is consumed;
/// NO CyclePhase edge (the room-chain hard rule).
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(RunDirectorSystem))]
public partial struct RouteSelectSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<RouteSelectRequest, ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
state.RequireForUpdate<RunInfo>();
state.RequireForUpdate<RunRuntime>();
state.RequireForUpdate<RouteCommand>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
bool gateOpen = info.Lifecycle == RunLifecycle.RouteSelect;
// Sender-region lookup (N3): connection NetworkId -> the player's CURRENT region. A player entity
// without RegionTag simply never enters the map -> its pick is a clean reject, never a throw.
var regionByConn = new NativeHashMap<int, byte>(8, Allocator.Temp);
foreach (var (owner, region) in
SystemAPI.Query<RefRO<GhostOwner>, RefRO<RegionTag>>().WithAll<PlayerTag>())
regionByConn[owner.ValueRO.NetworkId] = region.ValueRO.Region;
// Local accepted flag beside the in-place write = the first-commit latch (nothing else writes
// RouteCommand mid-loop; RunDirector's gate-entry clear ran on a previous tick by construction).
bool accepted = SystemAPI.GetComponent<RouteCommand>(dirEntity).HasPick != 0;
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (receive, req, requestEntity) in
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>, RefRO<RouteSelectRequest>>().WithEntityAccess())
{
var conn = receive.ValueRO.SourceConnection;
bool valid = gateOpen
&& !accepted
&& (uint)req.ValueRO.ForRunEpoch == run.RunSeed
&& req.ValueRO.ForLayer == info.CurrentRoom
&& req.ValueRO.OptionIndex < info.RouteOptionCount
&& SystemAPI.HasComponent<NetworkId>(conn)
&& regionByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out byte senderRegion)
&& senderRegion == RegionId.Expedition;
if (valid)
{
// IMMEDIATE in-place commit (never an ECB-deferred write) + the local flag: first pick wins.
SystemAPI.SetComponent(dirEntity, new RouteCommand
{
HasPick = 1,
OptionIndex = req.ValueRO.OptionIndex,
ForRunEpoch = run.RunEpoch, // the TRUE server epoch — never echo the client value
ForLayer = req.ValueRO.ForLayer,
});
accepted = true;
}
ecb.DestroyEntity(requestEntity);
}
ecb.Playback(state.EntityManager);
regionByConn.Dispose();
}
}
}