EJECT/PANIC wired: Mech::EjectPilot (id 0x19 @0049f854) + the crippled-mech gate

The button died twice before reaching game logic: no handler (0x19 was
unregistered) and no sender (the pod's panic was a control bit, not a
mappable). Both halves reconstructed from raw disasm -- the handler AND
its permission evaluator sat in export gaps.

@0049f854 EjectPilot: press-only; gated on ejectPermitted (@0x414) and
!IsDisabled; console eject notice (relay wire = tracked tail; the
suppressConsole@0x258 latch that prevents the death double-notify IS
wired); graphicAlarm -> 10, which kills via the >=9 predicate; then a
self TakeDamage {inflicting=SELF, zone -1, Explosive, amount =
ScenarioRole::killBonus}. Role layout byte-settled via the role reader
@00429bec dest offsets + the ctor record copy: +0x1c IS killBonus, +0x20
is the 4.10-only SpecialCaseDeathPenalty, +0x28 returnFromDeath (all
prior citations reconciled). KillBonus authors NOWHERE in shipped
content -> the charge is 0 and the ALARM does the killing: our bench
outcome is the pod outcome.

@0049fa1c EvaluateEjectPermission: eject only from a CRIPPLED mech --
bank coolant fraction < 0.05 | zero live generators | live weapons
below mech+0x448 (no exported writer: zero, clause inert) | leg-gimped
novice. A healthy mech REFUSES the button; no free resets.

Input: binding-engine "Eject" action -- Backspace / pad LeftThumb
(default profile + shipped CONTROLS.MAP). Bridges per the databinding
rule: weapon/generator/bank/player reads land in their complete TUs;
MechSubsystem gains the both-cells destroyed accessors (gotcha #22).
Bench scalpels: BT_EJECT_AT=<frame> (path-identical synthetic press),
BT_KILL_SUBSYS now takes a comma list.

Verified single-node: healthy press REFUSED (x2), four generators
killed, next press PUNCH-OUT -> death, wreck smoke, respawn; the
respawned mech refuses again (permission re-evaluates after Reset).
Death rides the normal damage/death chain, so MP replication is the
proven path. Tails tracked: console relay notice, RIO 0x38 panic
control, alarm-10 eject audio/canopy, SpecialCaseDeathPenalty consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-02 16:45:54 -05:00
co-authored by Claude Fable 5
parent e1ae92264a
commit e996be249d
16 changed files with 345 additions and 14 deletions
+53 -12
View File
@@ -3227,6 +3227,39 @@ void
sVolDn = dn;
sVolUp = up;
}
// PANIC/EJECT (edge): dispatch Mech msg 0x19 (@0049f854) at
// ourselves -- the handler owns every gate (crippled-mech
// permission @0x414, IsDisabled), so a stray press on a
// healthy mech is a logged no-op.
{
static int sEjectHeld = 0;
int ejectPress = gBTInput.eject && !sEjectHeld;
sEjectHeld = gBTInput.eject;
// BENCH (BT_EJECT_AT=<frame>): synthesize the SAME press
// at that frame and every 300 after -- path-identical to
// the key (one message, one dispatch), so a refused-then-
// permitted sequence is provable in a single headless run.
{
static int sEjFrame = 0;
static int sEjAt = -2;
if (sEjAt == -2)
{
const char *e = getenv("BT_EJECT_AT");
sEjAt = (e && *e) ? atoi(e) : -1;
}
++sEjFrame;
if (sEjAt > 0 && sEjFrame >= sEjAt
&& ((sEjFrame - sEjAt) % 300) == 0)
ejectPress = 1;
}
if (ejectPress)
{
ReceiverDataMessageOf<int> punch_out(
Mech::EjectPilotMessageID,
sizeof(ReceiverDataMessageOf<int>), 1);
Dispatch(&punch_out);
}
}
// task #13: the Valve action cycles the coolant valve.
gBTValveKey = gBTInput.valve;
// Gitea #7: the Flush action HELD = the coolant MFD's
@@ -6198,19 +6231,27 @@ void
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)
// COMMA-LIST (eject bench): "GeneratorA,GeneratorB,..." kills
// each named subsystem; single-name behavior unchanged.
char want_list[256];
strncpy(want_list, getenv("BT_KILL_SUBSYS"), sizeof(want_list) - 1);
want_list[sizeof(want_list) - 1] = 0;
for (char *want = strtok(want_list, ","); want != 0;
want = strtok(0, ","))
{
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;
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;
}
}
}
}