Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_HQWatch.c
T

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