Files
EscapistsReintroduced/docs/evidence/ui-bmp-corrections-2026-10-04/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

131 lines
9.6 KiB
Diff

--- 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<IEntity> 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<Managed> 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<BaseMuzzleComponent> muzzles = {}; weapon.GetMuzzlesList(muzzles);
+ return muzzles.IsEmpty();
+ }
protected static void AddWeapon(BaseWeaponComponent weapon, array<IEntity> allowed, array<BaseWeaponComponent> 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<Mount*>(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<Mount*>(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<Weapon*> mounts;Entity* GetParent(){return parent;}void FindMounts(Array<Weapon*>&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<Entity> parents(count),dummies(count);std::vector<Mount> mounts(count);std::vector<Slot> slots(count);std::vector<Weapon> inert(count);
+ root.child=&parents[0];for(int i=0;i<count;i++){if(i+1<count)parents[i].sibling=&parents[i+1];parents[i].child=&dummies[i];parents[i].mounts={&mounts[i]};dummies[i].parent=&parents[i];dummies[i].weapon=&inert[i];inert[i].owner=&dummies[i];
+ dummies[i].prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";mounts[i].owner=&parents[i];mounts[i].weapon=&dummies[i];mounts[i].slot=&slots[i];slots[i].mount=&mounts[i];slots[i].attached=&dummies[i];}
+ auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete==(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")}){