Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
154 lines
12 KiB
Python
154 lines
12 KiB
Python
"""Execute dry native eligibility/slot proof; never launch the runner or inventory operation."""
|
|
import ast,os,re
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from policy_harness import ROOT,body,run
|
|
R=Path(os.environ.get('ERR_NATIVE_SWAP_ROOT',ROOT))
|
|
p=Path(os.environ.get('ERR_NATIVE_SWAP_SOURCE',R/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedSwapProbe.c'))
|
|
s=p.read_text();fixture=(R/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedAmmoProbe.c').read_text()
|
|
smoke=(R/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
|
|
runner=(R/'tools/headless_workbench.py').read_text()
|
|
assert s.startswith('#ifdef WORKBENCH') and s.rstrip().endswith('#endif')
|
|
for forbidden in ('TrySwap','TryMove','TrySpawn','Spawn(', 'SetAmmoCount','SetOrigin','DeleteEntity','.Reserve(','.Commit(','.BindVehicle(','.Sample(','.Advance(','.Ingest(','.AddWaypoint(','.RemoveWaypoint('):
|
|
assert forbidden not in s,forbidden
|
|
assert s.count('.CanSwapItemStorages(')==1
|
|
assert 'Collect(vehicle.Entity, shape, allowed, weapons)' in s
|
|
assert 'GetChildren(' not in s and 'GetSibling(' not in s # exact shared production collector, no parallel traversal
|
|
for guard in ('index < 6','weapons.Count() > 32','muzzles.Count() > 8','seen.Count() >= 128','cargo.Count() > 128','stores.Count() > 64','InitialStrength() > 32','CanAttribute(slot, worker, false)','ValidateBoundVehicle(vehicle.Entity)','cargo.Contains(outgoing)','ExactGun()'):
|
|
assert guard in s,guard
|
|
assert 'now + 180000' in fixture and 'now > m_Deadline' in fixture
|
|
assert fixture.index('if (m_SwapEligibility)\n {') < fixture.index('Capture(hq, task.Vehicles[slot], now)')
|
|
assert 'if (!m_ProofTask) m_ProofTask = Candidate(hq)' in fixture
|
|
assert 'native mounted swap eligibility only; no transfer or radio proof' in smoke
|
|
assert "['-errMountedSwapEligibility', '1']" in runner
|
|
node=next(n for n in ast.parse(runner).body if isinstance(n,ast.If) and 'a.mounted_swap_eligibility' in ast.unparse(n.test))
|
|
condition=compile(ast.Expression(node.test),'<flag>','eval')
|
|
for flag,outcome,bad in [(True,'mounted-ammo',False),(False,'convoy',False),(True,'convoy',True)]:
|
|
assert eval(condition,{'a':SimpleNamespace(mounted_swap_eligibility=flag,smoke=outcome)})==bad
|
|
# Check exact dry call/body with controlled native result and original inventory slots.
|
|
code='\n'.join(body(p,x) for x in ('protected static bool Attached(', 'protected int DryRun(', 'void Stop('))
|
|
code=code.replace('protected ','').replace('InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent))','item->inventory')
|
|
for t in ('IEntity','InventoryStorageSlot','InventoryStorageManagerComponent','BaseMuzzleComponent','InventoryItemComponent'):
|
|
code=re.sub(r'\b'+t+r' (?=\w)',t+'* ',code)
|
|
for var in ('slot','inventoryItem','source','muzzle'):code=code.replace(var+'.',var+'->')
|
|
code=code.replace('GetMagazine().','GetMagazine()->')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <functional>
|
|
struct IEntity;struct InventoryStorageSlot;
|
|
struct InventoryItemComponent{InventoryStorageSlot* slot=nullptr;InventoryStorageSlot* GetParentSlot(){return slot;}};
|
|
struct IEntity{InventoryItemComponent* inventory=nullptr;};
|
|
struct InventoryStorageSlot{IEntity* attached=nullptr;IEntity* GetAttachedEntity(){return attached;}};
|
|
struct Magazine{IEntity* owner=nullptr;IEntity* GetOwner(){return owner;}};
|
|
struct BaseMuzzleComponent{Magazine* loaded=nullptr;Magazine* GetMagazine(){return loaded;}};
|
|
struct InventoryStorageManagerComponent{int calls=0;bool result=true;std::function<void()> effect;
|
|
bool CanSwapItemStorages(IEntity* incoming,IEntity* outgoing){assert(incoming&&outgoing);calls++;if(effect)effect();return result;}};
|
|
template<class... T>void PrintFormat(T...){}
|
|
struct Probe{bool m_Stopped=false,m_Called=false;int m_Result=0;
|
|
'''+code+r'''
|
|
};
|
|
struct Fixture{IEntity incoming,outgoing,foreign;InventoryItemComponent a,b;InventoryStorageSlot from,to,other;
|
|
Magazine magazine;BaseMuzzleComponent muzzle;InventoryStorageManagerComponent source;Probe p;
|
|
Fixture(){incoming.inventory=&a;outgoing.inventory=&b;a.slot=&from;b.slot=&to;from.attached=&incoming;to.attached=&outgoing;magazine.owner=&outgoing;muzzle.loaded=&magazine;}
|
|
int Run(){return p.DryRun(&source,&incoming,&outgoing,&from,&to,&muzzle);}
|
|
};
|
|
int main(){
|
|
{Fixture f;assert(f.Run()==1&&f.source.calls==1&&f.p.m_Called);assert(f.Run()==1&&f.source.calls==1);
|
|
assert(f.from.attached==&f.incoming&&f.to.attached==&f.outgoing&&f.muzzle.loaded==&f.magazine);}
|
|
{Fixture f;f.source.result=false;assert(f.Run()==-1&&f.source.calls==1);f.source.result=true;assert(f.Run()==-1&&f.source.calls==1);}
|
|
for(int bad=0;bad<6;bad++){
|
|
Fixture f;if(bad==0){f.incoming.inventory=nullptr;}
|
|
if(bad==1){f.a.slot=&f.other;}
|
|
if(bad==2){f.from.attached=&f.foreign;}
|
|
if(bad==3){f.to.attached=&f.foreign;}
|
|
if(bad==4){f.muzzle.loaded=nullptr;}
|
|
if(bad==5){f.magazine.owner=&f.foreign;}
|
|
assert(f.Run()==-1&&f.source.calls==0&&!f.p.m_Called);}
|
|
// Even a positive native query cannot pass if exact ownership changes during it.
|
|
for(int changed=0;changed<3;changed++){
|
|
Fixture f;f.source.effect=[&](){if(changed==0)f.from.attached=&f.foreign;if(changed==1)f.b.slot=&f.other;if(changed==2)f.muzzle.loaded=nullptr;};
|
|
assert(f.Run()==-1&&f.source.calls==1&&f.p.m_Called);assert(f.Run()==-1&&f.source.calls==1);}
|
|
{Fixture f;f.p.Stop();assert(f.Run()==-1&&f.source.calls==0);}
|
|
{Fixture f;assert(f.Run()==1);f.p.Stop();assert(f.Run()==-1&&f.source.calls==1);}
|
|
{Fixture f;assert(f.p.DryRun(nullptr,&f.incoming,&f.outgoing,&f.from,&f.to,&f.muzzle)==-1&&f.source.calls==0);}
|
|
}
|
|
''')
|
|
print('PASS: actual once-only native dry query, exact before/after slots, native refusal, shutdown; shared bounded discovery and distinct opt-in verdict')
|
|
|
|
# Execute the actual final-completion selector; the step verdict alone does not
|
|
# prevent the round-end branch from wrongly claiming radio delivery.
|
|
completion=body(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedAmmoProbe.c','static string CompletionText(')
|
|
run('#include <cassert>\n#include <string>\nusing string=std::string;\n'+completion+r'''
|
|
int main(){
|
|
auto ordinary=CompletionText(false),swap=CompletionText(true);
|
|
assert(ordinary.find("actual radio delivery")!=string::npos);
|
|
assert(swap.find("eligibility only")!=string::npos);
|
|
assert(swap.find("no transfer or radio proof")!=string::npos);
|
|
assert(swap.find("actual radio delivery")==string::npos);
|
|
assert(swap!=ordinary);
|
|
}
|
|
''')
|
|
terminal=smoke.split('if (m_ERRTestMountedAmmo)\n\t\t{',1)[1].split('if (m_ERRTestConvoy)',1)[0]
|
|
assert terminal.index('CompletionText(m_ERRMountedAmmo && m_ERRMountedAmmo.SwapEligibility())') < terminal.index('ERR_StopHQPatrol()') < terminal.index('Print(mountedCompletion)')
|
|
assert 'actual radio delivery' not in terminal
|
|
print('PASS: actual completion selector and pre-cleanup variant capture preserve separate radio and eligibility verdicts')
|
|
|
|
# Exercise the production single-pass candidate classifier, not only its log text.
|
|
stage='\n'.join(body(p,x) for x in ('protected static bool Attached(', 'protected int CargoStage('))
|
|
stage=stage.replace('protected ','').replace('null','nullptr')
|
|
stage=stage.replace('BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent))','candidate->magazine')
|
|
stage=stage.replace('InventoryItemComponent.Cast(candidate.FindComponent(InventoryItemComponent))','candidate->inventory')
|
|
stage=stage.replace('InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent))','item->inventory')
|
|
stage=stage.replace('array<BaseInventoryStorageComponent> stores','Array<BaseInventoryStorageComponent*>& stores')
|
|
for typ in ('BaseMagazineComponent','InventoryStorageSlot','BaseInventoryStorageComponent'):
|
|
stage=stage.replace('out '+typ+' ',typ+'*& ')
|
|
for typ in ('IEntity','InventoryItemComponent','InventoryStorageSlot','BaseInventoryStorageComponent'):
|
|
stage=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',stage)
|
|
for name in ('candidate','magazine','item','from','storage','slot','inventoryItem','m_Muzzle'):
|
|
stage=stage.replace(name+'.',name+'->')
|
|
stage=stage.replace('GetMagazineWell().','GetMagazineWell()->').replace('ERR_InfantryAmmoTransfer.','ERR_InfantryAmmoTransfer::')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
struct IEntity;struct InventoryStorageSlot;struct BaseMagazineComponent;struct InventoryItemComponent;
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool Contains(T value){return std::find(this->begin(),this->end(),value)!=this->end();}};
|
|
struct Well{int kind=1;int Type(){return kind;}bool IsInherited(int type){return kind==type;}};
|
|
struct IEntity{BaseMagazineComponent* magazine=nullptr;InventoryItemComponent* inventory=nullptr;IEntity* truck=nullptr;};
|
|
struct BaseMagazineComponent{IEntity* owner=nullptr;Well* well=nullptr;bool used=false;int rounds=100,capacity=100;
|
|
IEntity* GetOwner(){return owner;}bool IsUsed(){return used;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}Well* GetMagazineWell(){return well;}};
|
|
struct BaseInventoryStorageComponent{IEntity* owner=nullptr;IEntity* GetOwner(){return owner;}};
|
|
struct InventoryStorageSlot{IEntity* attached=nullptr;BaseInventoryStorageComponent* storage=nullptr;IEntity* GetAttachedEntity(){return attached;}BaseInventoryStorageComponent* GetStorage(){return storage;}};
|
|
struct InventoryItemComponent{InventoryStorageSlot* slot=nullptr;InventoryStorageSlot* GetParentSlot(){return slot;}};
|
|
struct Muzzle{Well* well=nullptr;Well* GetMagazineWell(){return well;}};
|
|
namespace ERR_InfantryAmmoTransfer{int checks=0;bool StoredUnder(IEntity* owner,IEntity* truck){checks++;return owner&&owner->truck==truck;}}
|
|
struct Probe{Muzzle* m_Muzzle=nullptr;
|
|
'''+stage+r'''
|
|
};
|
|
struct Fixture{IEntity candidate,truck,storageOwner,foreign;Well well,wrong;BaseMagazineComponent magazine;
|
|
BaseInventoryStorageComponent storage;InventoryStorageSlot slot,other;InventoryItemComponent item;Muzzle muzzle;Probe probe;
|
|
Array<BaseInventoryStorageComponent*> stores;BaseMagazineComponent* selected=nullptr;InventoryStorageSlot* from=nullptr;BaseInventoryStorageComponent* selectedStorage=nullptr;
|
|
Fixture(){wrong.kind=2;candidate.magazine=&magazine;candidate.inventory=&item;magazine.owner=&candidate;magazine.well=&well;
|
|
storageOwner.truck=&truck;storage.owner=&storageOwner;slot.attached=&candidate;slot.storage=&storage;item.slot=&slot;stores={&storage};muzzle.well=&well;probe.m_Muzzle=&muzzle;}
|
|
int Run(){return probe.CargoStage(&candidate,&truck,stores,selected,from,selectedStorage);}
|
|
};
|
|
int main(){
|
|
{Fixture f;assert(f.Run()==4&&f.selected==&f.magazine&&f.from==&f.slot&&f.selectedStorage==&f.storage);}
|
|
for(int bad=0;bad<2;bad++){Fixture f;if(bad==0){f.candidate.magazine=nullptr;}else f.magazine.owner=&f.foreign;
|
|
assert(f.Run()==0&&f.from==nullptr&&f.selectedStorage==nullptr);}
|
|
for(int bad=0;bad<5;bad++){Fixture f;if(bad==0){f.magazine.used=true;}if(bad==1){f.magazine.rounds=0;}
|
|
if(bad==2){f.magazine.capacity=0;}if(bad==3){f.magazine.capacity=100001;}if(bad==4){f.magazine.rounds=101;}
|
|
ERR_InfantryAmmoTransfer::checks=0;assert(f.Run()==1&&f.from==nullptr&&ERR_InfantryAmmoTransfer::checks==0);}
|
|
for(int bad=0;bad<3;bad++){Fixture f;if(bad==0){f.magazine.well=nullptr;}if(bad==1){f.muzzle.well=nullptr;}if(bad==2){f.magazine.well=&f.wrong;}
|
|
ERR_InfantryAmmoTransfer::checks=0;assert(f.Run()==2&&f.from==nullptr&&ERR_InfantryAmmoTransfer::checks==0);}
|
|
for(int bad=0;bad<6;bad++){Fixture f;if(bad==0){f.candidate.inventory=nullptr;}if(bad==1){f.item.slot=nullptr;}
|
|
if(bad==2){f.slot.storage=nullptr;}if(bad==3){f.stores.clear();}if(bad==4){f.storageOwner.truck=&f.foreign;}if(bad==5){f.slot.attached=&f.foreign;}
|
|
assert(f.Run()==3);}
|
|
// Exact counters are cumulative stages; classification neither transfers nor consumes.
|
|
Fixture valid;assert(valid.Run()==4&&!valid.magazine.used&&valid.magazine.rounds==100&&valid.slot.attached==&valid.candidate);
|
|
// Reusing output arguments cannot accidentally retain a prior eligible slot/storage.
|
|
valid.candidate.magazine=nullptr;assert(valid.Run()==0&&valid.from==nullptr&&valid.selectedStorage==nullptr);
|
|
}
|
|
''')
|
|
print('PASS: actual cargo diagnostic stages separate absent/foreign magazines, used/empty/invalid rounds, well mismatch and exact truck storage/attachment without mutation')
|