// Detached mounted counts only. Carried and cargo ammunition never enter these totals. class ERR_HQMountedAmmoSnapshot { bool Complete; int Weapons; int Muzzles; int Magazines; int Rounds; int Capacity; int Chambers; int LowMagazines; ERR_HQMountedAmmoSnapshot Snapshot() { ERR_HQMountedAmmoSnapshot copy = new ERR_HQMountedAmmoSnapshot(); copy.Complete = Complete; copy.Weapons = Weapons; copy.Muzzles = Muzzles; copy.Magazines = Magazines; copy.Rounds = Rounds; copy.Capacity = Capacity; copy.Chambers = Chambers; copy.LowMagazines = LowMagazines; return copy; } bool Valid() { if (Weapons < 0 || Weapons > 32 || Muzzles < 0 || Muzzles > 128 || Muzzles > Weapons * 8) return false; if (Magazines < 0 || Magazines > Muzzles || Magazines > 128) return false; if (Capacity < Magazines || Capacity > Magazines * 100000 || Rounds < 0 || Rounds > Capacity) return false; if (Chambers < 0 || Chambers > Muzzles * 32 || LowMagazines < 0 || LowMagazines > Magazines) return false; return !Complete || (Weapons == 0 || (Muzzles > 0 && Magazines > 0)); } } #ifdef WORKBENCH // Ephemeral opt-in fixture diagnostics; never part of radio reports or policy. class ERR_HQMountedAmmoDiagnostic { int Reasons; int Nodes; int Structural; int Cargo; int Allowed; int Managers; string Detail; string FirstPrefab; int FirstPurpose = -1; void Reject(int reason, string detail, IEntity entity, int purpose = -1) { if ((Reasons & reason) == 0) Detail += detail + ";"; if (Reasons == 0) { FirstPurpose = purpose; if (entity && entity.GetPrefabData()) FirstPrefab = entity.GetPrefabData().GetPrefabName(); } Reasons = Reasons | reason; } } #endif class ERR_HQMountedAmmoSampler { #ifdef WORKBENCH protected static ref ERR_HQMountedAmmoDiagnostic s_Diagnostic; static void BeginDiagnostic() { s_Diagnostic = new ERR_HQMountedAmmoDiagnostic(); } static ERR_HQMountedAmmoDiagnostic EndDiagnostic() { ref ERR_HQMountedAmmoDiagnostic result = s_Diagnostic; s_Diagnostic = null; return result; } #endif // Native mounted weapons and loaded magazines use InventoryStorageSlot too, // but their container is a weapon slot/muzzle rather than deposit storage. static bool Installed(IEntity entity, InventoryStorageSlot slot, array allowed) { if (!entity || !slot) return false; WeaponSlotComponent mount = WeaponSlotComponent.Cast(slot.GetParentContainer()); if (mount && allowed.Contains(mount.GetOwner()) && mount.GetSlotInfo() == slot && mount.GetWeaponEntity() == entity) return true; BaseMuzzleComponent muzzle = BaseMuzzleComponent.Cast(slot.GetParentContainer()); if (!muzzle || !allowed.Contains(muzzle.GetOwner())) return false; BaseMagazineComponent magazine = muzzle.GetMagazine(); return magazine && magazine.GetOwner() == entity; } protected static void AddWeapon(BaseWeaponComponent weapon, array allowed, array weapons, ERR_HQMountedAmmoSnapshot result) { if (!weapon || !weapon.GetOwner() || !allowed.Contains(weapon.GetOwner())) { #ifdef WORKBENCH IEntity owner = null; if (weapon) owner = weapon.GetOwner(); if (s_Diagnostic) s_Diagnostic.Reject(16, "weapon-owner-not-allowed", owner); #endif result.Complete = false; return; } // Native managers can expose both a slot and its physical weapon. Normalize // one exact current installation before deduplication, never another slot. WeaponSlotComponent mount = WeaponSlotComponent.Cast(weapon); if (mount) { IEntity mountedEntity = mount.GetWeaponEntity(); InventoryStorageSlot binding = mount.GetSlotInfo(); if (!mountedEntity || !allowed.Contains(mountedEntity) || !binding || binding.GetParentContainer() != mount) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(32, "mount-binding-invalid", mountedEntity); #endif result.Complete = false; return; } InventoryItemComponent mountedItem = InventoryItemComponent.Cast(mountedEntity.FindComponent(InventoryItemComponent)); if (!mountedItem || mountedItem.GetParentSlot() != binding) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(64, "mount-item-slot-mismatch", mountedEntity); #endif result.Complete = false; return; } weapon = BaseWeaponComponent.Cast(mountedEntity.FindComponent(BaseWeaponComponent)); if (!weapon || WeaponSlotComponent.Cast(weapon) || weapon.GetOwner() != mountedEntity) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(128, "mount-weapon-invalid", mountedEntity); #endif result.Complete = false; return; } } if (weapons.Contains(weapon)) return; if (weapons.Count() >= 32) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(256, "weapon-limit32", weapon.GetOwner()); #endif result.Complete = false; return; } weapons.Insert(weapon); } protected static void Collect(IEntity root, ERR_HQMountedAmmoSnapshot result, array allowed, array weapons) { if (!root || ChimeraCharacter.Cast(root)) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(1, "invalid-root", root); #endif result.Complete = false; return; } result.Complete = true; array nodes = {root}; // Known deposit cargo must not consume the mounted hierarchy budget. Both // categories retain independent caps; the unique queue is capped at their sum. // Skipped crew/unknown nodes remain structural and cannot borrow cargo space. int structuralNodes = 0; int cargoNodes = 0; for (int cursor = 0; cursor < nodes.Count(); cursor++) { IEntity entity = nodes[cursor]; bool character = ChimeraCharacter.Cast(entity) != null; bool excludedCargo; bool unknownStorage; // Exact installed slots are checked before deposit exclusion. Never walk // the children of known cargo or seated character roots. if (!character) { InventoryItemComponent item = InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent)); if (item && item.GetParentSlot() && !Installed(entity, item.GetParentSlot(), allowed)) { BaseInventoryStorageComponent storage = item.GetParentSlot().GetStorage(); if (!storage || storage.GetPurpose() == EStoragePurpose.PURPOSE_UNSET || storage.GetPurpose() == EStoragePurpose.PURPOSE_ANY) { #ifdef WORKBENCH int purpose = -1; if (storage) purpose = storage.GetPurpose(); if (s_Diagnostic) s_Diagnostic.Reject(2, "ambiguous-storage", entity, purpose); #endif result.Complete = false; unknownStorage = true; } else excludedCargo = (storage.GetPurpose() & EStoragePurpose.PURPOSE_DEPOSIT) != 0; } } if (excludedCargo) { cargoNodes++; if (cargoNodes > 128) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(2048, "cargo-limit128", entity); #endif result.Complete = false; break; } continue; } structuralNodes++; if (structuralNodes > 256) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(1024, "structural-limit128", entity); #endif result.Complete = false; break; } if (character || unknownStorage) continue; allowed.Insert(entity); IEntity child = entity.GetChildren(); while (child) { bool repeated = nodes.Contains(child); if (repeated || nodes.Count() >= 256) { #ifdef WORKBENCH if (s_Diagnostic) { if (repeated) s_Diagnostic.Reject(4, "repeated-node", child); else s_Diagnostic.Reject(8, "queue-limit256", child); } #endif result.Complete = false; break; } nodes.Insert(child); child = child.GetSibling(); } } #ifdef WORKBENCH if (s_Diagnostic) { s_Diagnostic.Nodes = nodes.Count(); s_Diagnostic.Allowed = allowed.Count(); s_Diagnostic.Structural = structuralNodes; s_Diagnostic.Cargo = cargoNodes; } #endif foreach (IEntity part : allowed) { BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(part.FindComponent(BaseWeaponManagerComponent)); if (manager) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Managers++; #endif array listed = {}; manager.GetWeapons(listed); if (listed.Count() > 8) { #ifdef WORKBENCH if (s_Diagnostic) s_Diagnostic.Reject(512, "manager-limit8", part); #endif result.Complete = false; } else foreach (BaseWeaponComponent entry : listed) AddWeapon(entry, allowed, weapons, result); } BaseWeaponComponent mounted = BaseWeaponComponent.Cast(part.FindComponent(BaseWeaponComponent)); if (mounted) AddWeapon(mounted, allowed, weapons, result); } } static ERR_HQMountedAmmoSnapshot Inspect(IEntity root) { ERR_HQMountedAmmoSnapshot result = new ERR_HQMountedAmmoSnapshot(); array allowed = {}; array weapons = {}; Collect(root, result, allowed, weapons); result.Weapons = weapons.Count(); array seenMuzzles = {}; array seenMagazines = {}; array magazineOwners = {}; foreach (BaseWeaponComponent weapon : weapons) { array muzzles = {}; weapon.GetMuzzlesList(muzzles); if (muzzles.IsEmpty() || muzzles.Count() > 8) { result.Complete = false; continue; } foreach (BaseMuzzleComponent muzzle : muzzles) { if (!muzzle || !muzzle.GetOwner() || !allowed.Contains(muzzle.GetOwner())) { result.Complete = false; continue; } if (seenMuzzles.Contains(muzzle)) continue; if (seenMuzzles.Count() >= 128) { result.Complete = false; continue; } seenMuzzles.Insert(muzzle); result.Muzzles++; int barrels = muzzle.GetBarrelsCount(); if (barrels < 1 || barrels > 32) result.Complete = false; else for (int barrel = 0; barrel < barrels; barrel++) if (muzzle.IsBarrelChambered(barrel)) result.Chambers++; BaseMagazineComponent magazine = muzzle.GetMagazine(); // Projectile-only weapons and absent magazines are unknown, never empty. if (!magazine || !magazine.GetOwner() || !allowed.Contains(magazine.GetOwner())) { result.Complete = false; continue; } if (seenMagazines.Contains(magazine)) continue; if (magazineOwners.Contains(magazine.GetOwner())) { result.Complete = false; continue; } seenMagazines.Insert(magazine); magazineOwners.Insert(magazine.GetOwner()); int rounds = magazine.GetAmmoCount(); int capacity = magazine.GetMaxAmmoCount(); if (rounds < 0 || capacity < 1 || capacity > 100000 || rounds > capacity) { result.Complete = false; continue; } result.Magazines++; result.Rounds += rounds; result.Capacity += capacity; if (rounds <= capacity * 0.25) result.LowMagazines++; } } return result; } }