--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c @@ -40,6 +40,51 @@ string Detail; string FirstPrefab; int FirstPurpose = -1; + // Called before Reject, once per reason in this explicit diagnostic sample. + // These labels describe sampled relationships; they never authorize a weapon. + void SlotEvidence(int reason, IEntity entity, WeaponSlotComponent mount, InventoryStorageSlot binding, InventoryItemComponent item, array allowed) + { + if ((Reasons & reason) != 0) return; + if (reason == 64) Detail += "mount-slot-evidence:"; + else Detail += "zero-muzzle-slot-evidence:"; + if (item) Detail += "item=present;"; + else Detail += "item=absent;"; + InventoryStorageSlot parent = null; + if (item) parent = item.GetParentSlot(); + if (!parent) Detail += "parent=null;"; + else if (parent == binding) Detail += "parent=same-binding;"; + else Detail += "parent=different-binding;"; + if (!binding) Detail += "binding=null;"; + else + { + if (binding.GetAttachedEntity() == entity) Detail += "binding-attached=exact;"; + else Detail += "binding-attached=other-or-null;"; + if (binding.GetParentContainer() == mount) Detail += "binding-container=exact-mount;"; + else Detail += "binding-container=other-or-null;"; + } + IEntity owner = null; + if (mount) owner = mount.GetOwner(); + if (owner && allowed.Contains(owner)) Detail += "mount-owner=allowed;"; + else Detail += "mount-owner=absent-or-foreign;"; + if (owner && owner.GetPrefabData()) Detail += "mount-prefab=" + owner.GetPrefabData().GetPrefabName() + ";"; + if (!parent) return; + if (parent.GetAttachedEntity() == entity) Detail += "parent-attached=exact;"; + else Detail += "parent-attached=other-or-null;"; + WeaponSlotComponent parentMount = WeaponSlotComponent.Cast(parent.GetParentContainer()); + if (parent.GetParentContainer() == mount && mount) Detail += "parent-container=exact-mount;"; + else if (parentMount) Detail += "parent-container=other-mount;"; + else if (!parent.GetParentContainer()) Detail += "parent-container=null;"; + else Detail += "parent-container=non-mount;"; + if (!parentMount) return; + IEntity parentOwner = parentMount.GetOwner(); + if (parentOwner && allowed.Contains(parentOwner)) Detail += "parent-mount-owner=allowed;"; + else Detail += "parent-mount-owner=absent-or-foreign;"; + if (parentOwner && parentOwner.GetPrefabData()) Detail += "parent-mount-prefab=" + parentOwner.GetPrefabData().GetPrefabName() + ";"; + if (parentMount.GetSlotInfo() == parent) Detail += "parent-mount-slot=exact;"; + else Detail += "parent-mount-slot=other-or-null;"; + if (parentMount.GetWeaponEntity() == entity) Detail += "parent-mount-weapon=exact;"; + else Detail += "parent-mount-weapon=other-or-null;"; + } void Reject(int reason, string detail, IEntity entity, int purpose = -1) { if ((Reasons & reason) == 0) Detail += detail + ";"; @@ -104,7 +149,11 @@ if (!mountedItem || mountedItem.GetParentSlot() != binding) { #ifdef WORKBENCH - if (s_Diagnostic) s_Diagnostic.Reject(64, "mount-item-slot-mismatch", mountedEntity); + if (s_Diagnostic) + { + s_Diagnostic.SlotEvidence(64, mountedEntity, mount, binding, mountedItem, allowed); + s_Diagnostic.Reject(64, "mount-item-slot-mismatch", mountedEntity); + } #endif result.Complete = false; return; } @@ -272,7 +321,23 @@ if (muzzles.IsEmpty()) { if (NonFiringPlaceholder(weapon, allowed)) result.Weapons--; - else result.Complete = false; + else + { +#ifdef WORKBENCH + if (s_Diagnostic) + { + IEntity owner = weapon.GetOwner(); + InventoryItemComponent item = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent)); + WeaponSlotComponent mount = null; + InventoryStorageSlot binding = null; + if (item && item.GetParentSlot()) mount = WeaponSlotComponent.Cast(item.GetParentSlot().GetParentContainer()); + if (mount) binding = mount.GetSlotInfo(); + s_Diagnostic.SlotEvidence(4096, owner, mount, binding, item, allowed); + s_Diagnostic.Reject(4096, "zero-muzzle-not-proven-placeholder", owner); + } +#endif + result.Complete = false; + } continue; } if (muzzles.Count() > 8) { result.Complete = false; continue; } --- a/tests/test_hq_mounted_ammo_sampler.py +++ b/tests/test_hq_mounted_ammo_sampler.py @@ -15,6 +15,8 @@ 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('WeaponSlotComponent.Cast(parent.GetParentContainer())','parent->mount').replace('WeaponSlotComponent.Cast(item.GetParentSlot().GetParentContainer())','item->GetParentSlot()->mount') +code=code.replace('parent.GetParentContainer()', 'parent->Container()') code=code.replace('EStoragePurpose.','EStoragePurpose::').replace('GetParentSlot().','GetParentSlot()->') code=code.replace('ChimeraCharacter.Cast(entity) != nullptr','entity->character').replace('ChimeraCharacter.Cast(entity) != null','entity->character') code=code.replace('ChimeraCharacter.Cast(root)','root->character').replace('ChimeraCharacter.Cast(entity)','entity->character') @@ -22,10 +24,11 @@ code=re.sub(r'\bERR_HQMountedAmmoDiagnostic (?=\w)', 'ERR_HQMountedAmmoDiagnostic* ',code) code=code.replace('s_Diagnostic.', 's_Diagnostic->').replace('GetPrefabData().', 'GetPrefabData()->') code=re.sub(r'\bERR_HQMountedAmmoSnapshot (?=\w)','ERR_HQMountedAmmoSnapshot* ',code) -code=code.replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr') +code=code.replace('array<','Array<').replace('foreach (','for (') +code=re.sub(r'"[^"]*"|\bnull\b',lambda m:m.group() if m.group().startswith(chr(34)) else 'nullptr',code) code=code.replace('Array allowed, Array weapons,','Array& allowed, Array& weapons,') code=code.replace('Array allowed, Array weapons)', 'Array& allowed, Array& weapons)') -for name in ('copy','result','weapon','root','entity','child','part','manager','mounted','muzzle','magazine','item','storage','mount','mountedEntity','mountedItem','owner'): +for name in ('copy','result','weapon','root','entity','child','part','manager','mounted','muzzle','magazine','item','storage','mount','mountedEntity','mountedItem','owner','parent','binding','parentMount','parentOwner'): 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''' @@ -41,7 +44,7 @@ struct Entity;struct Weapon;struct Muzzle;struct Magazine;struct Manager;struct Mount; struct EStoragePurpose{static const int PURPOSE_UNSET=0,PURPOSE_DEPOSIT=1,PURPOSE_WEAPON_PROXY=2,PURPOSE_ATTACHMENT_PROXY=4,PURPOSE_ANY=127;}; struct Storage{int purpose=EStoragePurpose::PURPOSE_DEPOSIT;int GetPurpose(){return purpose;}}; -struct Slot{Mount* mount=nullptr;Muzzle* muzzle=nullptr;Storage* storage=nullptr;Storage* GetStorage(){return storage;}}; +struct Slot{Mount* mount=nullptr;Muzzle* muzzle=nullptr;Storage* storage=nullptr;Entity* attached=nullptr;Entity* GetAttachedEntity(){return attached;}void* Container(){return mount?static_cast(mount):muzzle?static_cast(muzzle):static_cast(storage);}Storage* GetStorage(){return storage;}}; struct Item{Slot* slot=nullptr;Slot* GetParentSlot(){return slot;}}; using InventoryStorageSlot=Slot*;using WeaponSlotComponent=Mount*; using InventoryItemComponent=Item*;using BaseInventoryStorageComponent=Storage*; @@ -259,6 +262,26 @@ # WORKBENCH records are opt-in. Re-run every original geometry/count assertion # in that build too, plus exact failure witnesses from the same traversal. cases=r''' + // Exact reason64 distinguishes absent item, null slot and differing slot, + // while actual Inspect remains incomplete and retains the same ammunition. + for(int fault=0;fault<3;fault++){ + Rig r;Mount mount;Item item;Slot binding,other;Storage storage;storage.purpose=EStoragePurpose::PURPOSE_ATTACHMENT_PROXY;other.storage=&storage; + mount.owner=&r.root;mount.weapon=&r.gun;mount.slot=&binding;binding.mount=&mount;binding.attached=&r.gun; + r.manager.weapons={&mount};r.gun.item=fault==0?nullptr:&item; + if(fault==2)item.slot=&other; + other.mount=&mount;other.attached=&r.gun; + ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic(); + assert(!snap->Complete&&snap->Weapons==1&&snap->Rounds==25&&(d->Reasons&64)); + assert(d->Detail.find(fault==0?"item=absent;":"item=present;")!=string::npos); + assert(d->Detail.find(fault==2?"parent=different-binding;":"parent=null;")!=string::npos); + assert(d->Detail.find("binding-attached=exact;")!=string::npos&&d->Detail.find("binding-container=exact-mount;")!=string::npos); + assert(d->Detail.find("mount-owner=allowed;")!=string::npos&&d->Detail.find("mount-prefab=fixture;")!=string::npos); + if(fault==2)assert(d->Detail.find("parent-mount-slot=other-or-null;")!=string::npos); + } + {Rig r;r.weapon.muzzles={};ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic(); + assert(!snap->Complete&&snap->Weapons==1&&snap->Rounds==0&&(d->Reasons&4096)); + assert(d->Detail.find("zero-muzzle-not-proven-placeholder;")!=string::npos&&d->Detail.find("zero-muzzle-slot-evidence:item=absent;parent=null;")!=string::npos);} +''' + r''' {Rig r;ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(snap->Complete&&d->Reasons==0&&d->Nodes==4&&d->Allowed==3&&d->Managers==1);assert(!ERR_HQMountedAmmoSampler::EndDiagnostic());} {Entity root;root.child=&root;ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=ERR_HQMountedAmmoSampler::Inspect(&root);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&d->Reasons==4&&d->Nodes==1&&d->Allowed==1&&root.reads==1);} {Entity root;std::vector children(128);root.child=&children[0];for(int i=0;i<127;i++)children[i].sibling=&children[i+1];ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=ERR_HQMountedAmmoSampler::Inspect(&root);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&d->Reasons==1024&&d->Nodes==129&&d->Allowed==128&&d->Structural==129&&d->Cargo==0&&children[127].reads==0);}