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:
co-authored by
Claude Fable 5
parent
d9ceddb12a
commit
2ae9bd43ae
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user