Files
EscapistsReintroduced/tests/test_hq_ammo_sampler.py
T

163 lines
14 KiB
Python

"""Execute actual bounded native-inventory inspection through deterministic native API adapters."""
from pathlib import Path
import os,re
from policy_harness import ROOT,run
P=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoSampler.c'))
s=P.read_text()
for forbidden in ('Spawn','SetAmmoCount','GetOrigin','GetWorld','GetAgents','TryMove','DeleteEntity'):
assert forbidden not in s,forbidden
code=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('class ','struct ')
code=code.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory')
code=code.replace('BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))','actor->manager')
code=code.replace('BaseWeaponComponent.Cast(weaponEntity.FindComponent(BaseWeaponComponent))','weaponEntity->weapon')
code=code.replace('BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent))','item->magazine')
code=re.sub(r'\bERR_HQAmmoSnapshot (?=\w)', 'ERR_HQAmmoSnapshot* ',code)
code=code.replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
for name in ('EWeaponType','EMuzzleType','Replication','ERR_Actors'):code=code.replace(name+'.',name+'::')
code=code.replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
for name in ('weapon','muzzle','magazine','actor','group','result','inventory','manager','loaded','copy'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('GetMagazineWell().','GetMagazineWell()->')
code=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',code)
code=code.replace('\n}\n','\n};\n')
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
using std::vector;
template<class T>struct Array:vector<T>{using vector<T>::vector;
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
void Insert(T n){this->push_back(n);}bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}};
struct Entity;struct Muzzle;struct Magazine;struct Weapon;struct Group;struct Inventory;struct Manager;
using IEntity=Entity*;using SCR_AIGroup=Group*;using BaseWeaponComponent=Weapon*;
using BaseMuzzleComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
using InventoryStorageManagerComponent=Inventory*;using BaseWeaponManagerComponent=Manager*;
struct EWeaponType{static constexpr int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,
WT_ROCKETLAUNCHER=5,WT_GRENADELAUNCHER=6,WT_FRAGGRENADE=7,WT_SMOKEGRENADE=8;};
struct EMuzzleType{static constexpr int MT_BaseMuzzle=0,MT_RPGMuzzle=1,MT_UGLMuzzle=2;};
struct Well{int kind=1,baseKind=0;int Type(){return kind;}bool IsInherited(int type){return kind==type||baseKind==type;}};
struct Magazine{IEntity owner=nullptr;Well* well=nullptr;int rounds=0,capacity=30;
IEntity GetOwner(){return owner;}Well* GetMagazineWell(){return well;}
int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
struct Muzzle{Well* well=nullptr;Magazine* magazine=nullptr;int kind=0,barrels=1;bool chamber=false;
int GetMuzzleType(){return kind;}Well* GetMagazineWell(){return well;}
int GetBarrelsCount(){return barrels;}bool IsBarrelChambered(int){return chamber;}
Magazine* GetMagazine(){return magazine;}};
struct Weapon{int kind=1;Array<Muzzle*>muzzles;int GetWeaponType(){return kind;}
void GetMuzzlesList(Array<Muzzle*>& out){out=muzzles;}};
struct Inventory{Array<IEntity>items;int reads=0;void GetItems(Array<IEntity>& out){reads++;out=items;}};
struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>& out){out=weapons;}};
struct Group{bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}};
struct Entity{Inventory* inventory=nullptr;Manager* manager=nullptr;Weapon* weapon=nullptr;Magazine* magazine=nullptr;
Group* group=nullptr;bool alive=true,player=false;};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_Actors{static bool Alive(IEntity a){return a&&a->alive;}static bool Player(IEntity a){return a&&a->player;}
static struct Group* Group(IEntity a){return a?a->group:nullptr;}};
'''+code+r'''
ERR_HQAmmoSnapshot Fresh(){ERR_HQAmmoSnapshot s;s.Complete=true;s.LauncherComplete=true;return s;}
int main(){
Group group,other;Well rifleWell,rocketWell;rocketWell.kind=2;
Entity actor,weaponEntity,loadedEntity,spareEntity,unrelatedEntity;
Inventory inv;Manager manager;Weapon weapon;Muzzle muzzle;Magazine loaded,spare,unrelated;
actor.group=&group;actor.inventory=&inv;actor.manager=&manager;
weaponEntity.weapon=&weapon;manager.weapons={&weaponEntity,&weaponEntity};weapon.muzzles={&muzzle,&muzzle};
muzzle.well=&rifleWell;muzzle.magazine=&loaded;muzzle.chamber=true;
loaded.owner=&loadedEntity;loaded.well=&rifleWell;loaded.rounds=30;loadedEntity.magazine=&loaded;
spare.owner=&spareEntity;spare.well=&rifleWell;spare.rounds=17;spareEntity.magazine=&spare;
unrelated.owner=&unrelatedEntity;unrelated.well=&rocketWell;unrelated.rounds=1;unrelated.capacity=1;unrelatedEntity.magazine=&unrelated;
inv.items={&loadedEntity,&loadedEntity,&spareEntity,&unrelatedEntity};
auto full=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&full);
assert(full.Valid(1)&&full.Complete&&full.Members==1&&full.SmallArmsMuzzles==1);
assert(full.SmallArmsRounds==47&&full.SmallArmsMagazines==2&&full.SmallArmsChambers==1&&!full.KnownSmallArmsEmpty());
auto wire=full.Snapshot();full.SmallArmsRounds=0;assert(wire->SmallArmsRounds==47); // detached delivery value
// Loaded magazine omitted from native storage enumeration is still counted exactly once.
inv.items={&spareEntity};auto omitted=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&omitted);
assert(omitted.SmallArmsRounds==47&&omitted.SmallArmsMagazines==2);
// Multiple weapons sharing one well never multiply a single physical magazine.
Entity secondEntity;Weapon second;Muzzle secondMuzzle;secondEntity.weapon=&second;
second.muzzles={&secondMuzzle};secondMuzzle.well=&rifleWell;secondMuzzle.magazine=&loaded;
manager.weapons.Insert(&secondEntity);auto shared=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&shared);
assert(shared.SmallArmsMuzzles==2&&shared.SmallArmsRounds==47&&shared.SmallArmsMagazines==2);
manager.weapons={&weaponEntity};
loaded.rounds=0;spare.rounds=0;muzzle.chamber=false;auto empty=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&empty);
assert(empty.KnownSmallArmsEmpty()&&empty.SmallArmsMagazines==2);
muzzle.chamber=true;auto chamber=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&chamber);
assert(chamber.SmallArmsRounds==0&&chamber.SmallArmsChambers==1&&!chamber.KnownSmallArmsEmpty());muzzle.chamber=false;
// Under-barrel launcher is classified by muzzle, independently of its parent rifle type.
Muzzle launcher;launcher.kind=EMuzzleType::MT_UGLMuzzle;launcher.well=&rocketWell;launcher.magazine=&unrelated;
weapon.muzzles.Insert(&launcher);auto mixed=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&mixed);
assert(mixed.Complete&&mixed.LauncherComplete&&mixed.SmallArmsRounds==0&&mixed.LauncherRounds==1);
assert(mixed.LauncherMagazines==1&&mixed.KnownSmallArmsEmpty()); // launcher stock is not rifle stock
launcher.well=nullptr;launcher.magazine=nullptr;auto loose=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&loose);
assert(loose.Complete&&!loose.LauncherComplete);weapon.muzzles={&muzzle};
// Empty/unavailable native launcher muzzles affect only launcher completeness.
Entity launcherEntity;Weapon launcherWeapon;launcherEntity.weapon=&launcherWeapon;
launcherWeapon.kind=EWeaponType::WT_ROCKETLAUNCHER;manager.weapons.Insert(&launcherEntity);
auto unavailableLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&unavailableLauncher);
assert(unavailableLauncher.Complete&&!unavailableLauncher.LauncherComplete&&unavailableLauncher.KnownSmallArmsEmpty());
launcherWeapon.muzzles={nullptr};auto nullLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&nullLauncher);
assert(nullLauncher.Complete&&!nullLauncher.LauncherComplete);
launcherWeapon.muzzles={nullptr,nullptr,nullptr,nullptr,nullptr};
auto cappedLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&cappedLauncher);
assert(cappedLauncher.Complete&&!cappedLauncher.LauncherComplete);
// A native loaded projectile with no magazine well is known to belong to the launcher.
launcherWeapon.muzzles={&launcher};launcher.well=nullptr;launcher.magazine=&unrelated;unrelated.well=nullptr;
auto knownProjectile=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&knownProjectile);
assert(knownProjectile.Complete&&!knownProjectile.LauncherComplete&&knownProjectile.KnownSmallArmsEmpty());
// Do not waive uncertainty if that same physical magazine also occurs on a small-arms muzzle.
muzzle.magazine=&unrelated;auto sharedUnknown=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&sharedUnknown);
assert(!sharedUnknown.Complete&&!sharedUnknown.KnownSmallArmsEmpty());muzzle.magazine=&loaded;
manager.weapons={&weaponEntity};inv.items.Insert(&unrelatedEntity);
auto looseUnknown=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&looseUnknown);
assert(looseUnknown.Complete&&looseUnknown.KnownSmallArmsEmpty()&&!looseUnknown.LauncherComplete);
assert(!ERR_HQAmmoSampler::Compatible(&unrelated,weapon.muzzles)); // stock requires a magazine well for reload
inv.items={&spareEntity};unrelated.well=&rocketWell;
// Stock search accepts a derived magazine well; exact reload compatibility is
// narrower. Preserve exact totals but make derived-only empty knowledge unknown.
Well derivedRifle;derivedRifle.kind=3;derivedRifle.baseKind=rifleWell.kind;
spare.well=&derivedRifle;spare.rounds=17;
assert(!ERR_HQAmmoSampler::Compatible(&spare,weapon.muzzles));
assert(ERR_HQAmmoSampler::InheritedCandidate(&spare,weapon.muzzles));
auto derived=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&derived);
assert(!derived.Complete&&!derived.KnownSmallArmsEmpty()&&derived.SmallArmsRounds==0);
assert(derived.LauncherComplete); // no launcher candidate from this well
spare.well=&rocketWell;auto unrelatedWell=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&unrelatedWell);
assert(unrelatedWell.Complete&&unrelatedWell.KnownSmallArmsEmpty());
assert(!ERR_HQAmmoSampler::InheritedCandidate(&spare,weapon.muzzles));
// Inheritance direction matches stock: magazine derives from the muzzle well.
spare.well=&rifleWell;muzzle.well=&derivedRifle;muzzle.magazine=nullptr;
auto reverse=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&reverse);
assert(reverse.Complete&&reverse.KnownSmallArmsEmpty());
muzzle.well=&rifleWell;muzzle.magazine=&loaded;spare.rounds=0;
assert(ERR_HQAmmoSampler::Compatible(&spare,weapon.muzzles));
assert(!ERR_HQAmmoSampler::InheritedCandidate(&spare,weapon.muzzles));
auto exactEmpty=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&exactEmpty);
assert(exactEmpty.Complete&&exactEmpty.KnownSmallArmsEmpty());
// The corresponding launcher category independently becomes unknown.
Well derivedRocket;derivedRocket.kind=4;derivedRocket.baseKind=rocketWell.kind;
unrelated.well=&derivedRocket;launcher.well=&rocketWell;launcher.magazine=nullptr;
weapon.muzzles.Insert(&launcher);inv.items.Insert(&unrelatedEntity);
auto derivedLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&derivedLauncher);
assert(derivedLauncher.Complete&&!derivedLauncher.LauncherComplete&&derivedLauncher.LauncherRounds==0);
weapon.muzzles={&muzzle};inv.items={&spareEntity};unrelated.well=&rocketWell;
// Missing/unsupported/malformed native data never establishes known empty ammunition.
manager.weapons.Insert(nullptr);auto vanishedWeapon=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&vanishedWeapon);
assert(!vanishedWeapon.Complete&&!vanishedWeapon.KnownSmallArmsEmpty());manager.weapons={&weaponEntity};
inv.items.Insert(nullptr);auto vanishedItem=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&vanishedItem);
assert(!vanishedItem.Complete&&!vanishedItem.KnownSmallArmsEmpty());inv.items={&spareEntity};
actor.manager=nullptr;auto missing=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&missing);assert(!missing.KnownSmallArmsEmpty());actor.manager=&manager;
loaded.well=nullptr;auto noWell=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&noWell);assert(!noWell.Complete);loaded.well=&rifleWell;
loaded.rounds=31;auto malformed=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&malformed);assert(!malformed.Complete);loaded.rounds=0;
weapon.kind=99;auto unsupported=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&unsupported);assert(!unsupported.Complete);weapon.kind=1;
muzzle.barrels=5;auto barrels=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&barrels);assert(!barrels.Complete);muzzle.barrels=1;
inv.items.resize(129,&spareEntity);auto capped=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&capped);assert(!capped.Complete);inv.items={&spareEntity};
int before=inv.reads;actor.player=true;auto player=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&player);assert(!player.Complete&&inv.reads==before);actor.player=false;
actor.group=&other;auto transferred=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&transferred);assert(!transferred.Complete&&inv.reads==before);actor.group=&group;
actor.alive=false;auto dead=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&dead);assert(!dead.Complete&&inv.reads==before);actor.alive=true;
Replication::server=false;auto client=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&client);assert(!client.Complete&&inv.reads==before);Replication::server=true;
auto count=Fresh();count.Members=32;ERR_HQAmmoSampler::Inspect(&actor,&group,&count);assert(!count.Complete&&count.Members==32);
assert(!empty.Valid(0));empty.SmallArmsRounds=-1;assert(!empty.Valid(1));
}
''')
print('PASS: actual own-inventory ammo inspection deduplicates loaded/carried/shared wells, separates chambers/launchers, freezes scalar snapshots and treats unsupported/capped/foreign inventory as unknown')