--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c @@ -120,6 +120,36 @@ BaseMagazineComponent magazine = muzzle.GetMagazine(); return magazine && magazine.GetOwner() == entity; } + // The observed WCS BMP empty control has no runtime InventoryItemComponent. + // Native structural attachment can prove this control only, never gun custody. + protected static bool StructuralEmpty(BaseWeaponComponent weapon, array allowed, WeaponSlotComponent expectedMount = null) + { + if (!weapon || WeaponSlotComponent.Cast(weapon) || !weapon.GetOwner()) return false; + IEntity owner = weapon.GetOwner(); + if (!allowed.Contains(owner) || !owner.GetPrefabData() + || owner.GetPrefabData().GetPrefabName() != "{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et") return false; + if (BaseWeaponComponent.Cast(owner.FindComponent(BaseWeaponComponent)) != weapon) return false; + InventoryItemComponent item = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent)); + // A different nonnull inventory binding is conflicting evidence, not absence. + if (item && item.GetParentSlot()) return false; + IEntity parent = owner.GetParent(); + if (!parent || !allowed.Contains(parent)) return false; + array slots = {}; parent.FindComponents(WeaponSlotComponent, slots); + if (slots.IsEmpty() || slots.Count() > 8) return false; + WeaponSlotComponent matched = null; + foreach (Managed component : slots) + { + WeaponSlotComponent mount = WeaponSlotComponent.Cast(component); + if (!mount || mount.GetOwner() != parent || mount.GetWeaponEntity() != owner) continue; + InventoryStorageSlot binding = mount.GetSlotInfo(); + if (!binding || binding.GetParentContainer() != mount || binding.GetAttachedEntity() != owner) return false; + if (matched) return false; + matched = mount; + } + if (!matched || (expectedMount && matched != expectedMount)) return false; + array muzzles = {}; weapon.GetMuzzlesList(muzzles); + return muzzles.IsEmpty(); + } protected static void AddWeapon(BaseWeaponComponent weapon, array allowed, array weapons, ERR_HQMountedAmmoSnapshot result) { if (!weapon || !weapon.GetOwner() || !allowed.Contains(weapon.GetOwner())) @@ -146,7 +176,8 @@ result.Complete = false; return; } InventoryItemComponent mountedItem = InventoryItemComponent.Cast(mountedEntity.FindComponent(InventoryItemComponent)); - if (!mountedItem || mountedItem.GetParentSlot() != binding) + if ((!mountedItem || mountedItem.GetParentSlot() != binding) + && !StructuralEmpty(BaseWeaponComponent.Cast(mountedEntity.FindComponent(BaseWeaponComponent)), allowed, mount)) { #ifdef WORKBENCH if (s_Diagnostic) @@ -300,7 +331,7 @@ 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; + if (!item || !item.GetParentSlot()) return StructuralEmpty(weapon, allowed); InventoryStorageSlot slot = item.GetParentSlot(); WeaponSlotComponent mount = WeaponSlotComponent.Cast(slot.GetParentContainer()); return mount && allowed.Contains(mount.GetOwner()) && mount.GetSlotInfo() == slot && mount.GetWeaponEntity() == owner; --- a/tests/test_hq_mounted_ammo_sampler.py +++ b/tests/test_hq_mounted_ammo_sampler.py @@ -10,6 +10,8 @@ code=code.replace('BaseWeaponManagerComponent.Cast(part.FindComponent(BaseWeaponManagerComponent))','part->manager') code=code.replace('BaseWeaponComponent.Cast(part.FindComponent(BaseWeaponComponent))','part->weapon') code=code.replace('BaseWeaponComponent.Cast(mountedEntity.FindComponent(BaseWeaponComponent))','mountedEntity->weapon') +code=code.replace('BaseWeaponComponent.Cast(owner.FindComponent(BaseWeaponComponent))','owner->weapon').replace('WeaponSlotComponent.Cast(component)','dynamic_cast(component)') +code=code.replace('parent.FindComponents(WeaponSlotComponent, slots)','parent->FindMounts(slots)') code=code.replace('InventoryItemComponent.Cast(mountedEntity.FindComponent(InventoryItemComponent))','mountedEntity->item') code=code.replace('WeaponSlotComponent.Cast(weapon)','dynamic_cast(weapon)').replace('binding.GetParentContainer()','binding->mount') code=code.replace('InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent))','entity->item') @@ -48,9 +50,10 @@ struct Item{Slot* slot=nullptr;Slot* GetParentSlot(){return slot;}}; using InventoryStorageSlot=Slot*;using WeaponSlotComponent=Mount*; using InventoryItemComponent=Item*;using BaseInventoryStorageComponent=Storage*; -using IEntity=Entity*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*; +using Managed=Weapon*;using IEntity=Entity*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*; using BaseMagazineComponent=Magazine*;using BaseWeaponManagerComponent=Manager*; struct Entity{Prefab prefab;Prefab*GetPrefabData(){return &prefab;}Item* item=nullptr;bool character=false;Entity* child=nullptr;Entity* sibling=nullptr;Manager* manager=nullptr;Weapon* weapon=nullptr; + Entity* parent=nullptr;Array mounts;Entity* GetParent(){return parent;}void FindMounts(Array&out){out=mounts;} int reads=0;Entity* GetChildren(){reads++;assert(reads<=1);return child;}Entity* GetSibling(){return sibling;}}; struct Magazine{IEntity owner=nullptr;int rounds=25,capacity=100,reads=0; IEntity GetOwner(){return owner;}int GetAmmoCount(){reads++;return rounds;}int GetMaxAmmoCount(){return capacity;}}; @@ -73,6 +76,48 @@ ERR_HQMountedAmmoSnapshot* Inspect(){return ERR_HQMountedAmmoSampler::Inspect(&root);} }; int main(){ + // Native observed WCS empty control: no item or null reverse slot, but exact + // immediate-parent structural slot. Keep real guns and the weapon cap intact. + for(bool hasItem:{false,true})for(bool directFirst:{false,true}){ + Rig r;Entity dummy;Weapon inert;Mount mount;Item item;Slot slot; + dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et"; + dummy.parent=&r.root;r.root.mounts={&mount};r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy; + if(hasItem)dummy.item=&item; + mount.owner=&r.root;mount.weapon=&dummy;mount.slot=&slot;slot.mount=&mount;slot.attached=&dummy; + if(directFirst)r.manager.weapons={&inert,&mount,&inert,&r.weapon};else r.manager.weapons={&mount,&inert,&mount,&r.weapon}; + auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Chambers==1); + } + for(int fault=0;fault<15;fault++){ + Rig r;Entity dummy,foreign;Weapon inert,otherWeapon;Mount mount,otherMount;Item item;Slot slot,otherSlot; + dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et"; + dummy.parent=&r.root;r.root.mounts={&mount};r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy; + mount.owner=&r.root;mount.weapon=&dummy;mount.slot=&slot;slot.mount=&mount;slot.attached=&dummy;r.manager.weapons={&mount,&inert,&r.weapon}; + if(fault==0){dummy.item=&item;item.slot=&otherSlot;} + if(fault==1)dummy.prefab.name="{0000000000000000}Prefabs/Weapons/Core/Armament_Empty.et"; + if(fault==2)dummy.prefab.name="{52120E4BA120661B}Prefabs/Other.et"; + if(fault==3)dummy.prefab.name="{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et"; + if(fault==4)inert.muzzles={&r.muzzle}; + if(fault==5)dummy.parent=nullptr; + if(fault==6)dummy.parent=&foreign; + if(fault==7)mount.slot=nullptr; + if(fault==8)slot.mount=&otherMount; + if(fault==9)slot.attached=&foreign; + if(fault==10)mount.weapon=&foreign; + if(fault==11)mount.owner=&foreign; + if(fault==12)r.root.mounts.resize(9,&mount); + if(fault==13)r.root.mounts={&mount,&mount}; + if(fault==14){otherWeapon.owner=&dummy;dummy.weapon=&otherWeapon;} + auto s=r.Inspect();assert(!s->Complete&&s->Valid()); + } + {Entity root,dummy;Weapon inert;Mount mount;Slot slot;root.child=&dummy;dummy.parent=&root;root.mounts={&mount}; + dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";dummy.weapon=&inert;inert.owner=&dummy; + mount.owner=&root;mount.weapon=&dummy;mount.slot=&slot;slot.mount=&mount;slot.attached=&dummy; + auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==0&&s->Magazines==0&&s->Rounds==0);} + // Controls still occupy bounded collection slots before Inspect omits them. + for(int count:{32,33}){Entity root;std::vector parents(count),dummies(count);std::vector mounts(count);std::vector slots(count);std::vector inert(count); + root.child=&parents[0];for(int i=0;iComplete==(count==32)&&s->Valid()&&s->Weapons==0);} // 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")}){