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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user