Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
171 lines
15 KiB
Python
171 lines
15 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('SCR_MuzzleInMagComponent.Cast(muzzle)', '(muzzle->inMag ? muzzle : null)')
|
|
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','reloadable','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 SCR_MuzzleInMagComponent=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{bool inMag=false,canReload=true;bool CanBeReloaded(){return canReload;}bool disposable=false;bool IsDisposable(){return disposable;}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.LauncherMuzzles==1&&mixed.KnownSmallArmsEmpty());
|
|
auto launcherWire=mixed.Snapshot();mixed.LauncherMuzzles=0;assert(launcherWire->LauncherMuzzles==1);
|
|
launcher.magazine=nullptr;inv.items.clear();auto expended=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&expended);
|
|
assert(expended.LauncherComplete&&expended.LauncherMuzzles==1&&expended.LauncherRounds==0&&expended.LauncherMagazines==0);
|
|
launcher.disposable=true;auto disposable=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&disposable);assert(!disposable.LauncherComplete&&disposable.LauncherMuzzles==0);launcher.disposable=false;
|
|
launcher.inMag=true;launcher.canReload=false;auto singleUse=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&singleUse);assert(!singleUse.LauncherComplete&&singleUse.LauncherMuzzles==0);launcher.inMag=false;launcher.canReload=true;
|
|
auto badMuzzles=expended;badMuzzles.LauncherMuzzles=-1;assert(!badMuzzles.Valid(1));badMuzzles.LauncherMuzzles=33;assert(!badMuzzles.Valid(1));badMuzzles.LauncherMuzzles=32;assert(badMuzzles.Valid(1));
|
|
launcher.magazine=&unrelated;inv.items={&loadedEntity,&spareEntity,&unrelatedEntity}; // 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')
|