diff --git a/context/decomp-reference.md b/context/decomp-reference.md index cd373e0..02d88bb 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -249,8 +249,18 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a RealMaxSpeed@0049f604, **0x16 BalanceCoolant@0049f728**, 0x17 SetBurningState@0049f674, 0x18 ClearBurningState@0049f700, **0x19 EjectPilot@0049f854**, **0x1a DuckRequest@0049fa00** (the manual's CROUCH; streamed button 0x13 sends it — unreconstructed). -- **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8} (the Eng-page msg-3 button on - every weapon/avionics — unwired in the reconstruction). +- **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. **Disassembled 2026-07-20** + (`tools/disas2.py 0x4ad6f8`): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle + priority" — that's the emergent effect, not the mechanism): + `if (FUN_004ac9c8(this)) return;` (== `BTPlayerRoleLocksAdvanced` NOVICE lockout, owner→ + BTPlayer→`+0x274`==0) → `if (msg->dataContents(+0xc) <= 0) return;` (press only) → toggle + `coolantAvailable(+0x134)` 0↔1 and `coolantFlowScale(+0x15C)` 0.0f↔1.0f. So a pilot can cut a + subsystem's coolant flow (raising its heat) to free the shared loop for others. **UNWIRED cause + found:** the reconstruction's `PoweredSubsystem::GetMessageHandlers` chains straight onto the + `Receiver` root (powersub.cpp:144), SKIPPING `HeatSink`→`HeatableSubsystem` — so a weapon's + handler chain never reaches id 3. Faithful wiring = define `HeatableSubsystem::GetMessageHandlers` + {3, ToggleCooling} + restore the chain `PoweredSubsystem→HeatSink→HeatableSubsystem`. Sibling + template: `Condenser::MoveValveMessageHandler` (id 4, same novice guard). See [[experience-levels]]. - **Generator** @0x50FB90: {4, "ToggleGeneratorOnOff"→@004b1ed0} (streamed buttons 0x1A-0x1D → GeneratorA-D — unreconstructed). - **Searchlight** @0x51120C: {3, "ToggleLamp"→@004b860c}; PowerWatcher/ThermalSight-side variant diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index 5f40ae7..0f6eb8b 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -508,6 +508,19 @@ inline int void UpdateCoolant(Scalar time_slice); // @004adbf8 + // ToggleCooling (msg 3, table @0x50E41C, @004ad6f8) -- the Eng-page + // "Coolant" button on every weapon/avionics. Per-subsystem coolant + // on/off TOGGLE (coolantAvailable + coolantFlowScale); cutting one + // system's cooling frees the shared loop for the rest (Lynx's "coolant + // priority"). Novice-locked (same guard as MoveValve). The binary + // registers it on the abstract HeatableSubsystem base; we register it on + // HeatSink -- identical coverage (every concrete heatable subsystem is a + // HeatSink) + the fields live here, so no downcast. + enum { ToggleCoolingMessageID = 3 }; + void + ToggleCoolingMessageHandler(ReceiverDataMessageOf *message); // @004ad6f8 + static Receiver::MessageHandlerSet& GetMessageHandlers(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data for the heat sink class. // Offsets are byte offsets into the shipped object. diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index ea2f78e..9c034a4 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -193,6 +193,72 @@ Receiver::MessageHandlerSet& { MESSAGE_ENTRY(Condenser, MoveValve) // id 4 @4ae464 }; + static Receiver::MessageHandlerSet messageHandlers( + ELEMENTS(entries), entries, + HeatSink::GetMessageHandlers() // chain via HeatSink so id 3 ToggleCooling reaches condensers too + ); + return messageHandlers; +} + +// +// @004ad6f8 -- the "ToggleCooling" message handler (binary table @0x50E41C id 3; +// the Eng-page "Coolant" button on every weapon/avionics). DISASSEMBLED +// 2026-07-20 (tools/disas2.py 0x4ad6f8): NOVICE-locked (the SAME guard MoveValve +// uses -- owner->BTPlayer->roleClassIndex(+0x274)==0), press only, then TOGGLE +// this heat sink's coolant flow: coolantAvailable(+0x134) 0<->1 and +// coolantFlowScale(+0x15C) 0.0f<->1.0f. Cutting a system's cooling frees the +// shared loop for the rest -- the "coolant priority" Lynx described (the effect; +// the mechanism is a per-subsystem on/off toggle). Reconstructed at HeatSink +// (the abstract HeatableSubsystem never instantiates; every concrete heatable +// subsystem is a HeatSink, where coolantAvailable/coolantFlowScale live) -- +// identical coverage to the binary's abstract-base registration, no downcast. +// +void + HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf *message) // FUN_004ad6f8 +{ + extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8) + int coolLog = getenv("BT_COOL_LOG") ? 1 : 0; + if (coolLog) // proves the dispatch chain REACHED id 3 + DEBUG_STREAM << "[cool] " << GetName() << " ToggleCooling reached (data=" + << message->dataContents << ")\n" << std::flush; + if (BTPlayerRoleLocksAdvanced(owner)) // @4ad700 novice lockout + { + if (coolLog) DEBUG_STREAM << "[cool] -> NOVICE-locked, no toggle\n" << std::flush; + return; + } + if (message->dataContents <= 0) // @4ad70d press only (*(int*)(msg+0xc)) + return; + if (coolantAvailable != 0) // @4ad714 cooling ON -> OFF + { + coolantAvailable = 0; // @4ad722 [this+0x134] + coolantFlowScale = 0.0f; // @4ad728 [this+0x15c] + } + else // @4ad731 OFF -> ON + { + coolantAvailable = 1; // [this+0x134] = 1 + coolantFlowScale = 1.0f; // [this+0x15c] = 0x3f800000 + } + if (coolLog) + DEBUG_STREAM << "[cool] -> " << GetName() << " coolant " + << (coolantAvailable ? "ON (flow 1.0)" : "OFF (flow 0.0)") << "\n" << std::flush; +} + +// +// HeatSink handler registration: {3, ToggleCooling}. The binary put id 3 on the +// abstract HeatableSubsystem table; we put it on HeatSink (same reach -- every +// concrete heatable subsystem is a HeatSink). Chained onto the Receiver root +// (HeatableSubsystem/MechSubsystem carry no own handlers). PoweredSubsystem +// (weapons/avionics) chains onto THIS so a weapon's dispatch reaches id 3 -- +// restoring the base-table reach the old PoweredSubsystem->Receiver-root +// shortcut skipped. Function-local statics (static-init-order rule). +// +Receiver::MessageHandlerSet& + HeatSink::GetMessageHandlers() +{ + static const Receiver::HandlerEntry entries[]= + { + MESSAGE_ENTRY(HeatSink, ToggleCooling) // id 3 @004ad6f8 + }; static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(entries), entries, Receiver::GetMessageHandlers() @@ -673,7 +739,7 @@ Receiver::MessageHandlerSet& }; static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(entries), entries, - Receiver::GetMessageHandlers() + HeatSink::GetMessageHandlers() // chain via HeatSink so id 3 ToggleCooling reaches the reservoir too ); return messageHandlers; } diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index fd30a7b..cb18318 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -5585,6 +5585,34 @@ void } } + // #1 verify (BT_COOLTOGGLE_TEST): dispatch ToggleCooling (id 3) to the + // first weapon every ~2s so BT_COOL_LOG shows the on/off toggle -- proves + // the restored PoweredSubsystem->HeatSink chain reaches the Eng-page + // "Coolant" button (the real MFD button routing is the #2 audit). + if (getenv("BT_COOLTOGGLE_TEST") && (Entity *)this == application->GetViewpointEntity()) + { + static int s_coolTest = 0; + if ((++s_coolTest % 120) == 0) + { + for (int s = 1; s < GetSubsystemCount(); ++s) + { + Subsystem *sub = GetSubsystem(s); + if (sub == 0) + continue; + int cid = (int)sub->GetClassID(); + if (cid == 0xBC8 || cid == 0xBCA || cid == 0xBCE || cid == 0xBD0 || cid == 0xBD4) + { + ReceiverDataMessageOf msg( + 3 /*HeatSink::ToggleCoolingMessageID*/, + sizeof(ReceiverDataMessageOf), + (ControlsButton)1 /*press*/); + sub->Dispatch(&msg); + 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/powersub.cpp b/game/reconstructed/powersub.cpp index cedf078..655cc42 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -139,9 +139,14 @@ Receiver::MessageHandlerSet& MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorD), // id 7 @004b0a74 MESSAGE_ENTRY(PoweredSubsystem, ToggleGeneratorMode) // id 8 @004b0abc }; + // Chain through HeatSink (id 3 ToggleCooling) so a powered subsystem's + // dispatch -- weapons/avionics -- reaches the Eng-page "Coolant" button. The + // old chain went straight to the Receiver root, SKIPPING HeatSink/ + // HeatableSubsystem, which is why msg 3 was unreachable (decomp-reference.md + // "ToggleCooling ... UNWIRED cause"). HeatSink then chains to the root. static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(entries), entries, - Receiver::GetMessageHandlers() + HeatSink::GetMessageHandlers() ); return messageHandlers; }