the REVERSE DISABLED voice (#78): SimulationState 3/4 IS the trigger -- mirror the gimp level into the engine cell

User + old-timers were right; the earlier "no voice exists" verdict was wrong.
The authored mech audio has state watchers on Entity.SimulationState==3/4
(the binary's one-cell mech+0x40) that start sequence notes 29,16,40 -- two
klaxon hits then Warnings01 zone 8 (key 40-41) = the "reverse disabled"
voice line the testers remember.  The port's cell split (graphicAlarm vs
engine simulationState, gotcha #23) meant the trigger value never arrived.

- mechdmg: mirror gimp 3/4 into SetSimulationState at the leg-half crossing
  (guarded: never stomps disabled/fall/dead states).  Voice verified playing
  end-to-end on the bench: SetupPatch bank2 patch113 note=40 ->
  Warnings01_z7.wav.
- mech.cpp: BT_GIMP_SAFE_BASE_READ on all seven Simulation::ReadUpdateRecord
  sites -- gimp is monotonic per life; a record captured pre-gimp must not
  stomp the cell (the loopback otherwise perpetuates the stale value and
  restarts the warning on every damage event).
- diagnostics (all env-gated): [statefire]/[startreq]/[animind]/[gimp-sim]/
  [simstomp]/[indstomp] + StateIndicator::DebugAudioWatcherCount + raised
  spatial-log caps.  These traced the whole chain and PROVED no SetState
  path stomps the cell.

OPEN (follow-up): a RAW writer (bypasses SetState entirely; invisible to the
indicator-level trap) resets the cell between damage events -- under the
bench's 1 Hz metronome harness it restarted the sequence before the 1.8 s
voice note; sporadic real-play damage is unaffected (one edge -> full
sequence).  Needs a cdb write-watchpoint session; candidates: a recon raw
+0x2c-equivalent write or a struct copy spanning it.

Also decoded en route: the AnimationState triggers on the limp states play
EngineShiftRev01 (the downshift foley) -- working, and NOT the voice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-29 19:22:01 -05:00
co-authored by Claude Fable 5
parent 7838df2924
commit 1671b7d4b2
9 changed files with 101 additions and 11 deletions
+23 -7
View File
@@ -2176,6 +2176,22 @@ Logical
// airborne 3,4) tracks the master automatically; the case-0 / tail edge
// tests below then see the old->current transition.
//
// #78 GIMP-MONOTONIC RECORD GUARD: the binary's mech+0x40 is one cell, so its
// records always carried the live gimp level (3/4). The port's mirror write
// (mechdmg) can be STOMPED by a record whose simulationState was captured
// pre-gimp -- and the loopback then perpetuates the stale value forever
// (write captures the stomped 1, read re-applies it): the "reverse disabled"
// voice sequence restarted on every damage tick and never reached the voice
// note. Gimp is monotonic within a life, so a record carrying <=1 while the
// live state is 3/4 is stale by construction: patch the RECORD field to the
// live value before the base read -- SetState then dedupes (no edge, no
// restart) and the next WriteUpdateRecord captures 3/4, converging the loop.
#define BT_GIMP_SAFE_BASE_READ(msg) \
do { unsigned _cur = GetSimulationState(); \
if ((_cur == 3 || _cur == 4) && (msg)->simulationState <= 1) \
(msg)->simulationState = _cur; \
Simulation::ReadUpdateRecord(msg); } while (0)
void
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
{
@@ -2280,7 +2296,7 @@ void
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
} // FUN_0041bd34
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10
@@ -2310,7 +2326,7 @@ void
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
@@ -2335,7 +2351,7 @@ void
case 4: // orientation + angular-velocity re-sync
{
Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
creationTime = Now(); // @0x778 -- the dead-reckon ref time
{
EulerAngles e(Radian(record->eulerX), Radian(record->eulerY),
@@ -2374,7 +2390,7 @@ void
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10
@@ -2405,7 +2421,7 @@ void
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel);
@@ -2438,7 +2454,7 @@ void
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel);
@@ -2466,7 +2482,7 @@ void
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
BT_GIMP_SAFE_BASE_READ(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
airborneSelect = record->airborne; // @0x3f4 <- rec+0x10
+11
View File
@@ -250,6 +250,17 @@ void
// StateIndicator::SetState's Verify(state<stateCount). [T2]
if (state >= 0 && state < 0x21)
{
// #78 audio-flake diag: print the LIVE indicator address once per mech
// so a session's [attrbind] ptr can be checked against it (stale-bind
// hypothesis: watchers bound to a recreated mech's dead indicator).
if (getenv("BT_AUDIO_SPATIAL")) { static int s_ai=0;
if (s_ai++ < 6 || (state >= 22 && state <= 27 && s_ai < 200))
DEBUG_STREAM << "[animind] mech=" << (int)GetEntityID()
<< " &animationState=" << (void*)&animationState
<< " inst=" << (int)(GetInstance() == ReplicantInstance)
<< " state=" << state
<< " audioWatchers=" << animationState.DebugAudioWatcherCount()
<< "\n" << std::flush; }
animationState.SetState(state);
replicantAnimationState.SetState(state);
}
+22
View File
@@ -487,6 +487,28 @@ void
mech->graphicAlarm.SetLevel(4);
else if (leftLeg != 0 && damageLevel >= LegHalfStructure)
mech->graphicAlarm.SetLevel(3);
//
// #78 the "REVERSE DISABLED" voice: the binary's mech+0x40 is ONE
// cell -- the same 3/4 write was ALSO Entity.SimulationState, and
// the authored audio has state watchers on SimulationState==3/4
// that start the warning sequence (notes 29,16,40 -> two klaxon
// hits + the Warnings01 zone-8 "reverse disabled" voice line).
// The port split the cell (graphicAlarm vs engine simulationState,
// gotcha #23), so the voice never triggered. Mirror the gimp
// level into the engine indicator -- guarded so a fall/disabled/
// dead state (2, 5-8, 9) is never stomped; SetState dedupes, so
// the voice fires once per onset (again only on a side switch),
// which is the binary's own edge semantics.
//
if (damageLevel >= LegHalfStructure && (rightLeg != 0 || leftLeg != 0))
{
unsigned ss = mech->GetSimulationState();
if (getenv("BT_AUDIO_SPATIAL")) { static int s_gm=0; if (s_gm++<30)
DEBUG_STREAM << "[gimp-sim] ss=" << ss << " -> "
<< ((rightLeg != 0) ? 4 : 3) << "\n" << std::flush; }
if (ss <= 1 || ss == 3 || ss == 4)
mech->SetSimulationState((rightLeg != 0) ? 4 : 3);
}
}
}
else