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

131 lines
11 KiB
Python

"""Actual convoy dispatch transaction and exact native seat-allocation policy."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c')))
source=path.read_text()
# Dispatch's native calls are doubled; budget behavior is executed separately.
code=body(path,'ERR_ConvoyTask Dispatch(')
code=re.sub(r'//[^\n]*','',code)
code=code.replace('array<vector>','Array<vector>&')
code=code.replace('SCR_AIGroup.Cast(', 'static_cast<SCR_AIGroup*>(').replace('Vehicle.Cast(', 'static_cast<Vehicle*>(')
code=code.replace('FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent))','&unit->Entity->affiliation').replace('AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent))','&unit->Entity->movement')
for cls in ['ERR_ConvoyTask','ERR_ConvoyReceipt','ERR_ForceRoute','ERR_ConvoyVehicle','FactionAffiliationComponent','AICarMovementComponent']:
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
for obj in ['m_Mode','m_Pool','task','unit','affiliation','movement']:
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
for cls in ['ERR_ForceRules','ERR_ConvoyRules','ERR_ConvoyResources','ERR_Actors','ERR_HQ']:
code=code.replace(cls+'.',cls+'::')
code=code.replace('EnemyFaction().','EnemyFaction()->').replace('Group.Get','Group->Get').replace('null','nullptr')
run(r'''
#include <cassert>
#include <vector>
#include <string>
using string=std::string;struct vector{};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
struct Faction{string key="USSR";string GetFactionKey(){return key;}};Faction enemy;
struct IEntity{virtual ~IEntity()=default;};
struct FactionAffiliationComponent{void SetAffiliatedFaction(Faction*){}};
struct AICarMovementComponent{int speed=0;void SetCruiseSpeed(int v){speed=v;}};
struct SCR_AIGroup:IEntity{Faction* GetFaction(){return &enemy;}int GetAgentsCount(){return 0;}};
struct Vehicle:IEntity{FactionAffiliationComponent affiliation;AICarMovementComponent movement;};
struct ERR_GameMode{Array<IEntity*>owned;Faction* EnemyFaction(){return &enemy;}void Own(IEntity* e){owned.Insert(e);}};
struct ERR_ForceRoute{};
struct ERR_ConvoyReceipt{bool used=false;};
struct ERR_ConvoyPool{int consumes=0;bool Consume(ERR_ConvoyReceipt* r){if(!r||r->used)return false;r->used=true;consumes++;return true;}};
struct ERR_ConvoyVehicle{bool Released=false;int Index=0;Vehicle* Entity=nullptr;SCR_AIGroup* Group=nullptr;};
struct ERR_ConvoyTask{ERR_ConvoyReceipt* Receipt=nullptr;ERR_ForceRoute* Route=nullptr;float Deadline=0;Array<ERR_ConvoyVehicle*>Vehicles;bool stopped=false;void Stop(ERR_GameMode*,string){stopped=true;}};
namespace ERR_ForceRules{bool Finite(float f){return f==f;}}
namespace ERR_ConvoyRules{bool valid=true,clear=true,empty=true;bool EmptyCrewTemplate(SCR_AIGroup*){return empty;}bool GroundClear(Array<vector>&,Array<vector>&){return clear;}bool Plan(Array<vector>&,Array<vector>&,ERR_ForceRoute*){return valid;}}
namespace ERR_ConvoyResources{bool available=true;bool Available(){return available;}string GroupPrefab(){return "group";}string VehiclePrefab(int i){return "vehicle"+std::to_string(i);}}
namespace ERR_HQ{void Event(string,string){}}
namespace ERR_Actors{int calls=0,fail=0;IEntity* Spawn(string prefab,vector,vector=vector{}){calls++;if(calls==fail)return nullptr;if(prefab=="group")return new SCR_AIGroup();return new Vehicle();}}
struct Service{ERR_GameMode* m_Mode;ERR_ConvoyPool* m_Pool;Array<ERR_ConvoyTask*>m_Tasks;bool current=true;int crews=0,failCrew=-1;
ERR_ConvoyTask* NewTask(){return new ERR_ConvoyTask();} ERR_ConvoyVehicle* NewVehicle(){return new ERR_ConvoyVehicle();}
bool Current(){return current;}bool FillCrew(ERR_ConvoyVehicle* unit){crews++;return unit->Index!=failCrew;}
''' + code + r'''
};
int main(){
ERR_GameMode mode;ERR_ConvoyPool pool;Service service{&mode,&pool};ERR_ConvoyReceipt receipt;ERR_ForceRoute route;Array<vector>positions(6),angles(6);
ERR_ConvoyRules::valid=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyRules::valid=true;
ERR_ConvoyRules::clear=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyRules::clear=true;
ERR_ConvoyResources::available=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyResources::available=true;
enemy.key="US"; // A selected non-USSR enemy uses the same finite dispatch transaction.
auto task=service.Dispatch(&receipt,positions,angles,&route,100);assert(task&&task->Vehicles.Count()==6&&!task->stopped&&receipt.used&&pool.consumes==1&&ERR_Actors::calls==10&&mode.owned.Count()==10&&service.crews==5);
assert(task->Vehicles[2]->Released&&!task->Vehicles[2]->Entity&&!task->Vehicles[2]->Group);
assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(ERR_Actors::calls==10);
for(int failed=1;failed<=10;failed++){
ERR_GameMode partial;ERR_ConvoyPool budget;Service run{&partial,&budget};ERR_ConvoyReceipt ticket;ERR_Actors::calls=0;ERR_Actors::fail=failed;
auto result=run.Dispatch(&ticket,positions,angles,&route,0);assert(result&&result->stopped&&ticket.used&&budget.consumes==1&&run.m_Tasks.Count()==1);int attempted=ERR_Actors::calls;
assert(!run.Dispatch(&ticket,positions,angles,&route,1)&&ERR_Actors::calls==attempted);assert(partial.owned.Count()==attempted-1);
}
ERR_Actors::fail=0;ERR_ConvoyRules::empty=false;ERR_GameMode unexpected;ERR_ConvoyPool rejectBudget;Service wrongTemplate{&unexpected,&rejectBudget};ERR_ConvoyReceipt rejectTicket;
auto rejected=wrongTemplate.Dispatch(&rejectTicket,positions,angles,&route,100);assert(rejected&&rejected->stopped&&rejectTicket.used&&wrongTemplate.crews==0);ERR_ConvoyRules::empty=true;
ERR_Actors::fail=0;ERR_GameMode seats;ERR_ConvoyPool budget;Service noCrew{&seats,&budget};noCrew.failCrew=3;ERR_ConvoyReceipt ticket;
auto partial=noCrew.Dispatch(&ticket,positions,angles,&route,100);assert(partial&&partial->stopped&&partial->Vehicles.Count()==4&&ticket.used&&seats.owned.Count()==6);
}
''')
# Execute the seat-selection and creation body. Failed capacity cannot shrink
# the plan, and every native successful character is immediately owned.
fill=body(path,'protected bool FillCrew(').replace('protected ','')
fill=re.sub(r'//[^\n]*','',fill).replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>')
for cls in ['ERR_ConvoyVehicle','BaseCompartmentSlot','AIGroup','IEntity']:
fill=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',fill)
for obj in ['unit','slot','seat','m_Mode']:
fill=re.sub(r'\b'+obj+r'\.',obj+'->',fill)
for cls in ['ERR_ConvoyRules','ECompartmentType','ERR_ConvoyPool','ERR_ConvoyResources']:
fill=fill.replace(cls+'.',cls+'::')
fill=fill.replace('foreach (','for (')
run(r'''
#include <cassert>
#include <vector>
using ResourceName=int;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
enum class ECompartmentType{PILOT,TURRET,CARGO};struct IEntity{};struct AIGroup{};
struct BaseCompartmentSlot{ECompartmentType type;bool occupied=false,accessible=true,fail=false;int spawns=0;ECompartmentType GetType(){return type;}bool IsCompartmentAccessible(){return accessible;}bool IsOccupied(){return occupied;}IEntity* SpawnCharacterInCompartment(ResourceName,AIGroup*&,ResourceName){spawns++;return fail?nullptr:new IEntity();}};
struct Vehicle{Array<BaseCompartmentSlot*>seats;};
struct ERR_ConvoyVehicle{int Index=0;Vehicle* Entity;AIGroup* Group;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity* Driver=nullptr;};
namespace ERR_ConvoyRules{bool CollectSeats(Vehicle* v,Array<BaseCompartmentSlot*>& out){out=v->seats;return true;}}
namespace ERR_ConvoyPool{int CrewCount(int i){if(i==1)return 1;if(i==2)return 0;if(i==4)return 3;return 2;}}
namespace ERR_ConvoyResources{int Soldier(){return 1;}int GroupPrefab(){return 2;}}
struct Mode{int owned=0;void Own(IEntity*){owned++;}};
struct Service{Mode* m_Mode;
''' + fill + r'''
};
int main(){
for(int i=0;i<6;i++){
if(i==2)continue;
Vehicle vehicle;AIGroup group;Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;unit.Index=i;unit.Entity=&vehicle;unit.Group=&group;
BaseCompartmentSlot driver{ECompartmentType::PILOT},gunner{ECompartmentType::TURRET},commander{ECompartmentType::TURRET};vehicle.seats={&driver,&gunner,&commander};
BaseCompartmentSlot seats[8];for(auto& seat:seats){seat.type=ECompartmentType::CARGO;vehicle.seats.Insert(&seat);}
assert(service.FillCrew(&unit));assert(unit.Members.Count()==ERR_ConvoyPool::CrewCount(i)&&mode.owned==unit.Members.Count()&&unit.Driver==unit.Members[0]);
}
Vehicle vehicle;AIGroup group;Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;unit.Index=2;unit.Entity=&vehicle;unit.Group=&group;
BaseCompartmentSlot driver{ECompartmentType::PILOT};vehicle.seats={&driver};assert(!service.FillCrew(&unit)&&driver.spawns==0&&mode.owned==0);
BaseCompartmentSlot seats[8];for(auto& seat:seats){seat.type=ECompartmentType::CARGO;vehicle.seats.Insert(&seat);}seats[3].fail=true;
assert(!service.FillCrew(&unit)&&unit.Members.Count()==0&&mode.owned==0);assert(seats[4].spawns==0);
}
''')
print('PASS: actual convoy dispatch validates before receipt consumption, consumes once before native mutation, retains all10 partial-failure paths, accepts the selected non-USSR enemy, and allocates exact10 bodies without capacity shrinking')
# The resource must suppress authored allocation before native EOnInit, not try
# to purge a queued default squad after custom seat occupants have been added.
prefab=ROOT/'addons/core/Prefabs/ERR/ConvoyCrew.et'
text=prefab.read_text()
assert '"{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et"' in text
assert 'm_bSpawnImmediately 0' in text and 'm_faction "USSR"' in text
assert re.search(r'm_aUnitPrefabSlots\s*{\s*}',text)
assert 'Group_USSR_Base.et' not in text
identity=re.search(r'Name "([^"\n]+)"',prefab.with_name(prefab.name+'.meta').read_text())[1]
assert identity in body(path,'static ResourceName GroupPrefab(')
empty=body(path,'static bool EmptyCrewTemplate(')
empty=empty.replace('SCR_AIGroup group','SCR_AIGroup* group').replace('group.','group->').replace('m_aUnitPrefabSlots.IsEmpty()', 'm_aUnitPrefabSlots->IsEmpty()')
run(r'''
#include <cassert>
struct Slots{bool empty=true;bool IsEmpty(){return empty;}};
struct SCR_AIGroup{bool immediate=false;int count=0;Slots* m_aUnitPrefabSlots=nullptr;bool GetSpawnImmediately(){return immediate;}int GetAgentsCount(){return count;}};
'''+empty+r'''
int main(){SCR_AIGroup group;Slots slots;assert(EmptyCrewTemplate(&group));group.m_aUnitPrefabSlots=&slots;assert(EmptyCrewTemplate(&group));slots.empty=false;assert(!EmptyCrewTemplate(&group));slots.empty=true;group.immediate=true;assert(!EmptyCrewTemplate(&group));group.immediate=false;group.count=1;assert(!EmptyCrewTemplate(&group));assert(!EmptyCrewTemplate(nullptr));}
''')
print('PASS: own memberless prefab disables automatic spawn before initialization; actual admission rejects populated, immediate or authored-slot templates before finite crew creation')