From 1df2c571ee1202dc05f6dc86be1feb312db40869 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sun, 2 Aug 2026 20:05:06 -0500 Subject: [PATCH] #119: heatLoad filter restored to the POD's 28 Hz sample cadence The clipping coolant-leak voice ("warning... war... warning coo...") measured to a ReportLeak relaxation oscillation at a drained tank: draw = zoneDamage x heatLoad hunts across the authored 0.0025/0.003 hysteresis band (constants byte-verified) as the dry sink's conduction collapses and the tank trickle refills it. State machine, constants, refill dynamics and the sequencer's chase-and-cut stop (T0 AUDSEQ.cpp) are all the binary's own -- the ONE divergence was the 15-sample heatLoad filter: per-Perform in the binary = a 0.536 s window at the pod's 28 Hz, but only 0.25 s at the port's ~59 fps -- heatLoad twice as twitchy, the flap at double the pod cadence (the myomer-kinetic dt-less class, third instance). UpdateHeatLoad now accrues real time and samples at 28 Hz on any machine (catch-up capped at the 15-sample window). Per-instance clock in a static map -- the heat-family layouts are factory-size-locked (sizeof(Myomers) == 0x358 exact), no new members. Ctor/reset prime calls sample unconditionally. A/B (260 s leak-to-empty bench): trigger fires 12 -> 8, transitions 11 -> 7. The residual slow restart at a bone-dry tank is authentic 1995 behavior (same engine sequencer chase-cut); a minimum-retrigger interval would be an opt-in deviation for the operator to decide. Co-Authored-By: Claude Fable 5 --- game/reconstructed/heat.cpp | 44 +++++++++++++++++++++-- game/reconstructed/heat.hpp | 5 ++- game/reconstructed/heatfamily_reslice.cpp | 2 +- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index fe6cffe..a63f4f0 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -777,7 +777,7 @@ void { heatEnergy += pendingHeat; currentTemperature = heatEnergy / thermalMass; - UpdateHeatLoad(); + UpdateHeatLoad(time_slice); // 28 Hz filter cadence (#119) // DIAG census (BT_HEAT_LOG, viewpoint mech): PER-INSTANCE 5-s timers -- // the old shared static timer aliased to whichever instance crossed the @@ -850,8 +850,22 @@ void // @004ad7f0 -- recompute the radiated/instantaneous heat and feed it through // the running-average filter to produce the smoothed heatLoad reading. // +// POD-CADENCE SAMPLING (#119, 2026-08-02). The binary adds ONE sample per +// Perform -- a dt-less per-frame term (the myomer-kinetic class, FUN_0041c018 +// proves Performs ride the frame rate). 15 samples at the pod's 28 Hz = a +// 0.536 s smoothing window; sampled at the port's ~59 fps the window halves +// and heatLoad turns 2x as twitchy -- which doubles the cadence of the +// ReportLeak relaxation oscillation at a drained tank (draw = damage x +// heatLoad hunting across the authored 0.0025/0.003 hysteresis band), and +// THAT is the rapidly-clipping "warning coo-- war--" leak voice Oracle +// reproduced twice in 716. Sim calls pass their slice and samples accrue at +// 28 Hz on any machine; ctor/reset prime calls (negative dt) sample +// unconditionally. Per-instance clock lives in a static map (the census +// precedent above) because the heat-family layouts are factory-size-locked +// (sizeof(Myomers) == 0x358 exact) -- no new members. +// void - HeatSink::UpdateHeatLoad() + HeatSink::UpdateHeatLoad(Scalar time_slice) { radiatedHeat = currentTemperature * coolantLevel; @@ -865,7 +879,31 @@ void sample = HeatLoadMaximum; } - heatFilter.AddSample(sample); // FUN_0043ade4 + if (time_slice >= 0.0f) + { + static std::map s_filterClock; + Scalar &clock = s_filterClock[this]; + clock += time_slice; + const Scalar kPodTick = 1.0f / 28.0f; // [T1] pod Perform cadence + if (clock < kPodTick) + { + return; // heatLoad holds the last average + } + int catchUp = 0; + while (clock >= kPodTick && ++catchUp <= 15) // >15 samples saturate the window + { + clock -= kPodTick; + heatFilter.AddSample(sample); // FUN_0043ade4 + } + if (catchUp > 15) + { + clock = 0.0f; // hitch: window already saturated + } + heatLoad = heatFilter.Average(); // FUN_0043ae0b + return; + } + + heatFilter.AddSample(sample); // FUN_0043ade4 (prime call) heatLoad = heatFilter.Average(); // FUN_0043ae0b } diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index eef1827..4787412 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -512,8 +512,11 @@ inline int // Internal model helpers // public: + // time_slice >= 0: sample the filter at the POD's 28 Hz cadence (#119 + // -- the per-Perform filter is frame-rate-dependent, the myomer-kinetic + // class); negative (the ctor/reset prime calls) samples unconditionally. void - UpdateHeatLoad(); // @004ad7f0 + UpdateHeatLoad(Scalar time_slice = -1.0f); // @004ad7f0 void ClearHeatFilter(); // @004ad884 void diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index e295c4a..8fe9c96 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -1233,7 +1233,7 @@ void } heatEnergy += pendingHeat; // [0x56] += [0x72] currentTemperature = heatEnergy / thermalMass; // [0x45] = [0x56]/[0x55] - UpdateHeatLoad(); // FUN_004ad7f0 + UpdateHeatLoad(time_slice); // FUN_004ad7f0 (28 Hz cadence, #119) pendingHeat = 0.0f; Scalar target = 300.0f