Files
EscapistsReintroduced/docs/evidence/mounted-interception-2026-10-02/final-implementation.patch
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

1182 lines
106 KiB
Diff

--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c
@@ -119,10 +119,108 @@
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
int SupplyToken;
bool SupplyHeld;
+ int InterceptToken;
+ bool InterceptActive;
+ AIWaypoint InterceptWaypoint;
+ ref ERR_ConvoyInterceptOrder InterceptOrder;
+ ERR_ConvoyReceipt InterceptReceipt;
+ ERR_ForceRecord InterceptRecord;
+ ERR_ForceRoute InterceptRoute;
+ protected AIWaypoint m_InterceptWaypoint;
+ protected Vehicle m_InterceptEntity;
+ protected SCR_AIGroup m_InterceptGroup;
+ protected IEntity m_InterceptDriver;
+ protected ref array<IEntity> m_InterceptMembers = {};
+ protected ref array<BaseCompartmentSlot> m_InterceptSeats = {};
+ protected int m_InterceptToken;
+ protected int m_LastInterceptToken;
+ protected vector m_InterceptPosition;
+ protected float m_InterceptExpiresAt;
+ protected int m_InterceptRouteIndex;
+
bool Arrived;
float LegDeadline;
float NextReadinessNote;
+ // Pending downlink owns a claim only; normal authored travel continues.
+ bool InterceptCrew(ERR_GameMode mode)
+ {
+ if (!mode || !CrewValid(mode) || !Entity || !Group || ERR_ConvoyRules.Captured(Entity, Group)) return false;
+ if (Members.IsEmpty() || Members.Count() > 8 || Seats.Count() != Members.Count()) return false;
+ if (InterceptOrder && (Members.Count() != m_InterceptMembers.Count() || Seats.Count() != m_InterceptSeats.Count())) return false;
+ array<AIAgent> agents = {}; Group.GetAgents(agents);
+ if (agents.Count() != Members.Count()) return false;
+ for (int i = 0; i < Members.Count(); i++)
+ {
+ if (!Members[i] || !ERR_Actors.CanFight(Members[i]) || !Seats[i] || Seats[i].GetOccupant() != Members[i]) return false;
+ int attributed;
+ foreach (AIAgent agent : agents)
+ {
+ if (!agent || !Members.Contains(agent.GetControlledEntity())) return false;
+ if (agent.GetControlledEntity() == Members[i]) attributed++;
+ }
+ if (attributed != 1) return false;
+ if (InterceptOrder && (Members[i] != m_InterceptMembers[i] || Seats[i] != m_InterceptSeats[i])) return false;
+ }
+ if (InterceptOrder && (Entity != m_InterceptEntity || Group != m_InterceptGroup || Driver != m_InterceptDriver)) return false;
+ return true;
+ }
+ bool InterceptMatches(ERR_ConvoyInterceptOrder order)
+ {
+ return order && InterceptOrder == order && InterceptToken == m_InterceptToken && m_InterceptToken != 0;
+ }
+ bool ClaimIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order)
+ {
+ if (!order || order.Token <= m_LastInterceptToken || InterceptToken != 0 || InterceptOrder || InterceptWaypoint || !InterceptCrew(mode)) return false;
+ m_InterceptEntity = Entity; m_InterceptGroup = Group; m_InterceptDriver = Driver;
+ m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
+ foreach (IEntity member : Members) m_InterceptMembers.Insert(member);
+ foreach (BaseCompartmentSlot seat : Seats) m_InterceptSeats.Insert(seat);
+ m_InterceptToken = order.Token; m_LastInterceptToken = order.Token; InterceptToken = order.Token; InterceptOrder = order;
+ m_InterceptPosition = order.Position; m_InterceptExpiresAt = order.ExpiresAt;
+ return true;
+ }
+ void ClearIntercept(ERR_ConvoyInterceptOrder order)
+ {
+ if (!InterceptMatches(order)) return;
+ // Delete only the exact waypoint this owner created, never an adopted handle.
+ if (m_InterceptWaypoint && m_InterceptGroup) m_InterceptGroup.RemoveWaypoint(m_InterceptWaypoint);
+ if (m_InterceptWaypoint) SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
+ if (InterceptWaypoint == m_InterceptWaypoint) InterceptWaypoint = null;
+ m_InterceptWaypoint = null; InterceptToken = 0; m_InterceptToken = 0;
+ InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null;
+ m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
+ }
+ bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
+ {
+ if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
+ if (order.Position != m_InterceptPosition || order.ExpiresAt != m_InterceptExpiresAt || now >= m_InterceptExpiresAt) return false;
+ AIWaypoint detour = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), m_InterceptPosition));
+ if (!detour) return false;
+ detour.SetCompletionRadius(15);
+ ClearOrder(); m_InterceptRouteIndex = RouteIndex;
+ m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
+ Group.AddWaypoint(detour);
+ return true;
+ }
+ // True suppresses ordinary travel only after actual radio delivery. No route
+ // index is consumed by arrival at the detour; removal leaves the seated group held.
+ bool TickIntercept(ERR_GameMode mode, float now)
+ {
+ if (!InterceptOrder) return false;
+ if (!InterceptMatches(InterceptOrder)) return InterceptActive;
+ if (Released || Arrived || now >= m_InterceptExpiresAt || !InterceptCrew(mode) || Combat.Held || SupplyHeld || SupplyToken != 0)
+ { bool delivered = InterceptActive; ClearIntercept(InterceptOrder); if (delivered) LegDeadline = now + 600000; return false; }
+ if (!InterceptActive) return false;
+ if (RouteIndex != m_InterceptRouteIndex || InterceptWaypoint != m_InterceptWaypoint)
+ { ClearIntercept(InterceptOrder); return false; }
+ if (m_InterceptWaypoint && vector.DistanceXZ(Entity.GetOrigin(), m_InterceptPosition) <= 15)
+ {
+ Group.RemoveWaypoint(m_InterceptWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
+ m_InterceptWaypoint = null; InterceptWaypoint = null;
+ }
+ return true;
+ }
void ClearOrder()
{
if (Waypoint && Group) Group.RemoveWaypoint(Waypoint);
@@ -132,6 +230,7 @@
void Release(ERR_GameMode mode)
{
if (Released) return;
+ ClearIntercept(InterceptOrder);
Released = true;
Combat.Held = false; SupplyToken = 0; SupplyHeld = false;
ClearOrder();
@@ -159,7 +258,7 @@
}
bool Order(ERR_ForceRoute route, float now)
{
- if (Released || Arrived || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
+ if (Released || Arrived || InterceptActive || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
if (Waypoint) return true;
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
if (!Waypoint) return false;
@@ -170,6 +269,7 @@
}
void Tick(ERR_GameMode mode, ERR_ForceRoute route, float now)
{
+ if (TickIntercept(mode, now)) return;
if (Released || Arrived) return;
if (!CrewValid(mode)) { Release(mode); return; }
if (Combat.Held || SupplyHeld) return;
@@ -417,9 +517,61 @@
if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released");
}
}
+ protected bool InterceptEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
+ {
+ if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
+ if (!m_Mode.Forces()) return false;
+ ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
+ if (!hq || hq.LogisticsToken(order.Group) != order.Token) return false;
+ if (order.Token < 400001 || order.Token > 400256 || order.SourceId != task.Receipt.SourceId) return false;
+ if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
+ if (order.VehicleIndex >= task.Vehicles.Count() || order.VehicleIndex >= task.Receipt.Records.Count()) return false;
+ ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
+ if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group) return false;
+ if (!unit.InterceptCrew(m_Mode) || !task.Route || unit.RouteIndex < 0 || unit.RouteIndex >= task.Route.Count()) return false;
+ ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
+ if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
+ if (record.Group != unit.Group || record.SourceId != task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString()) return false;
+ if (unit.InterceptOrder && (unit.InterceptReceipt != task.Receipt || unit.InterceptRecord != record || unit.InterceptRoute != task.Route)) return false;
+ foreach (ERR_ConvoyVehicle other : task.Vehicles)
+ if (!other || other.SupplyToken != 0 || other.SupplyHeld || other.Combat.Held) return false;
+ return true;
+ }
+ bool InterceptCurrent(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
+ {
+ return InterceptEligible(task, order) && task.Vehicles[order.VehicleIndex].InterceptToken == order.Token
+ && task.Vehicles[order.VehicleIndex].InterceptMatches(order);
+ }
+ bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
+ {
+ if (!InterceptEligible(task, order) || !ERR_ForceRules.Finite(now) || now < 0) return false;
+ if (!ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
+ if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt) return false;
+ if (order.ExpiresAt > order.ObservedAt + 120000 || !ERR_ForceRules.FiniteVector(order.Position)) return false;
+ if (order.BeliefId <= 0 || order.BeliefRevision <= 0 || order.ReportId <= 0) return false;
+ foreach (ERR_ConvoyTask existing : m_Tasks)
+ foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
+ ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
+ if (!unit.ClaimIntercept(m_Mode, order)) return false;
+ unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route;
+ return true;
+ }
+ bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
+ {
+ if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false;
+ return task.Vehicles[order.VehicleIndex].DeliverIntercept(m_Mode, order, now);
+ }
+ void ReleaseIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
+ {
+ if (!task || !order || !m_Tasks.Contains(task) || !ERR_ForceRules.Finite(now)) return;
+ foreach (ERR_ConvoyVehicle unit : task.Vehicles)
+ if (unit && unit.InterceptMatches(order))
+ { bool delivered = unit.InterceptActive; unit.ClearIntercept(order); if (delivered) unit.LegDeadline = now + 600000; }
+ }
bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
{
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
+ foreach (ERR_ConvoyVehicle intercept : task.Vehicles) if (intercept && intercept.InterceptToken != 0) return false;
if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false;
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false;
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -1306,6 +1306,10 @@
if (supply.TryPlanMines(mineRecipient, donor, m_Watch.Find(mineRecipient.ForceSourceId), donorStatus, now)) return true;
return false;
}
+ // Base declarations allow native script compilation to resolve the optional
+ // mounted-dispatch extension through ordinary virtual method overrides.
+ protected void TickMountedIntercept(float now) {}
+ protected void PlanMountedIntercept(float now) {}
protected void PlanConvoy(float now)
{
for (int i = 0; i < m_Convoys.Count(); i++) PlanConvoyTask(m_Convoys.At(i), now);
@@ -1318,7 +1322,7 @@
if (vehicle.Released || vehicle.Arrived || vehicle.SupplyToken > 0 || !vehicle.Registered || !vehicle.Group) continue;
foreach (ERR_HQUnit unit : m_Units)
{
- if (unit.Group != vehicle.Group || !unit.ReservedForSupport || unit.Downlink || !unit.Radio) continue;
+ if (unit.Group != vehicle.Group || !unit.ReservedForSupport || unit.LogisticsReservation > 0 || unit.Downlink || !unit.Radio) continue;
if (!ConvoyPositionKnown(unit, now)) break;
ERR_HQReport contact = ConvoyContact(unit, now);
if (contact)
@@ -1523,6 +1527,7 @@
m_Hunters.Tick(now);
m_ObjectiveDefence.Tick(now);
m_Towns.Tick(now);
+ TickMountedIntercept(now);
m_Convoys.Tick(now);
AutomaticConvoy(now);
m_Bodies.TickBudget(now);
@@ -1565,7 +1570,7 @@
if (!unit.Group && !unit.Uplink) { ReleaseLostForce(unit, now); m_Units.Remove(i); continue; }
QueueReports(unit, now);
}
- if (now >= m_NextAssignment) { PlanConvoy(now); PlanResupply(now); Assign(now); m_NextAssignment = now + 5000; }
+ if (now >= m_NextAssignment) { PlanConvoy(now); PlanResupply(now); PlanMountedIntercept(now); Assign(now); m_NextAssignment = now + 5000; }
MaintainEngineerMines(now);
m_Resupply.Tick(now);
m_Logistics.Tick(now);
--- /dev/null
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c
@@ -0,0 +1,207 @@
+// 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<int> m_MountedAttemptBeliefs = {};
+ protected ref array<int> 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<vector> 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<vector> 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();
+ }
+}
--- /dev/null
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c
@@ -0,0 +1,109 @@
+// Detached radio order. Only its issuer chooses Position and the fixed deadline.
+class ERR_ConvoyInterceptOrder
+{
+ int Token;
+ SCR_AIGroup Group;
+ string SourceId;
+ int VehicleIndex;
+ vector Position;
+ float IssuedAt;
+ float ExpiresAt;
+ float ObservedAt;
+ int BeliefId;
+ int BeliefRevision;
+ int ReportId;
+}
+
+// Delivered ground history only; no target entity reads, force creation or orders.
+class ERR_HQMountedInterceptRules
+{
+ static bool Eligible(ERR_HQBelief belief, float now)
+ {
+ if (!ERR_HQRoadIntercept.Eligible(belief, now)) return false;
+ if (belief.BeliefId <= 0 || belief.BeliefRevision <= 0 || belief.ObservedAt < 0) return false;
+ return ERR_HQPursuitRules.PositionValid(belief.Position) && ERR_HQPursuitRules.PositionValid(belief.PredictedPosition);
+ }
+ static bool Current(ERR_ConvoyInterceptOrder order, ERR_HQBelief belief, float now)
+ {
+ if (!order || !Eligible(belief, now) || !order.Group || order.SourceId.IsEmpty()) return false;
+ if (order.Token < 400001 || order.Token > 400256) return false;
+ if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
+ if (order.BeliefId != belief.BeliefId || order.BeliefRevision != belief.BeliefRevision
+ || order.ReportId != belief.MessageId || order.ObservedAt != belief.ObservedAt) return false;
+ if (!ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
+ if (order.IssuedAt < belief.LastDeliveredAt || now < order.IssuedAt || now - order.IssuedAt > 30000) return false;
+ if (order.ExpiresAt <= order.IssuedAt || order.ExpiresAt > order.ObservedAt + 120000 || now >= order.ExpiresAt) return false;
+ return ERR_HQPursuitRules.PositionValid(order.Position);
+ }
+ static bool Ahead(vector origin, vector velocity, vector point)
+ {
+ if (!ERR_ForceRules.FiniteVector(origin) || !ERR_ForceRules.FiniteVector(point) || !ERR_HQPursuitRules.Heading(velocity, true)) return false;
+ vector delta = point - origin; delta[1] = 0; velocity[1] = 0;
+ float distance = delta.Length();
+ return distance >= 300 && distance <= 900 && vector.Dot(delta.Normalized(), velocity.Normalized()) >= 0.8;
+ }
+ static bool FromPolyline(array<vector> points, vector origin, vector velocity, vector seed, array<vector> reserved, out vector result, out float score, out float heading)
+ {
+ result = vector.Zero; score = 1e20; heading = 0;
+ if (!points || points.Count() < 2 || points.Count() > 1024 || !ERR_ForceRules.FiniteVector(seed)) return false;
+ foreach (vector point : points) if (!ERR_ForceRules.FiniteVector(point)) return false;
+ bool found;
+ for (int i = 1; i < points.Count(); i++)
+ {
+ vector delta = points[i] - points[i - 1]; delta[1] = 0;
+ float length = delta.Length();
+ if (length < 0.01) continue;
+ vector offset = seed - points[i - 1]; offset[1] = 0;
+ float fraction = Math.Clamp(vector.Dot(offset, delta) / (length * length), 0, 1);
+ vector candidate = vector.Lerp(points[i - 1], points[i], fraction);
+ float distance = vector.DistanceXZ(seed, candidate);
+ if (distance > 100 || !Ahead(origin, velocity, candidate) || !ERR_HQRoadIntercept.Unreserved(candidate, reserved)) continue;
+ if (!found || ERR_HQRoadIntercept.Better(distance, candidate, score, result)) { result = candidate; score = distance; heading = Math.Atan2(delta[0], delta[2]) * Math.RAD2DEG; found = true; }
+ }
+ return found;
+ }
+ static bool Select(ERR_HQBelief belief, float now, array<vector> reserved, out vector result)
+ {
+ result = vector.Zero;
+ if (!Replication.IsServer() || !Eligible(belief, now) || !reserved || reserved.Count() > 128) return false;
+ if (!GetGame() || !GetGame().GetWorld()) return false;
+ vector velocity = belief.Velocity; velocity[1] = 0;
+ vector seed = belief.PredictedPosition + velocity.Normalized() * Math.Clamp(velocity.Length() * 60, 300, 900);
+ if (!ERR_HQPursuitRules.PositionValid(seed)) return false;
+ SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
+ if (!world || !world.GetRoadNetworkManager()) return false;
+ array<BaseRoad> roads = {};
+ world.GetRoadNetworkManager().GetRoadsInAABB(seed + "-100 -1000 -100", seed + "100 1000 100", roads);
+ if (roads.Count() > 128) return false;
+ int segments;
+ bool found;
+ vector best;
+ float bestScore = 1e20;
+ float bestHeading;
+ foreach (BaseRoad road : roads)
+ {
+ if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 6) continue;
+ array<vector> points = {}; road.GetPoints(points);
+ if (points.Count() < 2 || points.Count() > 1024) continue;
+ segments += points.Count() - 1;
+ if (segments > 4096) return false;
+ vector candidate;
+ float score, heading;
+ if (!FromPolyline(points, belief.PredictedPosition, velocity, seed, reserved, candidate, score, heading)) continue;
+ if (!found || ERR_HQRoadIntercept.Better(score, candidate, bestScore, best))
+ { best = candidate; bestScore = score; bestHeading = heading; found = true; }
+ }
+ if (!found) return false;
+ vector grounded = ERR_Actors.Ground(best);
+ // Never convert an elevated bridge point into terrain beneath the road.
+ if (!ERR_HQPursuitRules.PositionValid(grounded) || Math.AbsFloat(grounded[1] - best[1]) > 2) return false;
+ ERR_SettlementSafety settlements = new ERR_SettlementSafety();
+ if (!settlements.OutsideSettlements(grounded)) return false;
+ // Existing conservative vehicle footprint: nine terrain/water samples,
+ // filled collision volume and segment-aligned heading; no scenery changes.
+ ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
+ if (!placement.Clear(grounded, bestHeading)) return false;
+ result = grounded;
+ return true;
+ }
+}
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c
@@ -40,6 +40,7 @@
class ERR_ConvoyColumnVehicle
{
bool Holding;
+ bool InterceptDetached;
vector Progress;
float LastProgress = -1;
float NextRetry;
@@ -135,22 +136,38 @@
}
bool Tick(float now)
{
- int leader = -1, previous = -1; float largestGap; bool supplyHold;
+ // Rejoining escorts follow the surviving leader; their older saved route
+ // index must not rewind the rest of the formation. A sole survivor keeps its own.
+ bool rejoining;
+ for (int returning = 0; returning < m_State.Count(); returning++)
+ if (m_State[returning].InterceptDetached && !m_Task.Vehicles[returning].InterceptActive) rejoining = true;
+ if (rejoining)
+ {
+ array<int> continuingOrder = {};
+ foreach (int continuing : m_Order)
+ if (!m_State[continuing].InterceptDetached) continuingOrder.Insert(continuing);
+ foreach (int detached : m_Order)
+ if (m_State[detached].InterceptDetached) continuingOrder.Insert(detached);
+ m_Order = continuingOrder;
+ }
+ int leader = -1, previous = -1; float largestGap; bool supplyHold; bool interceptLive;
for (int slot = 0; slot < m_Order.Count(); slot++)
{
int i = m_Order[slot];
ERR_ConvoyVehicle scan = m_Task.Vehicles[i];
if (scan.Released || !scan.Entity) continue;
+ if (scan.InterceptActive) { m_State[i].InterceptDetached = true; interceptLive = true; continue; }
if (leader < 0) leader = i;
if (previous >= 0) largestGap = Math.Max(largestGap, vector.DistanceXZ(scan.Entity.GetOrigin(), m_Task.Vehicles[previous].Entity.GetOrigin()));
if (scan.SupplyHeld) supplyHold = true;
previous = i;
}
- if (leader < 0) return false;
+ if (leader < 0) return interceptLive;
if (m_Leader != leader)
{
ERR_ConvoyVehicle promoted = m_Task.Vehicles[leader]; promoted.ClearOrder(); promoted.Arrived = false;
- promoted.RouteIndex = m_RouteIndex;
+ if (m_State[leader].InterceptDetached) m_RouteIndex = promoted.RouteIndex;
+ else promoted.RouteIndex = m_RouteIndex;
m_Leader = leader;
ERR_HQ.Event("CONVOY LEADER", string.Format("%1: vehicle %2 leads surviving column", m_Task.Receipt.SourceId, leader + 1));
}
@@ -164,7 +181,9 @@
int index = m_Order[ordinal];
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
if (vehicle.Released || !vehicle.Entity) continue;
+ if (vehicle.InterceptActive) { m_State[index].LastProgress = -1; m_State[index].NextTarget = now; continue; }
ERR_ConvoyColumnVehicle state = m_State[index];
+ state.InterceptDetached = false;
if (vehicle.SupplyHeld || vehicle.Combat.Held)
{
// The external owner controls its own waypoint. Only suspend our watchdog.
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c
@@ -234,7 +234,7 @@
if (!task.Column || !task.Column.Tick(now)) { task.Stop(m_Mode, "no surviving drivable convoy vehicles"); return; }
bool pending;
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
- if (!vehicle.Released && (!vehicle.Arrived || vehicle.SupplyHeld || vehicle.Combat.Held)) pending = true;
+ if (!vehicle.Released && (!vehicle.Arrived || vehicle.SupplyHeld || vehicle.Combat.Held || vehicle.InterceptToken != 0)) pending = true;
if (pending) return;
if (!task.Waiting) { task.Waiting = true; task.Departure = now + Math.RandomFloat(45000, 90000); return; }
if (now < task.Departure || task.Bases.Count() < 2) return;
@@ -270,9 +270,16 @@
if (task.MineStock) task.MineStock.Tick(now, MountedStockOwner(task, 1, ERR_HQ.Cast(m_Mode.Command())));
foreach (ERR_ConvoyVehicle checked : task.Vehicles)
if (!checked.Released && !checked.CrewValid(m_Mode)) checked.Release(m_Mode);
+ foreach (ERR_ConvoyVehicle intercept : task.Vehicles) intercept.TickIntercept(m_Mode, now);
if (task.Tactics && task.Tactics.Tick(now)) continue;
Circuit(task, now);
}
+ }
+ override bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
+ {
+ ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(task);
+ if (!supply || !supply.Tactics || supply.Tactics.State() != 0) return false;
+ return super.ReserveIntercept(task, order, now);
}
override bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
{
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c
@@ -369,6 +369,13 @@
}
protected void Start(vector contact, float now)
{
+ // A local threat takes precedence, but may release only this exact detour owner.
+ foreach (ERR_ConvoyVehicle intercept : m_Task.Vehicles)
+ {
+ if (intercept.InterceptToken == 0) continue;
+ intercept.ClearIntercept(intercept.InterceptOrder);
+ if (intercept.InterceptToken != 0) return;
+ }
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false; m_OrdersIssued = false;
if (m_Task.Column) m_Task.Column.Reset(now);
m_Task.Waiting = false;
--- a/tests/test_convoy_column.py
+++ b/tests/test_convoy_column.py
@@ -1,21 +1,25 @@
"""Run the production column controller against physical-position/native-order doubles."""
from policy_harness import ROOT, body, run
-import re
-p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'
+import re,os
+from pathlib import Path
+p=Path(os.environ.get('ERR_SUPPLY_COLUMN_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'))
def cpp(s):
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out ', '')
s=s.replace('foreach (', 'for (').replace('null','nullptr')
for typ in ('ERR_ConvoyVehicle','ERR_ConvoyColumnVehicle'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
- for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','m_Task->Vehicles[previous]'):
+ for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','currentLeader','m_Task->Vehicles[previous]'):
s=s.replace(obj+'.',obj+'->')
for obj in ('Entity','Group','Waypoint','Route','Receipt','Combat'):
s=s.replace(obj+'.',obj+'->')
for cls in ('ERR_ConvoyColumnRules','ERR_ConvoyDriving','ERR_SupplyConvoyTactics','ERR_HQ','Math','vector','string'):
s=s.replace(cls+'.',cls+'::')
+ s=s.replace('array<int>', 'Array<int>').replace('m_State[continuing].', 'm_State[continuing]->').replace('m_State[detached].', 'm_State[detached]->')
+ s=s.replace('bool rejoining;', 'bool rejoining=false;').replace('m_State[returning].','m_State[returning]->').replace('m_Task->Vehicles[returning].','m_Task->Vehicles[returning]->')
+ s=s.replace('m_State[index].', 'm_State[index]->').replace('m_State[leader].', 'm_State[leader]->').replace('m_State[survivor].', 'm_State[survivor]->')
s=s.replace('m_Task->Vehicles[index].', 'm_Task->Vehicles[index]->').replace('(index + 1).ToString()', 'std::to_string(index + 1)')
- s=s.replace('float largestGap; bool supplyHold;', 'float largestGap=0; bool supplyHold=false;')
+ s=s.replace('float largestGap; bool supplyHold; bool interceptLive;', 'float largestGap=0; bool supplyHold=false; bool interceptLive=false;')
return s
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed('))
methods='\n'.join(cpp(body(p,x)) for x in ('protected float DistanceToTarget(', 'protected void Reorder(', 'void Reset(', 'protected void Hold(', 'protected bool Progress(', 'bool Tick('))
@@ -28,7 +32,7 @@
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
namespace Math{float Max(float a,float b){return a>b?a:b;}int Min(int a,int b){return a<b?a:b;}float Round(float x){return std::round(x);}}
namespace ERR_HQ{int stranded=0,retries=0;void Event(string type,string){if(type=="CONVOY STRANDED")stranded++;if(type=="CONVOY ROUTE RETRY")retries++;}}
-template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T value){this->push_back(value);}};
struct ERR_ConvoyColumnRules{'''+rules+r'''};
struct Entity{vector position;vector GetOrigin(){return position;}};
struct Waypoint:Entity{};
@@ -36,7 +40,7 @@
struct Route{Array<vector>points={{0},{400}};int Count(){return points.Count();}vector At(int i){return points[i];}};
struct Combat{bool Held=false;};
struct ERR_ConvoyVehicle{
- int Index=0,RouteIndex=0,clears=0;bool Released=false,Arrived=false,SupplyHeld=false;
+ int Index=0,RouteIndex=0,clears=0;bool InterceptActive=false,Released=false,Arrived=false,SupplyHeld=false;
struct Entity entity;struct Entity* Entity=&entity;struct Group group;struct Group* Group=&group;
struct Combat combat;struct Combat* Combat=&combat;struct Waypoint* Waypoint=nullptr;
void ClearOrder(){if(Waypoint)clears++;delete Waypoint;Waypoint=nullptr;}
@@ -48,13 +52,28 @@
namespace ERR_SupplyConvoyTactics{Waypoint* Move(vector p,float){auto w=new Waypoint();w->position=p;return w;}}
struct Receipt{string SourceId="test";};
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*>Vehicles;struct Route route;struct Route* Route=&route;int Legs=0;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
-struct ERR_ConvoyColumnVehicle{bool Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
+struct ERR_ConvoyColumnVehicle{bool InterceptDetached=false,Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
struct Column{
void* m_Mode=nullptr;ERR_SupplyConvoyTask* m_Task;Array<ERR_ConvoyColumnVehicle*>m_State;
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0;
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
'''+methods+r'''};
int main(){
+ {ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;
+ ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
+ column.m_RouteIndex=1;a.RouteIndex=0;a.entity.position={-20};b.RouteIndex=1;b.entity.position={100};c.RouteIndex=1;c.entity.position={40};assert(column.Tick(0));assert(column.m_Leader==1);
+ a.InterceptActive=false;b.Released=true;assert(column.Tick(1000));assert(column.m_Leader==2&&c.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1);}
+
+ {ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;c.Released=true;
+ ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
+ column.m_RouteIndex=1;a.RouteIndex=0;a.entity.position={-20};b.RouteIndex=1;b.entity.position={100};assert(column.Tick(0));assert(column.m_Leader==1&&b.RouteIndex==1);
+ a.InterceptActive=false;assert(column.Tick(1000));assert(column.m_Leader==1&&b.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1&&b.Waypoint->position.x==400);}
+
+ {ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;b.Released=c.Released=true;
+ ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
+ a.RouteIndex=0;a.entity.position={-200};assert(column.Tick(0));assert(sa.InterceptDetached&&a.RouteIndex==0&&!a.Waypoint);
+ column.m_RouteIndex=1;a.InterceptActive=false;assert(column.Tick(1000));assert(a.RouteIndex==0&&a.Waypoint&&column.m_RouteIndex==0);}
+
assert(!ERR_ConvoyColumnRules::WaitForRear(160,false));assert(ERR_ConvoyColumnRules::WaitForRear(161,false));
assert(ERR_ConvoyColumnRules::WaitForRear(91,true));assert(!ERR_ConvoyColumnRules::WaitForRear(90,true));
assert(ERR_ConvoyColumnRules::TooClose(39,false));assert(!ERR_ConvoyColumnRules::TooClose(40,false));
--- a/tests/test_convoy_combat.py
+++ b/tests/test_convoy_combat.py
@@ -39,7 +39,7 @@
for cls in ['ERR_ConvoyTask','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle','ERR_ForceRecord','AIAgent']:
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
code=code.replace('array<AIAgent>','Array<AIAgent*>').replace('foreach (','for (')
-for name in ['task','order','unit','record','agent','m_Mode']:code=code.replace(name+'.',name+'->')
+for name in ['task','order','unit','record','agent','m_Mode','intercept']:code=code.replace(name+'.',name+'->')
code=code.replace('Receipt.','Receipt->').replace('Combat.','Combat->').replace('Route.','Route->').replace('Group.','Group->').replace('Forces().','Forces()->')
for cls in ['ERR_ConvoyCombatRules','ERR_ConvoyRules','ERR_ForceKind','ERR_HQ']:code=code.replace(cls+'.',cls+'::')
code=code.replace('string.Format','Format')
@@ -52,7 +52,7 @@
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group*){return &record;}};
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}};
struct ERR_ConvoyReceipt{bool Consumed=true;string SourceId="convoy-1";Array<int>Records{3};};
-struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool SupplyHeld=false,Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
+struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool SupplyHeld=false,Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int InterceptToken=0,Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array<ERR_ConvoyVehicle*>Vehicles;};
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}};
struct ERR_ConvoyCombatRules{'''+accept+r'''};
--- a/tests/test_convoy_lifecycle.py
+++ b/tests/test_convoy_lifecycle.py
@@ -1,50 +1,107 @@
"""Actual native convoy ownership/waypoint policy with small engine API doubles."""
import os,re
from pathlib import Path
-from policy_harness import ROOT,run
+from policy_harness import ROOT,run,body
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c')))
source=path.read_text();code=source[source.index('class ERR_ConvoyVehicle'):source.index('class ERR_ConvoyCargoCallback')]
-code=re.sub(r'//[^\n]*','',code).replace('class ERR_ConvoyVehicle','struct ERR_ConvoyVehicle').replace('ref ','')
+code=re.sub(r'//[^\n]*','',code).replace('class ERR_ConvoyVehicle','struct ERR_ConvoyVehicle').replace('ref ','').replace('protected ','')
code=code.replace('array<IEntity>','Array<IEntity*>').replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>').replace('array<AIAgent>','Array<AIAgent*>')
-for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent','ERR_ConvoyCombatState']:
+for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent','ERR_ConvoyCombatState','ERR_ConvoyInterceptOrder','ERR_ConvoyReceipt']:
code=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',code)
-for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route','Combat']:
+for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route','Combat','order','detour','m_InterceptWaypoint','m_InterceptGroup','InterceptOrder']:
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
for cls in ['SCR_EntityHelper','ERR_Actors','ERR_ConvoyRules','ERR_Resources','CompartmentAccessComponent','vector']:
code=code.replace(cls+'.',cls+'::')
-code=code.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n')
+code=code.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('Seats[i].','Seats[i]->').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n')
+code=code.replace('int attributed;', 'int attributed=0;')
code=re.sub(r'\b(bool|int|float|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
+service='\n'.join(body(path, x) for x in ('protected bool InterceptEligible(', 'bool InterceptCurrent(', 'bool ReserveIntercept(', 'bool ApplyIntercept(', 'void ReleaseIntercept('))
+service=service.replace('protected ','').replace('foreach (','for (').replace('null','nullptr')
+for typ in ['ERR_ConvoyTask','ERR_ConvoyInterceptOrder','ERR_ConvoyVehicle','ERR_ForceRecord']:
+ service=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',service)
+for obj in ['task','order','unit','record','other','existing','claimed','m_Mode','hq']:
+ service=service.replace(obj+'.',obj+'->')
+for obj in ['Receipt','Route','Combat']:
+ service=service.replace(obj+'.',obj+'->')
+service=service.replace('ERR_HQ.Cast(m_Mode->Command())','m_Mode->Command()').replace('unit->Index.ToString()', 'std::to_string(unit->Index)')
+service=service.replace('Forces().','Forces()->').replace('task->Vehicles[order->VehicleIndex].','task->Vehicles[order->VehicleIndex]->')
+service=service.replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_ForceKind.','ERR_ForceKind::')
+service='struct ERR_ConvoyReceipt{bool Consumed=true;std::string SourceId="original";Array<int> Records={7,8,9,10,11,12};}; struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array<ERR_ConvoyVehicle*> Vehicles;};struct Service{ERR_GameMode* m_Mode;bool current=true;Array<ERR_ConvoyTask*>m_Tasks;bool Current(){return current;}'+service+'};'
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <cmath>
-struct vector{float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
-template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
+#include <string>
+struct vector{float x=0,y=0,z=0;bool operator!=(vector b){return x!=b.x||y!=b.y||z!=b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
struct SCR_AIGroup;struct Vehicle;struct ERR_ConvoyCombatState{bool Held=false;};
struct IEntity{bool alive=true,player=false,awake=true;Vehicle* vehicle=nullptr;SCR_AIGroup* group=nullptr;IEntity* parent=nullptr;IEntity* GetParent(){return parent;}};
-struct AIAgent{int lod=0;void SetPermanentLOD(int value){lod=value;}};
+struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}int lod=0;void SetPermanentLOD(int value){lod=value;}};
struct AIWaypoint:IEntity{float radius=0;void SetCompletionRadius(float value){radius=value;}bool IsWithinCompletionRadius(SCR_AIGroup* group);};
struct SCR_AIGroup:IEntity{bool within=false;AIWaypoint* current=nullptr;int lod=0,faction=1,removes=0,adds=0;Array<AIAgent*> agents;int GetFaction(){return faction;}void SetPermanentLOD(int value){lod=value;}void GetAgents(Array<AIAgent*>& out){out=agents;}void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}};
bool AIWaypoint::IsWithinCompletionRadius(SCR_AIGroup* group){return group->within;}
struct Vehicle:IEntity{vector position;vector GetOrigin(){return position;}};
-struct BaseCompartmentSlot{};
-struct ERR_ForceRecord{bool Pinned=true;};
+struct BaseCompartmentSlot{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
+namespace ERR_ForceKind{const int CONVOY=3;}
+struct ERR_ForceRecord{SCR_AIGroup* Group=nullptr;std::string SourceId="original-vehicle-0";int Id=7,Kind=3;bool Physical=true,Eliminated=false,Pinned=true;};
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
-struct Command{int unregistrations=0;void UnregisterGroup(SCR_AIGroup*){unregistrations++;}};
+struct Command{int token=400001;int LogisticsToken(SCR_AIGroup*){return token;}int unregistrations=0;void UnregisterGroup(SCR_AIGroup*){unregistrations++;}};
+using ERR_HQ=Command*;struct ERR_ConvoyReceipt;
struct ERR_GameMode{struct Ledger ledger;struct Command command;int EnemyFaction(){return 1;}struct Ledger* Forces(){return &ledger;}struct Command* Command(){return &command;}};
struct ERR_ForceRoute{Array<vector> points;int Count(){return points.size();}vector At(int i){assert(i>=0&&i<Count());return points[i];}};
namespace ERR_Actors{int spawns=0;bool fail=false;bool Alive(IEntity* a){return a&&a->alive;}bool CanFight(IEntity* a){return Alive(a)&&a->awake;}bool Player(IEntity* a){return a&&a->player;}SCR_AIGroup* Group(IEntity* a){return a?a->group:nullptr;}IEntity* Spawn(int,vector){spawns++;return fail?nullptr:new AIWaypoint();}}
namespace ERR_Resources{int MoveWaypoint(){return 1;}}
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* a){return a?a->vehicle:nullptr;}}
-namespace ERR_ConvoyRules{bool PartOf(IEntity* e,IEntity* root){int depth=0;while(e&&depth++<32){if(e==root)return true;e=e->GetParent();}return false;}}
+namespace ERR_ConvoyRules{inline bool captured=false;bool Captured(Vehicle*,SCR_AIGroup*){return captured;}bool PartOf(IEntity* e,IEntity* root){int depth=0;while(e&&depth++<32){if(e==root)return true;e=e->GetParent();}return false;}}
namespace SCR_EntityHelper{int deletes=0;void DeleteEntityAndChildren(AIWaypoint* p){deletes++;delete p;}}
-''' + code + r'''
+namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
+struct ERR_ConvoyInterceptOrder{int Token=400001,VehicleIndex=0,BeliefId=1,BeliefRevision=1,ReportId=1;SCR_AIGroup* Group=nullptr;std::string SourceId="original";vector Position{700,0,0};float ObservedAt=1000,IssuedAt=1000,ExpiresAt=121000;};
+''' + code + service + r'''
struct Case{
- ERR_GameMode mode;Vehicle vehicle;SCR_AIGroup group;IEntity driver;AIAgent agent;ERR_ConvoyVehicle unit;ERR_ForceRoute route;
- Case(){unit.Entity=&vehicle;unit.Group=&group;unit.Driver=&driver;unit.Members.Insert(&driver);driver.vehicle=&vehicle;driver.group=&group;group.agents.Insert(&agent);route.points={{100,0,0},{300,0,0}};}
+ ERR_GameMode mode;Vehicle vehicle;SCR_AIGroup group;IEntity driver;AIAgent agent;BaseCompartmentSlot seat;ERR_ConvoyVehicle unit;ERR_ForceRoute route;
+ Case(){mode.ledger.record.Group=&group;agent.actor=&driver;seat.occupant=&driver;unit.Seats.Insert(&seat);unit.Registered=true;unit.Entity=&vehicle;unit.Group=&group;unit.Driver=&driver;unit.Members.Insert(&driver);driver.vehicle=&vehicle;driver.group=&group;group.agents.Insert(&agent);route.points={{100,0,0},{300,0,0}};}
+};
+struct InterceptCase:Case{Service service;ERR_ConvoyReceipt receipt;ERR_ConvoyTask task;ERR_ConvoyInterceptOrder order;
+ InterceptCase(){service.m_Mode=&mode;task.Receipt=&receipt;task.Route=&route;task.Vehicles={&unit};service.m_Tasks={&task};order.Group=&group;}
+ bool Reserve(){return service.ReserveIntercept(&task,&order,1000);}bool Apply(){return service.ApplyIntercept(&task,&order,2000);}
};
int main(){
+ {InterceptCase f;f.unit.Tick(&f.mode,&f.route,0);float deadline=f.unit.LegDeadline;auto original=f.unit.Waypoint;assert(f.Reserve());f.service.ReleaseIntercept(&f.task,&f.order,2000);assert(f.unit.Waypoint==original&&f.unit.LegDeadline==deadline);}
+ {InterceptCase f;f.unit.Tick(&f.mode,&f.route,0);float deadline=f.unit.LegDeadline;assert(f.Reserve());f.unit.Tick(&f.mode,&f.route,121000);assert(f.unit.LegDeadline==deadline);}
+ {InterceptCase f;assert(f.Reserve());assert(f.Apply());f.service.ReleaseIntercept(&f.task,&f.order,5000);assert(f.unit.LegDeadline==605000&&!f.unit.Waypoint);}
+
+ {InterceptCase f;f.unit.Tick(&f.mode,&f.route,0);auto route=f.unit.Waypoint;assert(f.Reserve());assert(f.unit.Waypoint==route&&!f.unit.InterceptActive);
+ f.unit.Tick(&f.mode,&f.route,1500);assert(f.unit.Waypoint==route&&f.unit.RouteIndex==0);assert(f.Apply());assert(!f.unit.Waypoint&&f.unit.InterceptWaypoint&&f.unit.InterceptActive);
+ assert(!f.Apply());f.group.within=true;f.unit.Tick(&f.mode,&f.route,3000);assert(f.unit.RouteIndex==0);f.vehicle.position=f.order.Position;
+ f.unit.Tick(&f.mode,&f.route,4000);assert(f.unit.InterceptActive&&!f.unit.InterceptWaypoint&&f.unit.RouteIndex==0);
+ f.unit.Tick(&f.mode,&f.route,120999);assert(!f.unit.Waypoint);f.group.within=false;f.unit.Tick(&f.mode,&f.route,121000);assert(!f.unit.InterceptToken&&f.unit.Waypoint&&f.unit.RouteIndex==0);assert(!f.Reserve());}
+ {InterceptCase f;assert(f.Reserve());f.unit.Tick(&f.mode,&f.route,121000);assert(!f.unit.InterceptToken&&f.unit.Waypoint);}
+ {InterceptCase f;assert(f.Reserve());f.group.within=true;f.unit.Tick(&f.mode,&f.route,1100);f.unit.Tick(&f.mode,&f.route,1200);assert(f.unit.RouteIndex==1);assert(f.Apply());assert(f.unit.RouteIndex==1);}
+ for(int bad=0;bad<14;bad++){InterceptCase f;
+ if(bad==0)f.order.Token=300001;
+ if(bad==1)f.order.VehicleIndex=1;
+ if(bad==2)f.order.ExpiresAt=121001;
+ if(bad==3)f.order.Position.x=NAN;
+ if(bad==4)f.order.IssuedAt=1001;
+ if(bad==5)f.unit.Arrived=true;
+ if(bad==6)f.unit.SupplyToken=100001;
+ if(bad==7)f.unit.SupplyHeld=true;
+ if(bad==8)f.unit.Combat->Held=true;
+ if(bad==9)f.seat.occupant=nullptr;
+ if(bad==10)f.mode.ledger.record.Id=99;
+ if(bad==11)f.agent.actor=nullptr;
+ if(bad==12)f.service.current=false;
+ if(bad==13)f.mode.command.token=0;
+ assert(!f.Reserve()&&!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);}
+ {InterceptCase f;assert(f.Reserve());assert(f.Apply());f.driver.alive=false;f.unit.Tick(&f.mode,&f.route,3000);assert(f.unit.Released&&!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);}
+ {InterceptCase f;assert(f.Reserve());assert(f.Apply());f.unit.Members.Clear();f.unit.Tick(&f.mode,&f.route,3000);assert(!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);}
+ {InterceptCase f;assert(f.Reserve());auto foreign=f.order;foreign.Token=400002;f.service.ReleaseIntercept(&f.task,&foreign,2000);assert(f.unit.InterceptToken==400001);
+ f.unit.InterceptToken=400002;f.service.ReleaseIntercept(&f.task,&f.order,2000);assert(f.unit.InterceptToken==400002);}
+ {InterceptCase f;assert(f.Reserve());assert(f.Apply());AIWaypoint foreign;f.unit.InterceptWaypoint=&foreign;f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(f.unit.InterceptWaypoint==&foreign&&!f.unit.InterceptToken);}
+ {InterceptCase f;assert(f.Reserve());f.order.Position.x++;assert(!f.Apply());f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken&&!f.unit.Waypoint);}
+ {InterceptCase f;assert(f.Reserve());ERR_Actors::fail=true;assert(!f.Apply()&&!f.unit.InterceptActive);ERR_Actors::fail=false;f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken);}
+
Case travel;travel.unit.Tick(&travel.mode,&travel.route,1000);assert(travel.group.adds==1&&travel.unit.Waypoint&&travel.unit.LegDeadline==601000);
travel.unit.Tick(&travel.mode,&travel.route,2000);assert(travel.group.adds==1&&travel.unit.LegDeadline==601000);
travel.vehicle.position={100,0,0};travel.group.within=true;travel.unit.Tick(&travel.mode,&travel.route,3000);assert(travel.unit.RouteIndex==1&&travel.group.adds==2&&travel.group.removes==1);
--- a/tests/test_hq_convoy_combat.py
+++ b/tests/test_hq_convoy_combat.py
@@ -49,7 +49,7 @@
int calls=0;bool valid=true;ERR_ConvoyCombatOrder* last=nullptr;
bool ApplyCombatOrder(ERR_ConvoyTask* task,ERR_ConvoyCombatOrder* o,float){calls++;last=o;if(!task||!valid)return false;
auto s=task->Vehicles.at(o->VehicleIndex)->Combat;s->Held=o->Hold;s->LastCommandId=o->CommandId;s->ReportId=o->ReportId;s->ObservedAt=o->ObservedAt;return true;}};
-struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;bool ReservedForSupport=true;void* Radio=this;string ForceSourceId="vehicle-0";
+struct ERR_HQUnit {int LogisticsReservation=0;SCR_AIGroup* Group=nullptr;bool ReservedForSupport=true;void* Radio=this;string ForceSourceId="vehicle-0";
ERR_HQRoute* Downlink=nullptr;ERR_ConvoyCombatOrder* PendingConvoyOrder=nullptr;void* PendingOrder=nullptr;void* PendingTask=nullptr;
bool PendingCheckin=false;float NextStatus=0;
'''+cancel+r'''
--- /dev/null
+++ b/tests/test_hq_mounted_dispatch.py
@@ -0,0 +1,109 @@
+"""Execute actual mounted HQ dispatch methods; radio/physical service are adversarial doubles."""
+from policy_harness import ROOT, body, run
+from pathlib import Path
+import os,re
+P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
+names=['override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
+def cpp(s):
+ s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
+ for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief']:
+ s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
+ s=s.replace('array<vector> reserved','Array<vector> reserved')
+ for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string']:
+ s=s.replace(typ+'.',typ+'::')
+ for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute']:
+ s=re.sub(r'\b'+var+r'\.',var+'->',s)
+ for name in ['StatusSampler','MountedAmmo','Combat','Receipt']:
+ s=s.replace('->'+name+'.','->'+name+'->')
+ s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->')
+ s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
+ s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
+ s=s.replace('Array<vector> reserved,','Array<vector>& reserved,')
+ return s
+methods='\n'.join(cpp(body(P,n)) for n in names if n not in ('protected bool HasMountedInterceptCandidate(', 'protected bool MountedInterceptTaskAvailable(') or n in P.read_text())
+run(r'''
+#include <cassert>
+#include <vector>
+#include <map>
+#include <string>
+#include <algorithm>
+#include <cmath>
+#include <functional>
+#include <sstream>
+struct string:std::string{using std::string::string;string(std::string s):std::string(s){} template<class...T>static string Format(const char*fmt,T...v){std::ostringstream out;out<<fmt;((out<<"|"<<v),...);return out.str();}};
+struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
+std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
+struct Group{};struct Entity{};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
+struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
+struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
+struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
+struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";};
+struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
+struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
+struct ERR_ConvoyInterceptOrder{int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
+struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
+struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
+struct ERR_HQRoute{int result=0,calls=0,MessageId=0;string Payload;int Advance(float,Journal*){calls++;return result;}};
+struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr;int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:nullptr);}};
+struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
+struct Network{ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:route;}};
+struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
+struct Mode{struct Settings settings;struct Settings*Settings(){return &settings;}};
+struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
+ int Count(){return tasks.Count();}ERR_ConvoyTask*At(int n){return tasks[n];}
+ bool InterceptCurrent(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*){return current;}
+ bool ReserveIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){reserves++;return reserve;}
+ bool ApplyIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){applies++;return apply;}
+ void ReleaseIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){releases++;if(onRelease)onRelease();}
+};
+struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
+struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
+struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
+struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
+struct ERR_HQMountedInterceptRules{inline static int selects=0;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
+ static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now<o->ExpiresAt;}
+ static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
+struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
+struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
+ virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
+struct HQ:Base{
+ ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true;
+ Array<int>m_MountedAttemptBeliefs,m_MountedAttemptRevisions;Array<ERR_HQUnit*>m_Units;Watch*m_Watch;Mode*m_Mode;Service*m_Convoys;Memory*m_Memory;Journal*m_Journal;Network*m_Network;
+ bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;}
+'''+methods+r'''
+};
+struct Fixture{Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
+ Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
+ void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
+};
+int main(){
+ {Fixture f;assert(f.h.Status()=="base HQ status");f.queued();string queued=f.h.Status();assert(queued.find("base HQ status")!=string::npos&&queued.find("|0|queued|20|100,0")!=string::npos);f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.Status().find("|0|delivered|20|100,0")!=string::npos);f.h.Stop();assert(f.h.Status()=="base HQ status");}
+ {Fixture f;f.queued();assert(f.h.Assigned()==1&&f.h.HuntingAssigned()==1);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);assert(r.Count()==1&&r[0]==f.belief.destination);f.h.PlanMountedIntercept(4000);assert(f.service.reserves==1);f.h.TickMountedIntercept(4000);assert(f.route.calls==1&&f.service.applies==0);f.route.result=1;f.h.TickMountedIntercept(5000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.belief.BeliefRevision++;f.h.TickMountedIntercept(6000);assert(f.h.m_MountedInterceptDelivered&&f.service.releases==0);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&f.unit.LogisticsReservation==0);}
+ // Task-wide logistics and queued sibling downlinks cannot consume a revision or starve a free farther convoy.
+ for(int kind=0;kind<5;kind++){Fixture f,far;far.unit.ForceSourceId=far.entry.SourceId="crew2";far.receipt.SourceId="convoy2";far.entry.Position={-200,0};f.watch.second=&far.entry;f.service.tasks.Insert(&far.task);f.h.m_Units.Insert(&far.unit);ERR_HQUnit sibling; sibling.Group=&f.otherGroup;f.unused.Group=&f.otherGroup;f.h.m_Units.Insert(&sibling);if(kind==0)f.unused.SupplyToken=123;if(kind==1)f.unused.SupplyHeld=true;if(kind==2)sibling.PendingConvoyOrder=true;if(kind==3)sibling.Downlink=true;if(kind==4)sibling.LogisticsReservation=123;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&far.unit&&!f.unit.LogisticsReservation&&far.unit.LogisticsReservation==400001&&f.service.reserves==1);}
+ // A disconnected nearest escort cannot starve the farther reachable original escort.
+ {Fixture f;Group group2;Entity radio2,entity2;ERR_HQUnit u2=f.unit;u2.Group=&group2;u2.Radio=&radio2;u2.ForceSourceId="crew2";ERR_HQWatchEntry entry2=f.entry;entry2.SourceId="crew2";entry2.Position={-200,0};f.watch.second=&entry2;ERR_ConvoyVehicle vehicle2=f.vehicle;vehicle2.Group=&group2;vehicle2.Entity=&entity2;vehicle2.Index=3;f.task.Vehicles[3]=&vehicle2;f.h.m_Units.Insert(&u2);f.network.denied=&f.radio;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&u2&&f.h.m_MountedIntercept->VehicleIndex==3&&f.network.calls==2&&!f.unit.LogisticsReservation&&u2.LogisticsReservation==400001);}
+ // Invalid world clock must preserve the exact claim for the next finite tick.
+ {Fixture f;f.queued();f.h.TickMountedIntercept(std::nanf(""));assert(f.h.m_MountedIntercept&&f.unit.LogisticsReservation==400001&&f.service.releases==0&&f.route.calls==0);}
+ // No own vehicle/fresh crew evidence means no native road surveys.
+ for(int kind=0;kind<6;kind++){Fixture f;if(kind==0)f.service.tasks.clear();if(kind==1)f.task.Stopped=true;if(kind==2)f.unit.LogisticsReservation=1;if(kind==3)f.entry.Closed=true;if(kind==4)f.sampler.bound=false;if(kind==5)f.combat.Held=true;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&f.service.reserves==0);}
+ // Interrupted pending claims never apply physical orders, and the same delivered revision cannot retry.
+ {Fixture f;f.queued();f.route.result=-1;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.applies==0);f.plan();assert(f.service.reserves==1);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==2&&f.unit.LogisticsReservation==400002);}
+ {Fixture f;f.queued();f.belief.BeliefRevision++;f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.route.calls==0&&f.service.applies==0&&!f.h.m_MountedIntercept);}
+ // Physical validation changes after radio delivery cannot trigger a stale order.
+ for(int kind=0;kind<7;kind++){Fixture f;f.queued();f.route.result=1;if(kind==0)f.ammo.Complete=false;if(kind==1)f.ammo.valid=false;if(kind==2)f.sampler.bound=false;if(kind==3)f.entry.Fit--;if(kind==4)f.entry.LastDeliveredAt=9000;if(kind==5)f.unit.contact=true;if(kind==6)f.service.apply=false;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation);assert(f.service.applies==(kind==6?1:0));}
+ // Conditional release and recursive unregister preserve unrelated reservations.
+ {Fixture f;f.queued();f.unit.LogisticsReservation=123;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept);f.h.ReleaseUnit(&f.unit,4000);};f.h.TickMountedIntercept(4000);assert(f.unit.LogisticsReservation==123&&f.service.releases==1&&f.h.releaseUnits==1);}
+ {Fixture f;f.queued();f.h.Stop();assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1&&f.h.stops==1&&f.service.applies==0);}
+ // Ordinary assigned and pursuit capacities reserve space before any road query or radio action.
+ for(int kind=0;kind<3;kind++){Fixture f;if(kind==0)f.mode.settings.cap=0;if(kind==1)f.h.assigned=3;if(kind==2)f.h.hunting=3;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&ERR_HQMountedInterceptRules::selects==before);}
+ {Fixture f;f.queued();f.h.assigned=3;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&f.service.applies==0);}
+ // Admission is based on detached delivered position, not physical entity telemetry; stock/crew/roles guard it.
+ for(int kind=0;kind<14;kind++){Fixture f;if(kind==0)f.vehicle.Index=1;if(kind==1)f.vehicle.Index=2;if(kind==2)f.vehicle.Arrived=true;if(kind==3)f.vehicle.SupplyToken=1;if(kind==4)f.combat.Held=true;if(kind==5)f.unit.LogisticsReservation=300001;if(kind==6)f.sampler.roster=false;if(kind==7)f.entry.Position={2000,0};if(kind==8)f.entry.ObservedAt=4000;if(kind==9)f.entry.LastDeliveredAt=0;if(kind==10)f.ammo.Weapons=0;if(kind==11)f.ammo.Rounds=0;if(kind==12)f.entry.Count=2;if(kind==13)f.entry.Closed=true;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
+ {Fixture f;f.service.reserve=false;f.plan();assert(!f.unit.LogisticsReservation&&!f.h.m_MountedIntercept);f.service.reserve=true;f.plan();assert(f.service.reserves==1);}
+ {Fixture f;for(int i=0;i<256;i++){f.h.m_MountedAttemptBeliefs.Insert(i+20);f.h.m_MountedAttemptRevisions.Insert(1);}f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
+ {Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);}
+}
+''')
+print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
--- /dev/null
+++ b/tests/test_hq_mounted_intercept.py
@@ -0,0 +1,102 @@
+"""Actual delivered-memory road selector, geometry and native-query boundaries."""
+from pathlib import Path
+import os,re
+from policy_harness import ROOT,body,run
+p=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c'))
+source=p.read_text()
+base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
+source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
+for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
+ assert forbidden not in source,forbidden
+rules='\n'.join(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c', x) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static vector Direction(', 'static vector Objective('))
+rules=rules.replace('Math.','Math::').replace('vector.','vector::').replace('ERR_HQPursuitRole.','ERR_HQPursuitRole::').replace('vector offset;','vector offset{};')
+s=re.sub(r'//[^\n]*','',source).replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')
+s=s.replace('out vector','vector&').replace('out float','float&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
+s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
+for name in ('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement'):
+ s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
+for name in ('belief','world','road','body','order','settlements','placement'):
+ s=s.replace(name+'.',name+'->')
+s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->').replace('GetWorld().','GetWorld()->')
+for name in ('ERR_ForceRules','ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','vector','Math','ERR_Actors','ChimeraWorldUtils','TraceFlags','Replication','ERR_HQRoadIntercept'):
+ s=s.replace(name+'.',name+'::')
+s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')
+s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('float score, heading;', 'float score=0, heading=0;').replace('\n}\n','\n};\n')
+run(r'''
+#include <cassert>
+#include <algorithm>
+#include <cmath>
+#include <cstdio>
+#include <limits>
+#include <vector>
+#include <string>
+enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};enum class ERR_HQMemoryKind{SIGHTING,DISTRESS,BODY};
+struct SCR_AIGroup{};
+struct Math{static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float AbsFloat(float x){return std::abs(x);}};
+struct vector{float x=0,y=0,z=0;vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}vector(const char*s){sscanf(s,"%f %f %f",&x,&y,&z);}float&operator[](int i){return i==0?x:i==1?y:z;}float operator[](int i)const{return i==0?x:i==1?y:z;}
+ vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}bool operator==(vector b)const{return x==b.x&&y==b.y&&z==b.z;}
+ float Length()const{return std::sqrt(x*x+y*y+z*z);}vector Normalized()const{float n=Length();return{x/n,y/n,z/n};}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static vector Lerp(vector a,vector b,float n){return a+(b-a)*n;}static const vector Zero;};
+const vector vector::Zero{};vector Vector(float a,float b,float c){return{a,b,c};}
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Insert(T v){this->push_back(v);}explicit operator bool(){return true;}};
+struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}};
+struct ERR_HQPursuitRules{''' + rules + r'''};
+struct ERR_HQBelief{ERR_HQMemoryKind Kind=ERR_HQMemoryKind::SIGHTING;int SampleCount=2,MessageId=1,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};float ObservedAt=5000,LastDeliveredAt=7000,Confidence=.9,Radius=40;std::string TravelMode="on foot";vector PredictedPosition={1000,10,1000},Velocity={0,0,4};bool HasVelocity=true;};
+struct TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
+struct NativeWorld{int traces=0;float fraction=1;float TraceMove(TraceBox* t,void*){traces++;assert(t->Flags==3&&t->Mins.y==-.9f);return fraction;}};
+struct BaseRoad{float width=6;Array<vector>points={{900,10,1300},{1100,10,1300}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){p=points;}};
+struct RoadManager{Array<BaseRoad*>roads;int queries=0;void GetRoadsInAABB(vector low,vector high,Array<BaseRoad*>&out){queries++;assert(high.x-low.x==200&&high.z-low.z==200&&high.y-low.y==2000);out=roads;}};
+struct SCR_AIWorld{RoadManager manager;bool available=true;RoadManager*GetRoadNetworkManager(){return available?&manager:nullptr;}};
+struct Game{NativeWorld world;SCR_AIWorld ai;NativeWorld*GetWorld(){return &world;}SCR_AIWorld*GetAIWorld(){return &ai;}}game;Game*GetGame(){return &game;}
+struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
+struct ERR_Actors{static inline bool steep=false;static vector Ground(vector p){p.y=10;if(steep&&p.x>1000)p.y=11;return p;}};
+struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
+
+struct ERR_SettlementSafety{static inline bool clear=true;static inline int calls=0;bool OutsideSettlements(vector){calls++;return clear;}};
+struct ERR_ConvoyPlacement{static inline bool clear=true;static inline int calls=0;static inline float heading=0;bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear;}};
+''' + s + r'''
+int main(){
+ using Rules=ERR_HQMountedInterceptRules;ERR_HQBelief belief;SCR_AIGroup group;
+ assert(Rules::Eligible(&belief,10000));
+ ERR_ConvoyInterceptOrder order;order.Token=400001;order.Group=&group;order.SourceId="original-convoy";order.VehicleIndex=0;order.Position={1000,10,1300};order.IssuedAt=8000;order.ExpiresAt=125000;order.ObservedAt=5000;order.BeliefId=3;order.BeliefRevision=2;order.ReportId=1;
+ vector frozen=order.Position;assert(Rules::Current(&order,&belief,10000));belief.PredictedPosition={1050,10,1000};assert(Rules::Current(&order,&belief,11000)&&order.Position==frozen);belief.PredictedPosition={1000,10,1000};
+ for(int fault=0;fault<21;fault++){auto b=belief;auto o=order;float now=10000;
+ if(fault==0)o.Token=400000;
+ if(fault==1)o.Token=400257;
+ if(fault==2)o.Group=nullptr;
+ if(fault==3)o.SourceId="";
+ if(fault==4)o.VehicleIndex=1;
+ if(fault==5)o.BeliefId++;
+ if(fault==6)o.BeliefRevision++;
+ if(fault==7)o.ReportId++;
+ if(fault==8)o.ObservedAt++;
+ if(fault==9)o.IssuedAt=6000;
+ if(fault==10)o.IssuedAt=11000;
+ if(fault==11)o.ExpiresAt=125001;
+ if(fault==12)o.ExpiresAt=10000;
+ if(fault==13)o.IssuedAt=NAN;
+ if(fault==14)o.Position.x=NAN;
+ if(fault==15)b.HasVelocity=false;
+ if(fault==16)b.SampleCount=1;
+ if(fault==17)b.LastDeliveredAt=10001;
+ if(fault==18)b.TravelMode="aircraft";
+ if(fault==19)now=38001;
+ if(fault==20)b.ObservedAt=-1;
+ assert(!Rules::Current(&o,&b,now));}
+ order.VehicleIndex=3;assert(Rules::Current(&order,&belief,38000));order.VehicleIndex=5;assert(Rules::Current(&order,&belief,38000));
+ assert(!Rules::Eligible(&belief,50001));assert(!Rules::Eligible(nullptr,10000));
+ Array<vector> reserved;vector result;BaseRoad road;game.ai.manager.roads={&road};
+ assert(Rules::Select(&belief,10000,reserved,result)&&result==vector(1000,10,1300));assert(ERR_ConvoyPlacement::calls==1&&ERR_SettlementSafety::calls==1);assert(std::abs(ERR_ConvoyPlacement::heading-90)<.001);
+ road.width=5.99;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);road.width=6;
+ int placementCalls=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero&&ERR_ConvoyPlacement::calls==placementCalls);ERR_SettlementSafety::clear=true;
+ ERR_ConvoyPlacement::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);ERR_ConvoyPlacement::clear=true;
+ road.points={{900,20,1300},{1100,20,1300}};placementCalls=ERR_ConvoyPlacement::calls;assert(!Rules::Select(&belief,10000,reserved,result)&&ERR_ConvoyPlacement::calls==placementCalls);road.points={{900,10,1300},{1100,10,1300}};
+ reserved.Insert({1000,10,1300});assert(!Rules::Select(&belief,10000,reserved,result));reserved.clear();
+ game.ai.manager.roads.resize(129,&road);assert(!Rules::Select(&belief,10000,reserved,result));game.ai.manager.roads={&road};
+ auto unknown=belief;unknown.HasVelocity=false;int queries=game.ai.manager.queries;assert(!Rules::Select(&unknown,10000,reserved,result)&&game.ai.manager.queries==queries);
+ Replication::server=false;assert(!Rules::Select(&belief,10000,reserved,result)&&game.ai.manager.queries==queries);Replication::server=true;
+ float score,heading;assert(Rules::FromPolyline({{900,10,1300},{1100,10,1300}},belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading)&&result.z==1300);
+ assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1299}));assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1901}));
+ Array<vector> oversized(1025,{1000,10,1300});assert(!Rules::FromPolyline(oversized,belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading));
+}
+''')
+print('PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim')
--- a/tests/test_supply_convoys.py
+++ b/tests/test_supply_convoys.py
@@ -42,13 +42,13 @@
struct Receipt{string SourceId="test";};
struct Combat{bool Held=false;};
struct Column{bool live=true;int resets=0;bool Tick(float){return live;}void Reset(float,bool){resets++;}};
-struct ERR_ConvoyVehicle{bool Released=false,Arrived=false,SupplyHeld=false;struct Combat combat;struct Combat* Combat=&combat;int cleared=0,RouteIndex=3;struct Entity entity;struct Entity* Entity=&entity;
+struct ERR_ConvoyVehicle{int InterceptToken=0;bool Released=false,Arrived=false,SupplyHeld=false;struct Combat combat;struct Combat* Combat=&combat;int cleared=0,RouteIndex=3;struct Entity entity;struct Entity* Entity=&entity;
void ClearOrder(){cleared++;}void Tick(void*,ERR_ForceRoute*,float){}};
struct ERR_SupplyConvoyTask{struct Column column;struct Column* Column=&column;Array<ERR_ConvoyVehicle*> Vehicles;float TravelDeadline=1000,Departure=0;bool Waiting=false,stopped=false;int LastBase=-1,Legs=0;
Array<vector> Bases={{0},{1000},{2000}};ERR_ForceRoute* Route=nullptr;struct Receipt receipt;struct Receipt* Receipt=&receipt;
void Stop(void*,string){stopped=true;}};
struct Service{void* m_Mode=nullptr;
-'''+cpp(body(D/'ERR_SupplyConvoyService.c','protected void Circuit('))+r'''};
+'''+cpp(body(Path(os.environ.get('ERR_SUPPLY_SERVICE_SOURCE',D/'ERR_SupplyConvoyService.c')),'protected void Circuit('))+r'''};
int main(){
for(int last=0;last<5;last++)for(int roll=0;roll<4;roll++){int next=ERR_SupplyConvoyRules::NextBase(5,last,roll);assert(next>=0&&next<5&&next!=last);}
assert(ERR_SupplyConvoyRules::NextBase(1,0,0)==-1);assert(ERR_SupplyConvoyRules::NextBase(3,3,0)==-1);
@@ -352,3 +352,33 @@
}
''')
print('PASS: actual supply startup reasons expose unchanged body budget/requirement, journal only failure changes, preserve retry cadence/caps and report the true final refusal')
+# Execute the entire actual combat-start method: exact detour cancellation must
+# precede ordinary waypoint/tactics ownership; foreign claims prevent takeover.
+start=cpp(body(TACTICS,'protected void Start(')).replace('intercept.','intercept->')
+start=start.replace('"0 0 1"','vector(0,0,1)').replace('Vector(','vector(')
+run(r'''
+#include <cassert>
+#include <vector>
+#include <string>
+#include <cmath>
+using string=std::string;
+struct vector{float v[3]={};vector(float x=0,float y=0,float z=0):v{x,y,z}{}float&operator[](int i){return v[i];}vector operator-(vector b){return {v[0]-b[0],v[1]-b[1],v[2]-b[2]};}float Length(){return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}void Normalize(){float n=Length();for(auto&x:v)x/=n;}};
+template<class T>using Array=std::vector<T>;
+struct Order{};struct Combat{bool Held=false;};struct Entity{vector GetOrigin(){return {};}};
+struct ERR_ConvoyVehicle{int InterceptToken=0,clears=0,cancels=0;bool own=true,Released=false;Order order;Order*InterceptOrder=&order;struct Combat combat;struct Combat*Combat=&combat;struct Entity entity;struct Entity*Entity=&entity;
+ void ClearIntercept(Order*p){cancels++;if(own&&p==&order)InterceptToken=0;}void ClearOrder(){clears++;}};
+struct Column{int resets=0;void Reset(float){resets++;}};struct Receipt{string SourceId="original";};
+struct Task{bool Waiting=true;Array<ERR_ConvoyVehicle*>Vehicles;struct Column column;struct Column*Column=&column;struct Receipt receipt;struct Receipt*Receipt=&receipt;};
+namespace ERR_HQ{int events=0;void Event(string,string){events++;}}
+struct Tactics{Task*m_Task;int m_State=0,m_DismountAttempts=0,m_NextDismount=0,clears=0,orders=0;bool m_TruckWithdrawn=true,m_OrdersIssued=true;vector m_Contact,m_Forward,m_Left;
+ void Clear(){clears++;}void Orders(bool,float){orders++;}
+'''+start+r'''
+};
+int main(){
+ ERR_ConvoyVehicle a,b;Task task;task.Vehicles={&a,&b};Tactics tactics{&task};
+ a.InterceptToken=400001;b.InterceptToken=400002;b.own=false;
+ tactics.Start({100,0,0},1000);assert(a.InterceptToken==0&&b.InterceptToken==400002&&tactics.clears==0&&task.column.resets==0&&tactics.orders==0&&!a.combat.Held);
+ b.own=true;tactics.Start({100,0,0},2000);assert(b.InterceptToken==0&&tactics.clears==1&&task.column.resets==1&&tactics.orders==1&&a.combat.Held&&b.combat.Held&&a.clears==1);
+}
+''')
+print('PASS: actual combat start releases only exact intercept claims before owning route/orders; foreign claim defers combat takeover')
--- a/tools/check.py
+++ b/tools/check.py
@@ -84,7 +84,7 @@
assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries'
print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities')
print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla/authorized external references')
- for name in ['test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_opening.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
+ for name in ['test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_opening.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True)
print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.')
if __name__=='__main__':check()