Reconcile radio-delivered body and status casualties by original roster identity

This commit is contained in:
2026-09-20 03:09:16 +02:00
parent a2aab17171
commit 9b6596eb3e
44 changed files with 6042 additions and 86 deletions
+64 -34
View File
@@ -10,6 +10,7 @@ class ERR_HQStatusReport
int Count;
int Fit;
int ConfirmedLosses;
ref array<int> DeathSlots = {};
float ObservedAt = -1;
}
@@ -58,9 +59,11 @@ class ERR_HQWatchEntry
int MessageId;
vector Position;
int Strength;
int Count;
int Fit;
int Count = -1;
int Fit = -1;
int ConfirmedLosses;
ref ERR_HQDeliveredCasualties Casualties = new ERR_HQDeliveredCasualties();
float LastBodyDeliveredAt = -1;
float ObservedAt = -1;
float LastDeliveredAt = -1;
float AskedAt = -1;
@@ -84,44 +87,71 @@ class ERR_HQWatch
}
int ConfirmedLosses() { return m_ConfirmedLosses; }
int SquadLimit(int baseline) { return ERR_HQWatchRules.SquadLimit(baseline, m_ConfirmedLosses); }
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)
if (entry && (report.MessageId <= entry.MessageId || report.ObservedAt <= entry.ObservedAt
|| now < entry.LastDeliveredAt || report.Strength != entry.Strength)) return false;
if (!entry)
{
if (report.MessageId <= entry.MessageId || report.ObservedAt <= entry.ObservedAt
|| now < entry.LastDeliveredAt || report.Strength != entry.Strength
|| report.ConfirmedLosses < entry.ConfirmedLosses) return false;
// 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;
}
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;
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.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)