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

133 lines
6.2 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.AmmoState = new ERR_HQAmmoSnapshot();
report.AmmoState.Complete = true; report.AmmoState.LauncherComplete = true;
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)
{ report.AmmoState.Complete = false; report.AmmoState.LauncherComplete = false; continue; }
report.Count++;
ERR_HQAmmoSampler.Inspect(actor, m_Group, report.AmmoState);
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();
}
}