using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Server { /// /// Server receiver for — the co-op route choice (any-player-first-commits, the /// operator's locked authority model). Validates each pick server-authoritatively: /// Lifecycle == RouteSelect · run identity (uint)ForRunEpoch == RunRuntime.RunSeed (the Step-8 /// review re-mean: the replicated seed IS the run token; the server-only RunEpoch is not client-knowable) · /// ForLayer == RunInfo.CurrentRoom (the gate's un-incremented cleared layer) · OptionIndex within /// the replicated RouteOptionCount · the SENDER's 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 via an /// IMMEDIATE in-place SystemAPI.SetComponent 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. 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). /// [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(); state.RequireForUpdate(state.GetEntityQuery(builder)); state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var dirEntity = SystemAPI.GetSingletonEntity(); var info = SystemAPI.GetComponent(dirEntity); var run = SystemAPI.GetComponent(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(8, Allocator.Temp); foreach (var (owner, region) in SystemAPI.Query, RefRO>().WithAll()) 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(dirEntity).HasPick != 0; var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (receive, req, requestEntity) in SystemAPI.Query, RefRO>().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(conn) && regionByConn.TryGetValue(SystemAPI.GetComponent(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(); } } }