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

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