From 2ae9bd43aeb2d3641cbf956491e0c1f6ef2046d1 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sun, 19 Jul 2026 15:15:44 -0500 Subject: [PATCH] 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 --- content/CONTROLS.MAP | 3 +- context/gauges-hud.md | 8 + context/subsystems.md | 31 +++ game/reconstructed/btinput.cpp | 5 +- game/reconstructed/btinput.hpp | 1 + game/reconstructed/heatfamily_reslice.cpp | 274 ++++++++++++++++++---- game/reconstructed/heatfamily_reslice.hpp | 16 +- game/reconstructed/mech4.cpp | 92 ++++++++ 8 files changed, 384 insertions(+), 46 deletions(-) diff --git a/content/CONTROLS.MAP b/content/CONTROLS.MAP index 31c847a..9083631 100644 --- a/content/CONTROLS.MAP +++ b/content/CONTROLS.MAP @@ -31,7 +31,7 @@ # LeftTrigger RightTrigger (as digital, >= 50% pressed) # 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 diff --git a/context/gauges-hud.md b/context/gauges-hud.md index eee5d7e..a883052 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -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=`. 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] diff --git a/context/subsystems.md b/context/subsystems.md index ed0abaa..c5b2e8a 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -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=` 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 diff --git a/game/reconstructed/btinput.cpp b/game/reconstructed/btinput.cpp index 2a0eceb..a2c32e9 100644 --- a/game/reconstructed/btinput.cpp +++ b/game/reconstructed/btinput.cpp @@ -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; diff --git a/game/reconstructed/btinput.hpp b/game/reconstructed/btinput.hpp index 9698e6c..031ce12 100644 --- a/game/reconstructed/btinput.hpp +++ b/game/reconstructed/btinput.hpp @@ -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 }; diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index 0722b72..ea2f78e 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -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 *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 + } } // diff --git a/game/reconstructed/heatfamily_reslice.hpp b/game/reconstructed/heatfamily_reslice.hpp index af79695..dbda458 100644 --- a/game/reconstructed/heatfamily_reslice.hpp +++ b/game/reconstructed/heatfamily_reslice.hpp @@ -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 *message); void PrintState(); // @4aeef8 (ReservoirState) void GetState(/*...*/); // @4aef40 (slot 7 state query) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 25f7f59..3b5eea1 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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=): 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 msg( + 4 /*Reservoir::InjectCoolantMessageID*/, + sizeof(ReceiverDataMessageOf), + (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