#86: destroyed weapons can no longer fire -- the fire gates read a

never-written cell (the split-cell gotcha)

Both weapon fire gates (ProjectileWeapon gate 1 @4bbd36, Emitter
hard-failure @4baab9) test the binary's subsystem+0x40 for Destroyed(1).
In the 1995 layout that offset sits INSIDE the embedded status alarm
(statusAlarm@0x2C + level@+0x14 = 0x40) -- ONE cell, written by
ForceCriticalFailure when a zone's crit cascade kills the subsystem.  The
port models the same address as TWO members: the AlarmIndicator AND a
plain int simulationState@0x40.  Every destruction path writes the ALARM
(so the MFD draws its X correctly) while the gates read the plain int,
which nothing ever writes -- so a weapon on a blown-off arm showed
destroyed on every panel and kept firing and scoring, locally and on
peers (night-7: all three testers, screenshots of a missile leaving a
destroyed pod).

The tell had been sitting in our own logs for weeks:
  [ammo] SRM6_1 -> NoAmmo (gate1): destroyed=0 ...
on a mech whose launcher was X'd out.

Fix: both gates now read statusAlarm.GetLevel()==1 as well as the int.
Also added: the crit-cascade log names the destroyed subsystem, a
BT_SELF_DAMAGE_ZONE=dz_* named-zone bench mode, and BT_KILL_SUBSYS=<name>
(force ForceCriticalFailure on one named subsystem -- the exact call the
zone cascade makes, so a bench can ask 'the panel says dead, does it
still shoot?' without hunting for the zone that carries a given weapon).

Verified A/B in ONE run: before the kill both SRM6 launchers fired 2
salvos each; after, the destroyed launcher fired ZERO (gate log
destroyed=1) while its twin kept firing normally.

KB: new gotcha #22 (the SPLIT CELL -- one binary offset, two port
members, only one written; sibling of #1) + combat-damage entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
This commit is contained in:
Joe DiPrima
2026-07-31 12:11:09 -05:00
co-authored by Claude Fable 5
parent 62fc8409b6
commit 5410371b0c
6 changed files with 86 additions and 2 deletions
+38
View File
@@ -6035,6 +6035,14 @@ void
if (zz != 0 && zz->IsLegZone()) { sdZone = zi; break; }
}
}
else if (ze[0] == 'd' && ze[1] == 'z')
{
// #86 bench: a NAMED zone (e.g. dz_rarm) resolved via
// the engine's own name->index map -- lets a bench
// destroy a specific ARM and watch its weapons brick.
int byName = Entity::GetDamageZoneIndex(CString(ze));
if (byName >= 0) sdZone = byName;
}
else
sdZone = atoi(ze);
}
@@ -6052,6 +6060,36 @@ void
}
}
// #86 BENCH (BT_KILL_SUBSYS=<name>): force the AUTHENTIC destruction of
// one named subsystem -- MechSubsystem::ForceCriticalFailure, the exact
// call the zone crit-cascade (SendSubsystemDamage) makes, and the thing
// that raises the MFD's X. Lets a bench answer "the panel says it's
// dead -- does it still shoot?" without hunting for the zone that
// happens to carry a given weapon (per-mech, and dz_* names vary).
// Env-gated, one-shot, viewpoint mech only.
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_KILL_SUBSYS"))
{
static int s_ksFrame = 0;
if (++s_ksFrame == 900) // ~15 s in, after the mech settles
{
const char *want = getenv("BT_KILL_SUBSYS");
for (int i = 2; i < GetSubsystemCount(); ++i)
{
Subsystem *s = GetSubsystem(i);
if (s == 0 || s->GetName() == 0) continue;
if (stricmp(s->GetName(), want) != 0) continue;
if (!s->IsDerivedFrom(MechSubsystem::ClassDerivations)) continue;
((MechSubsystem *)s)->SetSubsystemDamageLevel(1.0f);
((MechSubsystem *)s)->ForceCriticalFailure();
DEBUG_STREAM << "[killsub] '" << s->GetName()
<< "' force-destroyed (statusAlarm=1, zone=1.0)"
<< std::endl << std::flush;
break;
}
}
}
// task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at
// frame ~600 -- Condenser1's valveState cycles 1 -> 5, so the flow
// redistribution gives it 5/10 of the total coolant flow ([valve] log