diff --git a/docs/RESPAWN_REARM_PLAN.md b/docs/RESPAWN_REARM_PLAN.md index d7f3de2..1ee1aeb 100644 --- a/docs/RESPAWN_REARM_PLAN.md +++ b/docs/RESPAWN_REARM_PLAN.md @@ -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`. \ No newline at end of file +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). diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index a63f4f0..107d68b 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -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) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 1023d4a..88f69dd 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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; diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index 513b480..628c343 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -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, diff --git a/scratchpad/night11/respawnreset.sh b/scratchpad/night11/respawnreset.sh new file mode 100644 index 0000000..2182816 --- /dev/null +++ b/scratchpad/night11/respawnreset.sh @@ -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