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 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 m_Units = {}; protected ref array 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 entities) { entities.Clear(); foreach (ERR_HQUnit unit : m_Units) { if (!unit.Group) continue; array 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 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 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 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_Resources.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); } }