Build independent Everon successor foundation and native regressions
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
class ERR_HQUnit
|
||||
{
|
||||
int Id;
|
||||
SCR_AIGroup Group;
|
||||
SCR_AIGroupUtilityComponent Utility;
|
||||
IEntity Radio;
|
||||
float NextReport;
|
||||
ref ERR_HQRoute Uplink;
|
||||
ref ERR_HQReport PendingReport;
|
||||
ref ERR_HQRoute Downlink;
|
||||
ref ERR_HQReport PendingOrder;
|
||||
ref ERR_HQReport Assignment;
|
||||
AIWaypoint Waypoint;
|
||||
|
||||
bool Eligible(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
|
||||
array<AIAgent> agents = {};
|
||||
Group.GetAgents(agents);
|
||||
bool survivor;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) continue;
|
||||
survivor = true;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(agent.GetControlledEntity())) return false;
|
||||
}
|
||||
return survivor;
|
||||
}
|
||||
|
||||
void Release()
|
||||
{
|
||||
if (Waypoint)
|
||||
{
|
||||
if (Group) Group.RemoveWaypoint(Waypoint);
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
|
||||
}
|
||||
Waypoint = null;
|
||||
Assignment = null;
|
||||
PendingOrder = null;
|
||||
Downlink = null;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQ : ERR_CommandService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_HQUnit> m_Units = {};
|
||||
protected ref array<ref ERR_HQReport> m_Contacts = {};
|
||||
protected ref ERR_HQNetwork m_Network = new ERR_HQNetwork();
|
||||
protected ref ERR_HQJournal m_Journal = new ERR_HQJournal();
|
||||
protected int m_NextId;
|
||||
protected float m_NextNetwork;
|
||||
protected float m_NextAssignment;
|
||||
|
||||
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; }
|
||||
ERR_HQJournal Journal() { return m_Journal; }
|
||||
string Status()
|
||||
{
|
||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned());
|
||||
}
|
||||
|
||||
override void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group || m_Units.Count() >= 128) return;
|
||||
foreach (ERR_HQUnit known : m_Units) if (known.Group == group) return;
|
||||
SCR_AIGroupUtilityComponent utility = SCR_AIGroupUtilityComponent.Cast(group.FindComponent(SCR_AIGroupUtilityComponent));
|
||||
if (!utility || group.GetFaction() != m_Mode.EnemyFaction()) return;
|
||||
ERR_HQUnit unit = new ERR_HQUnit();
|
||||
unit.Id = ++m_NextId;
|
||||
unit.Group = group;
|
||||
unit.Utility = utility;
|
||||
int phase = unit.Id % 5;
|
||||
unit.NextReport = GetGame().GetWorld().GetWorldTime() + phase * 1000;
|
||||
m_Units.Insert(unit);
|
||||
m_Journal.Write("REGISTER", string.Format("group %1", unit.Id));
|
||||
}
|
||||
|
||||
override void UnregisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
unit.Release();
|
||||
unit.Group = null;
|
||||
unit.Utility = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected int Assigned()
|
||||
{
|
||||
int total;
|
||||
foreach (ERR_HQUnit unit : m_Units) if (unit.Assignment || unit.PendingOrder) total++;
|
||||
return total;
|
||||
}
|
||||
|
||||
protected void IndexRadios()
|
||||
{
|
||||
m_Network.Clear();
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
unit.Radio = null;
|
||||
if (!unit.Group || unit.Group.GetPlayerCount(true) > 0 || unit.Group.GetFaction() != m_Mode.EnemyFaction()) continue;
|
||||
array<AIAgent> agents = {};
|
||||
unit.Group.GetAgents(agents);
|
||||
int best;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
int capability = ERR_HQRadio.Capability(actor);
|
||||
if (capability <= 0) continue;
|
||||
m_Network.Add(actor, unit.Group, unit.Id, capability);
|
||||
if (capability > best) { unit.Radio = actor; best = capability; }
|
||||
}
|
||||
}
|
||||
m_Network.Build();
|
||||
}
|
||||
|
||||
protected void Observe(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (!unit.Utility || !unit.Radio || unit.Uplink || now < unit.NextReport) return;
|
||||
unit.NextReport = now + m_Mode.Settings().Get("reportIntervalSeconds") * 1000;
|
||||
array<ref ERR_HQVisualContact> samples = {};
|
||||
ERR_HQObservation.Gather(unit.Utility, now, samples);
|
||||
if (samples.IsEmpty()) return;
|
||||
ERR_HQVisualContact seen = samples[0];
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!route) return;
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
report.MessageId = m_Journal.Message();
|
||||
report.Target = seen.Entity;
|
||||
report.Source = unit.Group;
|
||||
report.Position = seen.Position;
|
||||
report.ObservedAt = seen.ObservedAt;
|
||||
ERR_HQObservation.Describe(report, samples, now);
|
||||
route.MessageId = report.MessageId;
|
||||
route.Payload = report.Description(now);
|
||||
unit.PendingReport = report;
|
||||
unit.Uplink = route;
|
||||
m_Journal.Write("REPORT", route.Payload, report.MessageId);
|
||||
}
|
||||
|
||||
protected void Accept(ERR_HQReport report, float now)
|
||||
{
|
||||
if (!report || !ERR_MissionRules.ContactFresh(report.ObservedAt, now)) return;
|
||||
for (int i = 0; i < m_Contacts.Count(); i++)
|
||||
{
|
||||
ERR_HQReport previous = m_Contacts[i];
|
||||
if (previous.Target != report.Target) continue;
|
||||
if (previous.ObservedAt >= report.ObservedAt) return;
|
||||
float seconds = (report.ObservedAt - previous.ObservedAt) / 1000;
|
||||
if (seconds > 0 && seconds < 30 && vector.DistanceXZ(previous.Position, report.Position) < 500)
|
||||
{
|
||||
report.Velocity = (report.Position - previous.Position) / seconds;
|
||||
report.HasVelocity = report.Velocity.Length() <= 100;
|
||||
}
|
||||
m_Contacts[i] = report.Snapshot();
|
||||
return;
|
||||
}
|
||||
if (m_Contacts.Count() >= 64) m_Contacts.Remove(0);
|
||||
m_Contacts.Insert(report.Snapshot());
|
||||
}
|
||||
|
||||
protected void DeliverOrder(ERR_HQUnit unit, float now)
|
||||
{
|
||||
ERR_HQReport order = unit.PendingOrder;
|
||||
if (!order || !ERR_MissionRules.ContactFresh(order.ObservedAt, now) || !unit.Eligible(m_Mode)) { unit.Release(); return; }
|
||||
SCR_AIGroupPerception perception = unit.Utility.m_Perception;
|
||||
if (perception && !perception.m_aTargets.IsEmpty()) { unit.Release(); return; }
|
||||
unit.Release();
|
||||
vector position = order.Position;
|
||||
if (order.HasVelocity) position = ERR_HQReport.Predict(position, order.Velocity, (now - order.ObservedAt) / 1000);
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_FactionCatalog.SearchWaypoint(), ERR_Actors.Ground(position)));
|
||||
if (!waypoint) { m_Journal.Write("ORDER FAILED", "waypoint spawn failed", order.MessageId); return; }
|
||||
unit.Waypoint = waypoint;
|
||||
unit.Assignment = order;
|
||||
unit.Group.AddWaypointAt(waypoint, 0);
|
||||
m_Journal.Write("ORDER DELIVERED", string.Format("group %1: %2", unit.Id, order.Description(now)), order.MessageId);
|
||||
}
|
||||
|
||||
protected void Assign(float now)
|
||||
{
|
||||
int limit = m_Mode.Settings().Get("supportSquadLimit");
|
||||
int radius = m_Mode.Settings().Get("responseRadiusMetres");
|
||||
int assigned = Assigned();
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Assignment && (!ERR_MissionRules.ContactFresh(unit.Assignment.ObservedAt, now) || !unit.Eligible(m_Mode) || assigned > limit))
|
||||
{
|
||||
unit.Release(); assigned--;
|
||||
}
|
||||
if (unit.PendingOrder && (!ERR_MissionRules.ContactFresh(unit.PendingOrder.ObservedAt, now) || !unit.Eligible(m_Mode) || assigned > limit))
|
||||
{
|
||||
unit.Release(); assigned--;
|
||||
}
|
||||
}
|
||||
foreach (ERR_HQReport report : m_Contacts)
|
||||
{
|
||||
if (!ERR_MissionRules.ContactFresh(report.ObservedAt, now)) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (assigned >= limit) return;
|
||||
if (unit.Assignment || unit.PendingOrder || !unit.Eligible(m_Mode) || !unit.Radio) continue;
|
||||
if (vector.DistanceXZ(unit.Group.GetOrigin(), report.Position) > radius) continue;
|
||||
SCR_AIGroupPerception perception = unit.Utility.m_Perception;
|
||||
if (perception && !perception.m_aTargets.IsEmpty()) continue;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
unit.PendingOrder = report.Snapshot();
|
||||
unit.Downlink = route;
|
||||
route.MessageId = m_Journal.Message();
|
||||
route.Payload = "order from report M" + report.MessageId.ToString();
|
||||
assigned++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (now >= m_NextNetwork) { IndexRadios(); m_NextNetwork = now + 5000; }
|
||||
for (int i = m_Contacts.Count() - 1; i >= 0; i--)
|
||||
if (!ERR_MissionRules.ContactFresh(m_Contacts[i].ObservedAt, now)) m_Contacts.Remove(i);
|
||||
for (int i = m_Units.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
ERR_HQUnit unit = m_Units[i];
|
||||
if (unit.Uplink)
|
||||
{
|
||||
int delivered = unit.Uplink.Advance(now, m_Journal);
|
||||
if (delivered == 1) Accept(unit.PendingReport, now);
|
||||
if (delivered != 0) { unit.Uplink = null; unit.PendingReport = null; }
|
||||
}
|
||||
if (unit.Downlink)
|
||||
{
|
||||
int delivered = unit.Downlink.Advance(now, m_Journal);
|
||||
if (delivered == 1) DeliverOrder(unit, now);
|
||||
else if (delivered < 0) unit.Release();
|
||||
}
|
||||
if (!unit.Group && !unit.Uplink) { unit.Release(); m_Units.Remove(i); continue; }
|
||||
Observe(unit, now);
|
||||
}
|
||||
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
|
||||
m_Journal.Flush();
|
||||
}
|
||||
|
||||
override void Stop()
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units) unit.Release();
|
||||
m_Units.Clear();
|
||||
m_Contacts.Clear();
|
||||
m_Network.Clear();
|
||||
m_Journal.Write("STOP", "HQ stopped; tactical orders released");
|
||||
m_Journal.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected override ERR_CommandService CreateCommandService() { return new ERR_HQ(this); }
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Server-owned audit stream. The file contains every event; chat reads a bounded
|
||||
// recent-history window with explicit gap notices for clients that fall behind.
|
||||
class ERR_HQJournal
|
||||
{
|
||||
protected ref array<string> m_Recent = {};
|
||||
protected ref array<string> m_Pending = {};
|
||||
protected int m_Sequence;
|
||||
protected int m_Message;
|
||||
protected static int s_Part;
|
||||
protected static int s_Session;
|
||||
protected string m_Session;
|
||||
|
||||
void ERR_HQJournal()
|
||||
{
|
||||
s_Session++;
|
||||
m_Session = string.Format("%1-%2", System.GetUnixTime(), s_Session);
|
||||
}
|
||||
protected float m_RetryAt;
|
||||
protected int m_Lost;
|
||||
|
||||
string Path()
|
||||
{
|
||||
if (s_Part == 0)
|
||||
return "$logs:rereintroduced-hq.log";
|
||||
return "$logs:rereintroduced-hq-" + s_Part.ToString() + ".log";
|
||||
}
|
||||
|
||||
int Message()
|
||||
{
|
||||
return ++m_Message;
|
||||
}
|
||||
|
||||
int Sequence()
|
||||
{
|
||||
return m_Sequence;
|
||||
}
|
||||
|
||||
void Write(string kind, string message, int messageId = 0)
|
||||
{
|
||||
if (!Replication.IsServer())
|
||||
return;
|
||||
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
|
||||
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
|
||||
if (m_Recent.Count() >= 300)
|
||||
m_Recent.Remove(0);
|
||||
m_Recent.Insert(line);
|
||||
if (m_Pending.Count() < 4096)
|
||||
m_Pending.Insert(line);
|
||||
else
|
||||
m_Lost++;
|
||||
}
|
||||
|
||||
void Read(int after, out array<string> lines, out int cursor, int limit = 8)
|
||||
{
|
||||
lines.Clear();
|
||||
cursor = Math.Clamp(after, 0, m_Sequence);
|
||||
int first = m_Sequence - m_Recent.Count();
|
||||
if (cursor < first)
|
||||
{
|
||||
lines.Insert(string.Format("HQ log: %1 older entries are outside chat history; see %2", first - cursor, Path()));
|
||||
cursor = first;
|
||||
}
|
||||
int characters;
|
||||
for (int i = cursor - first; i < m_Recent.Count() && lines.Count() < Math.Clamp(limit, 1, 40); i++)
|
||||
{
|
||||
// Bound each menu RPC without skipping unread records. Chat still
|
||||
// requests eight; the live menu can drain a busy journal faster.
|
||||
if (characters + m_Recent[i].Length() > 16000 && !lines.IsEmpty())
|
||||
break;
|
||||
lines.Insert(m_Recent[i]);
|
||||
characters += m_Recent[i].Length();
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (m_Pending.IsEmpty() || !Replication.IsServer())
|
||||
return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now < m_RetryAt)
|
||||
return;
|
||||
FileHandle file = FileIO.OpenFile(Path(), FileMode.APPEND);
|
||||
if (!file)
|
||||
{
|
||||
m_RetryAt = now + 30000;
|
||||
PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
foreach (string line : m_Pending)
|
||||
file.WriteLine(line);
|
||||
if (m_Lost > 0)
|
||||
file.WriteLine(string.Format("HQ log: %1 entries lost while file was unavailable and buffer full", m_Lost));
|
||||
bool rotate = file.GetLength() >= 5242880;
|
||||
file.Close();
|
||||
m_Pending.Clear();
|
||||
m_Lost = 0;
|
||||
if (rotate)
|
||||
s_Part++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Radio propagation is simulated using actual living carriers and inventory.
|
||||
// A manpack is the HQ uplink; short-range hops are limited to 500 m and three
|
||||
// retransmissions. No virtual relay is fabricated when the network is broken.
|
||||
class ERR_HQRadioNode
|
||||
{
|
||||
IEntity Carrier;
|
||||
SCR_AIGroup Group;
|
||||
int UnitId;
|
||||
int Capability;
|
||||
protected float m_CheckedAt = -1;
|
||||
protected int m_LiveCapability;
|
||||
|
||||
bool Valid(bool manpack = false)
|
||||
{
|
||||
ERR_GameMode factions = ERR_GameMode.Get();
|
||||
if (!Carrier || !Group || Group.GetPlayerCount(true) > 0 || !factions || Group.GetFaction() != factions.EnemyFaction() || ERR_Actors.Group(Carrier) != Group)
|
||||
return false;
|
||||
int required = 1;
|
||||
if (manpack)
|
||||
required = 2;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (m_CheckedAt < 0 || now < m_CheckedAt || now - m_CheckedAt >= 1000)
|
||||
{
|
||||
m_CheckedAt = now;
|
||||
m_LiveCapability = ERR_HQRadio.Capability(Carrier);
|
||||
}
|
||||
return m_LiveCapability >= required;
|
||||
}
|
||||
|
||||
string Label()
|
||||
{
|
||||
return "unit " + UnitId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQRoute
|
||||
{
|
||||
ref array<ref ERR_HQRadioNode> Nodes = {};
|
||||
bool Downlink;
|
||||
int Cursor;
|
||||
bool Sending;
|
||||
float Due;
|
||||
vector SentFrom;
|
||||
float StartedAt = -1;
|
||||
int MessageId;
|
||||
string Payload;
|
||||
bool Failed;
|
||||
|
||||
// -1 failed, 0 travelling, 1 delivered. Each hop really gates delivery.
|
||||
int Advance(float now, ERR_HQJournal journal)
|
||||
{
|
||||
if (Failed)
|
||||
return -1;
|
||||
if (StartedAt < 0)
|
||||
{
|
||||
StartedAt = now;
|
||||
if (Downlink)
|
||||
Cursor = -1; // first leg is HQ to the manpack
|
||||
}
|
||||
if (now - StartedAt > 30000)
|
||||
return Fail(journal, "route timed out");
|
||||
int target = Cursor + 1;
|
||||
if (Sending)
|
||||
{
|
||||
if (now < Due)
|
||||
return 0;
|
||||
if (target < Nodes.Count())
|
||||
{
|
||||
if (!Nodes[target].Valid(Downlink && target == 0))
|
||||
return Fail(journal, "receiving carrier lost radio, died or changed ownership");
|
||||
if (Cursor >= 0 && vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin()) > 500)
|
||||
return Fail(journal, "receiver moved beyond the 500m link");
|
||||
journal.Write("RECEIVE", Nodes[target].Label() + " received from " + SenderLabel(), MessageId);
|
||||
}
|
||||
else
|
||||
journal.Write("RECEIVE", "HQ received from " + SenderLabel(), MessageId);
|
||||
Cursor++;
|
||||
Sending = false;
|
||||
if ((!Downlink && Cursor >= Nodes.Count()) || (Downlink && Cursor == Nodes.Count() - 1))
|
||||
return 1;
|
||||
target = Cursor + 1;
|
||||
}
|
||||
if (Nodes.IsEmpty())
|
||||
return Fail(journal, "no physical radio route");
|
||||
if (Cursor >= 0 && !Nodes[Cursor].Valid(!Downlink && Cursor == Nodes.Count() - 1))
|
||||
return Fail(journal, "transmitting carrier lost radio, died or changed ownership");
|
||||
string recipient = "HQ";
|
||||
string link = "long-range";
|
||||
if (target < Nodes.Count())
|
||||
{
|
||||
if (!Nodes[target].Valid(Downlink && target == 0))
|
||||
return Fail(journal, "next relay unavailable");
|
||||
recipient = Nodes[target].Label();
|
||||
if (Cursor >= 0)
|
||||
{
|
||||
SentFrom = Nodes[Cursor].Carrier.GetOrigin();
|
||||
float distance = vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin());
|
||||
if (distance > 500)
|
||||
return Fail(journal, "next relay beyond 500m");
|
||||
link = Math.Round(distance).ToString() + "m short-range";
|
||||
}
|
||||
}
|
||||
journal.Write("TRANSMIT", SenderLabel() + " -> " + recipient + " (" + link + ")", MessageId);
|
||||
Sending = true;
|
||||
Due = now + 2000;
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected string SenderLabel()
|
||||
{
|
||||
if (Cursor < 0)
|
||||
return "HQ";
|
||||
return Nodes[Cursor].Label();
|
||||
}
|
||||
|
||||
protected int Fail(ERR_HQJournal journal, string reason)
|
||||
{
|
||||
Failed = true;
|
||||
journal.Write("LOST", reason + "; " + Payload, MessageId);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQNetwork
|
||||
{
|
||||
protected ref array<ref ERR_HQRadioNode> m_Nodes = {};
|
||||
protected ref array<int> m_Parents = {};
|
||||
protected ref array<int> m_Depths = {};
|
||||
protected ref map<IEntity, int> m_Indices = new map<IEntity, int>();
|
||||
protected ref map<string, ref array<int>> m_Cells = new map<string, ref array<int>>();
|
||||
bool Overflow;
|
||||
|
||||
void Clear()
|
||||
{
|
||||
m_Nodes.Clear();
|
||||
m_Parents.Clear();
|
||||
m_Depths.Clear();
|
||||
m_Indices.Clear();
|
||||
m_Cells.Clear();
|
||||
Overflow = false;
|
||||
}
|
||||
|
||||
protected string Cell(int x, int z)
|
||||
{
|
||||
return string.Format("%1:%2", x, z);
|
||||
}
|
||||
|
||||
void Add(IEntity carrier, SCR_AIGroup group, int unitId, int capability)
|
||||
{
|
||||
if (m_Indices.Contains(carrier))
|
||||
return;
|
||||
if (m_Nodes.Count() >= 2048)
|
||||
{
|
||||
Overflow = true;
|
||||
return;
|
||||
}
|
||||
ERR_HQRadioNode node = new ERR_HQRadioNode();
|
||||
node.Carrier = carrier;
|
||||
node.Group = group;
|
||||
node.UnitId = unitId;
|
||||
node.Capability = capability;
|
||||
int index = m_Nodes.Count();
|
||||
m_Nodes.Insert(node);
|
||||
m_Indices.Insert(carrier, index);
|
||||
m_Parents.Insert(-2);
|
||||
m_Depths.Insert(99);
|
||||
vector position = carrier.GetOrigin();
|
||||
string key = Cell(Math.Floor(position[0] / 500), Math.Floor(position[2] / 500));
|
||||
array<int> cell = m_Cells.Get(key);
|
||||
if (!cell)
|
||||
{
|
||||
cell = {};
|
||||
m_Cells.Insert(key, cell);
|
||||
}
|
||||
cell.Insert(index);
|
||||
}
|
||||
|
||||
void Build()
|
||||
{
|
||||
array<int> queue = {};
|
||||
int checks;
|
||||
for (int root = 0; root < m_Nodes.Count(); root++)
|
||||
{
|
||||
if (m_Nodes[root].Capability == 2)
|
||||
{
|
||||
m_Parents[root] = -1;
|
||||
m_Depths[root] = 0;
|
||||
queue.Insert(root);
|
||||
}
|
||||
}
|
||||
for (int head = 0; head < queue.Count(); head++)
|
||||
{
|
||||
int from = queue[head];
|
||||
if (m_Depths[from] >= 3)
|
||||
continue;
|
||||
vector position = m_Nodes[from].Carrier.GetOrigin();
|
||||
int x = Math.Floor(position[0] / 500);
|
||||
int z = Math.Floor(position[2] / 500);
|
||||
for (int dx = -1; dx <= 1; dx++)
|
||||
{
|
||||
for (int dz = -1; dz <= 1; dz++)
|
||||
{
|
||||
array<int> neighbors = m_Cells.Get(Cell(x + dx, z + dz));
|
||||
if (!neighbors)
|
||||
continue;
|
||||
foreach (int candidate : neighbors)
|
||||
{
|
||||
if (++checks > 100000)
|
||||
{
|
||||
Overflow = true;
|
||||
return;
|
||||
}
|
||||
if (m_Parents[candidate] != -2 || vector.DistanceXZ(position, m_Nodes[candidate].Carrier.GetOrigin()) > 500)
|
||||
continue;
|
||||
m_Parents[candidate] = from;
|
||||
m_Depths[candidate] = m_Depths[from] + 1;
|
||||
queue.Insert(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ERR_HQRoute Route(IEntity caller, bool downlink = false)
|
||||
{
|
||||
int index;
|
||||
if (!m_Indices.Find(caller, index) || m_Parents[index] == -2)
|
||||
return null;
|
||||
ERR_HQRoute route = new ERR_HQRoute();
|
||||
route.Downlink = downlink;
|
||||
while (index >= 0)
|
||||
{
|
||||
if (downlink)
|
||||
route.Nodes.InsertAt(m_Nodes[index], 0);
|
||||
else
|
||||
route.Nodes.Insert(m_Nodes[index]);
|
||||
index = m_Parents[index];
|
||||
}
|
||||
return route;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// These samples exist only at intake. HQ never revisits their entity loadouts.
|
||||
class ERR_HQVisualContact
|
||||
{
|
||||
IEntity Entity;
|
||||
vector Position;
|
||||
float ObservedAt;
|
||||
bool Escapist;
|
||||
string Travel = "unknown";
|
||||
string Weapon = "unknown";
|
||||
}
|
||||
|
||||
class ERR_HQObservation
|
||||
{
|
||||
// Require current native line of sight before reading visible equipment.
|
||||
static bool IsClearSighting(float age, float traceFraction, float exposure)
|
||||
{
|
||||
return age >= 0 && age <= 0.5 && traceFraction >= 1 && exposure > 0;
|
||||
}
|
||||
|
||||
static bool CanIdentifyWeapon(float distance, float exposure)
|
||||
{
|
||||
return distance >= 0 && distance <= 150 && exposure >= 0.5;
|
||||
}
|
||||
|
||||
static string WeaponCategory(EWeaponType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case EWeaponType.WT_HANDGUN: return "pistol";
|
||||
case EWeaponType.WT_RIFLE: return "rifle";
|
||||
case EWeaponType.WT_SNIPERRIFLE: return "sniper rifle";
|
||||
case EWeaponType.WT_MACHINEGUN: return "machine gun";
|
||||
case EWeaponType.WT_ROCKETLAUNCHER: return "rocket launcher";
|
||||
case EWeaponType.WT_GRENADELAUNCHER: return "grenade launcher";
|
||||
case EWeaponType.WT_FRAGGRENADE: return "grenade";
|
||||
case EWeaponType.WT_SMOKEGRENADE: return "smoke grenade";
|
||||
}
|
||||
return "unidentified weapon";
|
||||
}
|
||||
|
||||
static ERR_HQVisualContact Capture(BaseTarget target, float now)
|
||||
{
|
||||
if (!target || target.GetTargetCategory() != ETargetCategory.ENEMY || !IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
|
||||
return null;
|
||||
DamageManagerComponent damage = target.GetDamageManagerComponent();
|
||||
if (damage && damage.IsDestroyed())
|
||||
return null;
|
||||
IEntity entity = target.GetTargetEntity();
|
||||
if (!entity)
|
||||
return null;
|
||||
ERR_HQVisualContact seen = new ERR_HQVisualContact();
|
||||
seen.Entity = entity;
|
||||
seen.Position = target.GetLastSeenPosition();
|
||||
seen.ObservedAt = now - target.GetTimeSinceSeen() * 1000;
|
||||
SCR_ChimeraCharacter character = SCR_ChimeraCharacter.Cast(entity);
|
||||
if (character)
|
||||
{
|
||||
if (GetGame().GetPlayerManager())
|
||||
seen.Escapist = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(entity) > 0;
|
||||
seen.Travel = "on foot";
|
||||
if (CompartmentAccessComponent.GetVehicleIn(entity))
|
||||
seen.Travel = "in vehicle";
|
||||
// Read only the drawn weapon; never enumerate weapons or inventory.
|
||||
if (CanIdentifyWeapon(target.GetDistance(), target.GetExposure()))
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(entity.FindComponent(CharacterControllerComponent));
|
||||
if (controller)
|
||||
{
|
||||
IEntity heldItem = controller.GetCurrentItemInHands();
|
||||
BaseWeaponComponent weapon;
|
||||
if (heldItem)
|
||||
weapon = BaseWeaponComponent.Cast(heldItem.FindComponent(BaseWeaponComponent));
|
||||
seen.Weapon = "none drawn";
|
||||
if (weapon)
|
||||
seen.Weapon = WeaponCategory(weapon.GetWeaponType());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Vehicle.Cast(entity))
|
||||
{
|
||||
seen.Travel = "vehicle";
|
||||
if (target.GetUnitType() == EAIUnitType.UnitType_Aircraft)
|
||||
seen.Travel = "aircraft";
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
static void Gather(SCR_AIGroupUtilityComponent utility, float now, out array<ref ERR_HQVisualContact> samples)
|
||||
{
|
||||
samples.Clear();
|
||||
if (!utility)
|
||||
return;
|
||||
array<BaseTarget> visible = {};
|
||||
utility.ERR_HQGetVisualTargets(visible);
|
||||
foreach (BaseTarget target : visible)
|
||||
{
|
||||
ERR_HQVisualContact seen = Capture(target, now);
|
||||
if (!seen)
|
||||
continue;
|
||||
ERR_HQVisualContact existing;
|
||||
foreach (ERR_HQVisualContact sample : samples)
|
||||
{
|
||||
if (sample.Entity == seen.Entity)
|
||||
{
|
||||
existing = sample;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!existing)
|
||||
{
|
||||
if (samples.Count() < 64)
|
||||
samples.Insert(seen);
|
||||
}
|
||||
else if (seen.ObservedAt > existing.ObservedAt || (seen.ObservedAt == existing.ObservedAt && existing.Weapon == "unknown" && seen.Weapon != "unknown"))
|
||||
{
|
||||
existing.Position = seen.Position;
|
||||
existing.ObservedAt = seen.ObservedAt;
|
||||
existing.Travel = seen.Travel;
|
||||
existing.Weapon = seen.Weapon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void Describe(ERR_HQReport report, array<ref ERR_HQVisualContact> samples, float now)
|
||||
{
|
||||
// New observations must not silently inherit old equipment/travel details.
|
||||
report.SeenEscapists = -1;
|
||||
report.TravelMode = "unknown";
|
||||
report.SeenWeapons = "unknown";
|
||||
report.DetailsObservedAt = -1;
|
||||
if (report.Distress)
|
||||
return;
|
||||
ERR_HQVisualContact anchor;
|
||||
foreach (ERR_HQVisualContact sample : samples)
|
||||
{
|
||||
if (sample.Entity == report.Target && now - sample.ObservedAt >= 0 && now - sample.ObservedAt <= 500)
|
||||
{
|
||||
anchor = sample;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anchor)
|
||||
return;
|
||||
report.DetailsObservedAt = anchor.ObservedAt;
|
||||
report.TravelMode = anchor.Travel;
|
||||
report.SeenWeapons = anchor.Weapon;
|
||||
array<IEntity> counted = {};
|
||||
array<string> weapons = {};
|
||||
string travel;
|
||||
foreach (ERR_HQVisualContact sighting : samples)
|
||||
{
|
||||
if (!sighting.Escapist || !sighting.Entity || counted.Contains(sighting.Entity) || now - sighting.ObservedAt < 0 || now - sighting.ObservedAt > 500 || vector.DistanceXZ(anchor.Position, sighting.Position) > 100)
|
||||
continue;
|
||||
counted.Insert(sighting.Entity);
|
||||
if (travel.IsEmpty())
|
||||
travel = sighting.Travel;
|
||||
else if (travel != sighting.Travel)
|
||||
travel = "mixed travel";
|
||||
if (sighting.Weapon != "unknown" && !weapons.Contains(sighting.Weapon) && weapons.Count() < 4)
|
||||
weapons.Insert(sighting.Weapon);
|
||||
}
|
||||
if (counted.IsEmpty())
|
||||
return; // A visible vehicle does not reveal its hidden occupants.
|
||||
report.SeenEscapists = counted.Count();
|
||||
// A vehicle anchor remains a vehicle report, even with people near it.
|
||||
if (anchor.Escapist)
|
||||
report.TravelMode = travel;
|
||||
report.SeenWeapons = "unknown";
|
||||
if (!weapons.IsEmpty())
|
||||
{
|
||||
report.SeenWeapons = weapons[0];
|
||||
for (int i = 1; i < weapons.Count(); i++)
|
||||
report.SeenWeapons += ", " + weapons[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIGroupUtilityComponent
|
||||
{
|
||||
void ERR_HQGetVisualTargets(out array<BaseTarget> targets)
|
||||
{
|
||||
targets.Clear();
|
||||
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
|
||||
{
|
||||
if (!info || !info.m_Perception)
|
||||
continue;
|
||||
AIAgent observer = AIAgent.Cast(info.GetOwner());
|
||||
if (!observer || !ERR_Actors.CanFight(observer.GetControlledEntity()))
|
||||
continue;
|
||||
array<BaseTarget> own = {};
|
||||
info.m_Perception.GetTargetsList(own, ETargetCategory.ENEMY);
|
||||
foreach (BaseTarget target : own)
|
||||
{
|
||||
if (target && ERR_HQObservation.IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
|
||||
targets.Insert(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// A radio's category is protected in the stock gadget component.
|
||||
modded class SCR_RadioComponent
|
||||
{
|
||||
bool ERR_IsHQManpack()
|
||||
{
|
||||
return m_iRadioCategory == ERadioCategory.MANPACK;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQRadio
|
||||
{
|
||||
// 0: no radio, 1: basic radio, 2: manpack. Inventory is checked live.
|
||||
static int Capability(IEntity character)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(character))
|
||||
return 0;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory)
|
||||
return 0;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
int result;
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent))
|
||||
continue;
|
||||
result = 1;
|
||||
SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (gadget && gadget.ERR_IsHQManpack())
|
||||
return 2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// HQ uses entity identity only for matching observations, never for its origin.
|
||||
// Visible details are captured at intake and copied as scalar snapshots.
|
||||
// Reports and orders contain immutable observed coordinates and timestamps.
|
||||
class ERR_HQReport
|
||||
{
|
||||
int MessageId;
|
||||
IEntity Target;
|
||||
SCR_AIGroup Source;
|
||||
vector Position;
|
||||
vector Velocity;
|
||||
float ObservedAt = -1;
|
||||
float LastSent = -60000;
|
||||
float PerceptionStamp = -1;
|
||||
bool HasVelocity;
|
||||
bool Engaged;
|
||||
bool Distress;
|
||||
int SeenEscapists = -1;
|
||||
string TravelMode = "unknown";
|
||||
string SeenWeapons = "unknown";
|
||||
float DetailsObservedAt = -1;
|
||||
|
||||
ERR_HQReport Snapshot()
|
||||
{
|
||||
ERR_HQReport copy = new ERR_HQReport();
|
||||
copy.MessageId = MessageId;
|
||||
copy.Target = Target;
|
||||
copy.Source = Source;
|
||||
copy.Position = Position;
|
||||
copy.Velocity = Velocity;
|
||||
copy.ObservedAt = ObservedAt;
|
||||
copy.Engaged = Engaged;
|
||||
copy.Distress = Distress;
|
||||
copy.HasVelocity = HasVelocity;
|
||||
copy.SeenEscapists = SeenEscapists;
|
||||
copy.TravelMode = TravelMode;
|
||||
copy.SeenWeapons = SeenWeapons;
|
||||
copy.DetailsObservedAt = DetailsObservedAt;
|
||||
return copy;
|
||||
}
|
||||
|
||||
string TravelDirection()
|
||||
{
|
||||
if (!HasVelocity)
|
||||
return "unknown";
|
||||
if (Velocity.Length() < 0.5)
|
||||
return "stationary";
|
||||
float x = Math.AbsFloat(Velocity[0]);
|
||||
float z = Math.AbsFloat(Velocity[2]);
|
||||
string northSouth = "N";
|
||||
string eastWest = "E";
|
||||
if (Velocity[2] < 0)
|
||||
northSouth = "S";
|
||||
if (Velocity[0] < 0)
|
||||
eastWest = "W";
|
||||
if (x < z * 0.414214)
|
||||
return northSouth;
|
||||
if (z < x * 0.414214)
|
||||
return eastWest;
|
||||
return northSouth + eastWest;
|
||||
}
|
||||
|
||||
string Description(float now, bool brief = false)
|
||||
{
|
||||
if (Distress)
|
||||
return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||
string count = "count unknown";
|
||||
if (SeenEscapists >= 0)
|
||||
count = "at least " + SeenEscapists.ToString() + " spotted nearby";
|
||||
string motion = "speed/direction unknown";
|
||||
if (HasVelocity)
|
||||
{
|
||||
float speed = Math.Round(Velocity.Length() * 10) / 10.0;
|
||||
float azimuth = Math.Atan2(Velocity[0], Velocity[2]) * Math.RAD2DEG;
|
||||
if (azimuth < 0)
|
||||
azimuth += 360;
|
||||
int heading = Math.Round(azimuth);
|
||||
motion = string.Format("%1 km/h, azimuth %2 degrees (%3)", Math.Round(speed * 3.6), heading % 360, TravelDirection());
|
||||
}
|
||||
string details = count + "; " + TravelMode + "; " + motion + "; seen weapons: " + SeenWeapons;
|
||||
if (brief)
|
||||
return details;
|
||||
string velocity = "unknown";
|
||||
if (HasVelocity)
|
||||
velocity = string.Format("[%1,%2] m/s", Math.Round(Velocity[0] * 10) / 10.0, Math.Round(Velocity[2] * 10) / 10.0);
|
||||
return string.Format("X=%1 Z=%2: %3; vector X/Z=%4 (%5s ago)", Math.Round(Position[0]), Math.Round(Position[2]), details, velocity, Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||
}
|
||||
|
||||
static vector Predict(vector position, vector velocity, float seconds)
|
||||
{
|
||||
vector offset = velocity * Math.Clamp(seconds, 0, 20);
|
||||
offset[1] = 0;
|
||||
if (offset.Length() > 300)
|
||||
offset = offset.Normalized() * 300;
|
||||
return position + offset;
|
||||
}
|
||||
|
||||
int Priority(float now)
|
||||
{
|
||||
if (Distress)
|
||||
return 0;
|
||||
if (now - ObservedAt <= 15000)
|
||||
{
|
||||
if (Engaged)
|
||||
return 3;
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// An idle rotor or zero speed alone is normal during takeoff, landing and reload.
|
||||
// Confirm prolonged grounding, but recognize actual loss of flight capability.
|
||||
class ERR_HelicopterAvailability
|
||||
{
|
||||
protected float m_fGroundedSince = -1;
|
||||
|
||||
string Check(float now, float spawnedAt, bool destroyed, bool disabled, bool livingPilot, bool playerControlled, bool servicing, float height, float speed)
|
||||
{
|
||||
if (destroyed) return "destroyed";
|
||||
if (now - spawnedAt < 20000)
|
||||
{
|
||||
m_fGroundedSince = -1;
|
||||
return "";
|
||||
}
|
||||
if (disabled) return "engine, rotor or airframe disabled";
|
||||
if (playerControlled)
|
||||
{
|
||||
m_fGroundedSince = -1;
|
||||
return "";
|
||||
}
|
||||
if (!livingPilot) return "no living pilot";
|
||||
if (servicing || height > 5 || speed > 2)
|
||||
{
|
||||
m_fGroundedSince = -1;
|
||||
return "";
|
||||
}
|
||||
if (m_fGroundedSince < 0) m_fGroundedSince = now;
|
||||
if (now - m_fGroundedSince >= 60000) return "grounded for 60 seconds";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// All target coordinates come from delivered HQ observations, never a player's
|
||||
// current transform. Road projection stops at an unknown junction/road end.
|
||||
class ERR_MortarPrediction
|
||||
{
|
||||
// Read immediately after a failed synchronous prediction; no extra world scan.
|
||||
static string FailureReason;
|
||||
// Includes observation uplink, HQ downlink and native aiming. All stages
|
||||
// retain the original observation time; radio delivery never renews it.
|
||||
static const float MAX_REPORT_AGE_MS = 60000;
|
||||
|
||||
static bool Fresh(ERR_HQReport report, float now)
|
||||
{
|
||||
return report && report.Target && !report.Distress && now >= report.ObservedAt && now - report.ObservedAt <= MAX_REPORT_AGE_MS && report.TravelMode != "aircraft";
|
||||
}
|
||||
|
||||
static bool Reject(string reason)
|
||||
{
|
||||
FailureReason = reason;
|
||||
return false;
|
||||
}
|
||||
|
||||
static vector ClosestOnSegment(vector point, vector start, vector end)
|
||||
{
|
||||
vector segment = end - start;
|
||||
segment[1] = 0;
|
||||
vector offset = point - start;
|
||||
offset[1] = 0;
|
||||
float lengthSq = segment.LengthSq();
|
||||
if (lengthSq < 0.01)
|
||||
return start;
|
||||
return vector.Lerp(start, end, Math.Clamp(vector.Dot(offset, segment) / lengthSq, 0, 1));
|
||||
}
|
||||
|
||||
static bool FollowRoad(array<vector> points, vector observed, vector velocity, float seconds, out vector impact)
|
||||
{
|
||||
if (!points || points.Count() < 2)
|
||||
return Reject("road polyline has fewer than two points");
|
||||
int segment = -1;
|
||||
float closest = 20;
|
||||
vector projected;
|
||||
for (int i = 0; i < points.Count() - 1; i++)
|
||||
{
|
||||
vector candidate = ClosestOnSegment(observed, points[i], points[i + 1]);
|
||||
float distance = vector.DistanceXZ(candidate, observed);
|
||||
if (distance < closest)
|
||||
{
|
||||
closest = distance;
|
||||
segment = i;
|
||||
projected = candidate;
|
||||
}
|
||||
}
|
||||
if (segment < 0)
|
||||
return Reject("observed vehicle is not within 20m of a road segment");
|
||||
vector forward = points[segment + 1] - points[segment];
|
||||
forward[1] = 0;
|
||||
forward.Normalize();
|
||||
vector travel = velocity;
|
||||
travel[1] = 0;
|
||||
float speed = travel.Length();
|
||||
travel.Normalize();
|
||||
float alignment = vector.Dot(forward, travel);
|
||||
if (Math.AbsFloat(alignment) < 0.7)
|
||||
return Reject("vehicle heading crosses road; alignment below 0.7");
|
||||
int step = 1;
|
||||
int next = segment + 1;
|
||||
if (alignment < 0)
|
||||
{
|
||||
step = -1;
|
||||
next = segment;
|
||||
}
|
||||
float remaining = speed * seconds;
|
||||
vector position = projected;
|
||||
while (next >= 0 && next < points.Count())
|
||||
{
|
||||
float length = vector.DistanceXZ(position, points[next]);
|
||||
if (length >= remaining && length > 0.01)
|
||||
{
|
||||
impact = vector.Lerp(position, points[next], remaining / length);
|
||||
return true;
|
||||
}
|
||||
remaining -= length;
|
||||
position = points[next];
|
||||
next += step;
|
||||
}
|
||||
return Reject("prediction reaches unknown road end/junction");
|
||||
}
|
||||
|
||||
static bool Predict(ERR_HQReport report, float now, float flightSeconds, out vector impact)
|
||||
{
|
||||
FailureReason = "";
|
||||
if (!report) return Reject("target report missing");
|
||||
if (report.Distress) return Reject("distress report cannot authorize fire");
|
||||
if (now < report.ObservedAt) return Reject("observation timestamp is in the future");
|
||||
if (now - report.ObservedAt > MAX_REPORT_AGE_MS) return Reject("observation older than 60s");
|
||||
float seconds = (now - report.ObservedAt) / 1000 + flightSeconds;
|
||||
if (seconds < 0 || seconds > 90)
|
||||
return Reject("observation age plus flight outside 0..90s");
|
||||
if (report.TravelMode == "aircraft") return Reject("aircraft target unsupported");
|
||||
if (report.TravelMode == "mixed travel") return Reject("mixed travel cannot be predicted");
|
||||
impact = report.Position;
|
||||
if (!report.HasVelocity || report.Velocity.Length() < 0.5)
|
||||
return true;
|
||||
if (report.Velocity.Length() * seconds > 1800)
|
||||
return Reject("predicted travel exceeds 1800m");
|
||||
bool vehicle = report.TravelMode == "vehicle" || report.TravelMode == "in vehicle";
|
||||
if (vehicle)
|
||||
{
|
||||
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!world || !world.GetRoadNetworkManager())
|
||||
return Reject("road network unavailable");
|
||||
BaseRoad road;
|
||||
float roadDistance;
|
||||
world.GetRoadNetworkManager().GetClosestRoad(report.Position, road, roadDistance, skipNavlinks: true);
|
||||
if (!road) return Reject("no road near reported vehicle position");
|
||||
if (roadDistance > 20) return Reject("reported vehicle farther than 20m from road");
|
||||
array<vector> points = {};
|
||||
road.GetPoints(points);
|
||||
return FollowRoad(points, report.Position, report.Velocity, seconds, impact);
|
||||
}
|
||||
impact += report.Velocity * seconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ThreatensFriendly(vector impact, vector position, vector velocity, float flightSeconds)
|
||||
{
|
||||
velocity[1] = 0;
|
||||
vector future = position + velocity * Math.Clamp(flightSeconds, 0, 90);
|
||||
// Protect the entire predicted path, not only where a soldier is now.
|
||||
return vector.DistanceXZ(impact, ClosestOnSegment(impact, position, future)) <= 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// One delivered HQ order drives a continuous native loading/firing cycle.
|
||||
class ERR_MortarSalvo
|
||||
{
|
||||
static const int ROUNDS = 8;
|
||||
static const float RADIUS_METRES = 40;
|
||||
static const float TIMEOUT_MS = 120000;
|
||||
|
||||
static vector Offset(int site, int order, int shot)
|
||||
{
|
||||
// Stable throughout one ballistic solve; successive shells cover the disk
|
||||
// instead of clustering on the same point or changing every iteration.
|
||||
float radius = RADIUS_METRES * Math.Sqrt((shot + 1.0) / ROUNDS);
|
||||
float angle = (site * 53 + order * 97 + shot * 137.5) * Math.DEG2RAD;
|
||||
return Vector(Math.Sin(angle) * radius, 0, Math.Cos(angle) * radius);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
GameProject {
|
||||
ID "ERR_Ai"
|
||||
GUID "5192138CB9D5B911"
|
||||
TITLE "Escapists Rereintroduced - Ai"
|
||||
Dependencies {
|
||||
"58D0FB3206B6F859"
|
||||
"2528466DE220D91C"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user