Cockpit: the COOLANT VALVE control -- the heat-management triad completes (task #13)

The pilot's third heat tool: Condenser::MoveValve (id 4, the Condenser handler
table @0x50E52C -- PE-verified, exactly one entry) registered via the
order-proof function-local accessor and GUARDED by the real FUN_004ac9c8
(task #12's BTPlayerRoleLocksAdvanced rookie-role bridge -- the old deferral
note blamed the 0xBD3 messmgr, a stale misattribution, swept).  Each press
cycles the valve 1 -> 5 -> 50 -> 0 -> 1 and RecomputeCondenserValves re-shares
the fixed total coolant flow across all six condensers -- opening one valve
wide starves the others: the pilot's cooling-triage lever.

Desktop: 'C' cycles the selected condenser's valve (BT_VALVE_SLOT picks the
roster slot; default Condenser1).  BT_VALVE_TEST scripts a headless press.

Verified live: press -> "[valve] condenser#1 valveState=5 flow=0.5 (total=10)"
-- exactly the authored redistribution math (5/(5+5x1)), through the real
role gate.

The heat-management triad is now complete and all-authentic:
  G + fire buttons  = regroup weapons across buttons     (task #6)
  F5-F8 / F9        = reassign weapon generators / mode  (task #12)
  C                 = coolant valve triage               (task #13)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 23:33:27 -05:00
co-authored by Claude Fable 5
parent d7b900d108
commit 80cadc98c7
5 changed files with 112 additions and 25 deletions
+8 -7
View File
@@ -89,13 +89,14 @@ At the authentic 1e7-unit economy (tasks #9-#10) the Heat MFD is fully dynamic:
fire, and the bank plateaus ~600 — temperatures, heatLoad and the heat alarms all animate. [T2]
## Remaining = DATA FEEDS, not widgets (deferred systems)
The **condenser valve CONTROL** gates on the owning **BTPlayer**'s game-mode flag (`mech+0x190`,
`GetPlayerLink()`, via `MechSubsystem::IsDamaged`/`FUN_004ac9c8` reading `player+0x274`) — NOT the
0xBD3 messmgr (a common earlier misconception; 0xBD3 is a damage/explosion hub at `Mech[0x10d]`=0x434).
In a basic mission the valve/mode flags are likely off, so the dormant valve is plausibly AUTHENTIC —
see [[open-questions]] `mech+0x190`. The **MessageBoard** feed needs **StatusMessagePool** (a NULL
stub) + the per-player status queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not
config-called). [T2]
**✅ The condenser valve CONTROL is LIVE (task #13, 2026-07-11) [T2]:** `MoveValve` (id 4, the
Condenser handler table @0x50E52C — exactly one entry) registered + guarded by the REAL
FUN_004ac9c8 = `player+0x274 roleClassIndex == 0` (the ROOKIE-role lockout; task #12's
`BTPlayerRoleLocksAdvanced` bridge — the old "game-mode flag / likely off in a basic mission"
hedge is superseded: bring-up role = 2 = UNLOCKED, verified live: press → valveState 1→5 →
flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected condenser (BT_VALVE_SLOT).
The **MessageBoard** feed needs **StatusMessagePool** (a NULL stub) + the per-player status
queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not config-called). [T2]
## Cockpit HUD reticle (main screen, inside view) — LIVE
`BTReticleRenderable` (0x358 bytes, ctor @004cc40c) draws over the finished 3D frame in cockpit
+10 -1
View File
@@ -535,6 +535,12 @@ inline int
//
public:
static Derivation *GetClassDerivations();
// task #13: the Condenser handler table @0x50E52C -- exactly ONE entry,
// {4, "MoveValve", @4ae464} (PE-verified). Per-receiver-class id space:
// id 4 to a condenser = MoveValve; to a weapon = SelectGeneratorA.
// Table + set are function-local statics in the accessor (the task #12
// static-init-order trap).
static Receiver::MessageHandlerSet& GetMessageHandlers();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -554,8 +560,11 @@ inline int
public:
void
RefrigerationSimulation(Scalar time_slice); // @4ae4d8 (vtable slot 9)
enum { MoveValveMessageID = 4 }; // table @0x50E52C [T1]
void
MoveValve(int message); // @4ae464 (valve cycle 1->5->50->0; dormant until 0xBD3)
MoveValveMessageHandler( // @4ae464 (valve cycle 1->5->50->0)
ReceiverDataMessageOf<int> *message
);
virtual void
SetValveSetting(int setting) { valveState = setting; }
+35 -16
View File
@@ -148,26 +148,26 @@ void
}
//
// @4ae464 -- the REAL "MoveValve" message handler (disassembled; the assert-anchored
// exporter did not capture it). Cycles the coolant valve through its FOUR settings
// 1 -> 5 -> 50 -> 0 -> 1 (NOT the 0/1/2 the earlier draft guessed -- that draft had
// FUN_004afbe0's body, which is a CONTROLS-mapper display-mode cycler @0x190, a
// mislabel). After changing valveState it redistributes coolant flow across all
// condensers (RecomputeCondenserValves) so every ValveSetting gauge updates.
// @4ae464 -- the "MoveValve" message handler (id 4, table @0x50E52C [T1]).
// Cycles the coolant valve through its FOUR settings 1 -> 5 -> 50 -> 0 -> 1,
// then redistributes coolant flow across all condensers
// (RecomputeCondenserValves) so every ValveSetting gauge updates.
//
// ROUTE/GUARD are DEFERRED to WAVE 8: the binary guards on
// owner->messageManager(+0x190)->field_0x274 == 0 (FUN_004ac9c8, the 0xBD3
// SubsystemMessageManager, unreconstructed) and the message that CALLS this arrives
// through that same manager. Reproducing the raw owner+0x190 deref would be a
// databinding trap in our layout, and there is no route to invoke this yet, so the
// handler is faithful-but-DORMANT: the valve gauge shows the authentic static 1/N
// (RecomputeCondenserValves at ctor) until the message manager is reconstructed.
// task #13 -- ROUTE + GUARD LANDED (the old deferral note claimed the guard
// was the 0xBD3 messmgr; task #12 proved FUN_004ac9c8 reads mech+0x190 ->
// the owning BTPlayer's roleClassIndex(+0x274) == 0 -- the ROOKIE-role
// lockout, implemented as the BTPlayerRoleLocksAdvanced bridge). Registered
// under id 4 via Condenser::GetMessageHandlers(); the pod route is the
// engineering-screen aux buttons (type-6 EventMappings), the desktop route
// is the 'C' key (mech4 harness).
//
void
Condenser::MoveValve(int message) // FUN_004ae464
Condenser::MoveValveMessageHandler(ReceiverDataMessageOf<int> *message) // FUN_004ae464
{
// binary: if (FUN_004ac9c8(this)) return; -- messmgr guard, deferred (see note)
if (message /* *(int*)(param_2+0xc) = msg->count */ <= 0)
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
if (BTPlayerRoleLocksAdvanced(owner))
return;
if (message->dataContents <= 0) // press only (*(int*)(msg+0xc))
return;
switch (valveState) // this[0x74] @0x1D0
@@ -181,6 +181,25 @@ void
BTRecomputeCondenserValves((Entity *)owner); // FUN_0049f788(this[0xd0]=owner Mech)
}
//
// task #13 -- the Condenser handler registration (table @0x50E52C: exactly
// one entry). Chained onto the engine Receiver root set; function-local
// statics per the static-init-order rule (reconstruction-gotchas #9).
//
Receiver::MessageHandlerSet&
Condenser::GetMessageHandlers()
{
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(Condenser, MoveValve) // id 4 @4ae464
};
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(entries), entries,
Receiver::GetMessageHandlers()
);
return messageHandlers;
}
//
// @4ae658 -- parse the Condenser resource record. Stamps classID 0x0BBD and
// model size 0x100, defaults RefrigerationFactor to -1.0f on pass 1, then
+1 -1
View File
@@ -108,7 +108,7 @@
// @4ae5fc ~Condenser(byte flags) -- vtable slot 0
// @4ae63c TestInstance() -- IsDerivedFrom 0x50e4fc
// @4ae658 CreateStreamedSubsystem(...) -- parses RefrigerationFactor
// @4afbe0 MoveValve(message) [best-effort] -- cycles valveState 0->1->2->0,
// @4afbe0 MoveValveMessageHandler (id 4) -- valve cycle 1->5->50->0 (live),
// drives owner HUD gauges
//###########################################################################
+58
View File
@@ -647,6 +647,7 @@ static int gBTMissileKey = 0;
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
// (0 idle; else the message id 4..8)
static int gBTValveKey = 0; // task #13: 'C' = MoveValve on the selected condenser
// Damage: each shot dispatches a REAL Entity::TakeDamageMessage to the target. Now
// that the damage zones are constructed (mech.cpp Pass-3 zone build), the engine base
@@ -1540,6 +1541,9 @@ void
: (focused && (pAsync(0x77 /*F8*/) & dn)) ? 7
: (focused && (pAsync(0x78 /*F9*/) & dn)) ? 8
: 0;
// task #13: 'C' cycles the coolant valve (BT_VALVE_SLOT
// picks the condenser roster slot; default = Condenser1).
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// damage dispatcher + the beam-visual keepalive)
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0;
@@ -3123,6 +3127,60 @@ void
gBTGenSelKey = (s_gsFrame >= 600 && s_gsFrame < 610) ? 7 : 0;
}
// task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at
// frame ~600 -- Condenser1's valveState cycles 1 -> 5, so the flow
// redistribution gives it 5/10 of the total coolant flow ([valve] log
// + its bank conduction speeds up 5x).
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_VALVE_TEST"))
{
static int s_vtFrame = 0;
++s_vtFrame;
gBTValveKey = (s_vtFrame >= 600 && s_vtFrame < 610) ? 1 : 0;
}
// task #13 dev harness: the COOLANT VALVE lever. 'C' edge -> dispatch
// MoveValve (id 4, the Condenser table @0x50E52C) to the selected
// condenser -- the same press payload the pod's engineering-screen aux
// buttons deliver.
if ((Entity *)this == application->GetViewpointEntity())
{
static int s_prevValve = 0;
if (gBTValveKey != s_prevValve)
{
s_prevValve = gBTValveKey;
if (gBTValveKey != 0)
{
Subsystem *condenser = 0;
const char *slotEnv = getenv("BT_VALVE_SLOT");
int wantSlot = (slotEnv != 0) ? atoi(slotEnv) : -1;
for (int s = 1; s < GetSubsystemCount(); ++s)
{
Subsystem *sub = GetSubsystem(s);
if (sub == 0)
continue;
if (wantSlot >= 0 ? (s == wantSlot)
: ((int)sub->GetClassID() == 0xBBD)) // Condenser
{
condenser = sub;
break;
}
}
if (condenser != 0)
{
if (getenv("BT_FIRE_LOG"))
DEBUG_STREAM << "[valve-tx] MoveValve -> "
<< condenser->GetName() << std::endl;
ReceiverDataMessageOf<ControlsButton> msg(
4 /*Condenser::MoveValveMessageID*/,
sizeof(ReceiverDataMessageOf<ControlsButton>),
(ControlsButton)1 /*press*/);
condenser->Dispatch(&msg);
}
}
}
}
// task #12 dev harness: the POWER-ROUTING buttons. F5..F9 edge ->
// dispatch SelectGeneratorA..D / ToggleGeneratorMode (ids 4..8) to the
// selected weapon -- the same ReceiverDataMessageOf<ControlsButton>