Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
282 lines
10 KiB
C
282 lines
10 KiB
C
// 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<IEntity> 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<IEntity> allowed, array<BaseWeaponComponent> 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<IEntity> allowed, array<BaseWeaponComponent> 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<IEntity> 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<BaseWeaponComponent> 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<IEntity> allowed = {};
|
|
array<BaseWeaponComponent> weapons = {};
|
|
Collect(root, result, allowed, weapons);
|
|
result.Weapons = weapons.Count();
|
|
array<BaseMuzzleComponent> seenMuzzles = {};
|
|
array<BaseMagazineComponent> seenMagazines = {};
|
|
array<IEntity> magazineOwners = {};
|
|
foreach (BaseWeaponComponent weapon : weapons)
|
|
{
|
|
array<BaseMuzzleComponent> 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;
|
|
}
|
|
}
|