173 lines
6.2 KiB
C
173 lines
6.2 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;
|
|
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;
|
|
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;
|
|
int Fit;
|
|
int ConfirmedLosses;
|
|
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); }
|
|
bool Ingest(ERR_HQStatusReport report, float now)
|
|
{
|
|
if (!ERR_HQWatchRules.Valid(report, now)) return false;
|
|
ERR_HQWatchEntry entry = Find(report.SourceId);
|
|
if (entry)
|
|
{
|
|
if (report.MessageId <= entry.MessageId || report.ObservedAt <= entry.ObservedAt
|
|
|| now < entry.LastDeliveredAt || report.Strength != entry.Strength
|
|
|| report.ConfirmedLosses < entry.ConfirmedLosses) return false;
|
|
}
|
|
else
|
|
{
|
|
// Keep closed identities as well: replay cannot resurrect a retired patrol
|
|
// or count the same cumulative casualty total again under its old source.
|
|
if (m_Entries.Count() >= 128)
|
|
{
|
|
if (!m_CapLogged) Print("Rereintroduced [watch]: 128-formation check-in cap reached.");
|
|
m_CapLogged = true;
|
|
return false;
|
|
}
|
|
entry = new ERR_HQWatchEntry();
|
|
entry.SourceId = report.SourceId;
|
|
m_Entries.Insert(entry);
|
|
m_BySource.Set(entry.SourceId, entry);
|
|
}
|
|
m_ConfirmedLosses += report.ConfirmedLosses - entry.ConfirmedLosses;
|
|
entry.MessageId = report.MessageId;
|
|
entry.Position = report.Position;
|
|
entry.Strength = report.Strength;
|
|
entry.Count = report.Count;
|
|
entry.Fit = report.Fit;
|
|
entry.ConfirmedLosses = report.ConfirmedLosses;
|
|
entry.ObservedAt = report.ObservedAt;
|
|
entry.LastDeliveredAt = now;
|
|
entry.AskedAt = -1;
|
|
entry.NextAttempt = 0;
|
|
entry.Investigating = false;
|
|
entry.Closed = entry.ConfirmedLosses == entry.Strength;
|
|
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;
|
|
}
|
|
}
|