212 lines
8.0 KiB
C
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;
|
|
}
|
|
}
|