torso twist died after a respawn: Myomers ran the inner integrator, never the wrapper

Gitea #70. Torso twist intermittently stops working after a death/respawn and
stays stopped, with the generator at a full 10000V and the voltage link
resolving fine. A stale state, not a live one.

The Torso is a PowerWatcher that mirrors the electrical level of the subsystem
it watches -- for the MadCat that is roster slot 15, Myomers -- and
torso.cpp:570 holds effectiveTwistRate at 0 whenever that mirrored level is not
Ready. So the question was never about the torso. It was: why does Myomers sit
at NoVoltage forever?

Because it never ran the state machine that would walk it back. The Myomers ctor
@004b8fec stores [0x511620] into activePerformance, and that pointer resolves to
0x4b8b9c -- whose first instruction is `call 0x4b0bd0`, which is
PoweredSubsystem::PoweredSubsystemSimulation. @004b8d18, the function this port
had registered, is the INNER drive-heat integrator that wrapper goes on to run.
We registered the inside of the onion. PoweredSubsystemSimulation is the only
code that both enters NoVoltage (source == 0) and leaves it
(NoVoltage -> Starting -> Ready), so a Myomers that lost power during the
death/reset window had no way back out.

Order is load-bearing: the base call precedes the heat-model gate in the binary.
Gating first -- as this did -- also denied the electrical machine to every
non-expert pilot, since OwnerAdvancedDamage() reads the +0x260 heat-model flag
and that is off below veteran.

Measured with BT_SELF_DAMAGE + BT_TORSO_LOG, before and after:

  pristine, before  48/48 Ready    after  38/38 Ready
  post-respawn      95/97 Ready           89/89 Ready

Two dead windows per respawn, now none, and the clean case stays clean.

The wrapper has a tail this does not yet reconstruct: @004b8bb9-0x4b8c1e runs an
effect when electricalStateAlarm == 1 and the myomer's damage zone is below 1.0,
building a small vector (const 0xbc343958, about -0.011f) against the owner's
localOrigin@0x100. Almost certainly the un-powered movement penalty. Flagged in
the KB as T4 rather than guessed at.

Worth generalizing, and noted as such in context/subsystems.md: when a
subsystem's Performance address in a PTR_LAB_* slot does not match the function
we reconstructed, suspect a wrapper that chains the base sim. The whole family
does it -- Generator and PoweredSubsystem both lead with
HeatSink::HeatSinkSimulation, and the Torso's own perf @004b5cf0 leads with
UpdateWatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-28 14:30:48 -05:00
co-authored by Claude Fable 5
parent c5537a1247
commit b70654dd11
2 changed files with 64 additions and 2 deletions
+20
View File
@@ -67,6 +67,26 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
- **WAVE 5** — Torso (aim twist/elevation; joint-I/O reconstructed; the BLH record disables torso → - **WAVE 5** — Torso (aim twist/elevation; joint-I/O reconstructed; the BLH record disables torso →
faithfully no visible twist). faithfully no visible twist).
- **WAVE 6** — Myomers (mover-coupled; structural un-stub is INERT — no live mover/heat coupling). - **WAVE 6** — Myomers (mover-coupled; structural un-stub is INERT — no live mover/heat coupling).
⚠ **The registered Performance is the WRAPPER @004b8b9c, not the inner integrator @004b8d18**
[T1, fixed 2026-07-28]. The ctor @004b8fec stores `[0x511620]` into activePerformance, and that
pointer resolves to `0x4b8b9c`, whose first instruction is `call 0x4b0bd0` =
`PoweredSubsystem::PoweredSubsystemSimulation`; @004b8d18 is the drive-heat integrator it then
runs. The port had registered the INNER function, so Myomers never advanced its own electrical
state machine — and since that machine is the only thing that both enters `NoVoltage`
(`source == 0`) and walks it back (`NoVoltage → Starting → Ready`), a Myomers that lost power in
the death/reset window stayed there **forever**. The Torso is a PowerWatcher that MIRRORS its
watched subsystem, so `torso.cpp:570` held `effectiveTwistRate` at 0 = **Gitea #70, "torso twist
stops working after a death/respawn."** Measured: 2 dead windows per respawn before, 0 after;
pristine missions never showed it. **Order is load-bearing** — the base call precedes the
heat-model gate, and gating first also denied the electrical machine to every non-expert pilot
(`OwnerAdvancedDamage()` is the +0x260 flag, off below veteran).
**General rule this establishes:** when a subsystem's Performance address in a `PTR_LAB_*` slot
does not match the function you reconstructed, check whether the real target is a wrapper that
chains the base sim — the whole family does (`Generator`/`PoweredSubsystem` lead with
`HeatSink::HeatSinkSimulation`; the Torso's own perf @004b5cf0 leads with `UpdateWatch`).
Still unreconstructed [T4]: @004b8bb9-0x4b8c1e, an effect when `electricalStateAlarm == 1` and the
myomer's damage zone is < 1.0, building a small vector (const `0xbc343958` ≈ -0.011f) against the
owner's `localOrigin@0x100` — almost certainly the un-powered movement penalty.
- **WAVE 7** — projectile/missile weapons (byte-exact; flying projectiles are a PORT reconstruction — - **WAVE 7** — projectile/missile weapons (byte-exact; flying projectiles are a PORT reconstruction —
the 2007 Entity is 0x1BC vs the binary's 0x300, so raw base-offset reads fail). the 2007 Entity is 0x1BC vs the binary's 0x300, so raw base-offset reads fail).
- **TASK #56 (2026-07-11)** — Gyroscope LIVE byte-exact (ctor @004b3778, sizeof 0x3D0 locked; - **TASK #56 (2026-07-11)** — Gyroscope LIVE byte-exact (ctor @004b3778, sizeof 0x3D0 locked;
+44 -2
View File
@@ -186,8 +186,12 @@ Myomers::Myomers(
// their accessors return neutral defaults directly. The mover handle lives at // their accessors return neutral defaults directly. The mover handle lives at
// base+0x110 (not a Myomers own field); the coupling is inert, so no init here. // base+0x110 (not a Myomers own field); the coupling is inert, so no init here.
// Register the per-tick Performance. The binary ctor @004b8fec stores the // Register the per-tick Performance. NB [0x511620] resolves to @004b8b9c,
// MyomersSimulation pointer-to-member into activePerformance (this[7..9] = // the WRAPPER (which calls PoweredSubsystemSimulation @0x4b0bd0 first and
// then the drive-heat integrator @004b8d18) -- our MyomersSimulation plays
// the wrapper's role and chains the base itself; see the note on its body.
// The binary ctor @004b8fec stores that
// pointer-to-member into activePerformance (this[7..9] =
// PTR_LAB_00511620 / DAT_00511624 / DAT_00511628) under the live-master // PTR_LAB_00511620 / DAT_00511624 / DAT_00511628) under the live-master
// guard (owner segment flags & 0xC)==0 && (flags & 0x100) -- the exact same // guard (owner segment flags & 0xC)==0 && (flags & 0x100) -- the exact same
// shape as the Gyroscope/Torso ctors (gyro this[7..9]=PTR_FUN_0050fe08, // shape as the Gyroscope/Torso ctors (gyro this[7..9]=PTR_FUN_0050fe08,
@@ -480,6 +484,44 @@ Logical Myomers::HasVoltage(Subsystem *source)
// //
void Myomers::MyomersSimulation(Scalar time_slice) void Myomers::MyomersSimulation(Scalar time_slice)
{ {
//
// CHAIN THE BASE FIRST (2026-07-28). The registered Performance in the
// binary is @004b8b9c, NOT @004b8d18: the ctor @004b8fec stores
// [0x511620] into activePerformance, and that pointer resolves to
// 0x4b8b9c, whose FIRST act is `call 0x4b0bd0` =
// PoweredSubsystem::PoweredSubsystemSimulation. @004b8d18 is the INNER
// drive-heat integrator it goes on to run. The port had registered the
// inner function and dropped the wrapper, so Myomers never advanced its
// own electrical state machine.
//
// WHY IT MATTERS: NoVoltage is only ever entered by that machine
// (`source == 0`), and only that machine walks it back
// (NoVoltage -> Starting -> Ready). With the machine never running, a
// Myomers that lost power during the death/reset window stayed at
// NoVoltage forever -- and the Torso is a PowerWatcher that MIRRORS the
// watched subsystem's electrical level, so torso.cpp:570 held
// effectiveTwistRate at 0. That is Gitea #70, "torso twist stops working
// after a death/respawn". Measured: 0 dips in 48 samples on a pristine
// mission, dips lasting seconds after a respawn, with the generator at a
// full 10000V and the voltage link resolving fine -- a stale state, not a
// live one. Every sibling chains its base the same way (Generator and
// PoweredSubsystem both call HeatSink::HeatSinkSimulation first; the
// Torso's own registered perf @004b5cf0 leads with UpdateWatch).
//
// ORDER IS LOAD-BEARING: the base call precedes the heat-model gate in the
// binary. Gating first (as this did) also denied the electrical machine
// to every NON-EXPERT pilot, since OwnerAdvancedDamage() is the +0x260
// heat-model flag and that is off below veteran.
//
PoweredSubsystemSimulation(time_slice); // @004b8bab -> call 0x4b0bd0
// UNRECONSTRUCTED [T4], flagged not guessed: @004b8bb9-0x4b8c1e runs an
// additional effect when electricalStateAlarm == 1 (NoVoltage) and the
// myomer's own damage zone is below 1.0 -- it builds a small vector
// (constant 0xbc343958 ~= -0.011f) against the owner's localOrigin@0x100.
// Almost certainly the un-powered myomer movement penalty. Not written
// here; it needs its own decode.
if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate
return; return;