Files
EscapistsReintroduced/tests/test_convoy_column.py
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

426 lines
35 KiB
Python

"""Run the production column controller against physical-position/native-order doubles."""
from policy_harness import ROOT, body, run
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 vector ', 'vector& ')
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]','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[previous].','m_State[previous]->')
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; 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(', 'static bool StraightAhead('))
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('))
trail='\n'.join(cpp(body(p,x)) for x in ('void Record(', 'bool Target('))
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
#include <string>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
struct vector{float x=0,z=0;float& operator[](int i){return i==0?x:z;}vector operator+(vector b){return{x+b.x,z+b.z};}vector operator-(vector b){return{x-b.x,z-b.z};}vector operator*(float n){return{x*n,z*n};}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
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();}void Insert(T value){this->push_back(value);}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Remove(int i){this->erase(this->begin()+i);}};
struct ERR_ConvoyTrail{Array<vector>Points;'''+trail+r'''};
struct ERR_ConvoyColumnRules{'''+rules+r'''};
struct Entity{vector position;vector GetOrigin(){return position;}};
struct Waypoint:Entity{};
struct Group{int added=0;void AddWaypoint(Waypoint*){added++;}};
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 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;}
void Release(void*){Released=true;ClearOrder();}
bool Order(struct Route* route,float){if(!Waypoint){Waypoint=new struct Waypoint();Waypoint->position=route->At(RouteIndex);Group->AddWaypoint(Waypoint);}return true;}
~ERR_ConvoyVehicle(){ClearOrder();}
};
namespace ERR_ConvoyDriving{bool Restore(void*,ERR_ConvoyVehicle*){return false;}}
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{ERR_ConvoyTrail Trail;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,m_RearWaitAt=-1;
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
'''+methods+r'''};
int main(){
// No stop/arrival dead zone behind a parked leader, including an empty trail after reform.
for(float gap:{75.1f,78.f,80.f,90.f}){
ERR_ConvoyVehicle lead,tail;ERR_ConvoyColumnVehicle ls,ts;ERR_SupplyConvoyTask task;
lead.entity.position={400};tail.entity.position={400-gap};task.Vehicles={&lead,&tail};
Column column{nullptr,&task,{&ls,&ts}};column.m_Order={0,1};column.m_RouteIndex=1;column.Tick(0);
assert(lead.Arrived&&!tail.Arrived&&tail.Waypoint&&tail.Waypoint->position.x==365);
tail.entity.position={326};column.Tick(1000);assert(tail.Arrived&&!tail.Waypoint);
}
// Dense straight samples select one lookahead target, then retain it while driving.
{ERR_ConvoyVehicle lead;ERR_ConvoyColumnVehicle state;ERR_SupplyConvoyTask task;
task.Vehicles={&lead};task.route.points={{0},{20},{40},{60},{80},{100},{120},{140},{160},{180},{200}};
Column column{nullptr,&task,{&state}};column.m_Order={0};column.Tick(0);
assert(lead.RouteIndex==5&&lead.Waypoint->position.x==100&&lead.group.added==1);
lead.entity.position={20};column.Tick(1000);assert(lead.RouteIndex==5&&lead.group.added==1);
lead.entity.position={60};column.Tick(2000);assert(lead.RouteIndex==5&&lead.group.added==1);
lead.entity.position={70};column.Tick(3000);assert(lead.RouteIndex==8&&lead.Waypoint->position.x==160&&lead.group.added==2);
lead.entity.position={150};column.Tick(4000);assert(!lead.Arrived&&lead.Waypoint->position.x==200);
lead.entity.position={164};column.Tick(5000);assert(!lead.Arrived);
lead.entity.position={165};column.Tick(6000);assert(lead.Arrived);
}
assert(ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{150,0}));
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{81,0},{150,0}));
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{70,80}));
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{0,0}));
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{70,0}));
{ERR_ConvoyTrail trail;vector target;assert(!trail.Target(target));trail.Record({0,0});trail.Record({0,50});trail.Record({50,50});assert(trail.Target(target)&&target.x==15&&target.z==50); // follows the bend, never a diagonal chord
trail.Record({500,500});assert(!trail.Target(target)&&trail.Points.Count()==1);for(int i=1;i<100;i++)trail.Record({500.f+i*10,500});assert(trail.Points.Count()==48);}
{ERR_ConvoyVehicle a,b,c;ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column col{nullptr,&task,{&sa,&sb,&sc}};a.entity.position={100};b.entity.position={-200};c.entity.position={-300};col.Tick(0);assert(col.m_RearWait&&!a.Waypoint&&sa.Speed==0);col.Tick(20000);assert(a.Waypoint&&sa.Speed==12);}
{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);}
// Two detached original escorts rejoin without changing the continuing leader's route.
for(int returnFirst=0;returnFirst<3;returnFirst++){
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=c.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.entity.position={-20};b.entity.position={100};c.entity.position={-80};
assert(column.Tick(0)&&column.m_Leader==1&&sa.InterceptDetached&&sc.InterceptDetached);
assert(!a.Waypoint&&!c.Waypoint&&b.RouteIndex==1);
if(returnFirst==0)a.InterceptActive=false;
if(returnFirst==1)c.InterceptActive=false;
if(returnFirst==2)a.InterceptActive=c.InterceptActive=false;
assert(column.Tick(1000)&&column.m_Leader==1&&column.m_RouteIndex==1&&b.RouteIndex==1);
a.InterceptActive=c.InterceptActive=false;
assert(column.Tick(2000)&&column.m_Leader==1&&column.m_RouteIndex==1&&!sa.InterceptDetached&&!sc.InterceptDetached);
assert(a.RouteIndex==0&&c.RouteIndex==0); // no artificial route advancement on detour return
}
// A returning escort becomes a continuing follower before the column leader is lost.
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=c.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.entity.position={-20};b.entity.position={100};c.entity.position={40};
assert(column.Tick(0));c.InterceptActive=false;assert(column.Tick(1000));
b.Release(nullptr);a.InterceptActive=false;assert(column.Tick(2000));
assert(column.m_Leader==2&&column.m_RouteIndex==1&&c.RouteIndex==1&&a.RouteIndex==0);
}
// With no continuing vehicle, a sole returning original uses its own saved route.
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=c.InterceptActive=true;b.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.entity.position={-200};c.entity.position={-260};
assert(column.Tick(0)&&column.m_Leader==-1&&!a.Waypoint&&!c.Waypoint);
c.Release(nullptr);a.InterceptActive=false;assert(column.Tick(1000));
assert(column.m_Leader==0&&column.m_RouteIndex==0&&a.RouteIndex==0&&a.Waypoint);
}
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));
assert(ERR_ConvoyColumnRules::TooClose(64,true));assert(!ERR_ConvoyColumnRules::TooClose(65,true));
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;b.entity.position={-35};c.entity.position={-70};
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
assert(column.Tick(0));assert(a.RouteIndex==1&&a.Waypoint&&a.Waypoint->position.x==400);assert(!b.Waypoint&&!c.Waypoint);
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==45&&sb.Speed==30);assert(!c.Waypoint);
int additions=b.group.added;a.entity.position={110};column.Tick(2000);assert(b.group.added==additions); // stable target, no per-second churn
a.entity.position={200};column.Tick(3000);assert(column.m_RearWait&&!a.Waypoint&&b.Waypoint); // leader waits for laggard
b.entity.position={130};c.entity.position={60};column.Tick(11000);assert(!column.m_RearWait&&a.Waypoint);
assert(b.Waypoint->position.x==165); // native target follows predecessor, never remote base marker
b.SupplyHeld=true;auto supplyOrder=b.Waypoint;column.Tick(12000);assert(!a.Waypoint&&b.Waypoint==supplyOrder&&sb.LastProgress==-1);
b.SupplyHeld=false;b.combat.Held=true;column.Tick(13000);assert(b.Waypoint==supplyOrder&&sb.LastProgress==-1);b.combat.Held=false;
// Real vehicle positions determine arrival; no stale seated-group origin is consulted.
a.entity.position={400};b.entity.position={330};c.entity.position={260};column.Tick(20000);
assert(a.Arrived&&b.Arrived&&c.Arrived&&!a.Waypoint&&!b.Waypoint&&!c.Waypoint);
// Loss promotes the next surviving vehicle at the current route cursor, no replacement/restart at spawn.
column.Reset(20500);assert(column.m_Leader==0&&column.m_RouteIndex==2); // combat reset preserves route progress for a later promotion
a.Release(nullptr);b.Arrived=c.Arrived=false;column.Tick(21000);assert(column.m_Leader==1&&b.RouteIndex==1&&!b.Arrived&&b.Waypoint->position.x==400);
// Commanded stationary time alone retries and eventually releases; holds reset this clock.
ERR_ConvoyVehicle stuck;ERR_ConvoyColumnVehicle ss;
assert(column.Progress(&stuck,&ss,0));assert(column.Progress(&stuck,&ss,45000));assert(ERR_HQ::retries==1&&!stuck.Released);
column.Hold(&stuck,&ss,170000);assert(column.Progress(&stuck,&ss,180000));assert(!stuck.Released);
stuck.entity.position={6};assert(column.Progress(&stuck,&ss,300000));assert(ss.LastProgress==300000);
assert(column.Progress(&stuck,&ss,479999));assert(!column.Progress(&stuck,&ss,480000)&&stuck.Released&&ERR_HQ::stranded==1);
column.Reset(500000,true);assert(sa.Trail.Points.IsEmpty()&&sb.Trail.Points.IsEmpty()&&sc.Trail.Points.IsEmpty());assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
// Post-combat physical order avoids sending the front truck toward a rear vehicle coming toward it.
b.entity.position={100};c.entity.position={300};b.Arrived=c.Arrived=false;column.m_RouteIndex=1;column.Reset(500001,false,true);
assert(column.m_Order[0]==2&&column.m_Order[1]==1&&column.m_Order[2]==0); // released vehicle last
assert(task.Vehicles[1]==&b&&task.Vehicles[2]==&c); // combat roles and canonical identities unchanged
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&!b.Waypoint); // fresh formation never follows an old leg trail
b.entity.position={230};column.Tick(500003);assert(c.Waypoint&&c.Waypoint->position.x==400);
b.Release(nullptr);c.Release(nullptr);assert(!column.Tick(510000));
}
''')
print('PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery')
q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
orders=body(q,'protected void Orders(').replace('protected ','').replace('m_Task.','m_Task->').replace('vector.','vector::')
run(r'''
#include <cassert>
#include <cmath>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
struct Items{int count;int Count(){return count;}};
struct Task{Items Vehicles{6},Teams{2};};
struct Controller{Task task;Task* m_Task=&task;float m_NextOrders=0;vector m_LastOrderContact,m_Contact;bool m_OrdersIssued=false,m_TruckWithdrawn=false;int support=0,flank=0;
void Support(int,bool,bool=false){support++;}void Flank(int,bool){flank++;}
'''+orders+r'''};
int main(){
Controller c;c.Orders(true,0);assert(c.support==6&&c.flank==2);
c.Orders(false,30000);assert(c.support==12&&c.flank==2&&c.m_NextOrders==60000);
c.m_Contact={39};c.Orders(false,60000);assert(c.support==18&&c.flank==2);
c.m_Contact={40};c.Orders(false,90000);assert(c.support==24&&c.flank==4);
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==30&&c.flank==6); // new engagement can issue at same location
}
''')
print('PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves')
probe=ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c'
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[1].','Vehicles[1]->').replace('Entity.','Entity->').replace('vector.','vector::')
run(r'''
#include <cassert>
#include <cmath>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
void Print(const char*){}
struct Entity{vector position;vector GetOrigin(){return position;}};
struct Vehicle{bool Released=false;struct Entity entity;struct Entity* Entity=&entity;};
struct Bases{int Count(){return 2;}};
struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Bases;};
struct Probe{Task* m_CombatTask;int m_Phase=3,m_TestLegs=0;vector m_RouteResumePosition;bool m_AllMoved=true;
'''+departure+r'''};
int main(){
Task task;Vehicle truck;task.Vehicles[1]=&truck;Probe p{&task};truck.entity.position={200};
assert(p.Departure()==0&&p.m_Phase==3); // earlier regroup displacement cannot pass
task.Legs=1;assert(p.Departure()==0&&p.m_Phase==4&&p.m_RouteResumePosition.x==200);
assert(p.Departure()==0);truck.entity.position={219};assert(p.Departure()==0);
truck.entity.position={220};assert(p.Departure()==1&&p.m_Phase==5);
truck.Released=true;assert(p.Departure()==-1);
}
''')
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
convoy=smoke.split('else if (m_ERRSmokeStep == 106)')[1].split('else if (m_ERRSmokeStep == 105)')[0]
assert 'Transition(ERR_Phase.DEFEAT)' in convoy and 'm_ERRSmokeStep = 1061' in convoy
assert 'damage.Kill' not in convoy and 'm_ERRSmokeStep = 9' not in convoy
print('PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death')
boarding=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c'
retry=body(boarding,'bool Retry(')
refresh=body(q,'protected void RefreshBoarding(').replace('protected ','').replace('foreach (','for (').replace('m_Task.','m_Task->').replace('team.','team->').replace('Vehicles[2].','Vehicles[2]->').replace('BoardingProgress.','BoardingProgress->').replace('Receipt.','Receipt->').replace('ERR_HQ.','ERR_HQ::').replace('Math.','Math::').replace('string.','string::').replace('ERR_SupplyTeam team','ERR_SupplyTeam* team')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct string{template<class...T>static string Format(T...){return {};}};
namespace Math{float Round(float x){return std::round(x);}}
namespace ERR_HQ{void Event(const char*,string){}}
struct ERR_ConvoyBoardingProgress{int Seated=-1,Retries=0;float Distance=-1,LastProgress=0,NextRetry=0;
'''+retry+r'''};
struct ERR_SupplyTeam{bool Released=false,aboard=false;float BoardingRetryAt=0,distance=200;int seated=0,clears=0,adds=0;ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;
bool Aboard(void*,bool){return aboard;}int BoardingCount(void*,float& d){d=distance;return seated;}void Clear(){clears++;}void Add(int){adds++;}};
struct Vehicle{void* Entity=nullptr;};struct Receipt{const char* SourceId="test";};
struct Task{std::vector<ERR_SupplyTeam*>Teams;Vehicle* Vehicles[3];struct Receipt receipt;struct Receipt* Receipt=&receipt;};
struct Tactics{Task* m_Task;int Board(void*,bool){return 1;}
'''+refresh+r'''};
int main(){
ERR_ConvoyBoardingProgress p;assert(!p.Retry(0,200,0));
assert(!p.Retry(0,194,29000));assert(!p.Retry(0,194,58000));assert(p.Retry(0,194,59000));
assert(!p.Retry(1,100,60000));assert(!p.Retry(1,100,89999));assert(p.Retry(1,100,90000));
assert(p.Retry(1,100,120000));assert(!p.Retry(1,100,150000)&&p.Retries==3); // bounded even if no progress
Task task;Vehicle truck;task.Vehicles[2]=&truck;ERR_SupplyTeam team,finished,released;finished.aboard=true;released.Released=true;
task.Teams={&team,&finished,&released};Tactics t{&task};t.RefreshBoarding(0);assert(team.BoardingProgress&&!finished.BoardingProgress&&!released.BoardingProgress);
t.RefreshBoarding(30000);assert(team.clears==1&&team.adds==0&&team.BoardingRetryAt==31000);
t.RefreshBoarding(30999);assert(team.adds==0);t.RefreshBoarding(31000);assert(team.adds==1&&team.BoardingRetryAt==0);
team.seated=6;team.distance=10;t.RefreshBoarding(40000);assert(team.clears==1);t.RefreshBoarding(69999);assert(team.clears==1);
t.RefreshBoarding(70000);assert(team.clears==2);team.aboard=true;t.RefreshBoarding(71000);assert(team.adds==1); // do not disturb completed boarding
delete team.BoardingProgress;
}
''')
print('PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation')
safe='\n'.join(body(q,s) for s in ('protected float InfantryDistance(', 'protected void ProtectInfantry('))
safe=safe.replace('protected ','').replace('foreach (','for (')
safe=safe.replace('AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));', 'AICarMovementComponent* movement = &vehicle->Entity->movement;')
for typ in ('IEntity','ERR_SupplyTeam','ERR_ConvoyVehicle'):safe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',safe)
for obj in ('m_Task','team','member','vehicle','movement'):safe=safe.replace(obj+'.',obj+'->')
for cls in ('ERR_Actors','CompartmentAccessComponent','Math','vector'):safe=safe.replace(cls+'.',cls+'::')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
namespace Math{float Min(float a,float b){return a<b?a:b;}}
struct AICarMovementComponent{float speed=99;void SetCruiseSpeed(float s){speed=s;}};
struct IEntity{vector position;bool fit=true,aboard=false;AICarMovementComponent movement;vector GetOrigin(){return position;}};
namespace ERR_Actors{bool CanFight(IEntity* m){return m&&m->fit;}}
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* m){return m->aboard?m:nullptr;}}
struct ERR_SupplyTeam{bool Released=false;std::vector<IEntity*>Members;};
struct ERR_ConvoyVehicle{bool Released=false;IEntity* Entity;};
struct Task{std::vector<ERR_SupplyTeam*>Teams;std::vector<ERR_ConvoyVehicle*>Vehicles;};
struct Tactics{Task* m_Task;
'''+safe+r'''};
int main(){
IEntity truck{{0}},car{{100}},foot{{30}},passenger{{0}};passenger.aboard=true;ERR_SupplyTeam team{false,{&foot,&passenger}};
ERR_ConvoyVehicle a{false,&truck},b{false,&car};Task task{{&team},{&a,&b}};Tactics t{&task};
assert(t.InfantryDistance(&truck)==30);t.ProtectInfantry();assert(truck.movement.speed==8&&car.movement.speed==35);
foot.aboard=true;t.ProtectInfantry();assert(truck.movement.speed==35&&car.movement.speed==35);
foot.aboard=false;foot.fit=false;assert(t.InfantryDistance(&truck)>100000);foot.fit=true;team.Released=true;assert(t.InfantryDistance(&truck)>100000);
team.Released=false;a.Released=true;truck.movement.speed=99;t.ProtectInfantry();assert(truck.movement.speed==99); // never retake a released vehicle
}
''')
print('PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles')
restore=body(p,'static bool Restore(').replace('ERR_GameMode mode','void* mode').replace('ERR_ConvoyVehicle vehicle','ERR_ConvoyVehicle* vehicle')
restore=restore.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));','SCR_AIVehicleUsageComponent* usage = vehicle->Entity->usage;')
restore=restore.replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
for obj in ('vehicle','Group','utility','usage'):restore=restore.replace(obj+'.',obj+'->')
restore=restore.replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::')
run(r'''
#include <cassert>
struct SCR_AIVehicleUsageComponent{bool pilot=true;bool CanBePiloted(){return pilot;}};
struct Entity{SCR_AIVehicleUsageComponent* usage=nullptr;};
struct SCR_AIGroupUtilityComponent{void* m_VehicleMgr=this;bool known=false;int added=0;bool IsUsableVehicle(Entity*){return known;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){known=true;added++;}};
struct Group{SCR_AIGroupUtilityComponent* utility=nullptr;SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
struct ERR_ConvoyVehicle{bool Released=false,valid=true;struct Group group;struct Group* Group=&group;struct Entity entity;struct Entity* Entity=&entity;void* Driver=this;bool CrewValid(void*){return valid;}};
namespace ERR_ConvoyMountedCrew{bool healthyOwned=true;bool Retain(void*,Entity*){return healthyOwned;}}
'''+restore+r'''
int main(){
int mode;ERR_ConvoyVehicle car;SCR_AIGroupUtilityComponent utility;SCR_AIVehicleUsageComponent usage;car.group.utility=&utility;car.entity.usage=&usage;
assert(!Restore(nullptr,&car)&&!Restore(&mode,nullptr));car.Released=true;assert(!Restore(&mode,&car));car.Released=false;
car.valid=false;assert(!Restore(&mode,&car));car.valid=true;ERR_ConvoyMountedCrew::healthyOwned=false;assert(!Restore(&mode,&car));ERR_ConvoyMountedCrew::healthyOwned=true;
car.group.utility=nullptr;assert(!Restore(&mode,&car));car.group.utility=&utility;utility.m_VehicleMgr=nullptr;assert(!Restore(&mode,&car));utility.m_VehicleMgr=&utility;
car.entity.usage=nullptr;assert(!Restore(&mode,&car));car.entity.usage=&usage;usage.pilot=false;assert(!Restore(&mode,&car));usage.pilot=true;
assert(Restore(&mode,&car)&&utility.known&&utility.added==1);assert(!Restore(&mode,&car)&&utility.added==1);
}
''')
print('PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles')
# Execute the production support decision, with native perception/navigation
# boundaries controlled: useful guns hold, masked guns reposition, moving
# drivers keep their order, and failure never claims working cover.
support=body(q,'protected void Support(').replace('protected ', '')
for typ in ('ERR_SupplyTeam','ERR_ConvoyVehicle','AIWaypoint'):
support=support.replace(typ+' ',typ+'* ')
for name in ('orders','vehicle','movement','m_Task'):
support=support.replace(name+'.',name+'->')
support=support.replace('m_Task->Receipt.', 'm_Task->Receipt->').replace('Entity.GetOrigin()', 'Entity->GetOrigin()').replace('Orders[0].', 'Orders[0]->')
support=support.replace('vector ', 'Vec ').replace('Vector(', 'Vec(').replace('vector.DistanceXZ', 'Distance')
support=support.replace('ERR_MountedCover.', 'ERR_MountedCover::').replace('GetGame().GetWorld().GetWorldTime()', 'clockNow')
support=support.replace('ERR_HQ.Event', 'Event').replace('string.Format', 'Format').replace('index.ToString()', 'std::to_string(index)')
support=support.replace('float rear = 180, side;', 'float rear = 180, side=0;').replace('advance += left * 70', 'advance = advance + left * 70').replace('advance -= left * 70', 'advance = advance - left * 70')
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
using string=std::string;float clockNow=1000;
struct Vec{float x=0,y=0,z=0;Vec(){}Vec(float a,float b,float c):x(a),y(b),z(c){}float operator[](int i){return i==0?x:i==1?y:z;}Vec operator-(Vec b){return{x-b.x,y-b.y,z-b.z};}Vec operator+(Vec b){return{x+b.x,y+b.y,z+b.z};}Vec operator*(float n){return{x*n,y*n,z*n};}string ToString(){return "point";}};
float Distance(Vec a,Vec 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;bool IsEmpty(){return this->empty();}};
template<class...T>string Format(string,T...){return "event";}void Event(string,string){}
struct AIWaypoint{Vec point;Vec GetOrigin(){return point;}};
struct Entity{Vec point{0,0,-250};Vec GetOrigin(){return point;}};
struct ERR_ConvoyVehicle{struct Entity entity;struct Entity* Entity=&entity;void* Group=nullptr;};
struct ERR_SupplyTeam{float SupportProgressAt=-1;Vec SupportProgressPosition;int SupportSearchStep=0;Array<AIWaypoint*>Orders;int clears=0;void Clear(){Orders.clear();clears++;}void Add(AIWaypoint*p){Orders.push_back(p);}};
struct Receipt{string SourceId="convoy";};struct Task{struct Receipt receipt;struct Receipt* Receipt=&receipt;Array<ERR_ConvoyVehicle*>Vehicles;};
namespace ERR_MountedCover{bool sees=false;bool SeesSector(void*,Vec){return sees;}Vec Approach(Vec,Vec,Vec){return{0,0,1};}}
struct Tactics{Task task;Task*m_Task=&task;Array<ERR_SupplyTeam*>m_Support;Vec m_Contact,m_Forward{0,0,1};bool usable=true,position=true,spawn=true;int plans=0,moves=0,claims=0;AIWaypoint waypoint;
bool fallback=false;Array<Vec> attempted;bool Usable(int){return usable;}bool SupportPosition(int,Vec desired,Vec&out,bool lane=true){plans++;attempted.push_back(desired);out=desired;return lane?position:fallback;}AIWaypoint*Move(Vec p){moves++;waypoint.point=p;return spawn?&waypoint:nullptr;}void Command(AIWaypoint*){}void Claim(void*,Vec,float){claims++;}
'''+support+r'''
};
struct Case{Tactics t;ERR_ConvoyVehicle car;ERR_SupplyTeam orders;AIWaypoint old;
Case(){clockNow=1000;t.task.Vehicles=Array<ERR_ConvoyVehicle*>(6,&car);t.m_Support=Array<ERR_SupplyTeam*>(6,&orders);old.point={0,0,-500};orders.Orders={&old};ERR_MountedCover::sees=false;}
};
int main(){
{Case c;c.t.Support(4,false,true);clockNow=30999;c.t.Support(4,false,true);assert(c.t.plans==0);
clockNow=31000;c.t.Support(4,false,true);assert(c.t.moves==1&&c.t.plans==1);}
{Case c;c.t.Support(4,false,true);clockNow=21000;c.car.entity.point.x+=20;c.t.Support(4,false,true);
clockNow=41000;c.car.entity.point.x+=20;c.t.Support(4,false,true);assert(c.t.plans==0&&c.t.moves==0);}
{Case c;c.t.position=false;c.t.Support(4,false);c.t.Support(4,false);c.t.Support(4,false);
assert(c.t.attempted.size()==3&&c.t.attempted[0].x!=c.t.attempted[1].x&&c.t.attempted[1].x!=c.t.attempted[2].x);}
{Case c;c.car.entity.point={0,0,-650};c.t.position=false;c.t.fallback=true;c.t.Support(4,false);
assert(c.t.plans==2&&c.t.moves==1&&c.t.waypoint.point.z==-500);}
{Case c;c.car.entity.point={0,0,-650};c.t.position=false;c.t.Support(4,false);assert(c.t.plans==2&&c.t.moves==0);}
{Case c;ERR_MountedCover::sees=true;c.t.Support(4,false);assert(c.orders.Orders.empty()&&c.t.plans==0&&c.t.moves==0&&c.t.claims==1);}
{Case c;c.t.Support(4,false,true);assert(c.orders.Orders[0]==&c.old&&c.t.plans==0&&c.t.moves==0);} // driver in transit
{Case c;c.old.point=c.car.entity.point;c.t.Support(4,false,true);assert(c.t.plans==1&&c.t.moves==1&&c.orders.Orders[0]==&c.t.waypoint);} // masked arrived gun retries
{Case c;c.t.position=false;c.t.Support(4,false);assert(c.orders.Orders.empty()&&c.t.moves==0&&c.t.claims==1);} // no unsafe invented position
{Case c;c.t.spawn=false;c.t.Support(4,false);assert(c.orders.Orders[0]==&c.old);} // failed allocation preserves old command
{Case c;ERR_MountedCover::sees=true;c.t.Support(1,false);assert(c.t.plans==1&&c.t.moves==1);} // ammunition truck still withdraws
{Case c;c.t.usable=false;c.t.Support(4,false);assert(c.orders.Orders.empty()&&c.t.plans==0);}
}
''')
print('PASS: actual convoy support preserves visual cover and ongoing approach, retries masked arrived positions, withholds unsafe moves and preserves ammo withdrawal')
# Exercise the actual geometry boundary for both firing positions and the
# deliberately non-firing intermediate approach used by distant escorts.
position=body(q,'protected bool SupportPosition(').replace('protected ','').replace('out vector ', 'Vec& ').replace('vector ', 'Vec ').replace('Vector(', 'Vec(')
for typ in ('ERR_ConvoyVehicle','SCR_AIGroupUtilityComponent'):
position=position.replace(typ+' ',typ+'* ')
position=position.replace('ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();','ERR_ConvoyPlacement placement;')
for name in ('m_Task','vehicle','utility'):
position=position.replace(name+'.',name+'->')
position=position.replace('Group.GetGroupUtilityComponent()', 'Group->GetGroupUtilityComponent()').replace('Entity.GetOrigin()', 'Entity->GetOrigin()')
for cls in ('ERR_MountedCover','ERR_SupplyConvoySystem','ERR_Actors','ERR_ForceRules','Math','vector'):
position=position.replace(cls+'.',cls+'::')
position=position.replace('candidate += Vec(', 'candidate = candidate + Vec(')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct Vec{float x=0,y=0,z=0;Vec(){}Vec(float a,float b,float c):x(a),y(b),z(c){}Vec operator-(Vec b){return{x-b.x,y-b.y,z-b.z};}Vec operator+(Vec b){return{x+b.x,y+b.y,z+b.z};}};
namespace vector{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
struct SCR_AIGroupUtilityComponent{bool allowed=true;bool IsPositionAllowed(Vec){return allowed;}};
struct Group{SCR_AIGroupUtilityComponent utility;auto GetGroupUtilityComponent(){return &utility;}};
struct Entity{Vec pos{0,0,-650};Vec GetOrigin(){return pos;}};
struct ERR_ConvoyVehicle{struct Entity entity;struct Entity*Entity=&entity;struct Group group;struct Group*Group=&group;};
struct Task{std::vector<ERR_ConvoyVehicle*>Vehicles;};
namespace ERR_MountedCover{bool lane=true;Vec Approach(Vec,Vec,Vec){return{0,0,1};}bool ClearLane(Entity*,Vec,Vec){return lane;}}
namespace ERR_SupplyConvoySystem{bool road=false;bool Approach(Vec p,Vec&out,float){out=p;return road;}}
namespace ERR_Actors{Vec Ground(Vec p){return p;}}
namespace ERR_ForceRules{float Heading(Vec,Vec,int){return 0;}}
namespace Math{float RandomFloat(float,float){return 0;}}
struct ERR_ConvoyPlacement{inline static bool clear=true;bool Clear(Vec,float,float){return clear;}};
struct Tactics{Task task;Task*m_Task=&task;Vec m_Contact,m_Forward;bool free=true;bool FreePosition(Group*,Vec){return free;}
'''+position+r'''
};
int main(){
Tactics t;ERR_ConvoyVehicle car;t.task.Vehicles=std::vector<ERR_ConvoyVehicle*>(6,&car);Vec out;
for(bool road : {false,true}){
ERR_SupplyConvoySystem::road=road;ERR_MountedCover::lane=false;
assert(!t.SupportPosition(4,{0,0,-500},out)); // masked point is not firing cover
assert(t.SupportPosition(4,{0,0,-500},out,false)); // explicit intermediate approach
assert(!t.SupportPosition(4,{0,0,-650},out,false)); // cannot select the current blind hold
assert(!t.SupportPosition(4,{0,0,-90},out,false)); // cannot rush through the reported sector
t.free=false;assert(!t.SupportPosition(4,{0,0,-500},out,false));t.free=true;
car.group.utility.allowed=false;assert(!t.SupportPosition(4,{0,0,-500},out,false));car.group.utility.allowed=true;
ERR_MountedCover::lane=true;assert(t.SupportPosition(4,{0,0,-500},out));
}
ERR_SupplyConvoySystem::road=false;ERR_ConvoyPlacement::clear=false;assert(!t.SupportPosition(4,{0,0,-500},out,false));
assert(!t.SupportPosition(1,{0,0,-500},out)); // trucks still require roads
}
''')
print('PASS: production support geometry rejects occupied, disallowed, blocked and too-close positions; only explicit intermediate approaches waive the firing-lane requirement')