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

193 lines
19 KiB
Python

"""Actual admission seed and one-time issuance methods; native boundaries are controlled doubles."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
R=Path(os.environ.get('ERR_MOUNTED_STOCK_ROOT',ROOT));D=R/'addons/core/Scripts/Game/ERR/10_AI'
seed=(D/'ERR_MountedMagazineSeed.c').read_text()
assert 'GetDefaultMagazineOrProjectileName' not in seed and 'IsUsed(' not in seed
for forbidden in ('TrySpawn','SetAmmoCount','DeleteEntity','GetChildren','GetSibling'):assert forbidden not in seed
s=re.sub(r'//[^\n]*','',seed).replace('protected ','').replace('ref ','').replace('class ERR_MountedMagazineSeed : ERR_HQMountedAmmoSampler','struct ERR_MountedMagazineSeed : ERR_HQMountedAmmoSampler')
for old,new in {
'InventoryItemComponent.Cast(gun.FindComponent(InventoryItemComponent))':'gun->item',
'InventoryItemComponent.Cast(loaded.FindComponent(InventoryItemComponent))':'loaded->item',
'InventoryItemComponent.Cast(weapon.GetOwner().FindComponent(InventoryItemComponent))':'weapon->GetOwner()->item',
'WeaponSlotComponent.Cast(weaponSlot.GetParentContainer())':'dynamic_cast<Mount*>(weaponSlot->GetParentContainer())',
}.items():s=s.replace(old,new)
s=s.replace('array<ERR_MountedMagazineSeed> seeds','Array<ERR_MountedMagazineSeed*>& seeds').replace('array<','Array<').replace('foreach (','for (')
s=re.sub(r'\bERR_MountedMagazineSeed (seed)\b',r'ERR_MountedMagazineSeed* \1',s)
s=s.replace('ERR_HQMountedAmmoSnapshot shape','ERR_HQMountedAmmoSnapshot* shape').replace('ERR_ConvoyRules.','ERR_ConvoyRules::').replace('EMuzzleType.','EMuzzleType::')
for obj in ['m_Weapon','m_Mount','m_WeaponSlot','m_Muzzle','m_Magazine','m_MagazineSlot','gun','loaded','gunItem','magazineItem','storage','shape','weapon','weaponSlot','mount','muzzle','magazine','item','seed']:
s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
for call in ['GetPrefabData()','GetMagazineWell()','GetOwner()']:s=s.replace(call+'.',call+'->')
s=s.replace('!seeds || ','') # native array parameter reference is non-null in C++ fixture
s=re.sub(r'\b(IEntity|BaseWeaponComponent|WeaponSlotComponent|InventoryStorageSlot|BaseMuzzleComponent|BaseMagazineComponent|int) (\w+);',r'\1 \2{};',s).replace('\n}\n','\n};\n')
common=r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};using ResourceName=string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T t){this->push_back(t);}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
struct Entity;struct Weapon;struct Muzzle;struct Magazine;struct Slot;
using IEntity=Entity*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
struct Well{int type=1;int Type(){return type;}bool IsInherited(int other){return type==other;}};
struct Prefab{string name="AP-box";string GetPrefabName(){return name;}};
struct Item{Slot* slot=nullptr;Slot* GetParentSlot(){return slot;}};using InventoryItemComponent=Item*;
struct Entity{Entity* parent=nullptr;Item* item=nullptr;Prefab* prefab=nullptr;Array<IEntity>allowed;Array<Weapon*> weapons;bool complete=true;Prefab* GetPrefabData(){return prefab;}};
struct Container{IEntity owner=nullptr;virtual ~Container(){}IEntity GetOwner(){return owner;}};
struct Storage:Container{};using BaseInventoryStorageComponent=Storage*;
struct Slot{Container* parent=nullptr;Storage* storage=nullptr;IEntity attached=nullptr;Container* GetParentContainer(){return parent;}IEntity GetAttachedEntity(){return attached;}Storage* GetStorage(){return storage;}};using InventoryStorageSlot=Slot*;
struct Magazine:Container{Well* well=nullptr;int capacity=100,rounds=99;Well* GetMagazineWell(){return well;}int GetMaxAmmoCount(){return capacity;}int GetAmmoCount(){return rounds;}};
struct Muzzle:Container{Magazine* magazine=nullptr;Well* well=nullptr;int kind=0;Magazine* GetMagazine(){return magazine;}Well* GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
struct Weapon:Container{Array<Muzzle*> muzzles;void GetMuzzlesList(Array<Muzzle*>& out){out=muzzles;}};
struct Mount:Container{Slot* slot=nullptr;IEntity gun=nullptr;Slot* GetSlotInfo(){return slot;}IEntity GetWeaponEntity(){return gun;}};using WeaponSlotComponent=Mount*;
struct EMuzzleType{static const int MT_BaseMuzzle=0;};
struct ERR_ConvoyRules{static bool PartOf(IEntity a,IEntity root){for(int n=0;a&&n<32;n++,a=a->parent)if(a==root)return true;return false;}};
struct ERR_HQMountedAmmoSnapshot{bool Complete=false;};
struct ERR_HQMountedAmmoSampler{static void Collect(IEntity root,ERR_HQMountedAmmoSnapshot* result,Array<IEntity>& allowed,Array<Weapon*>& weapons){result->Complete=root->complete;allowed=root->allowed;weapons=root->weapons;}};
'''
run(common+s+r'''
struct Rig{Entity root,gun,loaded;Item gunItem,magItem;Prefab prefab;Slot gunSlot,magSlot;Mount mount;Storage storage;Weapon weapon;Muzzle muzzle;Magazine magazine;Well well;
Rig(){gun.parent=&root;loaded.parent=&gun;gun.item=&gunItem;loaded.item=&magItem;loaded.prefab=&prefab;
gunItem.slot=&gunSlot;magItem.slot=&magSlot;gunSlot.parent=&mount;gunSlot.attached=&gun;magSlot.parent=&muzzle;magSlot.attached=&loaded;magSlot.storage=&storage;storage.owner=&gun;
mount.owner=&root;mount.slot=&gunSlot;mount.gun=&gun;weapon.owner=&gun;weapon.muzzles={&muzzle,&muzzle};muzzle.owner=&gun;muzzle.magazine=&magazine;muzzle.well=&well;
magazine.owner=&loaded;magazine.well=&well;root.allowed={&root,&gun,&loaded};root.weapons={&weapon,&weapon};}
};
int main(){
{Rig r;Array<ERR_MountedMagazineSeed*> seeds;ERR_MountedMagazineSeed::Capture(&r.root,0,seeds);assert(seeds.Count()==1&&seeds[0]->Current()&&seeds[0]->Prefab=="AP-box");
assert(seeds[0]->Root==&r.root&&seeds[0]->VehicleIndex==0);r.magazine.rounds=0;assert(seeds[0]->Current());
Magazine replacement=r.magazine;r.muzzle.magazine=&replacement;assert(!seeds[0]->Current());r.muzzle.magazine=&r.magazine;
r.prefab.name="replacement-box";assert(!seeds[0]->Current());}
for(int fault=0;fault<16;fault++){Rig r;Entity other;Slot replacement;Well wrong;wrong.type=2;
if(fault==0)r.root.complete=false;
if(fault==1)r.root.allowed={&r.root,&r.gun};
if(fault==2)r.muzzle.magazine=nullptr;
if(fault==3)r.loaded.prefab=nullptr;
if(fault==4)r.prefab.name="";
if(fault==5)r.muzzle.kind=1;
if(fault==6)r.magazine.well=nullptr;
if(fault==7)r.magazine.well=&wrong;
if(fault==8)r.magazine.capacity=0;
if(fault==9)r.magazine.rounds=101;
if(fault==10)r.storage.owner=&other;
if(fault==11)r.magItem.slot=&replacement;
if(fault==12)r.mount.gun=&other;
if(fault==13)r.gunSlot.attached=&other;
if(fault==14)r.weapon.muzzles.resize(9,&r.muzzle);
if(fault==15)r.magazine.rounds=-1;
Array<ERR_MountedMagazineSeed*> seeds;ERR_MountedMagazineSeed::Capture(&r.root,0,seeds);assert(seeds.Count()==0);}
{Rig r;Array<ERR_MountedMagazineSeed*> seeds;ERR_MountedMagazineSeed::Capture(&r.root,6,seeds);assert(seeds.Count()==0);seeds.resize(48,nullptr);ERR_MountedMagazineSeed::Capture(&r.root,0,seeds);assert(seeds.Count()==48);}
{Rig a,b;Array<ERR_MountedMagazineSeed*> seeds;ERR_MountedMagazineSeed::Capture(&a.root,0,seeds);ERR_MountedMagazineSeed::Capture(&b.root,3,seeds);assert(seeds.Count()==2);}
}
''')
print('PASS: actual admission seeds use exact installed loaded prefab, reject replacement/foreign/projectile paths and preserve bounded immutable identity')
service=D/'ERR_SupplyConvoyService.c'
raw=service.read_text()
assert raw.index('CaptureMountedStock(supply)') < raw.index('for (int i = 0; i < 8; i++)')
assert raw.index('task.MagazinesIssued = true;',raw.index('protected void IssueMagazines')) < raw.index('IssueMountedStock(task, inventory, issued,')
assert 'ref array<ref ERR_MountedMagazineSeed> MountedStock' in (D/'ERR_SupplyConvoyTactics.c').read_text()
code='\n'.join(body(service,x) for x in ['protected bool MountedStockOwner(', 'protected void CaptureMountedStock(', 'protected int IssueMountedStock(', 'protected void IssueMagazines('])
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('null','nullptr').replace('foreach (','for (').replace('array<','Array<')
for old,new in {
'ERR_HQ.Cast(m_Mode.Command())':'m_Mode->Command()',
'InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent))':'task->Vehicles[1]->Entity->inventory',
'BaseWeaponComponent.Cast(item.FindComponent(BaseWeaponComponent))':'item->weapon',
'existing.FindComponent(BaseMagazineComponent)':'existing->magazine',
}.items():code=code.replace(old,new)
for typ in ['ERR_SupplyConvoyTask','ERR_HQ','ERR_ConvoyVehicle','ERR_ForceRecord','InventoryStorageManagerComponent','ERR_MountedMagazineSeed','ERR_ConvoyCargoCallback','BaseWeaponComponent','BaseMuzzleComponent']:
code=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',code)
code=code.replace('Array<BaseMuzzleComponent>', 'Array<BaseMuzzleComponent*>')
for name in ['m_Mode','task','vehicle','hq','record','inventory','seed','load','operation','item','weaponOperation','weapon','muzzle']:
code=re.sub(r'\b'+name+r'\.',name+'->',code)
for call in ['Forces()','GetPrefabData()']:code=code.replace(call+'.',call+'->')
code=code.replace('->Receipt.','->Receipt->').replace('Vehicles[index].','Vehicles[index]->').replace('Vehicles[1].','Vehicles[1]->').replace('Vehicles[seed->VehicleIndex].','Vehicles[seed->VehicleIndex]->')
for ns in ['ERR_MountedMagazineSeed','ERR_Inventory','EStoragePurpose','ERR_HQ','string']:code=code.replace(ns+'.',ns+'::')
code=code.replace('int existingMagazines;', 'int existingMagazines=0;').replace('int issued;', 'int issued=0;')
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
#include <string>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}template<class...T>static string Format(T...){return "event";}};using ResourceName=string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T t){this->push_back(t);}void Clear(){this->clear();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}int Find(T t){auto f=std::find(this->begin(),this->end(),t);return f==this->end()?-1:f-this->begin();}void Remove(int i){this->erase(this->begin()+i);}};
struct Entity;using IEntity=Entity*;struct InventoryStorageManagerComponent;struct BaseWeaponComponent;
struct Prefab{string name;string GetPrefabName(){return name;}};
struct Entity{InventoryStorageManagerComponent* inventory=nullptr;BaseWeaponComponent* weapon=nullptr;bool magazine=false;Prefab prefab;Prefab* GetPrefabData(){return &prefab;}};
struct BaseMuzzleComponent{string resource="rifle-mag";string GetDefaultMagazineOrProjectileName(){return resource;}};
struct BaseWeaponComponent{Array<BaseMuzzleComponent*> muzzles;void GetMuzzlesList(Array<BaseMuzzleComponent*>& out){out=muzzles;}};
struct Group{int id=0;IEntity bound=nullptr;bool valid=true;};
struct ERR_ForceRecord{int Id=0;};struct Ledger{Array<ERR_ForceRecord>records;ERR_ForceRecord* FindGroup(Group* g){return g&&g->id<records.Count()?&records[g->id]:nullptr;}};
struct ERR_HQ{static void Event(string,string){}bool ValidateBoundVehicle(Group* g,IEntity e){return g&&g->valid&&g->bound==e;}};
struct Mode{Ledger ledger;ERR_HQ hq;Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
struct ERR_ConvoyVehicle{int Index=0;bool Registered=true,Released=false,crew=true;IEntity Entity=nullptr;struct Group* Group=nullptr;bool CrewValid(Mode*){return crew;}};
struct Receipt{bool Consumed=true;Array<int>Records={0,1,2,3,4,5};string SourceId="convoy";};
struct ERR_ConvoyCargoCallback{string Prefab;bool Finished=true,Succeeded=true;};
struct ERR_MountedMagazineSeed{IEntity Root=nullptr;int VehicleIndex=0;string Prefab="PKM-box";bool valid=true;bool Current(){return valid;}static inline int captures=0;static void Capture(IEntity,int,Array<ERR_MountedMagazineSeed*>&){captures++;}};
struct ERR_SupplyConvoyTask{bool Ready=true,Stopped=false,MagazinesIssued=false,CargoIssued=true;float Deadline=100;struct Receipt receipt;struct Receipt* Receipt=&receipt;Array<ERR_ConvoyVehicle*>Vehicles;Array<ERR_ConvoyCargoCallback*>Cargo;Array<ERR_MountedMagazineSeed*>MountedStock;Array<IEntity>CargoBefore;};
struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
struct InventoryStorageManagerComponent{IEntity owner=nullptr;Array<IEntity>items;Array<string>requests;bool accept=true,inlineDone=false;Entity spawned;IEntity GetOwner(){return owner;}
bool TrySpawnPrefabToStorage(string p,void*,int,int,ERR_ConvoyCargoCallback* cb){requests.Insert(p);cb->Finished=inlineDone;if(inlineDone){spawned.magazine=true;items.Insert(&spawned);}return accept;}};
struct ERR_Inventory{static void Items(InventoryStorageManagerComponent* i,Array<IEntity>& items){items=i->items;}};
struct Service{Mode* m_Mode;
'''+code+r'''
};
struct Fixture{Mode mode;Service service{&mode};ERR_SupplyConvoyTask task;InventoryStorageManagerComponent inventory;Array<Entity>roots{6},cargo{8},extra{129};Array<Group>groups{6};Array<ERR_ConvoyVehicle>vehicles{6};Array<BaseWeaponComponent>weapons{8};BaseMuzzleComponent muzzle;Array<ERR_ConvoyCargoCallback>callbacks{8};Array<ERR_MountedMagazineSeed>seeds{9};
Fixture(){mode.ledger.records.resize(6);for(int i=0;i<6;i++){groups[i].id=i;groups[i].bound=&roots[i];vehicles[i].Index=i;vehicles[i].Entity=&roots[i];vehicles[i].Group=&groups[i];task.Vehicles.Insert(&vehicles[i]);mode.ledger.records[i].Id=i;}
roots[1].inventory=&inventory;inventory.owner=&roots[1];
for(int i=0;i<8;i++){cargo[i].prefab.name="weapon"+std::to_string(i);cargo[i].weapon=&weapons[i];weapons[i].muzzles={&muzzle};callbacks[i].Prefab=cargo[i].prefab.name;task.Cargo.Insert(&callbacks[i]);inventory.items.Insert(&cargo[i]);}
for(int i=0;i<9;i++){seeds[i].Root=&roots[0];seeds[i].Prefab="box"+std::to_string(i);task.MountedStock.Insert(&seeds[i]);}}
};
int main(){
{Fixture f;f.service.IssueMagazines(&f.task,0);assert(f.task.MagazinesIssued&&f.inventory.requests.Count()==64&&f.task.Cargo.Count()==72&&f.task.MountedStock.Count()==0);
for(int i=0;i<48;i++)assert(f.inventory.requests[i]=="rifle-mag");for(int i=48;i<64;i++)assert(f.inventory.requests[i]=="box"+std::to_string((i-48)/2));
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.requests.Count()==64);}
{Fixture f;f.callbacks[0].Finished=false;f.service.IssueMagazines(&f.task,0);assert(!f.task.MagazinesIssued&&f.inventory.requests.Count()==0);f.callbacks[0].Finished=true;f.service.IssueMagazines(&f.task,1);assert(f.inventory.requests.Count()==64);}
for(int missing=0;missing<2;missing++){Fixture f;
if(missing==0)f.callbacks[0].Finished=false;else f.task.Cargo[0]=nullptr;
f.service.IssueMagazines(&f.task,30100);
assert(f.task.MagazinesIssued&&f.inventory.requests.Count()==42&&f.task.MountedStock.Count()==0);
for(auto request:f.inventory.requests){assert(request=="rifle-mag");}
f.callbacks[0].Finished=true;f.task.Cargo[0]=&f.callbacks[0];
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.requests.Count()==42);}
{Fixture f;f.inventory.accept=false;f.service.IssueMagazines(&f.task,0);assert(f.inventory.requests.Count()==64);for(auto cb:f.task.Cargo){assert(cb->Finished);}f.service.IssueMagazines(&f.task,99999);assert(f.inventory.requests.Count()==64);}
{Fixture f;f.inventory.inlineDone=true;f.service.IssueMagazines(&f.task,0);assert(f.inventory.requests.Count()==64);}
{Fixture f;f.service.IssueMountedStock(&f.task,&f.inventory,80,8,20);assert(f.inventory.requests.Count()==8);} //96 existing+attempts
{Fixture f;int total=f.service.IssueMountedStock(&f.task,&f.inventory,48,0,79);assert(total==49&&f.inventory.requests.Count()==1);} //128allitems
{Fixture f;f.task.Cargo.resize(127,&f.callbacks[0]);assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==49&&f.task.Cargo.Count()==128);}
{Fixture f;for(auto& seed:f.seeds){seed.Prefab="same";}assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==50);}
for(int bad=0;bad<8;bad++){Fixture f;
if(bad==0)f.vehicles[1].crew=false;if(bad==1)f.groups[1].valid=false;if(bad==2)f.mode.ledger.records[1].Id=9;
if(bad==3)f.task.Ready=false;if(bad==4)f.task.Receipt->Consumed=false;
if(bad==5)for(auto& seed:f.seeds)seed.valid=false;
if(bad==6)f.task.Vehicles[0]=&f.vehicles[3];if(bad==7)f.groups[0].valid=false;
assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==48&&f.inventory.requests.Count()==0);}
{Fixture f;f.inventory.items.clear();for(auto& e:f.extra){f.inventory.items.Insert(&e);}f.service.IssueMagazines(&f.task,0);
assert(f.task.MagazinesIssued&&f.inventory.requests.Count()==0&&f.task.MountedStock.Count()==0&&f.task.CargoBefore.Count()==0);}
{Fixture f;f.service.CaptureMountedStock(&f.task);assert(ERR_MountedMagazineSeed::captures==6);f.groups[1].valid=false;f.service.CaptureMountedStock(&f.task);assert(ERR_MountedMagazineSeed::captures==6);}
}
'''.replace(';if(', ';\n if(').replace(';for(', ';\n for('))
print('PASS: actual primary-before-mounted one-shot batch, shared96/128 caps, physical snapshot headroom, original ownership, callback refusal and admission pinning')
lookup=body(D/'ERR_HQ.c','bool ValidateBoundVehicle(SCR_AIGroup group, IEntity vehicle)')
lookup=lookup.replace('Replication.','Replication::').replace('null','nullptr')
for typ in ['SCR_AIGroup','IEntity','ERR_HQUnit']:lookup=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',lookup)
lookup=lookup.replace('unit.','unit->').replace('StatusSampler.','StatusSampler->')
run(r'''
#include <cassert>
#include <vector>
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
struct SCR_AIGroup{};struct IEntity{};
struct Sampler{SCR_AIGroup* group;IEntity* root;bool valid=true;int calls=0;bool OwnsRoster(SCR_AIGroup* g){return g==group;}bool ValidateBoundVehicle(IEntity* r){calls++;return valid&&root==r;}};
struct ERR_HQUnit{SCR_AIGroup* Group;Sampler* StatusSampler;};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
struct HQ{bool m_Mode=true;Array<ERR_HQUnit*>m_Units;
'''+lookup+r'''
};
int main(){SCR_AIGroup group,other;IEntity root,replacement;Sampler sampler{&group,&root};ERR_HQUnit unit{&group,&sampler};HQ h;h.m_Units={nullptr,&unit};
assert(h.ValidateBoundVehicle(&group,&root)&&!h.ValidateBoundVehicle(&group,&replacement)&&sampler.root==&root);
assert(!h.ValidateBoundVehicle(&other,&root));sampler.valid=false;assert(!h.ValidateBoundVehicle(&group,&root));sampler.valid=true;
int calls=sampler.calls;Replication::server=false;assert(!h.ValidateBoundVehicle(&group,&root)&&sampler.calls==calls);Replication::server=true;
h.m_Units.assign(256,nullptr);h.m_Units.push_back(&unit);assert(!h.ValidateBoundVehicle(&group,&root)&&sampler.calls==calls);
}
''')
print('PASS: actual bounded HQ query validates only existing original binding without admission or replacement lookup')