Files
EscapistsReintroduced/tests/test_hq_ammo_sampler.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

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/core/Scripts/Game/ERR/10_AI/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')