129 lines
5.9 KiB
C
129 lines
5.9 KiB
C
// 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<IEntity> m_Members = {};
|
|
protected ref array<bool> m_Dead = {};
|
|
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
|
|
&& 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;
|
|
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 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;
|
|
}
|
|
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 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;
|
|
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();
|
|
}
|
|
}
|