Wire ToggleCooling (msg 3): restore the coolant on/off button + handler chain

Coolant priority (Lynx: weapon MFD -> Display -> Coolant) was unreachable: the
ToggleCooling handler (@004ad6f8, HeatableSubsystem table @0x50E41C id 3) was
undefined AND the handler chain skipped it -- PoweredSubsystem::GetMessageHandlers
chained straight to the Receiver root, bypassing HeatSink/HeatableSubsystem, so a
weapon's dispatch never saw id 3.

Reconstructed ToggleCooling from the disassembly (tools/disas2.py 0x4ad6f8): a
per-subsystem coolant on/off TOGGLE -- novice-locked (owner->BTPlayer->
roleClassIndex+0x274==0, the same guard as MoveValve), press-only, then flip
coolantAvailable(+0x134) 0<->1 and coolantFlowScale(+0x15C) 0.0f<->1.0f.  Cutting a
system's cooling frees the shared loop for the rest -- the emergent "coolant
priority" (the mechanism is a toggle, not a multi-level cycle).

- heat.hpp / heatfamily_reslice.cpp: HeatSink::ToggleCoolingMessageHandler (id 3)
  + HeatSink::GetMessageHandlers.  Registered at HeatSink (the abstract
  HeatableSubsystem never instantiates; every concrete heatable subsystem is a
  HeatSink, where the coolant fields live) -- identical coverage to the binary's
  base-table registration, no downcast.
- powersub.cpp + Condenser/Reservoir: chain their handler sets through
  HeatSink::GetMessageHandlers so id 3 reaches every heatable subsystem.
- mech4.cpp: BT_COOLTOGGLE_TEST scripted inject (dispatch id 3 to the first weapon)
  for verification.

Verified (BT_COOLTOGGLE_TEST + BT_COOL_LOG, expert egg): "[cool] PPC_1 ToggleCooling
reached" then coolant OFF(flow 0.0) <-> ON(flow 1.0) each press -- chain routes,
handler toggles, novice guard honored.  Both build/ + build-glass/ compile clean;
existing ids 4-8 unaffected (found before id 3).  NEXT: the #2 MFD button routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-20 23:04:04 -05:00
co-authored by Claude Opus 4.8
parent cae616dc7c
commit cbc5ff9532
5 changed files with 126 additions and 4 deletions
+12 -2
View File
@@ -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
+13
View File
@@ -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<int> *message); // @004ad6f8
static Receiver::MessageHandlerSet& GetMessageHandlers();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data for the heat sink class.
// Offsets are byte offsets into the shipped object.
+67 -1
View File
@@ -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<int> *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;
}
+28
View File
@@ -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<ControlsButton> msg(
3 /*HeatSink::ToggleCoolingMessageID*/,
sizeof(ReceiverDataMessageOf<ControlsButton>),
(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<ControlsButton> payload
+6 -1
View File
@@ -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;
}