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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
This commit is contained in:
arcattack
2026-07-25 09:19:10 -05:00
co-authored by Claude Opus 5
parent 2518e43719
commit 08977ff128
5 changed files with 182 additions and 5 deletions
+70 -1
View File
@@ -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;
}
+33
View File
@@ -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
+55 -3
View File
@@ -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
}
//
+22
View File
@@ -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
+2 -1
View File
@@ -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: