using System; using System.Collections.Generic; namespace ProjectI.Enemies { // Shared frame cap; queued FIFO grants are reserved before repeat requests use spare capacity. public sealed class EnemyQueryBudget { private readonly LinkedList waiting = new LinkedList(); private readonly HashSet queued = new HashSet(), grants = new HashSet(); private readonly Dictionary enqueuedAt = new Dictionary(); private readonly Dictionary lastRequested = new Dictionary(); private readonly List abandoned = new List(); private int frame = -1; public int QueriesPerFrame { get; } public int UsedThisFrame { get; private set; } public int WaitingCount => waiting.Count; #if UNITY_EDITOR || DEVELOPMENT_BUILD public int InspectionPendingCount => enqueuedAt.Count; public double MaximumServicedAge { get; private set; } #endif public EnemyQueryBudget(int queriesPerFrame = 8) { if (queriesPerFrame < 0 || queriesPerFrame > 32) throw new ArgumentOutOfRangeException(nameof(queriesPerFrame)); QueriesPerFrame = queriesPerFrame; } public bool TrySpend(ulong request, int currentFrame, double now) { if (request == 0 || currentFrame < 0 || !EnemyObservationRules.Finite(now) || now < 0) return false; if (currentFrame != frame) { abandoned.Clear(); foreach (var pair in lastRequested) if (currentFrame - pair.Value > 2) abandoned.Add(pair.Key); foreach (ulong stale in abandoned) Cancel(stale); // Unused grants retain their old FIFO priority; no valid but inactive head can block a frame. foreach (ulong previous in grants) { waiting.AddFirst(previous); queued.Add(previous); } grants.Clear(); frame = currentFrame; UsedThisFrame = 0; GrantWaiting(); } if (!grants.Contains(request) && !queued.Contains(request)) { if (enqueuedAt.Count >= 64) return false; waiting.AddLast(request); queued.Add(request); enqueuedAt[request] = now; } lastRequested[request] = currentFrame; GrantWaiting(); if (!grants.Remove(request)) return false; UsedThisFrame++; #if UNITY_EDITOR || DEVELOPMENT_BUILD MaximumServicedAge = Math.Max(MaximumServicedAge, now - enqueuedAt[request]); #endif enqueuedAt.Remove(request); lastRequested.Remove(request); return true; } private void GrantWaiting() { while (waiting.Count > 0 && grants.Count + UsedThisFrame < QueriesPerFrame) { ulong request = waiting.First.Value; waiting.RemoveFirst(); queued.Remove(request); grants.Add(request); } } public void Cancel(ulong request) { waiting.Remove(request); queued.Remove(request); grants.Remove(request); enqueuedAt.Remove(request); lastRequested.Remove(request); } } }