Respawn-reset audit: valve restore + peer smoke cleanup (both binary-grounded)

Operator reports audited vs the binary (full matrix in RESPAWN_REARM_PLAN
addendum):
- VALVES (real gap): Condenser reset @004ae534 was missing from the decomp
  export -- raw disasm shows it chains HEATSINK (coolant refill runs; the
  old body chained HeatableSubsystem per the stale TCP shard) then, respawn-
  side, resets valveState to detent 1 and restores massScale from
  refrigerationFactor.  Mech::Reset now also runs the binary's tail call
  (@0049f788 BTRecomputeCondenserValves) so flow fractions rebuild from the
  reset detents.  Bench: detent 5 -> death -> "[respawn] Condenser1 valve
  detent 5 -> 1".
- #129 SMOKE (real gap, peers-only): the replicant un-wreck edge rebuilt
  the model without the @004d0c14 per-entity effect cleanup, so the
  observer's last 10s wreck-plume window rode the teleport onto the fresh
  mech.  BTStopEntityPfx now runs on the edge; bench shows no plume line
  after any un-wreck until the next death.
- AUTHENTIC (no fix): weapon->generator taps persist (@004b0e6c only
  resolves the link) and MFD display/control modes persist (mapper vtables
  0050f45c/0051e440 slots 8-11 = plain root bodies, read from the exe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-04 16:14:10 -05:00
co-authored by Claude Fable 5
parent b6656e35c5
commit 26ab2fee3a
5 changed files with 101 additions and 11 deletions
+17 -1
View File
@@ -254,4 +254,20 @@ Keep all three unconditional (matchlog only arms on `-net`, `matchlog.cpp:66-76`
7. **Do not remove the entity.** Nothing in this plan may issue `DestroyEntityMessage` on death — the wreck stays (P5 teardown crash).
8. **Don't reconstruct dtor glue** while touching the heat/power chain terminus; the trailing base-dtor calls run the chain twice.
9. **`AmmoBin +0x228`** is written to 0 by `@004bd26c` alongside `feedTimer`/`cookOffArmed` and is the object's last dword (`sizeof == 0x22C`); our `ammobin.hpp:270` calls it `reserved // unused`. It is a live per-feed latch — identify it from `AmmoBinSimulation @004bd394` / `FeedAmmo @004bd4f4` before shipping the refill move, or copy the write blind and flag it [T3].
10. **Roster-order sensitivity is a non-risk.** `PoweredSubsystem::RTIS @004b0e6c` gates the electrical restore on the source Generator's `stateAlarm == 2`, but `Generator @004b215c` sets level 2 **unconditionally** (`0x4b2194`) and the death sweep already ran `DeathReset(0)` over the whole roster with no index skip — so no authored segment order can produce a partial re-arm. Do not add ordering logic to `Mech::Reset`.
10. **Roster-order sensitivity is a non-risk.** `PoweredSubsystem::RTIS @004b0e6c` gates the electrical restore on the source Generator's `stateAlarm == 2`, but `Generator @004b215c` sets level 2 **unconditionally** (`0x4b2194`) and the death sweep already ran `DeathReset(0)` over the whole roster with no index skip — so no authored segment order can produce a partial re-arm. Do not add ordering logic to `Mech::Reset`.
---
# ADDENDUM 2026-08-04 — the full-reset audit (operator request): valves, gensel, MFD modes, spawn smoke
Steps 0-5 above all landed via #55 and are byte-consistent (re-audited today).
Four operator/field reports were then grounded and dispositioned:
| report | binary ground truth | disposition |
|---|---|---|
| coolant valves survive respawn | `@004ae534` (MISSING from the Ghidra export -- raw disasm): chains **HeatSink** `0x4ad760` (coolant refill RUNS), then arg-gated `valveState=1` + `massScale=refrigerationFactor`; `Mech::Reset` tail (`call 0x49f788`) re-runs RecomputeCondenserValves | **PORT GAP -> FIXED** (heat.cpp authentic body + mech4.cpp Reset tail call). Bench: detent 5 -> death -> `[respawn] Condenser1 valve detent 5 -> 1` |
| generator switching survives | `@004b215c` sets `generatorOn=1` (landed, #55); `@004b0e6c` only RESOLVES the source link -- **the weapon->generator taps are never rewritten** | gens re-light = already correct; **taps persisting = AUTHENTIC** |
| MFD display mode / control mode survive | mapper vtables `0050f45c` + `0051e440` slots 8-11 read from the shipped exe = plain root bodies -- **displayMode/controlMode have NO reset anywhere** | **AUTHENTIC persist** -- do not "fix" |
| #129 fresh spawns smoke briefly | the pod's respawn ran the per-entity effect cleanup `@004d0c14` | **PORT GAP -> FIXED**: only the master's Mech::Reset ran the cleanup; the REPLICANT un-wreck edge (mechdmg.cpp MechDeathHandler::Tick) rebuilt without it, so the observer's last 10s plume window rode the teleport (peers-only symptom). BTStopEntityPfx now runs on the un-wreck edge |
Bench: `scratchpad/night11/respawnreset.sh` (BT_VALVE_TEST press -> BT_MP_FORCE_DMG
kill -> read `[valve]`/`[respawn]` on A, plume/un-wreck ordering on B).
+28 -10
View File
@@ -359,22 +359,40 @@ Condenser::~Condenser()
#endif
//###########################################################################
// ResetToInitialState -- Condenser (HEAT.TCP)
// ResetToInitialState -- Condenser
//
// RE-TRANSCRIBED FROM THE BINARY (respawn-reset audit 2026-08-04; the function
// is MISSING from the Ghidra export -- disassembled raw, tools/disas2.py
// 0x4ae534 0x60). The old body chained HeatableSubsystem (temp+heatLoad only,
// per the stale HEAT.TCP shard) -- the binary chains HEATSINK (call 0x4ad760),
// so the coolant refill DOES run for a condenser; then, RESPAWN-side only:
//
// test esi, esi ; je ... -> if (powered) {
// [this+0x1d0] = 1 -> valveState = 1 (default detent --
// the pod RESETS every coolant valve)
// eax = [this+0x1d8]; [0x160] = eax -> massScale = refrigerationFactor
// (restore the refrigeration output) }
//
// Mech::Reset's tail then re-runs BTRecomputeCondenserValves (@0049f788), so
// the flow fractions rebuild from the reset detents (mech4.cpp). This is the
// operator-reported "coolant valves survive a respawn" gap [was T3, now T1].
//
void
Condenser::ResetToInitialState(Logical /*powered*/)
Condenser::ResetToInitialState(Logical powered)
{
HeatableSubsystem::ResetToInitialState(True);
HeatSink::ResetToInitialState(powered); // call 0x4ad760
if (powered)
{
if (getenv("BT_DEATH_LOG") && valveState != 1)
DEBUG_STREAM << "[respawn] " << GetName() << " valve detent "
<< valveState << " -> 1 (authentic @004ae534 restore)\n" << std::flush;
valveState = 1; // @0x1D0 = 1
massScale = refrigerationFactor; // @0x160 = @0x1D8
}
}
//
// 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.]
// DeathReset (Gitea #55): the respawn sweep's entry for a Condenser.
//
void
Condenser::DeathReset(int reset_command)
+11
View File
@@ -2142,6 +2142,17 @@ void
((MechSubsystem *)s)->RespawnRepair(); // fresh-mech: zone + status
}
// --- coolant-valve redistribution: the binary Reset tail calls @0049f788
// after the sweep and before ForceUpdate -- the Condenser resets just
// put every valve back at detent 1 (heat.cpp @004ae534), and this
// rebuilds the per-loop flow fractions from those defaults. Without it
// the fractions keep the previous life's split (the operator-reported
// "valves survive a respawn"). [T1 @0049fb74 + raw @004ae534]
{
extern void BTRecomputeCondenserValves(Entity *owner);
BTRecomputeCondenserValves((Entity *)this);
}
// --- locomotion pre-run + interest gates (a reset master must tick) ---
SetPreRunFlag();
if (interestCount == 0) interestCount = 1;
+8
View File
@@ -1239,6 +1239,14 @@ void
{
extern void BTRebuildMechModel(Entity *entity);
extern void BTStartWarpEffect(float x, float y, float z);
// #129 (fresh spawns smoking, peers' screens only): the wreck plume
// is re-armed LOCALLY on every node (entity-attached, 10s windows),
// and only the master's Mech::Reset ran the @004d0c14 effect
// cleanup -- the observer's last window rode the teleport and kept
// puffing on the fresh mech for up to ~16s. Kill the entity's
// effects on the un-wreck edge, same as the master path.
extern void BTStopEntityPfx(void *entity_v);
BTStopEntityPfx((void *)owner);
BTRebuildMechModel((Entity *)owner);
BTStartWarpEffect((float)owner->localOrigin.linearPosition.x,
(float)owner->localOrigin.linearPosition.y,
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# RESPAWN-RESET VERIFY (2026-08-04 audit): valve detents + peer smoke.
# A (victim): BT_VALVE_TEST moves a coolant valve to detent 5 (~10s in);
# B (killer): BT_MP_FORCE_DMG kills A; A respawns (5s). PASS:
# A log: [valve] ... detent 5 then [respawn] ... valve detent 5 -> 1
# B log: [respawn] replicant un-wrecked with NO later "wreck smoke plume
# re-armed" for that entity until the next death;
# B captures: fresh A mech smoke-free after un-wreck.
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
bt_assert_player_env
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
rm -f rr_a.log rr_b.log rrB_*.png
bt_expert_egg MP.EGG RR.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" RR.EGG
# A = victim: sets a valve, waits to die, logs the reset sweep.
BT_VALVE_TEST=1 BT_DEATH_LOG=1 BT_DMG_LOG=1 BT_MP_LOG=1 \
bt_launch rr_a.log RR.EGG 0x0C -net 1601
sleep 2
# B = killer: force-damages A's replicant; captures A's fresh spawn.
BT_MP_FORCE_DMG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 \
BT_SHOT_EVERY=150 BT_SHOT_PREFIX=rrB \
bt_launch rr_b.log RR.EGG 0x03 -net 1501
sleep 5
python ../tools/btconsole.py RR.EGG 127.0.0.1:1501 127.0.0.1:1601 > rr_relay.log 2>&1 &
RELAY=$!
sleep 200
kill $RELAY 2>/dev/null
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 3
echo "=== A valve + respawn lines:"
grep -E "\[valve\]|\[respawn\]" rr_a.log | head -12
echo "=== B un-wreck + smoke lines:"
grep -E "un-wrecked|plume" rr_b.log | head -12