diff --git a/content/CONTROLS.MAP b/content/CONTROLS.MAP index 4a65e1d..57a8e91 100644 --- a/content/CONTROLS.MAP +++ b/content/CONTROLS.MAP @@ -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 diff --git a/context/decomp-reference.md b/context/decomp-reference.md index f5def02..d9ff6db 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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=`, `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; diff --git a/context/pod-hardware.md b/context/pod-hardware.md index 562c6c4..9d3b392 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -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 diff --git a/game/reconstructed/btinput.cpp b/game/reconstructed/btinput.cpp index e50f0b4..0925e8e 100644 --- a/game/reconstructed/btinput.cpp +++ b/game/reconstructed/btinput.cpp @@ -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; diff --git a/game/reconstructed/btinput.hpp b/game/reconstructed/btinput.hpp index 11be1a6..7f76a4a 100644 --- a/game/reconstructed/btinput.hpp +++ b/game/reconstructed/btinput.hpp @@ -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) diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 18543ac..94e6ce7 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -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 +} diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 9fa81ee..64b4a5a 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -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 diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 6ae5a10..fe6cffe 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -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; +} diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index bab477a..eef1827 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -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") diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 036ad3b..a149d87 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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 *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. diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index d9e3217..367e8b2 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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 *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 *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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index dfdefb8..295d8b2 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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=): 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 punch_out( + Mech::EjectPilotMessageID, + sizeof(ReceiverDataMessageOf), 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; + } } } } diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index e8d0364..2841044 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -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: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 4dec870..1639633 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -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) diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 82a66dc..f8c3c01 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -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 +} diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 91650f6..81ff587 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -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; +}