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

212 lines
8.0 KiB
C

// Adapted from the user-owned AI body's visual discovery rules only.
// Candidate casualty identities are local perception work, never delivered HQ knowledge.
class ERR_HQBodyCandidate
{
IEntity Corpse;
ref ERR_HQStatusSampler Roster;
string SourceId;
int Slot = -1;
int Strength;
bool Confirmed;
float ReportAfter;
}
class ERR_HQBodyScan
{
SCR_AIGroup Group;
float NextScan;
float LastAsked = -1;
int Cursor;
}
class ERR_HQBodyRules
{
static bool InView(float distance, float facing)
{
return ERR_HQMemoryRules.Finite(distance) && ERR_HQMemoryRules.Finite(facing)
&& distance >= 0 && distance <= 60 && (distance <= 12 || facing >= 0.5);
}
static bool Clear(float fraction, bool hitBody)
{
return ERR_HQMemoryRules.Finite(fraction) && fraction >= 0 && (fraction >= 1 || hitBody);
}
}
class ERR_HQBodyObserver
{
protected ref array<ref ERR_HQBodyCandidate> m_Roster = {};
protected ref array<ref ERR_HQBodyCandidate> m_Bodies = {};
protected ref array<ref ERR_HQBodyScan> m_Groups = {};
protected float m_NextBudget;
protected float m_Now = -1;
protected int m_Traces;
protected int m_RosterCursor;
protected bool m_CapLogged;
protected bool OwnedPhysical(SCR_AIGroup group)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !group || group.GetFaction() != mode.EnemyFaction()) return false;
ERR_ForceRecord force = mode.Forces().FindGroup(group);
return force && force.Physical && !force.Eliminated;
}
protected bool Dead(IEntity actor)
{
ChimeraCharacter character = ChimeraCharacter.Cast(actor);
return character && character.GetCharacterController()
&& character.GetCharacterController().GetLifeState() == ECharacterLifeState.DEAD;
}
protected void Cap()
{
if (m_CapLogged) return;
m_CapLogged = true;
Print("Rereintroduced [body observer]: finite local registry limit reached; no discovery work added.");
}
protected void Prune()
{
for (int i = m_Bodies.Count() - 1; i >= 0; i--)
if (!m_Bodies[i].Corpse || m_Bodies[i].Confirmed || !Dead(m_Bodies[i].Corpse)) m_Bodies.Remove(i);
for (int i = m_Roster.Count() - 1; i >= 0; i--)
if (!m_Roster[i].Corpse) m_Roster.Remove(i);
for (int i = m_Groups.Count() - 1; i >= 0; i--)
if (!m_Groups[i].Group) m_Groups.Remove(i);
}
void RegisterGroup(SCR_AIGroup group, ERR_HQStatusSampler sampler)
{
if (!Replication.IsServer() || !OwnedPhysical(group) || !sampler || !sampler.OwnsRoster(group)) return;
Prune();
bool known;
foreach (ERR_HQBodyScan scan : m_Groups) if (scan.Group == group) known = true;
if (!known)
{
if (m_Groups.Count() >= 128) { Cap(); return; }
ERR_HQBodyScan created = new ERR_HQBodyScan(); created.Group = group;
m_Groups.Insert(created);
}
// Slot identity comes only from the immutable sampler bind, never current agent order.
for (int member = 0; member < sampler.InitialStrength(); member++)
{
IEntity actor = sampler.InitialMember(member);
if (!sampler.CanAttribute(member, actor)) continue;
bool found;
foreach (ERR_HQBodyCandidate entry : m_Roster) if (entry.Corpse == actor) found = true;
if (found) continue;
if (m_Roster.Count() >= 128) { Cap(); return; }
ERR_HQBodyCandidate candidate = new ERR_HQBodyCandidate(); candidate.Corpse = actor;
candidate.Roster = sampler; candidate.SourceId = sampler.InitialSourceId();
candidate.Slot = member; candidate.Strength = sampler.InitialStrength();
m_Roster.Insert(candidate);
}
}
void TickBudget(float now)
{
if (!Replication.IsServer() || !ERR_HQMemoryRules.Finite(now) || now < 0 || now < m_Now) return;
m_Now = now;
if (now < m_NextBudget) return;
m_NextBudget = now + 5000;
m_Traces = 12;
Prune();
// No position reads, messages or callbacks here: death only makes a local LOS candidate.
// Rotate the finite candidate window so sixteen remote corpses cannot hide later losses.
m_Bodies.Clear();
int total = m_Roster.Count();
for (int visited = 0; visited < total; visited++)
{
if (m_Bodies.Count() >= 16) { Cap(); break; }
if (m_RosterCursor >= total) m_RosterCursor = 0;
ERR_HQBodyCandidate candidate = m_Roster[m_RosterCursor++];
if (!candidate.Corpse || candidate.Confirmed || m_Bodies.Contains(candidate) || !Dead(candidate.Corpse)) continue;
m_Bodies.Insert(candidate);
}
}
protected bool Visible(IEntity observer, IEntity body, out vector observedPosition)
{
if (m_Traces <= 0 || !observer || !body || !ERR_Actors.CanFight(observer)
|| CompartmentAccessComponent.GetVehicleIn(observer)) return false;
vector bodyPosition = body.GetOrigin();
vector observerPosition = observer.GetOrigin();
vector delta = bodyPosition - observerPosition;
float distance = delta.Length();
delta[1] = 0;
vector transform[4]; observer.GetWorldTransform(transform);
vector forward = transform[2]; forward[1] = 0;
float facing;
if (delta.Length() > 0.01 && forward.Length() > 0.01)
facing = vector.Dot(forward.Normalized(), delta.Normalized());
if (!ERR_HQBodyRules.InView(distance, facing)) return false;
TraceParam trace = new TraceParam();
trace.Start = observerPosition + "0 1.5 0";
trace.End = bodyPosition + "0 0.3 0";
trace.Exclude = observer;
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
m_Traces--;
float fraction = GetGame().GetWorld().TraceMove(trace, null);
if (!ERR_HQBodyRules.Clear(fraction, trace.TraceEnt == body)) return false;
observedPosition = bodyPosition;
return true;
}
protected ERR_HQBodyScan OldestDue(float now)
{
ERR_HQBodyScan selected;
foreach (ERR_HQBodyScan scan : m_Groups)
{
// Recently requesting groups share turns; inactive callers cannot reserve work forever.
if (!scan.Group || now < scan.NextScan || scan.LastAsked < 0 || now - scan.LastAsked > 5000) continue;
if (!OwnedPhysical(scan.Group) || scan.Group.GetPlayerCount(true) > 0) continue;
if (!selected || scan.NextScan < selected.NextScan) selected = scan;
}
return selected;
}
bool Observe(SCR_AIGroup group, float now, out ERR_HQReport report)
{
report = null;
if (!Replication.IsServer() || !ERR_HQMemoryRules.Finite(now) || now < m_Now || now < 0) return false;
TickBudget(now);
if (!OwnedPhysical(group) || group.GetPlayerCount(true) > 0 || m_Bodies.IsEmpty()) return false;
ERR_HQBodyScan state;
foreach (ERR_HQBodyScan scan : m_Groups) if (scan.Group == group) state = scan;
if (!state) return false;
state.LastAsked = now;
if (now < state.NextScan || m_Traces <= 0 || OldestDue(now) != state) return false;
state.NextScan = now + 15000;
array<AIAgent> agents = {}; group.GetAgents(agents);
int count = m_Bodies.Count();
for (int visited = 0; visited < count; visited++)
{
if (state.Cursor >= count) state.Cursor = 0;
ERR_HQBodyCandidate candidate = m_Bodies[state.Cursor++];
if (!candidate.Corpse || candidate.Confirmed || now < candidate.ReportAfter || !Dead(candidate.Corpse)) continue;
if (!candidate.Roster || !candidate.Roster.CanAttribute(candidate.Slot, candidate.Corpse)) continue;
for (int member = 0; member < agents.Count() && member < 32; member++)
{
if (m_Traces <= 0) return false;
AIAgent agent = agents[member];
if (!agent) continue;
vector position;
if (!Visible(agent.GetControlledEntity(), candidate.Corpse, position)) continue;
report = new ERR_HQReport();
report.Target = candidate.Corpse; report.Source = group;
report.Position = position; report.ObservedAt = now; report.TravelMode = "body";
report.CasualtySourceId = candidate.SourceId; report.CasualtySlot = candidate.Slot;
report.CasualtyStrength = candidate.Strength;
// Local throttling only. A lost radio call can be retried after fresh LOS.
candidate.ReportAfter = now + 15000;
return true;
}
}
return false;
}
// Call only after the radio-delivered immutable body attribution was accepted by HQ.
void ConfirmDelivered(IEntity body)
{
if (!Replication.IsServer() || !body) return;
foreach (ERR_HQBodyCandidate candidate : m_Roster)
if (candidate.Corpse == body) candidate.Confirmed = true;
}
void Stop()
{
m_Roster.Clear(); m_Bodies.Clear(); m_Groups.Clear();
m_Traces = 0; m_Now = -1; m_NextBudget = 0; m_RosterCursor = 0; m_CapLogged = false;
}
}