using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
///
/// Terminal drain for RPC requests no receiver consumed. Destroys any entity still carrying an unconsumed
/// after it has survived a couple of frames, so a missing receiver
/// degrades to a silent drop instead of a permanent entity leak.
///
/// WHY (2026-08-06 audit, reproduced live): every RPC receiver in this project gates itself behind
/// RequireForUpdate<T> on a scene-baked singleton. In DevSandbox none of those singletons exist,
/// so eleven receivers never ran at all — one keypress on E/Q/G/T created a server request entity that
/// nothing ever visited. Netcode's own WarnAboutStaleRpcSystem logs "has not been consumed or
/// destroyed for 4 (MaxRpcAgeFrames) frames", then calls Consume() — it never destroys the entity, and it is
/// compiled out of player builds (#if UNITY_EDITOR && !NETCODE_NDEBUG). So the entities
/// accumulated forever, silently, and worst in a shipped build.
///
/// ORDERING: OrderLast in the plain server , so every same-frame receiver
/// (including those inside the nested predicted group) has already had its chance. The Age gate is belt and
/// braces on top of that: Age increments per frame the request survives, so requiring >= 2 means the request
/// lived through a full frame unhandled before we reap it. Consumed requests are skipped — their owning
/// receiver is responsible for destroying them, and Consume() sets Age to ushort.MaxValue anyway.
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup), OrderLast = true)]
public partial struct StaleRpcReaperSystem : ISystem
{
/// Frames an unconsumed request may survive before it is reaped. Must exceed 1 so a receiver
/// that legitimately runs later in the same frame is never robbed of its request.
const ushort k_MaxAgeFrames = 2;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var b = new EntityQueryBuilder(Allocator.Temp).WithAll();
state.RequireForUpdate(state.GetEntityQuery(b));
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (recv, entity) in
SystemAPI.Query>().WithEntityAccess())
{
if (recv.ValueRO.IsConsumed) continue; // a receiver owns it; not ours to destroy
if (recv.ValueRO.Age < k_MaxAgeFrames) continue;
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}