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
+2 -1
View File
@@ -31,7 +31,7 @@
# LeftTrigger RightTrigger (as digital, >= 50% pressed)
# <src> LeftStickX LeftStickY RightStickX RightStickY
# LeftTrigger RightTrigger
# action ViewToggle LookBehind AllStop ModeCycle Valve ConfigHold
# action ViewToggle LookBehind AllStop ModeCycle Valve Flush ConfigHold
# Generator1-4 Reconnect
# pckey sends an authentic 1995 PC-keyboard hotkey (the in-cockpit
# dispatcher): '+'/'-' = target zoom, 'w'..'o' = pilot select,
@@ -73,6 +73,7 @@ key D4 button 0x45 # Pinky (4th group)
# Systems.
key G action ConfigHold # hold + fire keys = regroup the weapon
key C action Valve # coolant valve cycle
key H action Flush # coolant flush (HOLD -- dumps reservoir coolant)
key M action ModeCycle # Basic -> Standard -> Veteran controls
key V action ViewToggle # cockpit <-> chase camera (dev)
key B action LookBehind # HOLD = the pod's rear-view button
+8
View File
@@ -155,6 +155,14 @@ so this is the NOVICE lockout, not a "ROOKIE role" — see [[experience-levels]]
`BTPlayerRoleLocksAdvanced` bridge; bring-up seeding = 2 ≈ veteran = UNLOCKED, verified live:
press → valveState 1→5 → flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected
condenser (BT_VALVE_SLOT).
**✅ The COOLANT FLUSH is LIVE (Gitea #7, 2026-07-19) [T2]:** `InjectCoolant` (id 4, the Reservoir
handler table @0x50e680, handler @4aee70) — HOLD flushes reservoir coolant through the loops
(InjectCoolant @4aefa4, set%-biased via each sink's flowScale); the coolant **vertBar C**
(`Reservoir/CoolantMass` = coolantLevel@0x12C, l4gauge.cfg:4526) drops live as the tank drains
(BLH: 6.0 → 0 in ~0.6 s held ≈ the manual's 3-4 punches); the bluish FLUSH.PFX condensation
cloud (psfx 19) spawns on the ReservoirState 0→1 edge. Desktop: **'H' HELD** (action Flush).
Full chain + the two ctor decode corrections it surfaced: [[subsystems]] §coolant FLUSH.
Diags: `BT_FLUSH_LOG`, `BT_FLUSH_TEST=<frame>`.
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]
+31
View File
@@ -89,6 +89,37 @@ its electrical state. The full chain, byte-verified [T1]:
- **STILL DEAD:** factory loops 2-4 (heatable/weapon/damageable capability rosters) go through the
`SubProxy` stub whose `IsDerivedFrom` returns 0 — they add NOTHING. See [[open-questions]].
## The coolant FLUSH (Gitea #7, 2026-07-19) — Reservoir InjectCoolant end-to-end [T2 live-verified]
The manual-p24 coolant button (coolant MFD top-right; punch or HOLD): message id **4 "InjectCoolant"**
on the Reservoir (handler table @**0x50e680**, one entry → @4aee70 — same per-receiver id space as the
Condenser's MoveValve @0x50E52C). Handler @4aee70: novice-lockout gate (FUN_004ac9c8), press
(data≥1) → `reservoirAlarm.SetLevel(1)` **gated on coolantLevel@0x12C > 0** (+300 decimal = 0x12C =
coolantLevel — the old "currentTemperature" reading was a misdecode), release → level 0; ForceUpdate.
While level==1 the CoolantSimulation Performance (@4aef78) runs **InjectCoolant @4aefa4** each frame:
work list = condenser chain @+0x7cc + weapon chain @+0x7bc + heatable chain @+0x7ac + linkedSinks
master (port walks the roster with the same membership tests — chains are SubProxy-dead; condensers/
weapons/bank get the binary's duplicate-visit weighting; watcher-branch members are filtered to
HeatSink [T2 guarded]); per sink with flowScale@0x15C≠0: move `squirtMass@0x224 × flowScale × dt`
clamped to [-resLevel, 0] (reservoir drains → the coolant vertBar C `Reservoir/CoolantMass` drops),
sink level clamped [0, capacity], and `pendingHeat@0x1C8 += -(sinkE×|Δ|/sinkLvl resE×|Δ|/resLvl)`
capped at `sinkMass@0x154 × res->startingTemperature@0x13C`, clamped ≤0 (only cools) — the heat
relief rides the existing heat model. **Two ctor corrections surfaced:** (1) the master gate @4af408
(and @4aeb40 HeatWatcher) reads `*(param_2+0x28)` = the **owner MECH's simulationFlags**, not the
resource's subsystemFlags (the resource read left the Reservoir Performance unregistered → no drain);
(2) `(float10)*(int*)(link+0x1d0)` is a **FILD of the bank's integer HeatSinkCount** — capacity =
0.05 (`_DAT_004af518`) × heatSinkCount × streamed CoolantCapacity (BLH: 0.05×6×20 = 6), not a float
reinterpret (which gave ~1e-44 → empty tank). Visual: the binary's mode-1 coolant-effect renderable
(FUN_00456a68, built for classID 0xBC0 on the "ReservoirState" attr, part_014.c:5439; tick
@part_007.c:8780) spawns **psfx 19 = FLUSH.PFX** ("Coolant flush", BTDPL.INI:1018, bluish smoke) on
the state's 0→1 edge — port: `BTSpawnFlushCloud` (mech4.cpp) on the same alarm edge, attached emitter
at torso height. Audio (CoolantDump layers) rides the ReservoirState watchers. Desktop input: **'H'
HELD = Flush** (CONTROLS.MAP action; press+release dispatch). Diags: `BT_FLUSH_LOG` ([flush-tx]
press/release, [flush] level drain, cloud spawn), `BT_FLUSH_TEST=<frame>` scripted ~2 s hold.
Verified live (FOGDAY): level 6→0 over ~0.6 s held (≈3-4 short punches to empty — matches the
manual), bluish cloud screenshot-confirmed (scratchpad/flush_cloud.png). Replicant note: the state
receive @4aeef8 sets the alarm from the state stream — the port's reservoir state replication is not
wired, so remote clouds are deferred (solo/master path complete).
## The four systemic checks (every subsystem)
See [[reconstruction-gotchas]]: (1) shadowing (re-declared engine-base fields), (2) the `Wword` trap,
(3) message-handler chaining, (4) entity validity. Plus resource-struct layout (must mirror the class
+4 -1
View File
@@ -71,6 +71,7 @@ enum BTActionID
BTActAllStop,
BTActModeCycle,
BTActValve,
BTActFlush, // Gitea #7: HELD = coolant flush (InjectCoolant)
BTActConfigHold,
BTActGenerator1,
BTActGenerator2,
@@ -192,7 +193,7 @@ static const BTName sActionNames[] =
{
{"ViewToggle", BTActViewToggle}, {"LookBehind", BTActLookBehind},
{"AllStop", BTActAllStop}, {"ModeCycle", BTActModeCycle},
{"Valve", BTActValve}, {"ConfigHold", BTActConfigHold},
{"Valve", BTActValve}, {"Flush", BTActFlush}, {"ConfigHold", BTActConfigHold},
{"Generator1", BTActGenerator1}, {"Generator2", BTActGenerator2},
{"Generator3", BTActGenerator3}, {"Generator4", BTActGenerator4},
{"Reconnect", BTActReconnect},
@@ -263,6 +264,7 @@ static const char *sDefaultProfile =
"key D4 button 0x45\n"
"key G action ConfigHold\n"
"key C action Valve\n"
"key H action Flush\n"
"key M action ModeCycle\n"
"key V action ViewToggle\n"
"key B action LookBehind\n"
@@ -962,6 +964,7 @@ void
case BTActAllStop: next.allStop = 1; break;
case BTActModeCycle: next.modeCycle = 1; break;
case BTActValve: next.valve = 1; break;
case BTActFlush: next.flush = 1; break;
case BTActConfigHold: next.configHold = 1; break;
case BTActGenerator1: next.genSel = 4; break;
case BTActGenerator2: next.genSel = 5; break;
+1
View File
@@ -52,6 +52,7 @@ struct BTInputState
int allStop;
int modeCycle;
int valve;
int flush; // Gitea #7: coolant flush HELD (InjectCoolant)
int configHold;
int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect
};
+231 -43
View File
@@ -293,8 +293,12 @@ HeatWatcher::HeatWatcher(
watchedSubsystem = subsystem_resource->watchedSubsystem; // this[0x4a] = res +0xE4
heatAlarm.Initialize(3); // FUN_0041b9ec(this+0x4b, 3)
if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0 // (flags & 0xC) == 0
&& (subsystem_resource->subsystemFlags & MasterHeatSinkFlag) != 0) // flags & 0x100
// (Gitea #7 sweep) the binary @4aeb40 tests *(param_2+0x28) = the OWNER
// MECH's simulationFlags (the same misread fixed in the Reservoir ctor;
// the watcher happened to work because its resource flags mirror the
// master bits on the player mech -- read the authentic source anyway).
if ((owner->simulationFlags & SegmentCopyMask) == 0 // (flags & 0xC) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0) // flags & 0x100
{
SetPerformance(&HeatWatcher::WatchSimulation); // this[7..9] = PTR 0050e634 -> @4aeac4
}
@@ -559,17 +563,28 @@ Reservoir::Reservoir(
coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57")
reservoirAlarm.SetLevel(0); // FUN_0041bbd8(this+0x74, 0) -- inject flag = 0 (was redundant injectActive=0)
if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0
&& (subsystem_resource->subsystemFlags & MasterHeatSinkFlag) != 0)
// Gitea #7 gate correction: the binary @4af408 tests *(param_2+0x28) --
// param_2 is the OWNER MECH (the factory passes the mech as arg 2), so
// this is owner->simulationFlags, NOT the resource's subsystemFlags (the
// old resource read was false on the player mech -> no CoolantSimulation
// Performance -> the flush never drained). Same gate the AggregateHeatSink
// ctor (@4ae8d0, below) already reads correctly. [T1]
if ((owner->simulationFlags & SegmentCopyMask) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0)
{
SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78
HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59)
link->Attach(this); // (**(link+0x1d8+4))(link+0x1d8, this)
// The binary reads *(float*)(link+0x1d0) -- the master heat sink's first OWN
// field (HeatSink now ends exactly at 0x1d0, so this is the master subclass's
// own scale word). field_1d0 was deleted from HeatSink; mirror the raw byte
// read (the master's dynamic subclass isn't known statically here).
Scalar masterScale = *(Scalar *)((char *)link + 0x1d0);
// Gitea #7 decode correction: the binary loads `*(int *)(link+0x1d0)`
// and FILDs it -- `(float10)*(int *)(iVar1 + 0x1d0)` @4af408 -- i.e.
// the master bank's INTEGER HeatSinkCount (AggregateHeatSink @0x1D0),
// not a float scale word. CoolantCapacity = 0.05 x heatSinkCount x
// streamed capacity (MadCat: 0.05 x 14 x 3800 = 2660). The old raw
// FLOAT reinterpret read the int bits as ~1e-44 -> capacity ~0 (latent
// while the master-gate bug kept this branch dead). The master's
// dynamic subclass isn't known statically here, so the read stays a
// guarded raw offset -- but of the documented int slot.
Scalar masterScale = (Scalar)*(int *)((char *)link + 0x1d0); // FILD heatSinkCount
thermalCapacity = CoolantCapacityScale * masterScale * thermalCapacity; // scale by master (coolantCapacity reuses thermalCapacity)
coolantLevel = thermalCapacity;
}
@@ -587,30 +602,80 @@ Logical Reservoir::TestInstance() const // @4af564 -> IsDerivedFrom 0x50e650
Logical Reservoir::TestClass(Mech &) { return True; }
//
// @4aee70 -- "InjectCoolant" message / per-state update. When the watched
// segment has positive count and the reservoir is active above 0 temperature,
// raise the inject alarm and zero the inject accumulator; otherwise clear it.
// @4aee70 -- the "InjectCoolant" (COOLANT FLUSH) message handler, registered
// under id 4 in the Reservoir handler table @0x50e680 (one entry, name string
// @0x50eba5 "InjectCoolant") [T1]. Gitea #7.
//
Logical
Reservoir::HandleMessage(int message) // FUN_004aee70
// Decode (task-#7-flush correction of the old best-effort body): the binary
// bVar1 = FUN_004ac9c8(this); // the novice lockout gate (the
// // same gate MoveValve uses; reads
// // owning BTPlayer @mech+0x190,
// // +0x274 experience level == 0)
// if (!bVar1) {
// if (*(int*)(msg+0xc) < 1) // RELEASE -> stop the flush
// SetLevel(alarm@0x1d0, 0);
// else { // PRESS (or hold)
// if (level@0x1e4 != 1 && _DAT_004aeedc(=0.0f) < *(float*)(this+0x12C))
// SetLevel(alarm@0x1d0, 1); // ... gate is +0x12C = COOLANT-
// *(this+0x228) = 0; // LEVEL (the old recon misread it
// } // as currentTemperature@0x114:
// *(ushort*)(this+0x18) |= 1; // can't flush an EMPTY tank)
// }
// While the alarm is 1 the CoolantSimulation Performance runs InjectCoolant
// every frame (the drain the coolant gauge reads); the audio watcher bound to
// ReservoirState fires the CoolantDump layers on the 0/1 transitions, and the
// view side spawns the condensation cloud (flush.pfx, psfx 19 -- BTDPL.INI
// "Coolant flush") on the 0->1 edge, matching the binary's mode-1
// BTTracerEffectRenderable (built for classID 0xBC0 on the "ReservoirState"
// attr, part_014.c:5439; its tick @part_007.c:8780 starts the pfx exactly
// when the watched state CHANGES to 1).
//
void
Reservoir::InjectCoolantMessageHandler(ReceiverDataMessageOf<int> *message) // FUN_004aee70
{
if (!HeatableSubsystem::HandleMessage(message)) // FUN_004ac9c8
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
if (BTPlayerRoleLocksAdvanced(owner))
return;
if (message->dataContents < 1) // *(int*)(msg+0xc) < 1 = release
{
if (message /*->count*/ < 1)
{
reservoirAlarm.SetLevel(0); // this+0x1d0
}
else
{
if (reservoirAlarm.GetLevel() != 1 && currentTemperature > 0.0f) // injectActive == alarm level @0x1e4
{
reservoirAlarm.SetLevel(1);
}
injectAccumulator = 0; // this+0x228
}
ForceUpdate(); // this+0x18
reservoirAlarm.SetLevel(0); // this+0x1d0 -- flush OFF
}
return True;
else
{
if (reservoirAlarm.GetLevel() != 1 // injectActive == alarm level @0x1e4
&& coolantLevel > 0.0f) // _DAT_004aeedc = 0.0f; this+0x12C
{
reservoirAlarm.SetLevel(1); // flush ON
// The binary's flush-cloud renderable spawns on this 0->1 edge
// (see the header comment); the port's effect renderables are
// consolidated in the psfx layer, so the spawn hook rides the
// same transition here.
extern void BTSpawnFlushCloud(void *owner_mech); // mech4.cpp (psfx 19)
BTSpawnFlushCloud(owner);
}
injectAccumulator = 0; // this+0x228
}
ForceUpdate(); // this+0x18 |= 1
}
//
// Gitea #7 -- the Reservoir handler registration (table @0x50e680: exactly
// one entry {4, "InjectCoolant", @4aee70}). Chained onto the engine Receiver
// root set exactly like the Condenser's (task #13); function-local statics
// per the static-init-order rule (reconstruction-gotchas #9).
//
Receiver::MessageHandlerSet&
Reservoir::GetMessageHandlers()
{
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(Reservoir, InjectCoolant) // id 4 @4aee70
};
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(entries), entries,
Receiver::GetMessageHandlers()
);
return messageHandlers;
}
//
@@ -623,6 +688,22 @@ void
if (reservoirAlarm.GetLevel() == 1) // injectActive == alarm level @0x1e4
{
injectAccumulator += time_slice; // this+0x228
// DIAG (BT_FLUSH_LOG, Gitea #7): the reservoir drain the coolant
// gauge reads (vertBar C binds Reservoir/CoolantMass = coolantLevel).
if (getenv("BT_FLUSH_LOG"))
{
static Scalar s_flAcc = 1.0e9f; // log the first active frame too
s_flAcc += time_slice;
if (s_flAcc >= 0.5f)
{
s_flAcc = 0.0f;
DEBUG_STREAM << "[flush] " << GetName()
<< " level=" << coolantLevel << "/" << thermalCapacity
<< " squirtMass=" << coolantSquirtMass
<< " held=" << injectAccumulator << "s\n" << std::flush;
}
}
InjectCoolant(time_slice); // FUN_004aefa4
}
}
@@ -648,28 +729,135 @@ Scalar
}
//
// @4aefa4 (1019 bytes) -- distribute the reservoir charge across the linked
// heat-sink network. Iterates three engine collections off the owner
// (+0x7ac/+0x7bc/+0x7cc) plus the local sink, builds a work list, and for each
// member moves a clamped, time-scaled "squirt" of coolant -- bounded by the
// per-sink remaining capacity and by the reservoir's own coolantLevel -- while
// tracking the per-sink coolant-flow delta (+0x1c8). Bails out as soon as the
// reservoir's currentTemperature drops to ~0 (|temp| <= 1e-4).
// @4aefa4 (1019 bytes) -- the COOLANT FLUSH proper (Gitea #7; was a stub that
// did NOTHING -- the reported "gauge doesn't show a drop"). Distribute the
// reservoir charge across the mech's heat-sink network: build a work list
// from the owner's three capability chains -- condensers @+0x7cc (GUID
// 0x50e4fc), weapons @+0x7bc (0x511830 = MechWeapon), heatables @+0x7ac
// (0x51155c) -- plus the linked master sink (linkedSinks.Resolve @+0x164);
// then, for each member with a nonzero coolant-flow share (+0x15C -- the
// valve set% bias, manual p24 "with a bias based on your set% levels"), move
// a squirt of coolantSquirtMass x flowScale x dt coolant out of the
// reservoir into the sink (clamped to what the reservoir still holds, and
// the sink to its capacity), and credit the sink's pendingHeat (+0x1C8) with
// the NEGATIVE heat delta carried by the fresh coolant -- capped at
// sink->thermalMass x reservoir->startingTemperature (+0x154 x +0x13C, the
// coolant enters at the reservoir's base temperature) and clamped to only
// ever COOL. Bails out as soon as the reservoir runs dry
// (|coolantLevel@0x12C| <= 1e-4 -- the old stub's "currentTemperature"
// reading of +0x12C was a misdecode; +300 decimal == 0x12C == coolantLevel).
// Constants: _DAT_004af3a0 = 1e-4f, _DAT_004af3a4 = _DAT_004af3a8 = 0.0f
// (byte-verified @4af3a0).
//
// (Uses the engine collection-iterator helpers @4af9cf..@4afb96 as locals --
// see notes at the foot of this file. Body reproduced structurally; the
// per-sink arithmetic mirrors HeatSink::ComputeHeatFlow.)
// PORT NOTE (chain emulation, same pattern as BTRecomputeCondenserValves):
// the factory's capability-chain fill loops ride the SubProxy stub in the
// port (loops 2/4 add nothing), so the work list is rebuilt here by walking
// the populated roster with the SAME membership tests, in the binary's
// order: condensers, weapons, HeatSink-derived heatables, the linked master
// sink. Condensers and weapons therefore appear TWICE (they are heatable-
// derived too), the bank twice (heatable + linked sink) -- exactly the
// binary's duplicate-visit weighting. ONE guarded deviation [T2]: the
// binary's heatable chain also contains the WATCHER branch (HeatWatcher
// chains the HeatableSubsystem derivation), whose bytes at +0x15C/+0x12C are
// interior alarm state in OUR layout (writing them = the databinding trap);
// real coolant carriers are all HeatSink-derived, so the walk filters to
// HeatSink -- a watcher's incidental +0x15C in the 1995 layout is alarm
// interior there too, so the faithful economics are identical.
//
void
Reservoir::InjectCoolant(Scalar time_slice) // FUN_004aefa4
{
if (fabsf(currentTemperature) <= 1.0e-4f) // _DAT_004af3a0
if (fabsf(coolantLevel) <= 1.0e-4f) // _DAT_004af3a0; +0x12C (tank empty)
{
return;
}
// ... iterate owner collections (+0x7ac/+0x7bc/+0x7cc) and self,
// squirt clamped coolant to each linked sink, update +0x1c8 ...
// (full per-sink math recovered in heatfamily_gap.c @4aefa4)
extern int BTWeaponIsRearFiring(::Subsystem *sub); // mechweap.cpp (-1 = not a weapon)
enum { kMaxWork = 96 }; // 20-subsystem roster x duplicate passes
HeatSink *work[kMaxWork];
int workCount = 0;
Derivation &condenserClass = *Condenser::GetClassDerivations(); // 0x50e4fc
Derivation &heatSinkClass = *HeatSink::GetClassDerivations();
Entity *own = (Entity *)owner; // roster access (Entity-complete TU)
int count = own->GetSubsystemCount();
for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7cc
{
::Subsystem *s = own->GetSubsystem(i);
if (s != 0 && s->IsDerivedFrom(condenserClass))
work[workCount++] = (HeatSink *)s;
}
for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7bc
{
::Subsystem *s = own->GetSubsystem(i);
if (s != 0 && BTWeaponIsRearFiring(s) >= 0) // MechWeapon-derived
work[workCount++] = (HeatSink *)s; // (weapons are HeatSinks)
}
for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7ac
{
::Subsystem *s = own->GetSubsystem(i);
if (s != 0 && s->IsDerivedFrom(heatSinkClass)) // see PORT NOTE
work[workCount++] = (HeatSink *)s;
}
{
HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164)
if (link != 0 && workCount < kMaxWork)
work[workCount++] = link;
}
for (int w = 0; w < workCount; ++w)
{
if (fabsf(coolantLevel) <= 1.0e-4f) // ran dry mid-pass -> stop
{
return;
}
HeatSink *sink = work[w];
if (sink->coolantFlowScale == 0.0f) // +0x15C == _DAT_004af3a4
{
continue;
}
// squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0]
Scalar move = -coolantSquirtMass // this+0x224
* sink->coolantFlowScale // sink+0x15C
* time_slice;
Scalar lo = -coolantLevel; // local_14
if (move < lo) move = lo; // can't move more than the tank holds
if (move > 0.0f) move = 0.0f; // squirts only ever LEAVE the tank
// the heat delta riding the moved mass (computed BEFORE the level
// updates, as in the binary):
Scalar den = (fabsf(sink->coolantLevel) > 1.0e-4f)
? sink->coolantLevel // sink+0x12C
: sink->thermalCapacity; // sink+0x128 (empty-sink fallback)
Scalar fracSink = fabsf(move) / den; // local_20
Scalar fracRes = fabsf(move) / coolantLevel; // local_1c
Scalar heatDelta =
sink->heatEnergy * fracSink // sink+0x158
- heatEnergy * fracRes; // this+0x158
coolantLevel += move; // the reservoir DRAINS (move <= 0)
sink->coolantLevel -= move; // the sink gains
if (sink->coolantLevel >= 0.0f) // clamp to [0, capacity]
{
if (sink->coolantLevel > sink->thermalCapacity)
sink->coolantLevel = sink->thermalCapacity;
}
else
{
sink->coolantLevel = 0.0f; // _DAT_004af3a8
}
Scalar cap = sink->thermalMass // sink+0x154
* startingTemperature; // this+0x13C (reservoir base temp)
if (heatDelta > cap)
heatDelta = cap;
Scalar chill = -heatDelta;
if (chill > 0.0f) // _DAT_004af3a4 < local_2c -> clamp
chill = 0.0f; // the flush only ever COOLS
sink->pendingHeat += chill; // sink+0x1C8
}
}
//
+15 -1
View File
@@ -222,6 +222,15 @@
{
public:
static Derivation *GetClassDerivations(); // name "Reservoir"
// Gitea #7: the Reservoir handler table @0x50e680 -- exactly ONE entry,
// {4, "InjectCoolant", @4aee70} (PE bytes: 04000000 a5eb5000 70ee4a00;
// the name string @0x50eba5 = "InjectCoolant"). Same per-receiver id
// space as the Condenser's: id 4 to a condenser = MoveValve, to the
// reservoir = InjectCoolant (the COOLANT FLUSH -- manual p24: "punch
// (or hold down) the coolant button to flush fresh coolant through
// your 'Mech's loops"). Function-local statics per the static-init-
// order rule. [T1]
static Receiver::MessageHandlerSet& GetMessageHandlers();
static SharedData DefaultData;
// Attribute Support -- audio binds an AudioStateWatcher to ReservoirState;
@@ -267,7 +276,12 @@
// @4af3b0 -- slot-14 DrawCoolant override: hand out up to coolantLevel.
Scalar DrawCoolant(Scalar requested);
Logical HandleMessage(int message); // @4aee70 ("InjectCoolant" msg)
// @4aee70 -- the "InjectCoolant" (COOLANT FLUSH) message handler, id 4
// (table @0x50e680 [T1]). Press (data >= 1) starts the flush while
// coolantLevel > 0; release (data < 1) stops it.
enum { InjectCoolantMessageID = 4 };
void InjectCoolantMessageHandler(
ReceiverDataMessageOf<int> *message);
void PrintState(); // @4aeef8 (ReservoirState)
void GetState(/*...*/); // @4aef40 (slot 7 state query)
+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