Run Re-Do
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user