217 lines
9.0 KiB
C
217 lines
9.0 KiB
C
// A local status snapshot is not HQ knowledge until its radio route delivers.
|
|
// Count includes living unconscious members; only explicit observed deaths add
|
|
// ConfirmedLosses. Neither a silent radio nor an absent member proves a death.
|
|
class ERR_HQStatusReport
|
|
{
|
|
string SourceId;
|
|
int MessageId;
|
|
vector Position;
|
|
int Strength;
|
|
int Count;
|
|
int Fit;
|
|
int ConfirmedLosses;
|
|
ref array<int> DeathSlots = {};
|
|
ref ERR_HQAmmoSnapshot AmmoState;
|
|
float ObservedAt = -1;
|
|
}
|
|
|
|
class ERR_HQWatchRules
|
|
{
|
|
static bool Valid(ERR_HQStatusReport report, float now)
|
|
{
|
|
if (!report || report.SourceId.IsEmpty() || report.SourceId.Length() > 96 || report.MessageId <= 0)
|
|
return false;
|
|
if (!ERR_HQPursuitRules.PositionValid(report.Position) || !ERR_HQPursuitRules.Finite(report.ObservedAt)
|
|
|| !ERR_HQPursuitRules.Finite(now) || report.ObservedAt < 0 || now < report.ObservedAt
|
|
|| now - report.ObservedAt > 120000) return false;
|
|
if (report.AmmoState && !report.AmmoState.Valid(report.Count)) return false;
|
|
return report.Strength > 0 && report.Strength <= 32 && report.Count >= 0
|
|
&& report.Count <= report.Strength && report.Fit >= 0 && report.Fit <= report.Count
|
|
&& report.ConfirmedLosses >= 0 && report.ConfirmedLosses <= report.Strength - report.Count;
|
|
}
|
|
static bool RequestDue(float observedAt, float askedAt, float nextAttempt, bool closed, float now)
|
|
{
|
|
return !closed && ERR_HQPursuitRules.Finite(now) && observedAt >= 0 && askedAt < 0
|
|
&& now >= nextAttempt && now - observedAt >= 360000;
|
|
}
|
|
static bool InvestigationDue(float askedAt, float nextAttempt, bool closed, bool investigating, float now)
|
|
{
|
|
return !closed && !investigating && ERR_HQPursuitRules.Finite(now) && askedAt >= 0
|
|
&& now >= nextAttempt && now - askedAt >= 30000;
|
|
}
|
|
static int SquadLimit(int baseline, int confirmedLosses)
|
|
{
|
|
if (baseline <= 0) return 0;
|
|
if (baseline >= 8) return 8;
|
|
// Confirmed losses increase the shared pool only. Individual hunts still
|
|
// obey the independent pursuit cap of three and actual force availability.
|
|
int losses = Math.Clamp(confirmedLosses, 0, 4096);
|
|
int bonus = losses / 2;
|
|
return Math.Min(8, baseline + bonus);
|
|
}
|
|
}
|
|
|
|
// Detached HQ roster state: no entity/group handle and no live position lookup.
|
|
// Entries returned by Find/At are borrowed for HQ inspection, never mutated by
|
|
// observers. Only delivered Ingest and explicit request lifecycle methods write.
|
|
class ERR_HQWatchEntry
|
|
{
|
|
int RequestId;
|
|
string SourceId;
|
|
int MessageId;
|
|
vector Position;
|
|
int Strength;
|
|
int Count = -1;
|
|
int Fit = -1;
|
|
int ConfirmedLosses;
|
|
ref ERR_HQAmmoSnapshot AmmoState;
|
|
ref ERR_HQDeliveredCasualties Casualties = new ERR_HQDeliveredCasualties();
|
|
float LastBodyDeliveredAt = -1;
|
|
float ObservedAt = -1;
|
|
float LastDeliveredAt = -1;
|
|
float AskedAt = -1;
|
|
float NextAttempt;
|
|
bool Investigating;
|
|
bool Closed;
|
|
}
|
|
|
|
class ERR_HQWatch
|
|
{
|
|
protected ref array<ref ERR_HQWatchEntry> m_Entries = {};
|
|
protected ref map<string, ERR_HQWatchEntry> m_BySource = new map<string, ERR_HQWatchEntry>();
|
|
protected int m_ConfirmedLosses;
|
|
protected bool m_CapLogged;
|
|
int Count() { return m_Entries.Count(); }
|
|
ERR_HQWatchEntry Find(string sourceId) { return m_BySource.Get(sourceId); }
|
|
ERR_HQWatchEntry At(int index)
|
|
{
|
|
if (index < 0 || index >= m_Entries.Count()) return null;
|
|
return m_Entries[index];
|
|
}
|
|
int ConfirmedLosses() { return m_ConfirmedLosses; }
|
|
int SquadLimit(int baseline) { return ERR_HQWatchRules.SquadLimit(baseline, m_ConfirmedLosses); }
|
|
// Conservative new-task veto, never a live inventory query or an active-order cancellation.
|
|
// A fresh complete own-roster check-in is required; silence or new bodily losses make no new ammo claims.
|
|
bool ArmedSearchBlocked(string sourceId, float now)
|
|
{
|
|
ERR_HQWatchEntry entry = Find(sourceId);
|
|
return entry && !entry.Closed && entry.AmmoState && entry.AmmoState.Members == entry.Count
|
|
&& ERR_HQPursuitRules.Finite(now) && entry.ObservedAt >= 0 && now >= entry.ObservedAt
|
|
&& now - entry.ObservedAt <= 180000 && entry.AmmoState.KnownSmallArmsEmpty();
|
|
}
|
|
protected ERR_HQWatchEntry Create(string sourceId, int strength)
|
|
{
|
|
// Closed identities remain tombstones: replays cannot recreate a formation.
|
|
if (m_Entries.Count() >= 128)
|
|
{
|
|
if (!m_CapLogged) Print("Rereintroduced [watch]: 128-formation check-in cap reached.");
|
|
m_CapLogged = true;
|
|
return null;
|
|
}
|
|
ERR_HQWatchEntry entry = new ERR_HQWatchEntry();
|
|
entry.SourceId = sourceId; entry.Strength = strength;
|
|
entry.Casualties.Bind(strength);
|
|
m_Entries.Insert(entry); m_BySource.Set(sourceId, entry);
|
|
return entry;
|
|
}
|
|
protected void ApplyLosses(ERR_HQWatchEntry entry)
|
|
{
|
|
int known = entry.Casualties.Count();
|
|
m_ConfirmedLosses += known - entry.ConfirmedLosses;
|
|
entry.ConfirmedLosses = known;
|
|
entry.Count = entry.Casualties.LivingUpperBound(entry.Count);
|
|
if (entry.Count >= 0) entry.Fit = Math.Min(entry.Fit, entry.Count);
|
|
entry.Closed = entry.Casualties.Closed();
|
|
if (entry.Closed) entry.Fit = 0;
|
|
}
|
|
bool Ingest(ERR_HQStatusReport report, float now)
|
|
{
|
|
if (!ERR_HQWatchRules.Valid(report, now)) return false;
|
|
ERR_HQWatchEntry entry = Find(report.SourceId);
|
|
if (entry && (report.MessageId <= entry.MessageId || report.ObservedAt <= entry.ObservedAt
|
|
|| now < entry.LastDeliveredAt || report.Strength != entry.Strength)) return false;
|
|
if (!entry)
|
|
{
|
|
// Validate slot payload before allocation: malformed snapshots cannot consume the cap.
|
|
ERR_HQDeliveredCasualties validation = new ERR_HQDeliveredCasualties();
|
|
if (!validation.Bind(report.Strength)
|
|
|| !validation.AcceptStatus(report.DeathSlots, report.ConfirmedLosses, report.Count)) return false;
|
|
entry = Create(report.SourceId, report.Strength);
|
|
if (!entry) return false;
|
|
}
|
|
if (!entry.Casualties.AcceptStatus(report.DeathSlots, report.ConfirmedLosses, report.Count)) return false;
|
|
entry.MessageId = report.MessageId; entry.Position = report.Position;
|
|
entry.Count = report.Count; entry.Fit = report.Fit;
|
|
entry.AmmoState = null;
|
|
if (report.AmmoState) entry.AmmoState = report.AmmoState.Snapshot();
|
|
entry.ObservedAt = report.ObservedAt; entry.LastDeliveredAt = now;
|
|
entry.AskedAt = -1; entry.NextAttempt = 0; entry.Investigating = false;
|
|
ApplyLosses(entry);
|
|
return true;
|
|
}
|
|
// Called only from delivered BODY intake; scalar evidence survives native target cleanup.
|
|
// Corpse coordinates and body message IDs never replace a formation status/check-in.
|
|
bool IngestBody(ERR_HQReport report, float now)
|
|
{
|
|
if (!report || report.TravelMode != "body" || report.Distress || report.MessageId <= 0
|
|
|| report.CasualtySourceId.IsEmpty() || report.CasualtySourceId.Length() > 96
|
|
|| report.CasualtyStrength < 1 || report.CasualtyStrength > 32
|
|
|| report.CasualtySlot < 0 || report.CasualtySlot >= report.CasualtyStrength
|
|
|| !ERR_HQPursuitRules.Finite(now) || !ERR_HQPursuitRules.Finite(report.ObservedAt)
|
|
|| !ERR_HQPursuitRules.PositionValid(report.Position) || report.ObservedAt < 0
|
|
|| now < report.ObservedAt || now - report.ObservedAt > 120000) return false;
|
|
ERR_HQWatchEntry entry = Find(report.CasualtySourceId);
|
|
if (entry && (entry.Strength != report.CasualtyStrength || now < entry.LastBodyDeliveredAt)) return false;
|
|
if (!entry) entry = Create(report.CasualtySourceId, report.CasualtyStrength);
|
|
if (!entry || !entry.Casualties.AcceptBody(report.CasualtySlot)) return false;
|
|
entry.LastBodyDeliveredAt = now;
|
|
ApplyLosses(entry);
|
|
return true;
|
|
}
|
|
bool RequestDue(string sourceId, float now)
|
|
{
|
|
ERR_HQWatchEntry entry = Find(sourceId);
|
|
return entry && ERR_HQWatchRules.RequestDue(entry.ObservedAt, entry.AskedAt,
|
|
entry.NextAttempt, entry.Closed, now);
|
|
}
|
|
// Begin the deadline on HQ request attempt, including no route to a silent
|
|
// patrol. A missing downlink cannot prevent an investigation indefinitely.
|
|
bool MarkRequested(string sourceId, int statusMessageId, float now)
|
|
{
|
|
ERR_HQWatchEntry entry = Find(sourceId);
|
|
if (!entry || entry.MessageId != statusMessageId || entry.RequestId >= 2147483646
|
|
|| !RequestDue(sourceId, now)) return false;
|
|
entry.RequestId++;
|
|
entry.AskedAt = now;
|
|
return true;
|
|
}
|
|
bool InvestigationDue(string sourceId, float now)
|
|
{
|
|
ERR_HQWatchEntry entry = Find(sourceId);
|
|
return entry && ERR_HQWatchRules.InvestigationDue(entry.AskedAt, entry.NextAttempt,
|
|
entry.Closed, entry.Investigating, now);
|
|
}
|
|
bool MarkInvestigating(string sourceId, int statusMessageId, int requestId, float now)
|
|
{
|
|
ERR_HQWatchEntry entry = Find(sourceId);
|
|
if (!entry || entry.MessageId != statusMessageId || entry.RequestId != requestId
|
|
|| !InvestigationDue(sourceId, now)) return false;
|
|
entry.Investigating = true;
|
|
return true;
|
|
}
|
|
bool FinishInvestigation(string sourceId, int statusMessageId, int requestId, float now)
|
|
{
|
|
ERR_HQWatchEntry entry = Find(sourceId);
|
|
if (!entry || entry.MessageId != statusMessageId || entry.RequestId != requestId || !entry.Investigating
|
|
|| !ERR_HQPursuitRules.Finite(now) || now < entry.AskedAt + 30000) return false;
|
|
entry.Investigating = false;
|
|
entry.AskedAt = -1;
|
|
entry.NextAttempt = now + 180000;
|
|
return true;
|
|
}
|
|
void Clear()
|
|
{
|
|
m_Entries.Clear(); m_BySource.Clear(); m_ConfirmedLosses = 0; m_CapLogged = false;
|
|
}
|
|
}
|