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:
co-authored by
Claude Fable 5
parent
e1ae92264a
commit
e996be249d
@@ -466,6 +466,7 @@ const Receiver::HandlerEntry
|
||||
MESSAGE_ENTRY(Mech, PlayerLink),
|
||||
MESSAGE_ENTRY(Mech, BalanceCoolant), // id 0x16 @0049f728 (issue #20)
|
||||
MESSAGE_ENTRY(Mech, DuckRequest), // id 0x1a @0049fa00 (CROUCH, 2026-07-26)
|
||||
MESSAGE_ENTRY(Mech, EjectPilot), // id 0x19 @0049f854 (PANIC/EJECT, 2026-08-02)
|
||||
};
|
||||
|
||||
//
|
||||
@@ -513,6 +514,125 @@ void
|
||||
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1" << std::endl << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// @0049fa1c -- the EJECT-PERMISSION evaluator (writes ejectPermitted @0x414).
|
||||
// Raw decomp, transcribed clause for clause: walk the roster from index 2 --
|
||||
// classID 0xBBE (HeatSinkBank): coolant fraction = coolantLevel/thermalCapacity
|
||||
// classID 0xBC1 (Generator): count live (not destroyed && state != 4)
|
||||
// MechWeapon-derived: count live (not destroyed; ammo-fed weapons
|
||||
// additionally need weaponAlarm != 7 = NoAmmo)
|
||||
// then permitted = weapons < ejectMinWeapons || generators == 0
|
||||
// || coolant < 0.05 (@0049fb50)
|
||||
// || (MovementMode 3/4 [leg-gimped] && player simLive == 0).
|
||||
// The binary refreshes this per frame from an UNEXPORTED caller; the port
|
||||
// evaluates on demand (the handler + any future panic-lamp consumer).
|
||||
//
|
||||
int
|
||||
Mech::EvaluateEjectPermission()
|
||||
{
|
||||
extern int BTWeaponCountsForEject(Subsystem *sub); // projweap.cpp (-1 = not a weapon)
|
||||
extern int BTGeneratorCountsForEject(Subsystem *sub); // powersub.cpp (-1 = not a generator)
|
||||
extern int BTHeatSinkBankCoolantFraction(Subsystem *sub, Scalar *out); // heat.cpp
|
||||
|
||||
int liveWeapons = 0;
|
||||
int liveGenerators = 0;
|
||||
Scalar coolantFrac = 0.0f; // local_10 (0 when no bank streams)
|
||||
|
||||
for (int i = 2; i < subsystemCount; ++i) // binary: from index 2
|
||||
{
|
||||
Subsystem *s = (Subsystem *)subsystemArray[i];
|
||||
if (s == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Scalar frac;
|
||||
if (BTHeatSinkBankCoolantFraction(s, &frac))
|
||||
{
|
||||
coolantFrac = frac;
|
||||
}
|
||||
else
|
||||
{
|
||||
int g = BTGeneratorCountsForEject(s);
|
||||
if (g >= 0)
|
||||
{
|
||||
liveGenerators += g;
|
||||
}
|
||||
else
|
||||
{
|
||||
int w = BTWeaponCountsForEject(s);
|
||||
if (w >= 0)
|
||||
{
|
||||
liveWeapons += w;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int gimped = (MovementMode() == 3 || MovementMode() == 4); // mech+0x40 - 3U < 2
|
||||
extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (+0x25c;
|
||||
int noviceSim = (BTPlayerExperienceSimLive(this) == 0); // NULL-player reads LIVE)
|
||||
|
||||
ejectPermitted =
|
||||
(liveWeapons < ejectMinWeapons) // @0x448 floor (inert at 0)
|
||||
|| (liveGenerators == 0)
|
||||
|| (coolantFrac < 0.05f) // _DAT_0049fb50
|
||||
|| (gimped && noviceSim);
|
||||
return ejectPermitted;
|
||||
}
|
||||
|
||||
//
|
||||
// @0049f854 -- EjectPilot (id 0x19): the cockpit PANIC/EJECT punch-out.
|
||||
// See the header comment (mech.hpp) for the byte-level story.
|
||||
//
|
||||
void
|
||||
Mech::EjectPilotMessageHandler(ReceiverDataMessageOf<int> *message)
|
||||
{
|
||||
if (message->dataContents <= 0) // press only (msg+0xc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (EvaluateEjectPermission() == 0) // @0x414 gate: healthy mechs refuse
|
||||
{
|
||||
DEBUG_STREAM << "[eject] " << GetEntityID()
|
||||
<< " REFUSED (mech not crippled enough)" << std::endl << std::flush;
|
||||
return;
|
||||
}
|
||||
if (IsDisabled()) // @0049fb54 -- no ejecting from a wreck
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Binary @0049f88e-0x49f8cf: build the console eject notice (FUN_004c198c
|
||||
// from ownerID @0x18c) and send it to the console host. CORE: forensic
|
||||
// log; the relay wire is the tracked tail. The suppressConsole latch set
|
||||
// below is the authentic other half (the following DEATH must not also
|
||||
// notify the console).
|
||||
extern Scalar BTPlayerEjectBookkeeping(void *player); // btplayer.cpp:
|
||||
void *player = GetPlayerLink(); // suppressConsole=1,
|
||||
Scalar charge = (player != 0) // returns role killBonus
|
||||
? BTPlayerEjectBookkeeping(player) : 0.0f;
|
||||
|
||||
DEBUG_STREAM << "[eject] " << GetEntityID()
|
||||
<< " PUNCH-OUT: charge=" << charge
|
||||
<< " (role killBonus)" << std::endl << std::flush;
|
||||
|
||||
graphicAlarm.SetLevel(10); // FUN_0041bbd8(this+0x2C, 0xA) -- EJECT state
|
||||
|
||||
Damage dmg; // FUN_0041db7c (Damage::Damage)
|
||||
dmg.damageType = (Enumeration)2; // Explosive
|
||||
dmg.damageAmount = charge; // role+0x1c (killBonus)
|
||||
dmg.impactPoint = localOrigin.linearPosition; // binary copies this+0x100
|
||||
dmg.burstCount = 1; // binary writes 0; our victim-side
|
||||
// guard clamps 0->1 -- same outcome
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||
GetEntityID(), // inflicting = SELF (this+0x184)
|
||||
-1, // unaimed -> cylinder resolves
|
||||
dmg,
|
||||
-1); // inflictingSubsystemID = -1
|
||||
Dispatch(&take_damage); // vtbl+0xC self-dispatch
|
||||
}
|
||||
|
||||
Receiver::MessageHandlerSet
|
||||
Mech::MessageHandlers(
|
||||
ELEMENTS(Mech::MessageHandlerEntries),
|
||||
@@ -1249,6 +1369,8 @@ Mech::Mech(
|
||||
mechNameFilter.Initialize(); // FUN_00435a7c(this+0xdb)
|
||||
masterAlarm = AlarmIndicator(0x21); // FUN_0041b9ec(this+0xe7,0x21)
|
||||
rearFiring = 0; // (task #68) ORed from the weapons below
|
||||
ejectPermitted = 0; // @0x414 (refreshed by EvaluateEjectPermission)
|
||||
ejectMinWeapons = 0; // @0x448 (no exported writer -- zero, clause inert [T3])
|
||||
// (F7 correction) the binary's 0x400 = FLT_MAX init is DistanceToMissile's
|
||||
// "no missile" far default (attr id 56), NOT a maxSpeed -- the old member
|
||||
// is retired; distanceToMissile (init below) owns the slot's meaning.
|
||||
|
||||
Reference in New Issue
Block a user