Coolant Flush end-to-end (Gitea #7): InjectCoolant id-4 handler + drain + FLUSH.PFX cloud + 'H' key

The playtest report (sound plays, no gauge drop, no cloud) traced to the
Reservoir's InjectCoolant chain being dead in three places:

- The handler was never REGISTERED: the binary's Reservoir handler table
  @0x50e680 has one entry {4, "InjectCoolant", @4aee70}; added
  Reservoir::GetMessageHandlers + the press/release handler (press starts
  the flush gated on coolantLevel@0x12C > 0 -- the old body misread +0x12C
  as currentTemperature; release stops it; novice lockout via FUN_004ac9c8).

- Reservoir::InjectCoolant (@4aefa4, 1019 bytes) was an empty stub -- the
  drain the coolant gauge reads.  Reconstructed in full: work list =
  condenser/weapon/heatable chains (+0x7cc/+0x7bc/+0x7ac, roster-walk
  emulation with the binary's duplicate-visit weighting; HeatSink-filtered
  [T2 guarded]) + the linked master sink; per sink with flowScale != 0 move
  squirtMass x flowScale x dt (clamped to the tank / sink capacity) and
  credit pendingHeat with the negative carried-heat delta capped at
  sinkMass x reservoir startingTemperature -- the set%-biased flush of the
  manual (p24), riding the existing heat model.

- Two latent ctor decode bugs surfaced and fixed: the master gate @4af408
  (and @4aeb40 HeatWatcher, swept) reads the OWNER MECH's simulationFlags
  (*(param_2+0x28)), not the resource's subsystemFlags (the misread left
  the CoolantSimulation Performance unregistered); and the capacity scale
  FILDs the bank's INTEGER HeatSinkCount ((float10)*(int*)(link+0x1d0)) --
  the float reinterpret gave ~1e-44 -> a permanently empty tank.
  Capacity = 0.05 x heatSinkCount x streamed CoolantCapacity (BLH: 6.0).

Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built
for classID 0xBC0 on "ReservoirState", part_014.c:5439; tick @part_007.c:
8780) starts psfx 19 = FLUSH.PFX ("Coolant flush", BTDPL.INI) when the
state changes to 1 -- BTSpawnFlushCloud (mech4.cpp) spawns it on the same
alarm edge as an attached emitter at torso height.

Input: new CONTROLS.MAP action "Flush" on 'H' (HELD; press+release both
dispatch, the held-button payload).  Diags: BT_FLUSH_LOG, BT_FLUSH_TEST.

Verified live (FOGDAY): [flush] Reservoir level 6 -> 0.13 -> 0 over ~0.6 s
held (= the manual's 3-4 punches to empty), the coolant vertBar source
Reservoir/CoolantMass drains, and the bluish condensation cloud rises from
the mech (scratchpad/flush_cloud.png vs flush_before.png).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 15:15:44 -05:00
co-authored by Claude Fable 5
parent d9ceddb12a
commit 2ae9bd43ae
8 changed files with 384 additions and 46 deletions
+92
View File
@@ -661,6 +661,7 @@ static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-con
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
static int gBTFlushKey = 0; // Gitea #7: 'H' HELD = InjectCoolant (the coolant flush)
// 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
@@ -899,6 +900,40 @@ void
<< " at(" << mx << "," << my << "," << mz << ")\n" << std::flush;
}
//###########################################################################
// COOLANT-FLUSH CONDENSATION CLOUD (Gitea #7) -- the flush visual.
//
// BTDPL.INI psfx 19 = FLUSH.PFX, commented "Coolant flush" (both day and
// night tables): a bluish (0.7/0.9/1.6 tint) firesmoke burst, ~3.5 s window.
// The binary drives it with the mode-1 "coolant/reservoir" effect renderable
// (FUN_00456a68, alloc 0x138), built for every classID-0xBC0 subsystem on
// its "ReservoirState" attribute (BTL4VID.CPP build loop, part_014.c:5439;
// missing attr = Verify "Reservoir did not have a ReservoirState...") and
// DCS-parented to the mech; its per-frame tick (part_007.c:8780) starts the
// effect exactly when the watched state CHANGES to 1. Manual p24: "other
// pilots will see steam rise from your 'Mech on the battlefield".
//
// The port consolidates the BT effect renderables in the psfx layer, so this
// helper is called on the same 0->1 alarm edge (Reservoir::
// InjectCoolantMessageHandler) and rides the mech via the attached-emitter
// follower (seg -1 = the torso-height entity fallback in
// BTResolveSegmentWorld -- the cloud rises FROM the mech and trails it).
//###########################################################################
void
BTSpawnFlushCloud(void *ownerMech)
{
if (ownerMech == 0)
return;
float pos[3], rows[9];
if (!BTResolveSegmentWorld(ownerMech, -1, pos, rows))
return;
extern void BTStartPfxAttached(int, void *, int, float, float, float, const float *);
BTStartPfxAttached(19, ownerMech, -1, pos[0], pos[1], pos[2], rows);
if (getenv("BT_FLUSH_LOG"))
DEBUG_STREAM << "[flush] condensation cloud (psfx 19 FLUSH.PFX) at("
<< pos[0] << "," << pos[1] << "," << pos[2] << ")\n" << std::flush;
}
//###########################################################################
// PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every
// round spawns ITS OWN ExplosionModelFile at its impact point, resource
@@ -2621,6 +2656,10 @@ void
gBTGenSelKey = gBTInput.genSel;
// task #13: the Valve action cycles the coolant valve.
gBTValveKey = gBTInput.valve;
// Gitea #7: the Flush action HELD = the coolant MFD's
// top-right button (manual p24) -- InjectCoolant while
// down, stop on release.
gBTFlushKey = gBTInput.flush;
// TORSO CONTROLS (2026-07-13): 'M' cycles the control mode
// (Basic -> Standard -> Veteran -- the pod console button,
// CycleControlModeMessageHandler); Q/E deflect the torso
@@ -5110,6 +5149,59 @@ void
gBTValveKey = (s_vtFrame >= 600 && s_vtFrame < 610) ? 1 : 0;
}
// Gitea #7 scripted verify (BT_FLUSH_TEST=<frame>): hold the coolant
// flush for ~60 sim frames starting at the given frame -- the
// reservoir drains ([flush] log), the coolant vertBar drops, and the
// FLUSH.PFX condensation cloud spawns on the 0->1 edge.
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_FLUSH_TEST"))
{
static int s_ftFrame = 0;
++s_ftFrame;
int t0 = atoi(getenv("BT_FLUSH_TEST"));
if (t0 < 1) t0 = 600;
gBTFlushKey = (s_ftFrame >= t0 && s_ftFrame < t0 + 60) ? 1 : 0;
}
// Gitea #7: the COOLANT FLUSH button (the coolant MFD's top-right
// button, manual p24 -- "punch (or hold down) the coolant button to
// flush fresh coolant through your 'Mech's loops"). Press AND
// release edges both dispatch: InjectCoolant (id 4, the Reservoir
// table @0x50e680) starts the flush on data>=1 and stops it on
// data<1 -- the same held-button payload the pod's streamed MFD
// EventMapping delivers.
if ((Entity *)this == application->GetViewpointEntity())
{
static int s_prevFlush = 0;
if (gBTFlushKey != s_prevFlush)
{
int pressed = (gBTFlushKey != 0);
s_prevFlush = gBTFlushKey;
Subsystem *reservoir = 0;
for (int s = 1; s < GetSubsystemCount(); ++s)
{
Subsystem *sub = GetSubsystem(s);
if (sub != 0 && (int)sub->GetClassID() == 0xBC0) // Reservoir
{
reservoir = sub;
break;
}
}
if (reservoir != 0)
{
if (getenv("BT_FLUSH_LOG"))
DEBUG_STREAM << "[flush-tx] InjectCoolant "
<< (pressed ? "PRESS" : "RELEASE") << " -> "
<< reservoir->GetName() << std::endl;
ReceiverDataMessageOf<ControlsButton> msg(
4 /*Reservoir::InjectCoolantMessageID*/,
sizeof(ReceiverDataMessageOf<ControlsButton>),
(ControlsButton)(pressed ? 1 : 0));
reservoir->Dispatch(&msg);
}
}
}
// 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