using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Server { /// /// Server receiver for + the reward-grace AUTO-PICK backstop. A valid pick /// (sender resolved, RunInfo.Lifecycle == RoomReward — the D-F4 gate — Pending == 1, index in /// range, option id known to the catalog) appends ONE in the run-scoped BOON band /// (Tuning.BoonSourceIdBase + BoonPickCounter++ — distinct rows, one range-strip clears the run) and /// clears Pending; the buffer mutation is non-structural and folds through the unchanged /// StatRecomputeSystem on both worlds (rollback-correct). When the reward grace elapses, every still-pending /// EXPEDITION player is auto-dealt Option0 (the operator's default un-picked policy — a player always /// gets something) so the run never stalls on an AFK picker. /// /// Ordering: [UpdateBefore(RunDirectorSystem)] — ALL RPC receivers sit before the director (the /// ReadyToggle/RouteSelect symmetry). This closes the D-F4 straggler race STRUCTURALLY: on the tick the /// director strips (Returning), a straggler pick is rejected here FIRST (lifecycle is already past RoomReward), /// so nothing can append after the strip; and the auto-pick lands before the director's exit gate reads /// Pending. Requests are ALWAYS destroyed. No CyclePhase edge (the room-chain hard rule). /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateBefore(typeof(RunDirectorSystem))] public partial struct BoonApplySystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); 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 rewarding = info.Lifecycle == RunLifecycle.RoomReward; var catalog = SystemAPI.GetComponent(SystemAPI.GetSingletonEntity()); if (!catalog.Value.IsCreated) return; ref var pool = ref catalog.Value.Value; bool runDirty = false; // ---- explicit picks (drained every tick so stale requests die even outside RoomReward) ---- var playerByConn = new NativeHashMap(8, Allocator.Temp); foreach (var (owner, entity) in SystemAPI.Query>().WithAll().WithEntityAccess()) playerByConn[owner.ValueRO.NetworkId] = entity; var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (receive, req, requestEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { var conn = receive.ValueRO.SourceConnection; if (rewarding && req.ValueRO.Index < 3 && SystemAPI.HasComponent(conn) && playerByConn.TryGetValue(SystemAPI.GetComponent(conn).Value, out var player)) { var offer = SystemAPI.GetComponent(player); if (offer.Pending == 1) { byte id = req.ValueRO.Index == 2 ? offer.Option2 : req.ValueRO.Index == 1 ? offer.Option1 : offer.Option0; if (Apply(ref state, player, id, ref pool, ref run)) { offer.Pending = 0; SystemAPI.SetComponent(player, offer); runDirty = true; } } } ecb.DestroyEntity(requestEntity); } ecb.Playback(state.EntityManager); playerByConn.Dispose(); // ---- reward-grace auto-pick backstop (Option0 — the player always gets something) ---- if (rewarding && run.RewardGraceTick != 0u) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (serverTick.IsValid && !new NetworkTick(run.RewardGraceTick).IsNewerThan(serverTick)) { foreach (var (offer, region, entity) in SystemAPI.Query, RefRO>() .WithAll().WithEntityAccess()) { if (offer.ValueRO.Pending != 1 || region.ValueRO.Region != RegionId.Expedition) continue; if (Apply(ref state, entity, offer.ValueRO.Option0, ref pool, ref run)) runDirty = true; offer.ValueRW.Pending = 0; // cleared even if the id was unknown — never wedge the gate } } } if (runDirty) SystemAPI.SetComponent(dirEntity, run); // the documented BoonPickCounter co-write (band provenance) } /// Append the boon's StatModifier in the run-scoped band. False iff the id is unknown/zero. static bool Apply(ref SystemState state, Entity player, byte boonId, ref BoonCatalogBlob pool, ref RunRuntime run) { if (boonId == 0) return false; int idx = BoonMath.FindDef(ref pool, boonId); if (idx < 0) return false; // unknown id (catalog drift) — preserve-and-skip, never throw var mods = state.EntityManager.GetBuffer(player); mods.Add(new StatModifier { Target = pool.Defs[idx].Target, Op = pool.Defs[idx].Op, Value = pool.Defs[idx].Value, SourceId = Tuning.BoonSourceIdBase + (run.BoonPickCounter % Tuning.BoonSourceIdSpan), }); run.BoonPickCounter += 1; return true; } } }