--- 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 m_InterceptMembers = {}; + protected ref array 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 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_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 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', 'Array').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 astruct Array:std::vector{using std::vector::vector;int Count(){return this->size();}}; +templatestruct Array:std::vector{using std::vector::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{Arraypoints={{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{ArrayVehicles;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;Arraym_State; Arraym_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','Array').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";ArrayRecords{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;ArrayMembers;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;ArrayMembers;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;ArrayVehicles;}; 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','Array').replace('array','Array').replace('array','Array') -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(').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n') +code=code.replace('AIWaypoint.Cast(', 'static_cast(').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 Records={7,8,9,10,11,12};}; struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array Vehicles;};struct Service{ERR_GameMode* m_Mode;bool current=true;Arraym_Tasks;bool Current(){return current;}'+service+'};' run(r''' #include #include #include #include -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);}}; -templatestruct Array:std::vector{using std::vector::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 +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);}}; +templatestruct Array:std::vector{using std::vector::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 agents;int GetFaction(){return faction;}void SetPermanentLOD(int value){lod=value;}void GetAgents(Array& 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 points;int Count(){return points.size();}vector At(int i){assert(i>=0&&ialive;}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_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 Vehicles;float TravelDeadline=1000,Departure=0;bool Waiting=false,stopped=false;int LastBase=-1,Legs=0; Array 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 +#include +#include +#include +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;}}; +templateusing Array=std::vector; +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=ℴ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;ArrayVehicles;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')