340 lines
11 KiB
C
340 lines
11 KiB
C
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 ReservedForSupport;
|
|
|
|
bool Eligible(ERR_GameMode mode)
|
|
{
|
|
if (ReservedForSupport || !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 ERR_MortarSystem m_Mortars;
|
|
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; m_Mortars = new ERR_MortarSystem(this); }
|
|
static ERR_HQ Get()
|
|
{
|
|
ERR_GameMode mode = ERR_GameMode.Get();
|
|
if (!mode) return null;
|
|
return ERR_HQ.Cast(mode.Command());
|
|
}
|
|
static bool MissionActive()
|
|
{
|
|
ERR_GameMode mode = ERR_GameMode.Get();
|
|
return mode && mode.Active();
|
|
}
|
|
static void Event(string kind, string message, int messageId = 0)
|
|
{
|
|
ERR_HQ hq = Get();
|
|
if (hq) hq.Journal().Write(kind, message, messageId);
|
|
}
|
|
ERR_MortarSystem Mortars() { return m_Mortars; }
|
|
bool PrepareSupport() { return m_Mortars.Prepare(); }
|
|
void ReserveGroup(SCR_AIGroup group)
|
|
{
|
|
foreach (ERR_HQUnit unit : m_Units)
|
|
{
|
|
if (unit.Group != group) continue;
|
|
unit.Release();
|
|
unit.ReservedForSupport = true;
|
|
}
|
|
}
|
|
void PhysicalForces(out array<IEntity> entities)
|
|
{
|
|
entities.Clear();
|
|
foreach (ERR_HQUnit unit : m_Units)
|
|
{
|
|
if (!unit.Group) continue;
|
|
array<AIAgent> agents = {};
|
|
unit.Group.GetAgents(agents);
|
|
foreach (AIAgent agent : agents)
|
|
{
|
|
if (!agent || !agent.GetControlledEntity()) continue;
|
|
IEntity actor = agent.GetControlledEntity();
|
|
if (!entities.Contains(actor)) entities.Insert(actor);
|
|
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(actor);
|
|
if (vehicle && !entities.Contains(vehicle)) entities.Insert(vehicle);
|
|
}
|
|
}
|
|
m_Mortars.AppendGuns(entities);
|
|
}
|
|
bool MortarConnected(SCR_AIGroup group)
|
|
{
|
|
foreach (ERR_HQUnit unit : m_Units)
|
|
if (unit.Group == group && unit.Radio) return m_Network.Route(unit.Radio) != null;
|
|
return false;
|
|
}
|
|
ERR_HQRoute Downlink(SCR_AIGroup group, string kind, string detail, int sourceId)
|
|
{
|
|
foreach (ERR_HQUnit unit : m_Units)
|
|
{
|
|
if (unit.Group != group || !unit.Radio) continue;
|
|
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
|
if (!route) return null;
|
|
route.MessageId = m_Journal.Message();
|
|
route.Payload = string.Format("%1 from report M%2; %3", kind, sourceId, detail);
|
|
m_Journal.Write("ORDER QUEUED", route.Payload, route.MessageId);
|
|
return route;
|
|
}
|
|
return null;
|
|
}
|
|
void SupportContacts(out array<ref ERR_HQReport> contacts, float now)
|
|
{
|
|
contacts.Clear();
|
|
foreach (ERR_HQReport contact : m_Contacts)
|
|
if (ERR_MortarPrediction.Fresh(contact, now)) contacts.Insert(contact.Snapshot());
|
|
}
|
|
|
|
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()) + "; " + m_Mortars.Status();
|
|
}
|
|
|
|
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; }
|
|
int deliveryId;
|
|
if (unit.Downlink) deliveryId = unit.Downlink.MessageId;
|
|
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 from report M%2: %3", unit.Id, order.MessageId, order.Description(now)), deliveryId);
|
|
}
|
|
|
|
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_Mortars.Tick(now);
|
|
m_Journal.Flush();
|
|
}
|
|
|
|
override void Stop()
|
|
{
|
|
m_Mortars.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); }
|
|
}
|