From 4aab10ba8978ea18dd2203b71a506b2f72a239a1 Mon Sep 17 00:00:00 2001 From: arcattack Date: Tue, 21 Jul 2026 12:57:18 -0500 Subject: [PATCH] Issue #19: wire ToggleSeekVoltage (Myomers id 9 + energy weapons id 0xb) Root cause -- NOT issue #2 (the experience mis-seed pins everyone to VETERAN, which PASSES the novice lockout; the gates were never the blocker). Two unwired handlers, the same silent-swallow pattern ToggleCooling had (Receiver::Receive -> NullHandler for an unregistered id): - Myomers (handler table @0x51158C {9,"ToggleSeekVoltage"@004b8a48}): the reconstruction had an EMPTY stub (the address falls in the untagged decomp gap 0x4b8837..0x4b8a8c) and never registered the id. Body reconstructed faithfully from the raw disassembly (tools/disas2.py 0x4b8a48): novice lockout (@4ac9c8) -> heat-model gate (@4ad7d4) -> press-only (msg+0xc>0) -> currentSeekVoltageIndex@0x320 = (idx+1) % (maxSeekVoltageIndex@0x32C + 1). Modulo from zero, min index not consulted, no ResetFiringState. - Emitter (energy-weapon table @0x511DB8 {0xb,"ToggleSeekVoltage"@004ba478}): the faithful body already existed as the orphaned "AdvanceSeekVoltage" (ZERO callers) -- converted to the registered message handler (same guards; cycle + ResetFiringState when not wrapping, disasm re-verified). PPC and GaussRifle copy-inherit the set; the ammo branch keeps its own id 0xb == EjectAmmo (still unwired, tracked separately). Both registered via MESSAGE_ENTRY on top of the inherited chains. Permanent diagnostics: [seek] gear log (BT_SEEK_LOG) + BT_SEEKTEST scripted dispatch harness (mech4.cpp, the cooltoggle pattern). VERIFIED headless: [seek] Myomers -> 3,0,1,2,3... and [seek] PPC_1 -> 3,0,1,2,3... (modulo-4 wrap, matching the binary's arithmetic). Awaiting the tester's live ADV-mode confirmation for the real MFD button route (the routing layer is the same streamed-EventMapping path ToggleCooling verified). KB: decomp-reference.md message-table rows marked wired. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/decomp-reference.md | 7 +++++- game/reconstructed/emitter.cpp | 37 ++++++++++++++++++++---------- game/reconstructed/emitter.hpp | 18 +++++++++++---- game/reconstructed/mech4.cpp | 41 ++++++++++++++++++++++++++++++++++ game/reconstructed/myomers.cpp | 41 +++++++++++++++++++++++++++------- game/reconstructed/myomers.hpp | 17 +++++++++++--- 6 files changed, 133 insertions(+), 28 deletions(-) diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 58f2368..f0c17b2 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -268,7 +268,12 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a MessageHandlers set is default-constructed EMPTY — unwired.) - **Myomers-class** @0x51158C: {9, "ToggleSeekVoltage"→@004b8a48}; **energy-weapon** variant @0x511DB8: {0xb, "ToggleSeekVoltage"→@004ba478}; **ammo-weapon** @0x512210: {0xb, - "EjectAmmo"→@004bb9b8}. + "EjectAmmo"→@004bb9b8}. **Both ToggleSeekVoltage handlers WIRED 2026-07-21 (issue #19)**: + Myomers' was an empty stub (body recovered by raw disasm of the untagged gap: guards @4ac9c8 + novice + @4ad7d4 heat-model + press-only, then idx@0x320=(idx+1)%(max@0x32C+1), NO + ResetFiringState); Emitter's faithful body existed as the orphaned "AdvanceSeekVoltage" (zero + callers) -- both registered via MESSAGE_ENTRY (ids 9/0xb). Verify: BT_SEEKTEST=1 + + BT_SEEK_LOG=1 → `[seek] -> gear N` cycling. EjectAmmo remains unwired. - **MechRIOMapper** @0x51DD30: RE-REGISTERS the base ids 3..0x13 (Aux1Quad..ZoomOut) with the RIO override handlers @004d22fc..@004d24f8, + {0x19, Keypress→@004d2514} (RIO's OWN keypress, unreconstructed — we use the shared L4 0x17 @004d1bf0) + {0x1a, Hotbox→@004d2574} @0x51DE98. diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index 9786774..6988ed9 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -155,8 +155,18 @@ Derivation Receiver::MessageHandlerSet& Emitter::GetMessageHandlers() { + // issue #19 (2026-07-21): register the energy-weapon handler table + // @0x511DB8 {0xb, "ToggleSeekVoltage" -> @004ba478} on top of MechWeapon's + // ids 9/10 -- the id was unregistered, so the ADV-mode weapon seek press + // was silently swallowed. (PPC/GaussRifle copy-inherit this set; the + // ammo-weapon branch binds 0xb to EjectAmmo in ITS OWN table instead.) + static const Receiver::HandlerEntry entries[]= + { + MESSAGE_ENTRY(Emitter, ToggleSeekVoltage) // id 0xb @004ba478 + }; static Receiver::MessageHandlerSet messageHandlers( - MechWeapon::GetMessageHandlers()); // copy-inherit + ELEMENTS(entries), entries, + MechWeapon::GetMessageHandlers()); // copy-inherit (ids 9/10) return messageHandlers; } // @@ -645,21 +655,23 @@ void } // -// @004ba478 -- advance the seek-voltage index, wrapping modulo (count+1). If -// the index wrapped past the recommended one, re-arm via ResetFiringState. -// Reached only when the heat-state query is clear and the message has payload. -// (best-effort -- exact role of param +0xc unconfirmed.) +// @004ba478 -- ToggleSeekVoltage (energy-weapon handler table @0x511DB8, id +// 0xb). Advance the seek-voltage index, wrapping modulo (count+1); when not +// wrapping, re-arm via ResetFiringState. Guards (disasm-verified): novice +// lockout (@4ac9c8) -> heat-model gate (@4ad7d4) -> press only (msg+0xc > 0). +// Issue #19 (2026-07-21): converted from the orphaned "AdvanceSeekVoltage" +// (no callers) into the registered message handler. // -int - Emitter::AdvanceSeekVoltage(UpdateRecord *message) +void + Emitter::ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message) { - if (MechSubsystem::IsDamaged()) // FUN_004ac9c8 -- NOVICE lockout (seek-voltage cycling is an advanced cockpit control; issue #2) + if (MechSubsystem::IsDamaged()) // FUN_004ac9c8 -- NOVICE lockout bridge { - return /*non-zero status*/ 1; + return; } - if (!HeatModelActive() || message->payloadCount <= 0) // FUN_004ad7d4 -- heat-model gate; message+0xc + if (!HeatModelActive() || message->dataContents <= 0) // FUN_004ad7d4 -- heat-model gate; msg+0xc { - return 0; + return; } int prev = seekVoltageIndex; // 0x3f0 @@ -670,7 +682,8 @@ int { ResetFiringState(); // @004ba9a8 } - return next; + if (getenv("BT_SEEK_LOG")) + DEBUG_STREAM << "[seek] " << GetName() << " -> gear " << next << std::endl; } diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index 5012361..3f41932 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -294,10 +294,20 @@ class NotationFile; void ContinueDischarge(); - // @004ba478 -- advance the seekVoltage index, wrapping modulo the count; - // re-arm if it wrapped. (best-effort name) - int - AdvanceSeekVoltage(UpdateRecord *message); + public: + // @004ba478 -- "ToggleSeekVoltage": the ENERGY-WEAPON handler table + // @0x511DB8 {0xb, "ToggleSeekVoltage"}. Issue #19 (2026-07-21): the + // faithful body existed (as AdvanceSeekVoltage) but the id was never + // registered, so the ADV-mode weapon seek-gear press was silently + // swallowed. Now the registered message handler; cycles + // seekVoltageIndex@0x3F0 modulo (maxSeekVoltageIndex@0x3FC + 1) and + // re-arms (ResetFiringState) when not wrapping. (The AMMO-weapon + // table @0x512210 binds id 0xb to EjectAmmo instead -- ProjectileWeapon + // branch, unaffected.) + enum { ToggleSeekVoltageMessageID = 0x0b }; + void + ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message); // @004ba478 + protected: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support (EMITTER.TCP) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index cb18318..0536577 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -5613,6 +5613,47 @@ void } } + // issue #19 verify (BT_SEEKTEST): dispatch the seek-gear messages every + // ~2s -- ToggleSeekVoltage id 9 to Myomers (0xBC6) and id 0xb to the + // first ENERGY weapon (Emitter 0xBC8 / GaussRifle 0xBCE / PPC 0xBD4; + // NOT the ammo branch, whose id 0xb is EjectAmmo). BT_SEEK_LOG shows + // the gear cycling. + if (getenv("BT_SEEKTEST") && (Entity *)this == application->GetViewpointEntity()) + { + static int s_seekTest = 0; + if ((++s_seekTest % 120) == 0) + { + int sentMyo = 0, sentWpn = 0; + for (int s = 1; s < GetSubsystemCount(); ++s) + { + Subsystem *sub = GetSubsystem(s); + if (sub == 0) + continue; + int cid = (int)sub->GetClassID(); + if (!sentMyo && cid == 0xBC6) + { + ReceiverDataMessageOf msg( + 9 /*Myomers::ToggleSeekVoltageMessageID*/, + sizeof(ReceiverDataMessageOf), + (ControlsButton)1 /*press*/); + sub->Dispatch(&msg); + sentMyo = 1; + } + else if (!sentWpn && (cid == 0xBC8 || cid == 0xBCE || cid == 0xBD4)) + { + ReceiverDataMessageOf msg( + 0x0b /*Emitter::ToggleSeekVoltageMessageID*/, + sizeof(ReceiverDataMessageOf), + (ControlsButton)1 /*press*/); + sub->Dispatch(&msg); + sentWpn = 1; + } + if (sentMyo && sentWpn) + break; + } + } + } + // task #6 dev harness: the config-session BRACKET. 'G' edge -> // dispatch ConfigureMappables (id 9) press/release to the selected // weapon -- the exact ReceiverDataMessageOf payload diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 99f3370..d3bb3f5 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -87,7 +87,16 @@ Derivation Receiver::MessageHandlerSet& Myomers::GetMessageHandlers() { + // issue #19 (2026-07-21): register the binary's Myomers handler table + // @0x51158C {9, "ToggleSeekVoltage" -> @004b8a48} on top of the inherited + // powered ids -- the id was unregistered, so the ADV-mode seek-gear press + // was silently swallowed (Receiver::Receive NullHandler). + static const Receiver::HandlerEntry entries[]= + { + MESSAGE_ENTRY(Myomers, ToggleSeekVoltage) // id 9 @004b8a48 + }; static Receiver::MessageHandlerSet messageHandlers( + ELEMENTS(entries), entries, PoweredSubsystem::GetMessageHandlers()); // copy-inherit (ids 4-8) return messageHandlers; } @@ -563,16 +572,32 @@ void Myomers::DisconnectFromMover(SubsystemMessage &message) //*************************************************************************** -// Myomers::ToggleSeekVoltage @004b8a48 +// Myomers::ToggleSeekVoltageMessageHandler @004b8a48 //*************************************************************************** -// Mappable command (attribute id 0x09 "ToggleSeekVoltage", bound in the -// @00511588 table to handler @004b8a48). Cycles currentSeekVoltageIndex -// @0x320 between minSeekVoltageIndex@0x328 and maxSeekVoltageIndex@0x32C. -// BEST-EFFORT: present in the binary but not captured in the decompiled -// shards (the address falls in the untagged gap 0x4b8837..0x4b8a8c). -void Myomers::ToggleSeekVoltage() +// Handler table @0x51158C: {9, "ToggleSeekVoltage"}. Reconstructed FAITHFULLY +// from the raw disassembly (tools/disas2.py 0x4b8a48; the decomp shards missed +// the untagged gap 0x4b8837..0x4b8a8c) -- issue #19 fix, 2026-07-21: +// 0x4b8a50 call 0x4ac9c8 novice lockout (BTPlayerRoleLocksAdvanced) -> ret +// 0x4b8a5b call 0x4ad7d4 heat-model experience gate -> ret if OFF +// 0x4b8a68 msg+0xc <= 0 press only (release is negative) +// 0x4b8a6f idx@0x320 = (idx + 1) % (maxSeekVoltageIndex@0x32C + 1) +// Note: modulo (max+1) from ZERO -- minSeekVoltageIndex is NOT consulted, and +// there is no ResetFiringState (both unlike the Emitter sibling @004ba478). +void Myomers::ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message) { - // cycle currentSeekVoltageIndex within [minSeekVoltageIndex, maxSeekVoltageIndex] + if (IsDamaged()) // FUN_004ac9c8 -- NOVICE lockout bridge + { + return; + } + if (!HeatModelActive() || message->dataContents <= 0) // FUN_004ad7d4; msg+0xc + { + return; + } + currentSeekVoltageIndex = // @0x320 + (currentSeekVoltageIndex + 1) % (maxSeekVoltageIndex + 1); // @0x32C + if (getenv("BT_SEEK_LOG")) + DEBUG_STREAM << "[seek] " << GetName() << " -> gear " + << currentSeekVoltageIndex << std::endl; } diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 88847e4..13e1c51 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -271,10 +271,21 @@ class Mech; void DisconnectFromMover(SubsystemMessage &message); // @004b95b8 (detach SpeedEffect feed) - // ToggleSeekVoltage command handler @004b8a48 (cycles currentSeekVoltageIndex; - // present in the binary, not captured in the decompiled shards -- best-effort). + public: + // ToggleSeekVoltage message handler @004b8a48 (Myomers handler table + // @0x51158C: {9, "ToggleSeekVoltage"}). Issue #19: was an EMPTY stub + + // never registered, so the ADV-mode seek-gear button was silently + // swallowed (Receiver::Receive NullHandler). Body reconstructed from + // the raw disassembly (tools/disas2.py 0x4b8a48 -- the address falls in + // the untagged decomp gap 0x4b8837..0x4b8a8c): + // novice lockout (@4ac9c8) -> heat-model gate (@4ad7d4) -> press only + // (msg+0xc > 0) -> currentSeekVoltageIndex@0x320 = + // (idx + 1) % (maxSeekVoltageIndex@0x32C + 1). No ResetFiringState + // (unlike the Emitter sibling @004ba478). + enum { ToggleSeekVoltageMessageID = 9 }; void - ToggleSeekVoltage(); // @004b8a48 (best-effort) + ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message); // @004b8a48 + protected: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CROSS-FAMILY compile shims (owning Mech / JointedMover roster / DamageZone).