Files
EscapistsReintroduced/docs/evidence/mounted-traversal-2026-10-01/fix-review/structural-cap-removed.c
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

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;
}
}