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
@@ -14,6 +14,30 @@ class ERR_HQStatusSampler
protected ref array<bool> m_Released = {};
protected ref array<AIAgent> m_Agents = {};
bool Bound() { return m_Bound; }
string InitialSourceId() { return m_SourceId; }
int InitialStrength() { return m_Strength; }
bool OwnsRoster(SCR_AIGroup group) { return m_Bound && group && group == m_Group; }
IEntity InitialMember(int slot)
{
if (!m_Bound || slot < 0 || slot >= m_Members.Count()) return null;
return m_Members[slot];
}
// Attribution retains the original slot even when native death detaches its agent.
// A known transfer/player conversion permanently releases that slot, never rebinds it.
bool CanAttribute(int slot, IEntity actor)
{
if (!Replication.IsServer() || !actor || InitialMember(slot) != actor || m_Released[slot]) return false;
if (ERR_Actors.Player(actor) || SCR_FactionManager.SGetFaction(actor) != m_Faction)
{ m_Released[slot] = true; return false; }
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
if (!damage) return false;
SCR_AIGroup actorGroup = ERR_Actors.Group(actor);
// A retained foreign group proves transfer even if death preceded this tick.
// A null group on a destroyed actor remains normal native corpse detachment.
if ((actorGroup && actorGroup != m_Group) || (!damage.IsDestroyed() && actorGroup != m_Group))
{ m_Released[slot] = true; return false; }
return true;
}
protected bool Eligible()
{
return Replication.IsServer() && m_Bound && m_Group && m_Faction
@@ -59,10 +83,12 @@ class ERR_HQStatusSampler
{ m_Released[i] = true; continue; }
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
if (!damage) continue;
SCR_AIGroup actorGroup = ERR_Actors.Group(actor);
if (actorGroup && actorGroup != m_Group) { m_Released[i] = true; continue; }
if (damage.IsDestroyed()) { m_Dead[i] = true; m_Deaths++; continue; }
// A living member who left this group is no longer ours to account for.
// Native death may detach a corpse, so group identity is checked after death.
if (ERR_Actors.Group(actor) != m_Group) m_Released[i] = true;
// A living detached member is no longer ours. A destroyed detached actor
// was handled above without mistaking native corpse cleanup for transfer.
if (actorGroup != m_Group) m_Released[i] = true;
}
return true;
}
@@ -79,6 +105,8 @@ class ERR_HQStatusSampler
ERR_HQStatusReport report = new ERR_HQStatusReport();
report.SourceId = m_SourceId; report.MessageId = messageId; report.Position = position;
report.Strength = m_Strength; report.ConfirmedLosses = m_Deaths; report.ObservedAt = now;
for (int slot = 0; slot < m_Dead.Count(); slot++)
if (m_Dead[slot]) report.DeathSlots.Insert(slot);
for (int i = 0; i < m_Members.Count(); i++)
{
if (m_Dead[i] || m_Released[i]) continue;