Files
EscapistsReintroduced/docs/evidence/bmp-ammo-accounting-2026-10-03/proposal/fix.patch
T
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

123 lines
9.4 KiB
Diff

--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c
@@ -239,6 +239,23 @@
if (mounted) AddWeapon(mounted, allowed, weapons, result);
}
}
+ // These installed resources are inert selection/periscope controls, not guns.
+ // Only omit them after the caller observed an empty native muzzle list and
+ // reciprocal current weapon-slot ownership in this same sampled hierarchy.
+ protected static bool NonFiringPlaceholder(BaseWeaponComponent weapon, array<IEntity> allowed)
+ {
+ if (!weapon || !weapon.GetOwner()) return false;
+ IEntity owner = weapon.GetOwner();
+ if (!allowed.Contains(owner) || !owner.GetPrefabData()) return false;
+ ResourceName resource = owner.GetPrefabData().GetPrefabName();
+ if (resource != "{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et"
+ && resource != "{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et") return false;
+ InventoryItemComponent item = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent));
+ if (!item || !item.GetParentSlot()) return false;
+ InventoryStorageSlot slot = item.GetParentSlot();
+ WeaponSlotComponent mount = WeaponSlotComponent.Cast(slot.GetParentContainer());
+ return mount && allowed.Contains(mount.GetOwner()) && mount.GetSlotInfo() == slot && mount.GetWeaponEntity() == owner;
+ }
static ERR_HQMountedAmmoSnapshot Inspect(IEntity root)
{
ERR_HQMountedAmmoSnapshot result = new ERR_HQMountedAmmoSnapshot();
@@ -252,7 +269,13 @@
foreach (BaseWeaponComponent weapon : weapons)
{
array<BaseMuzzleComponent> muzzles = {}; weapon.GetMuzzlesList(muzzles);
- if (muzzles.IsEmpty() || muzzles.Count() > 8) { result.Complete = false; continue; }
+ if (muzzles.IsEmpty())
+ {
+ if (NonFiringPlaceholder(weapon, allowed)) result.Weapons--;
+ else result.Complete = false;
+ continue;
+ }
+ if (muzzles.Count() > 8) { result.Complete = false; continue; }
foreach (BaseMuzzleComponent muzzle : muzzles)
{
if (!muzzle || !muzzle.GetOwner() || !allowed.Contains(muzzle.GetOwner())) { result.Complete = false; continue; }
--- a/tests/test_hq_mounted_ammo_sampler.py
+++ b/tests/test_hq_mounted_ammo_sampler.py
@@ -13,6 +13,7 @@
code=code.replace('InventoryItemComponent.Cast(mountedEntity.FindComponent(InventoryItemComponent))','mountedEntity->item')
code=code.replace('WeaponSlotComponent.Cast(weapon)','dynamic_cast<Mount*>(weapon)').replace('binding.GetParentContainer()','binding->mount')
code=code.replace('InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent))','entity->item')
+code=code.replace('InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent))','owner->item').replace('ResourceName resource', 'string resource')
code=code.replace('WeaponSlotComponent.Cast(slot.GetParentContainer())','slot->mount').replace('BaseMuzzleComponent.Cast(slot.GetParentContainer())','slot->muzzle')
code=code.replace('EStoragePurpose.','EStoragePurpose::').replace('GetParentSlot().','GetParentSlot()->')
code=code.replace('ChimeraCharacter.Cast(entity) != nullptr','entity->character').replace('ChimeraCharacter.Cast(entity) != null','entity->character')
@@ -24,7 +25,7 @@
code=code.replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
code=code.replace('Array<IEntity> allowed, Array<BaseWeaponComponent> weapons,','Array<IEntity>& allowed, Array<BaseWeaponComponent>& weapons,')
code=code.replace('Array<IEntity> allowed, Array<BaseWeaponComponent> weapons)', 'Array<IEntity>& allowed, Array<BaseWeaponComponent>& weapons)')
-for name in ('copy','result','weapon','root','entity','child','part','manager','mounted','muzzle','magazine','item','storage','mount','mountedEntity','mountedItem'):
+for name in ('copy','result','weapon','root','entity','child','part','manager','mounted','muzzle','magazine','item','storage','mount','mountedEntity','mountedItem','owner'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',code).replace('\n}\n','\n};\n')
program=(r'''
@@ -69,6 +70,37 @@
ERR_HQMountedAmmoSnapshot* Inspect(){return ERR_HQMountedAmmoSampler::Inspect(&root);}
};
int main(){
+ // Installed BMP has three live gun components plus two inert native controls.
+ // Exercise actual Inspect using exact documented resources and reciprocal slots.
+ for(string name:{string("{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et"),string("{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et")}){
+ Rig r;Entity dummy;Weapon inert;Mount mount;Item item;Slot slot;dummy.prefab.name=name;
+ r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy;dummy.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&r.root;mount.slot=&slot;mount.weapon=&dummy;
+ r.manager.weapons.Insert(&mount);r.manager.weapons.Insert(&inert);
+ auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Capacity==100&&s->Chambers==1&&inert.reads==1);
+ }
+ // A root containing only a known installed inert control is completely unarmed.
+ {Entity root,dummy;Weapon inert;Mount mount;Item item;Slot slot;root.child=&dummy;dummy.prefab.name="{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et";dummy.weapon=&inert;inert.owner=&dummy;dummy.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&root;mount.slot=&slot;mount.weapon=&dummy;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==0&&s->Muzzles==0&&s->Magazines==0&&s->Rounds==0&&s->Capacity==0&&s->Chambers==0&&inert.reads==1);}
+ // Both BMP controls coexist: decrement each exactly once while retaining the gun.
+ {Rig r;Entity dummy[2];Weapon inert[2];Mount mount[2];Item item[2];Slot slot[2];r.crew.sibling=&dummy[0];dummy[0].sibling=&dummy[1];
+ dummy[0].prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";dummy[1].prefab.name="{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et";
+ for(int i=0;i<2;i++){dummy[i].weapon=&inert[i];inert[i].owner=&dummy[i];dummy[i].item=&item[i];item[i].slot=&slot[i];slot[i].mount=&mount[i];mount[i].owner=&r.root;mount[i].slot=&slot[i];mount[i].weapon=&dummy[i];r.manager.weapons.Insert(&mount[i]);r.manager.weapons.Insert(&inert[i]);}
+ auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Capacity==100&&s->Chambers==1&&inert[0].reads==1&&inert[1].reads==1);}
+ // Unknown no-muzzle weapons and broken/foreign installation remain incomplete.
+ for(int bad=0;bad<6;bad++){
+ Rig r;Entity dummy,foreign;Weapon inert;Mount mount;Item item;Slot slot,other;dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";
+ r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy;dummy.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&r.root;mount.slot=&slot;mount.weapon=&dummy;
+ if(bad==0)dummy.prefab.name="{0000000000000000}Prefabs/Weapons/Core/Armament_Empty.et";
+ if(bad==1)dummy.item=nullptr;
+ if(bad==2)item.slot=nullptr;
+ if(bad==3)mount.owner=&foreign;
+ if(bad==4)mount.slot=&other;
+ if(bad==5)mount.weapon=&foreign;
+ auto s=r.Inspect();assert(!s->Complete&&s->Valid());
+ }
+ // A known resource carrying an actual muzzle is never silently omitted.
+ {Rig r;r.gun.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";auto s=r.Inspect();assert(s->Complete&&s->Weapons==1&&s->Rounds==25&&r.muzzle.reads==1);}
+ // An excluded cargo subtree cannot turn a stored placeholder into mounted armament.
+ {Rig r;Entity cargo;Weapon inert;Item item;Slot slot;Storage storage;r.crew.sibling=&cargo;cargo.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";cargo.item=&item;item.slot=&slot;slot.storage=&storage;cargo.weapon=&inert;inert.owner=&cargo;auto s=r.Inspect();assert(s->Complete&&s->Weapons==1&&inert.reads==0);}
{auto s=ERR_HQMountedAmmoSampler::Inspect(nullptr);assert(!s->Complete&&s->Valid());}
{Entity root;root.character=true;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(!s->Complete&&root.reads==0);}
{Entity root;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==0);}
--- a/docs/external-addons.json
+++ b/docs/external-addons.json
@@ -170,6 +170,11 @@
"guid": "0FA8ACE19A772327",
"addon": "595F2BF2F44836FB",
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
+ },
+ "Prefabs/Weapons/Core/Armament_Empty.et": {
+ "guid": "52120E4BA120661B",
+ "addon": "629B2BA37EFFD577",
+ "verification": "Exact path/type/GUID record in installed owning resourceDatabase.rdb; read-only audit 2026-10-03"
}
}
}
--- a/docs/vanilla-resources.json
+++ b/docs/vanilla-resources.json
@@ -125,6 +125,7 @@
"Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et": "BD496EE1B40DC510",
"Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et": "D2018EDB1BBF4C88",
"Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et": "C850A33226B8F9C1",
+ "Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et": "7E1DD1CE4D6A23F4",
"Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et": "B05A816C0BF50802",
"Prefabs/Weapons/Handguns/PM/Handgun_PM.et": "C0F7DD85A86B2900",
"Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et": "BBB50A815A2F916B",