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:
co-authored by
Claude Opus 4.8
parent
cae616dc7c
commit
cbc5ff9532
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user