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
+6
View File
@@ -164,3 +164,9 @@ pad X action LookBehind
pad Start action ModeCycle
pad RightThumb action DisplayCycle
pad Back action Valve
# PANIC / EJECT -- the punch-out (Mech msg 0x19). Only fires on a CRIPPLED
# mech (no generators, coolant leaked below 5%, or leg-gimped novice) -- a
# healthy mech refuses the button, so a stray press is a no-op.
key Back action Eject
pad LeftThumb action Eject
+21
View File
@@ -319,6 +319,27 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
- **EJECT/PANIC cluster DECODED + WIRED 2026-08-02 [T1 raw disasm]:** `@0049f854`
**Mech::EjectPilot** (table id 0x19; sat in the 0x49f854-0x49fa00 export gap) — gates
{msg+0xc > 0, ejectPermitted@0x414, !IsDisabled@0049fb54}, console eject notice
(`FUN_004c198c` ctor from ownerID@0x18c → network sender, code 5 — port tail),
player `suppressConsole`@0x258 = 1 (the following death must not double-notify),
`graphicAlarm → 10` (EJECT state; **kills via the ≥9 predicate** — IsMechDestroyed),
self TakeDamageMessage {inflicting=SELF, zone 1, Explosive, amount =
`ScenarioRole::killBonus` role+0x1c, burst 0}. Role layout byte-settled via the
reader `@00429bec` dest offsets + ctor record copy: +0xc inflicted, +0x10 received,
+0x14 ffPenalty, +0x18 damageBias, **+0x1c killBonus**, **+0x20
SpecialCaseDeathPenalty (4.10-only key)**, +0x28 returnFromDeath. `KillBonus`
authors NOWHERE in shipped content → charge 0, alarm does the killing — authentic.
`@0049fa1c` **EvaluateEjectPermission** (writes @0x414; unexported caller): roster
walk — 0xBBE bank coolant fraction (+0x12c/+0x128) < 0.05 (`@0049fb50`) | zero live
Generators (0xBC1, +0x40 ≠ 1 && stateAlarm@0x210 ≠ 4) | live MechWeapons
(0x511830; ProjectileWeapon family 0x5121a8 also needs weaponAlarm@0x364 ≠ 7 NoAmmo)
< mech+0x448 (**no exported writer — zero, clause inert** [T3]) | leg-gimped
(mode 3/4) novice (simLive 0). Port: mech.cpp handler+evaluator, bridges
BTWeaponCountsForEject / BTGeneratorCountsForEject / BTHeatSinkBankCoolantFraction /
BTPlayerEjectBookkeeping; input = binding-engine `Eject` action (Backspace /
pad LeftThumb; BENCH `BT_EJECT_AT=<frame>`, `BT_KILL_SUBSYS` now comma-list).
- **THE SIM TIME MODEL — CLOSED 2026-08-02 (issue #96) [T1]:** the arcade
`Simulation::PerformAndWatch` is **`FUN_0041c018`** (part_002.c:5101):
`slice = (till.ticks this[4].ticks) / DAT_0052140c; this[4] = till;
+7 -2
View File
@@ -232,8 +232,13 @@ every checked control behavior:
follows the BINARY (house rule); resurrecting manual-4.0 supercharge would be an opt-in
deviation for the operator to decide.
NEW LEADS (manual describes, port lacks input/UI): **CROUCH** (button by the secondary screen;
`Mech::duckState` attr 0x37 + SQUAT clips exist, nothing drives them), **EJECT** (sounds exist:
EjectButton01_z*.wav), hot-box viewscreen framing (the deferred PNAME marker chain). Per-mech
`Mech::duckState` attr 0x37 + SQUAT clips exist, nothing drives them), ~~EJECT~~ **EJECT WIRED
2026-08-02** (core: `Mech::EjectPilotMessageHandler` id 0x19 @0049f854 + the crippled-mech
permission evaluator @0049fa1c; input = binding-engine "Eject" action, Backspace / pad
LeftThumb; the punch-out kills via graphicAlarm 10 ≥ 9 — KillBonus authors 0 in ALL shipped
content, so the zero charge is authentic; tails: console relay notice, RIO 0x38 panic control,
alarm-10 audio/canopy presentation, `SpecialCaseDeathPenalty` role+0x20 consumer), hot-box
viewscreen framing (the deferred PNAME marker chain). Per-mech
stat tables = a systematic cross-check source for our streamed subsystem resources.
### Coolant-loop cross-check RESULT (2026-07-18) [T1] -- STRUCTURE strongly faithful
+5
View File
@@ -82,6 +82,7 @@ enum BTActionID
BTActMfd1Cycle, // Gitea #9: cycle the lower-left MFD preset page
BTActMfd2Cycle, // Gitea #9: cycle the upper-center MFD preset page
BTActMfd3Cycle, // Gitea #9: cycle the lower-right MFD preset page
BTActEject, // PANIC/EJECT punch-out -> Mech msg 0x19 (@0049f854)
BTActCount
};
@@ -204,6 +205,7 @@ static const BTName sActionNames[] =
{"Reconnect", BTActReconnect},
{"Mfd1Cycle", BTActMfd1Cycle}, {"Mfd2Cycle", BTActMfd2Cycle},
{"Mfd3Cycle", BTActMfd3Cycle},
{"Eject", BTActEject},
{0, 0}
};
@@ -288,6 +290,8 @@ static const char *sDefaultProfile =
"key L action Mfd3Cycle\n"
"key V action ViewToggle\n"
"key B action LookBehind\n"
"key Back action Eject\n"
"pad LeftThumb action Eject\n"
"key F5 action Generator1\n"
"key F6 action Generator2\n"
"key F7 action Generator3\n"
@@ -991,6 +995,7 @@ void
case BTActMfd1Cycle: next.mfdCycle[0] = 1; break;
case BTActMfd2Cycle: next.mfdCycle[1] = 1; break;
case BTActMfd3Cycle: next.mfdCycle[2] = 1; break;
case BTActEject: next.eject = 1; break;
case BTActGenerator1: next.genSel = 4; break;
case BTActGenerator2: next.genSel = 5; break;
case BTActGenerator3: next.genSel = 6; break;
+1
View File
@@ -55,6 +55,7 @@ struct BTInputState
int valve;
int flush; // Gitea #7: coolant flush HELD (InjectCoolant)
int configHold;
int eject; // PANIC/EJECT punch-out (level; mech4 edge-dispatches Mech 0x19)
int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect
// Gitea #9: per-MFD preset-page cycle (desktop senders for SetPresetMode;
// index 0/1/2 = Mfd1 lower-left / Mfd2 upper-center / Mfd3 lower-right)
+28
View File
@@ -2388,3 +2388,31 @@ Entity *BTPlayerObjectiveMechOf(void *player)
return 0;
return (Entity *)((BTPlayer *)player)->GetObjectiveMech(); // @0x284
}
//#############################################################################
// BTPlayerEjectBookkeeping -- complete-type bridge for Mech::EjectPilot
// (@0049f854, the PANIC/EJECT punch-out). The handler's two player-side
// writes, verbatim from the raw disasm:
// * suppressConsole (+0x258) = 1 -- the eject already notified the console
// (its own message, the tracked relay tail), so the DEATH that the
// returned charge causes must not double-notify;
// * the self-destruct amount = ScenarioRole::killBonus (role+0x1c -- the
// record map is byte-verified: reader @00429bec dest offsets + the role
// ctor's record copy [7]=rec[0]).
// Returns the charge; 0 with no player/role (binary derefs unguarded).
//#############################################################################
Scalar BTPlayerEjectBookkeeping(void *player_v)
{
BTPlayer *player = (BTPlayer *)player_v;
if (player == 0)
{
return 0.0f;
}
player->suppressConsole = 1; // +0x258
const ScenarioRole *role = player->GetScenarioRole(); // +0x208
if (role == 0)
{
return 0.0f;
}
return role->GetKillBonus(); // role+0x1c
}
+1
View File
@@ -437,6 +437,7 @@ class DropZone__ReplyMessage;
friend int BTPlayerExperienceSimLive(void *); // +0x25c reader (issue #2): jams / lights / powersub short path
friend int BTPlayerExperienceHeatModelOn(void *); // +0x260 reader (issue #2): FUN_004ad7d4 heat-model gate
friend int BTPlayerAdvancedDamageOn(void *); // +0x268 reader (#83): FUN_0049ffcc collision-rattle gate
friend Scalar BTPlayerEjectBookkeeping(void *); // +0x258 latch + role killBonus (Mech::EjectPilot @0049f854)
// TARGET DESIGNATION (Gitea #48/#57): the mapper used to write the target
// RAW at `pilot + 0x284` -- the BINARY's objectiveMech offset, which on our
// compiled object is `deathPending` (the respawn latch). These bridges
+18
View File
@@ -1372,3 +1372,21 @@ Derivation *
{
return HeatSink::GetClassDerivations();
}
//===========================================================================//
// BTHeatSinkBankCoolantFraction -- complete-type bridge for the EJECT-
// permission evaluator (Mech::EvaluateEjectPermission @0049fa1c). The binary
// clause: classID 0xBBE (the AGGREGATE heat-sink bank -- the factory case
// carries the known "Sensor" mislabel; ctor-address truth per CLASSMAP rule),
// fraction = coolantLevel(@0x12C) / thermalCapacity(@0x128). Eject unlocks
// below 0.05 -- i.e. the mech has LEAKED nearly dry. Returns 0 = not the
// bank, else 1 with *out filled.
//===========================================================================//
int BTHeatSinkBankCoolantFraction(Subsystem *sub, Scalar *out)
{
if (sub == 0 || (int)sub->GetClassID() != 0xBBE)
return 0;
HeatSink *bank = (HeatSink *)sub;
*out = bank->CoolantFractionOf();
return 1;
}
+5
View File
@@ -567,6 +567,11 @@ inline int
Scalar filterDecay; // @0x150 init 0.15f (@004b8fec) -- ALSO the
// leak gauge's full-scale divisor (#97)
Scalar LeakGaugeFullScale() const { return filterDecay; }
// Eject evaluator (@0049fa1c): the bank's remaining-coolant fraction
// (@0x12C / @0x128). Capacity is authored nonzero (ctor init 1.0);
// the guard only shields a malformed stream from a NaN.
Scalar CoolantFractionOf() const
{ return (thermalCapacity > 0.0f) ? (coolantLevel / thermalCapacity) : 0.0f; }
Scalar thermalMass; // @0x154 resource +0xF4
Scalar heatEnergy; // @0x158 init = thermalMass * startingTemperature
Scalar coolantFlowScale; // @0x15C init 1.0f (== "word57")
+122
View File
@@ -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.
+32
View File
@@ -739,6 +739,15 @@ protected:
// `stateFlags`): OR of every mounted weapon's rearFiring flag -- "this
// mech carries a rear arsenal". The look-back view arms those weapons.
int rearFiring; // @0x410 this[0x104]
int ejectPermitted; // @0x414 EvaluateEjectPermission (@0049fa1c) result:
// 1 = crippled enough that the PANIC/EJECT
// punch-out is allowed (binary refreshes per
// frame from an unexported caller; the port
// evaluates on demand in the 0x19 handler)
int ejectMinWeapons; // @0x448 live-weapon floor for the evaluator's first
// clause. NO writer in the exported decomp --
// zero-filled in the binary, so the clause is
// inert there too [T3]; init 0 to match.
// (task #68) the authored look-view angles (model record +0x50..0x5c,
// ctor converts degrees->radians; binary @0x564..0x570 -- previously
// parked in the Wword scratch bank). Consumed by the look-state
@@ -1237,6 +1246,29 @@ protected:
enum { DuckRequestMessageID = 0x1a };
void DuckRequestMessageHandler(ReceiverDataMessageOf<int> *message);
// @0049f854 -- "EjectPilot" (Mech table id 0x19): the cockpit PANIC/
// EJECT punch-out (raw disasm 2026-08-02 -- the handler sat in the
// 0x49f854-0x49fa00 export gap). Press-only (msg+0xc > 0), gated on
// ejectPermitted (@0x414) and !IsDisabled(). Body: notify the operator
// console of the eject (FUN_004c198c ctor -> network sender; port core
// LOGS it, the relay wire is the tail) and latch the player's
// suppressConsole (+0x258) so the death that follows doesn't double-
// notify; graphicAlarm -> 10 (the EJECT state, one past death's 9);
// then SELF-dispatch a TakeDamageMessage {inflicting = SELF, zone -1,
// Explosive, amount = ScenarioRole::killBonus (role+0x1c -- field map
// byte-verified via the role reader @00429bec dest offsets + the ctor
// record copy), burst 0 (binary; our victim guard clamps to 1 --
// identical outcome: the charge is lethal)}. The punch-out IS a
// death: wreck, respawn cycle and replication all ride the normal
// damage chain.
enum { EjectPilotMessageID = 0x19 };
void EjectPilotMessageHandler(ReceiverDataMessageOf<int> *message);
// @0049fa1c -- the eject-permission evaluator (ejectPermitted @0x414):
// roster walk -- live weapons < ejectMinWeapons(@0x448) | zero live
// generators | HeatSinkBank coolant fraction < 0.05 (@0049fb50) |
// (leg-gimped state 3/4 AND novice, player simLive == 0).
int EvaluateEjectPermission();
// --- damage-routing support (mechdmg / mech4) -----------------------
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
// our entries are Mech__DamageZone, populated by the Mech ctor). Defined in
+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;
}
}
}
}
+7
View File
@@ -341,6 +341,13 @@ class Damage;
public:
static Logical
LookupStatusType(const char *name, int *out_value);
// EJECT-evaluator support (@0049fa1c bridges): the both-cells
// destroyed test (gotcha #22 -- the binary's ONE cell @0x40 is split
// into statusAlarm + simulationState here, so every cross-family
// destroyed check must read BOTH).
int SimulationStateCell() const { return simulationState; }
int StatusAlarmLevel() { return statusAlarm.GetLevel(); }
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1
View File
@@ -360,6 +360,7 @@ class CockpitHud;
int PipExtendedRange() const { return pipExtendedRange; }
void *WithinRangePtr() { return &targetWithinRange; }
void *WeaponAlarmPtr() { return &weaponAlarm; }
int WeaponAlarmLevel() { return weaponAlarm.GetLevel(); } // @0x364 (eject evaluator: 7 = NoAmmo)
void *SimulationStatePtr() { return &simulationState; } // attr 1 ("SimulationState" -- damage state; 1 = Destroyed)
Scalar *RechargeLevelPtr() { return &rechargeLevel; } // the recharge-dial source (1.0 == fully charged); LIVE on BOTH families
// (projectile weapons write it via slot 17 @004b9c9c — Gitea #12)
+16
View File
@@ -1778,3 +1778,19 @@ int BTSubsystemIsGenerator(::Subsystem *sub)
{
return (sub != 0 && sub->IsDerivedFrom(*Generator::GetClassDerivations())) ? 1 : 0;
}
//===========================================================================//
// BTGeneratorCountsForEject -- complete-type bridge for the EJECT-permission
// evaluator (Mech::EvaluateEjectPermission @0049fa1c). Binary clause: classID
// 0xBC1, not destroyed (+0x40 != 1; both-cells here per gotcha #22), and
// stateAlarm level (@0x210) != 4. Returns -1 = not a generator, else 0/1.
//===========================================================================//
int BTGeneratorCountsForEject(Subsystem *sub)
{
if (sub == 0 || sub->GetClassID() != RegisteredClass::GeneratorClassID) // +4 == 0xbc1
return -1;
Generator *g = (Generator *)sub;
if (g->SimulationStateCell() == 1 || g->StatusAlarmLevel() == 1)
return 0; // destroyed
return (g->GeneratorStateOf() != 4) ? 1 : 0; // stateAlarm @0x210 != 4
}
+22
View File
@@ -1242,3 +1242,25 @@ Subsystem *CreateProjectileWeaponSubsystem(Mech *owner, int id, void *seg)
return (Subsystem *) new (Memory::Allocate(0x448))
ProjectileWeapon(owner, id, (ProjectileWeapon::SubsystemResource *)seg, ProjectileWeapon::DefaultData);
}
//===========================================================================//
// BTWeaponCountsForEject -- complete-type bridge for the EJECT-permission
// evaluator (Mech::EvaluateEjectPermission, @0049fa1c; issue: eject/panic).
// The binary's weapon clause: MechWeapon-derived (0x511830), not destroyed,
// and -- for the ProjectileWeapon family only (0x5121a8) -- weaponAlarm
// (@0x364) != 7 (NoAmmo): a dry ballistic rack is not a fighting weapon.
// Destroyed is the BOTH-CELLS test (gotcha #22: statusAlarm level + the
// port's split simulationState). Returns -1 = not a weapon, else 0/1.
//===========================================================================//
int BTWeaponCountsForEject(Subsystem *sub)
{
if (sub == 0 || !sub->IsDerivedFrom(MechWeapon::ClassDerivations))
return -1;
MechWeapon *w = (MechWeapon *)sub;
if (w->SimulationStateCell() == 1 || w->StatusAlarmLevel() == 1)
return 0; // destroyed (both cells)
if (sub->IsDerivedFrom(ProjectileWeapon::ClassDerivations)
&& ((ProjectileWeapon *)sub)->WeaponAlarmLevel() == 7)
return 0; // NoAmmo (@0x364 == 7)
return 1;
}