Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
131 lines
11 KiB
Python
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')
|