Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
131 lines
9.6 KiB
Diff
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")}){
|