From 08977ff1289d4b6f89496fbe8f9e3da47ce32e8f Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 25 Jul 2026 09:19:10 -0500 Subject: [PATCH] Gitea #55: respawn now restores COOLANT / heat / generator state (+ a Generator reset re-transcribed from the binary) THE GAP: Mech::Reset sweeps every subsystem with the virtual DeathReset (mech4.cpp:1789), but only 7 weapon/ammo classes overrode it -- the entire heat/coolant/power family fell through to the empty Subsystem::DeathReset base (SUBSYSTM.h:161), so the respawn reset was a SILENT NO-OP for them. All the ResetToInitialState bodies already existed; nothing ever called them. Hence the field report: 'respawned with drained coolant'. Added DeathReset forwarders (ResetToInitialState is not virtual, so each class with its own body needs one): HeatableSubsystem, HeatSink, Condenser, PoweredSubsystem, Generator. HeatSink's also serves Reservoir -- the coolant tank -- which has no body of its own; that is the one that refills coolant. TWO MIS-TRANSCRIPTIONS FIXED, both verified against the binary with capstone: 1. HeatSink::ResetToInitialState chained HeatableSubsystem::ResetToInitialState with the comment 'FUN_004ac22c'. But @004ac22c is Subsystem's terminus -- disassembled: it reads the damage zone at this+0xe0, clears zone+0x158, and Set_Alarm_Levels zone+0x10 and this+0x2c to 0. And the binary's HeatSink @004ad760 calls only @004ad884 (ClearHeatFilter), @004ad7f0 (UpdateHeatLoad) and @004ac22c -- never HeatableSubsystem. Worse, HeatableSubsystem's body sets currentTemperature = 300.0f, CLOBBERING the startingTemperature that HeatSink assigned four lines earlier. Dropped the call; the terminus work is already done for every subsystem by MechSubsystem::RespawnRepair (mech4.cpp:1790). 2. Generator::ResetToInitialState chained HeatableSubsystem and set outputVoltage = 0 -- which matches GNRATOR.TCP, but BT's BINARY DIVERGES from the TCP source, and the binary is what shipped. Disassembled @004b215c instruction by instruction: it chains HeatSink (so the coolant refill DOES run for a generator), then generatorOn=1, startTimer=0, stateAlarm 0 then 2, **outputVoltage = ratedVoltage**, coolantAvailable=1, coolantFlowScale=1.0, startTimer=startTime. Every offset matches our declared members exactly. The old version brought generators back from a respawn at ZERO output voltage, so energy weapons could never charge -- no recharge ring, no ready dot, nothing damaged. That is a strong candidate for #54 (David's three dead lasers). LIVE VERIFICATION (sim3.py, 3 mechs + force damage): 4 death/respawn cycles, and every heat sink logged coolant == capacity on every respawn, with temp restored to startingTemperature (77) rather than the clobbered 300. Left as a silent regression guard that only speaks if a respawn leaves coolant short. KNOWN REMAINING [T3, noted in code]: Condenser's body chains HeatableSubsystem rather than HeatSink, so a coolant loop's VALVE DETENT may still persist across a respawn -- the binary's Condenser reset is not yet decompiled. Do not claim valves are fixed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg --- game/reconstructed/heat.cpp | 71 ++++++++++++++++++++++++++++++++- game/reconstructed/heat.hpp | 33 +++++++++++++++ game/reconstructed/powersub.cpp | 58 +++++++++++++++++++++++++-- game/reconstructed/powersub.hpp | 22 ++++++++++ scratchpad/sim3.py | 3 +- 5 files changed, 182 insertions(+), 5 deletions(-) diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index c83d6a4..895fe04 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -180,6 +180,17 @@ void heatLoad = 0.0f; } +// +// DeathReset (Gitea #55): the respawn sweep's entry for a bare HeatableSubsystem. +// Before this, the sweep hit the empty `Subsystem::DeathReset` base and nothing in +// the heat family was reset on respawn at all. +// +void + HeatableSubsystem::DeathReset(int /*reset_command*/) +{ + ResetToInitialState(True); +} + //########################################################################### // TestClass -- HeatableSubsystem // @@ -337,6 +348,21 @@ void HeatableSubsystem::ResetToInitialState(True); } +// +// DeathReset (Gitea #55): the respawn sweep's entry for a Condenser -- a coolant +// loop's valve. NOTE the body above chains HeatableSubsystem's (temperature + +// heatLoad) and so does NOT run HeatSink's coolant refill, even though Condenser +// derives from HeatSink. That mirrors the existing reconstruction; whether the +// binary's Condenser reset also restores the VALVE SETTING is not established from +// the decomp yet, so valve detents may still persist across a respawn. +// [T3 -- do NOT claim valves are fixed; tracked on the issue.] +// +void + Condenser::DeathReset(int /*reset_command*/) +{ + ResetToInitialState(True); +} + //########################################################################### // TestClass / TestInstance / CreateStreamedSubsystem -- Condenser // @@ -646,7 +672,50 @@ void UpdateHeatLoad(); // FUN_004ad7f0 coolantActive = 0; - HeatableSubsystem::ResetToInitialState(True); // FUN_004ac22c + // + // CHAIN CORRECTION (Gitea #55). This used to call + // `HeatableSubsystem::ResetToInitialState(True)` with the comment + // "FUN_004ac22c" -- but @004ac22c is NOT HeatableSubsystem's body. + // Disassembled: @004ac22c reads the subsystem's damage zone (`this+0xe0`), + // clears `zone+0x158`, then `Set_Alarm_Level(zone+0x10, 0)` and + // `Set_Alarm_Level(this+0x2c, 0)` -- it is **Subsystem::ResetToInitialState**, + // the terminus that heals the subsystem's own crit zone and status alarm. + // And the binary's own HeatSink @004ad760 calls exactly three things: + // @004ad884 (ClearHeatFilter), @004ad7f0 (UpdateHeatLoad), @004ac22c -- it + // never touches HeatableSubsystem's body. + // + // Calling HeatableSubsystem's body here was actively harmful: it sets + // `currentTemperature = 300.0f`, **clobbering the `startingTemperature` this + // function assigned four lines above**. + // + // The terminus work (zone heal + status alarm) is already done for every + // subsystem by `MechSubsystem::RespawnRepair()`, which `Mech::Reset` calls + // immediately after `DeathReset` in the same loop (mech4.cpp:1790) -- so the + // chain ends here rather than duplicating it. + // +} + +// +// DeathReset -- the respawn sweep's entry point (Gitea #55). Serves HeatSink and +// every derived class that has no reset body of its own -- notably **Reservoir** +// (the coolant tank), which is what refills coolant on respawn. Before this, the +// sweep hit the empty `Subsystem::DeathReset` base and you respawned with whatever +// coolant your previous life had left (user-reported: "respawned with drained +// coolant"). +// +void + HeatSink::DeathReset(int /*reset_command*/) +{ + ResetToInitialState(True); // @004ad760 + // Gitea #55 regression guard: the respawn refill must actually land. Verified + // live 2026-07-25 -- coolant == capacity on every heat sink across 4 respawn + // cycles, and the temperature restored to startingTemperature (77) instead of + // the 300 the old HeatableSubsystem chain clobbered it with. Silent unless it + // regresses. + if (coolantLevel < thermalCapacity) + DEBUG_STREAM << "[heat] BUG (Gitea #55): respawn left coolant " + << coolantLevel << " / " << thermalCapacity << " -- not refilled\n" + << std::flush; } diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index 0f6eb8b..9df46eb 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -276,6 +276,17 @@ inline int // not new/parallel slots. ResetToInitialState takes (Logical powered) to match. virtual void ResetToInitialState(Logical powered); + // RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with + // the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo + // classes overrode it -- the whole heat/coolant/power family fell through + // to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the + // respawn reset was a silent no-op for them and coolant/heat/generator + // state persisted from the previous life. `ResetToInitialState` is NOT + // virtual here, so each class that has its own body needs its own + // forwarder (a derived class with no body inherits the nearest one -- + // e.g. Reservoir, the coolant tank, correctly uses HeatSink's). + void + DeathReset(int reset_command); virtual LWord GetStatusFlags(); virtual Logical @@ -440,6 +451,17 @@ inline int HandleMessage(int message); // slot 8, @004add6c void ResetToInitialState(Logical powered); // slot 10, @004ad760 + // RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with + // the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo + // classes overrode it -- the whole heat/coolant/power family fell through + // to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the + // respawn reset was a silent no-op for them and coolant/heat/generator + // state persisted from the previous life. `ResetToInitialState` is NOT + // virtual here, so each class that has its own body needs its own + // forwarder (a derived class with no body inherits the nearest one -- + // e.g. Reservoir, the coolant tank, correctly uses HeatSink's). + void + DeathReset(int reset_command); void PrintState(); // slot 13, @004ae050 @@ -624,6 +646,17 @@ inline int TestInstance() const; void ResetToInitialState(Logical powered); + // RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with + // the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo + // classes overrode it -- the whole heat/coolant/power family fell through + // to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the + // respawn reset was a silent no-op for them and coolant/heat/generator + // state persisted from the previous life. `ResetToInitialState` is NOT + // virtual here, so each class that has its own body needs its own + // forwarder (a derived class with no body inherits the nearest one -- + // e.g. Reservoir, the coolant tank, correctly uses HeatSink's). + void + DeathReset(int reset_command); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Simulation / overrides diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 3238254..258976b 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -426,6 +426,12 @@ Scalar // alarm goes idle; when restored it returns to "GeneratorOff" with the start // timer reset, but only while the source is actually Ready (state 2). // +void + PoweredSubsystem::DeathReset(int /*reset_command*/) +{ + ResetToInitialState(True); // @004b0e6c +} + void PoweredSubsystem::ResetToInitialState(Logical powered) { @@ -1086,10 +1092,56 @@ Logical Generator::TestInstance() const { return IsDerivedFrom(*GetClassDerivati // the generator output cold. // void - Generator::ResetToInitialState(Logical /*powered*/) + Generator::ResetToInitialState(Logical powered) { - HeatableSubsystem::ResetToInitialState(True); - outputVoltage = 0.0f; + // + // RE-TRANSCRIBED FROM THE BINARY (Gitea #55). The old body chained + // `HeatableSubsystem::ResetToInitialState` and set `outputVoltage = 0` -- which + // is what GNRATOR.TCP says (see gnrator.hpp), but **BT's binary diverges from + // the TCP source here**, and the binary is what shipped in the pod. + // + // Disassembled @004b215c, instruction for instruction: + // call 0x4ad760 (this, powered) -> HeatSink::ResetToInitialState (NOT + // HeatableSubsystem -- so the coolant + // refill DOES run for a generator) + // [this+0x1d4] = 1 -> generatorOn = 1 + // [this+0x1f0] = 0 -> startTimer = 0 + // Set_Alarm_Level(this+0x1fc, 0) -> stateAlarm 0 + // Set_Alarm_Level(this+0x1fc, 2) -> stateAlarm 2 (on line) + // eax = [this+0x1d8]; [this+0x1dc] = eax + // -> outputVoltage = ratedVoltage + // [this+0x134] = 1 -> coolantAvailable = 1 + // [this+0x15c] = 0x3f800000 -> coolantFlowScale = 1.0f + // ecx = [this+0x1ec]; [this+0x1f0] = ecx + // -> startTimer = startTime + // + // Why this matters: the old version brought a generator back from a respawn at + // **ZERO output voltage** with its state alarm untouched. Energy weapons charge + // from the generator's voltage, so a respawned pilot's lasers could never reach + // ready -- no recharge ring, no fire-ready dot, nothing damaged (a strong + // candidate for Gitea #54, David's three dead energy weapons). + // + HeatSink::ResetToInitialState(powered); // @004ad760 (was HeatableSubsystem) + + generatorOn = 1; // @0x1D4 + startTimer = 0.0f; // @0x1F0 (cleared, then seeded below) + stateAlarm.SetLevel(0); // @0x1FC + stateAlarm.SetLevel(2); // ... to GeneratorOnLine + outputVoltage = ratedVoltage; // @0x1DC = @0x1D8 -- back ON LINE + coolantAvailable = 1; // @0x134 + coolantFlowScale = 1.0f; // @0x15C + startTimer = startTime; // @0x1F0 = @0x1EC +} + +// +// DeathReset (Gitea #55): the respawn sweep's entry for a Generator. Without it +// the sweep hit the empty `Subsystem::DeathReset` base and a respawned mech kept +// its previous life's generator state. +// +void + Generator::DeathReset(int /*reset_command*/) +{ + ResetToInitialState(True); // @004b215c } // diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index 0d73758..2822dd7 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -223,6 +223,17 @@ class Generator; public: void ResetToInitialState(Logical powered = True); // slot 10, @004b0e6c + // RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with + // the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo + // classes overrode it -- the whole heat/coolant/power family fell through + // to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the + // respawn reset was a silent no-op for them and coolant/heat/generator + // state persisted from the previous life. `ResetToInitialState` is NOT + // virtual here, so each class that has its own body needs its own + // forwarder (a derived class with no body inherits the nearest one -- + // e.g. Reservoir, the coolant tank, correctly uses HeatSink's). + void + DeathReset(int reset_command); Logical HandleMessage(int message); // slot 9, @004b0efc LWord @@ -429,6 +440,17 @@ class Generator; HandleMessage(int message); // slot 9, @004b21d0 void ResetToInitialState(Logical powered = True); // slot 10 -- recovered verbatim from GNRATOR.TCP + // RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with + // the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo + // classes overrode it -- the whole heat/coolant/power family fell through + // to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the + // respawn reset was a silent no-op for them and coolant/heat/generator + // state persisted from the previous life. `ResetToInitialState` is NOT + // virtual here, so each class that has its own body needs its own + // forwarder (a derived class with no body inherits the nearest one -- + // e.g. Reservoir, the coolant tank, correctly uses HeatSink's). + void + DeathReset(int reset_command); // Source-state queries used by PoweredSubsystem / PowerWatcher. int diff --git a/scratchpad/sim3.py b/scratchpad/sim3.py index f8f17e2..7fb906d 100644 --- a/scratchpad/sim3.py +++ b/scratchpad/sim3.py @@ -67,7 +67,8 @@ for i in (1, 2, 3): "BT_AUTOFIRE": "1", # hold the trigger "BT_MATCHLOG": "1", "BT_SCORE_LOG": "1", - "BT_AUDIO_LOG": "1", # exposes AreWatchersDelayed (Gitea #59 probe) + "BT_AUDIO_LOG": "1", + "BT_HEAT_LOG": "1", # coolant/heat state (Gitea #55 probe) # exposes AreWatchersDelayed (Gitea #59 probe) "BT_LOG": os.path.join(OUT, "pod%d.log" % i), }) if i == 2: