235 lines
18 KiB
Python
235 lines
18 KiB
Python
"""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'
|
|
|
|
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]'):
|
|
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('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;')
|
|
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('))
|
|
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;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();}};
|
|
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 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{bool 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(){
|
|
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==80&&sb.Speed==45);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==200); // 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.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&&b.Waypoint->position.x==300);
|
|
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){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==6&&c.flank==2&&c.m_NextOrders==60000);
|
|
c.m_Contact={39};c.Orders(false,60000);assert(c.support==6&&c.flank==2);
|
|
c.m_Contact={40};c.Orders(false,90000);assert(c.support==12&&c.flank==4);
|
|
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==18&&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[2].','Vehicles[2]->').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[2]=&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')
|