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;
}
}
}