// Local evidence only. HQ must receive the resulting immutable status through // the normal radio route; sampling never writes the delivered watch roster. class ERR_HQStatusSampler { protected SCR_AIGroup m_Group; protected Faction m_Faction; protected string m_SourceId; protected int m_Strength; protected int m_Deaths; protected bool m_Bound; protected float m_LastSampleAt = -1; protected ref array m_Members = {}; protected ref array m_Dead = {}; protected ref array m_Released = {}; protected ref array m_Agents = {}; bool Bound() { return m_Bound; } protected bool Eligible() { return Replication.IsServer() && m_Bound && m_Group && m_Faction && m_Group.GetPlayerCount(true) == 0 && !m_Group.IsSlave() && m_Group.GetFaction() == m_Faction; } bool Bind(SCR_AIGroup group, string sourceId, int initialStrength, Faction enemyFaction) { if (!Replication.IsServer()) return false; if (m_Bound) return Eligible() && group == m_Group && sourceId == m_SourceId && initialStrength == m_Strength && enemyFaction == m_Faction; if (!Replication.IsServer() || !group || !enemyFaction || sourceId.IsEmpty() || sourceId.Length() > 96 || initialStrength < 1 || initialStrength > 32 || group.GetPlayerCount(true) > 0 || group.IsSlave() || group.GetFaction() != enemyFaction) return false; m_Agents.Clear(); m_Members.Clear(); group.GetAgents(m_Agents); if (m_Agents.Count() != initialStrength) { m_Agents.Clear(); return false; } foreach (AIAgent agent : m_Agents) { if (!agent) { m_Members.Clear(); return false; } IEntity actor = agent.GetControlledEntity(); if (!ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group || SCR_FactionManager.SGetFaction(actor) != enemyFaction || m_Members.Contains(actor)) { m_Members.Clear(); return false; } m_Members.Insert(actor); } m_Group = group; m_SourceId = sourceId; m_Strength = initialStrength; m_Faction = enemyFaction; for (int i = 0; i < initialStrength; i++) { m_Dead.Insert(false); m_Released.Insert(false); } m_Agents.Clear(); m_Bound = true; return true; } // Call from the existing physical slow tick, not just each 180-second radio // cycle: after native corpse deletion, a null reference supplies no evidence. bool ObserveDeaths() { if (!Eligible()) return false; for (int i = 0; i < m_Members.Count(); i++) { if (m_Dead[i] || m_Released[i]) continue; IEntity actor = m_Members[i]; if (!actor) continue; if (ERR_Actors.Player(actor) || SCR_FactionManager.SGetFaction(actor) != m_Faction) { m_Released[i] = true; continue; } DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent)); if (!damage) 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; } return true; } ERR_HQStatusReport Sample(float now, int messageId, IEntity caller) { if (!ERR_HQPursuitRules.Finite(now) || now < 0 || now < m_LastSampleAt || messageId <= 0 || !ObserveDeaths()) return null; int callerIndex = m_Members.Find(caller); if (!caller || callerIndex < 0 || m_Dead[callerIndex] || m_Released[callerIndex] || ERR_Actors.Player(caller) || ERR_Actors.Group(caller) != m_Group || SCR_FactionManager.SGetFaction(caller) != m_Faction || ERR_HQRadio.Capability(caller) <= 0) return null; vector position = m_Group.GetOrigin(); if (!ERR_HQPursuitRules.PositionValid(position)) return null; 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 i = 0; i < m_Members.Count(); i++) { if (m_Dead[i] || m_Released[i]) continue; IEntity actor = m_Members[i]; if (!ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Group || SCR_FactionManager.SGetFaction(actor) != m_Faction) continue; report.Count++; if (ERR_Actors.CanFight(actor)) report.Fit++; } m_LastSampleAt = now; return report; } void Clear() { m_Group = null; m_Faction = null; m_SourceId = ""; m_Strength = 0; m_Deaths = 0; m_Bound = false; m_LastSampleAt = -1; m_Members.Clear(); m_Dead.Clear(); m_Released.Clear(); m_Agents.Clear(); } }