// One physical escort may intercept a road ahead of delivered travel history. // This HQ-owned transport uses the ordinary radio graph and never reads players. modded class ERR_HQ { protected ref ERR_ConvoyInterceptOrder m_MountedIntercept; protected ERR_HQUnit m_MountedInterceptUnit; protected ERR_ConvoyTask m_MountedInterceptTask; protected ref ERR_HQRoute m_MountedInterceptRoute; protected bool m_MountedInterceptDelivered; protected ref array m_MountedAttemptBeliefs = {}; protected ref array m_MountedAttemptRevisions = {}; override string Status() { string result = super.Status(); if (!m_MountedIntercept) return result; string phase = "queued"; if (m_MountedInterceptDelivered) phase = "delivered"; return result + "\n" + string.Format("Mounted interception: escort %1; %2; report M%3; road %4", m_MountedIntercept.VehicleIndex, phase, m_MountedIntercept.ReportId, m_MountedIntercept.Position); } protected override int Assigned() { int count = super.Assigned(); if (m_MountedIntercept) count++; return count; } protected override int HuntingAssigned() { int count = super.HuntingAssigned(); if (m_MountedIntercept) count++; return count; } protected override void ReserveUnit(ERR_HQUnit unit, array reserved, out int first, out int second) { super.ReserveUnit(unit, reserved, first, second); if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position); } protected bool MountedInterceptAttempted(int beliefId, int revision) { for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++) if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true; return false; } protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now) { if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity || !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false; ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId); if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0 || !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt) || entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now || !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength || entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false; return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0; } protected void ReleaseMountedIntercept(float now, string reason) { ref ERR_ConvoyInterceptOrder order = m_MountedIntercept; ERR_HQUnit unit = m_MountedInterceptUnit; ERR_ConvoyTask task = m_MountedInterceptTask; // Detach first: physical release may synchronously unregister the group. m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null; m_MountedInterceptRoute = null; m_MountedInterceptDelivered = false; if (!order) return; if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now); if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0; m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId); } protected override void ReleaseUnit(ERR_HQUnit unit, float now) { if (m_MountedIntercept && unit == m_MountedInterceptUnit) ReleaseMountedIntercept(now, "original formation released"); super.ReleaseUnit(unit, now); } protected override void TickMountedIntercept(float now) { if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return; ERR_HQUnit unit = m_MountedInterceptUnit; ERR_ConvoyInterceptOrder order = m_MountedIntercept; int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit")); if (now < order.IssuedAt || now >= order.ExpiresAt || !MissionActive() || m_Stopping || !unit || !m_Units.Contains(unit) || unit.Group != order.Group || unit.LogisticsReservation != order.Token || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group) || Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit) || !m_Convoys.InterceptCurrent(m_MountedInterceptTask, order)) { ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; } if (m_MountedInterceptDelivered) return; ERR_HQBelief belief = m_Memory.GetById(order.BeliefId); if (!ERR_HQMountedInterceptRules.Current(order, belief, now) || !m_MountedInterceptRoute) { ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; } int delivered = m_MountedInterceptRoute.Advance(now, m_Journal); if (delivered == 0) return; if (delivered < 0) { ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; } ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex]; if (!MountedInterceptStatus(unit, vehicle, now) || ConvoyContact(unit, now) || !m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now)) { ReleaseMountedIntercept(now, "command refused by original crew"); return; } m_MountedInterceptRoute = null; m_MountedInterceptDelivered = true; m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escort ordered to the remembered road ahead", order.ReportId); } protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task) { if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false; // Service admission excludes the whole original convoy, including sibling // holds still in transit. Do not spend a delivered revision on a busy task. foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) { if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false; foreach (ERR_HQUnit unit : m_Units) if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false; } return true; } protected bool HasMountedInterceptCandidate(float now) { // Avoid native road surveys until one original escort has usable delivered status. for (int t = 0; t < m_Convoys.Count(); t++) { ERR_ConvoyTask task = m_Convoys.At(t); if (!MountedInterceptTaskAvailable(task)) continue; foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) { if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5) || vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue; foreach (ERR_HQUnit unit : m_Units) if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now) && MountedInterceptStatus(unit, vehicle, now)) return true; } } return false; } protected override void PlanMountedIntercept(float now) { if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept || m_MountedAttemptBeliefs.Count() >= 256) return; int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit")); if (Assigned() >= globalLimit || HuntingAssigned() >= ERR_HQPursuitRules.Limit(globalLimit)) return; if (!HasMountedInterceptCandidate(now)) return; array reserved = {}; int first = -1; int second = -1; foreach (ERR_HQUnit known : m_Units) ReserveUnit(known, reserved, first, second); // At most two bounded native road queries per normal five-second HQ pass. int queries; for (int b = 0; b < m_Memory.Count() && queries < 2; b++) { ERR_HQBelief belief = m_Memory.At(b); if (!ERR_HQMountedInterceptRules.Eligible(belief, now) || MountedInterceptAttempted(belief.BeliefId, belief.BeliefRevision)) continue; vector destination; queries++; if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue; ERR_HQUnit best; ERR_ConvoyTask bestTask; ERR_ConvoyVehicle bestVehicle; ERR_HQRoute bestRoute; float bestDistance = 1e20; for (int t = 0; t < m_Convoys.Count(); t++) { ERR_ConvoyTask task = m_Convoys.At(t); if (!MountedInterceptTaskAvailable(task)) continue; foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) { if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5) || vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue; foreach (ERR_HQUnit unit : m_Units) { if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue; ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId); float distance = vector.DistanceXZ(own.Position, destination); if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || distance >= bestDistance) continue; ERR_HQRoute candidateRoute = m_Network.Route(unit.Radio, true); if (!candidateRoute) continue; best = unit; bestTask = task; bestVehicle = vehicle; bestRoute = candidateRoute; bestDistance = distance; } } } if (!best) continue; ERR_HQRoute route = bestRoute; if (!route) continue; ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder(); order.Token = 400001 + m_MountedAttemptBeliefs.Count(); order.Group = best.Group; order.SourceId = bestTask.Receipt.SourceId; order.VehicleIndex = bestVehicle.Index; order.Position = destination; order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision; order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt; order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000; if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) continue; // The attempt stays spent after radio interruption, physical refusal or loss. m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision); best.LogisticsReservation = order.Token; if (!m_Convoys.ReserveIntercept(bestTask, order, now)) { if (best.LogisticsReservation == order.Token) best.LogisticsReservation = 0; return; } m_MountedIntercept = order; m_MountedInterceptUnit = best; m_MountedInterceptTask = bestTask; m_MountedInterceptRoute = route; m_MountedInterceptDelivered = false; route.MessageId = m_Journal.Message(); route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, destination, order.BeliefId, order.BeliefRevision, order.ReportId); m_Journal.Write("MOUNTED INTERCEPT QUEUED", route.Payload, route.MessageId); return; } } override void Stop() { float now; if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime(); ReleaseMountedIntercept(now, "mission stopped"); super.Stop(); } }