From 1671b7d4b299df7387fe30608cb63e9155c97684 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 29 Jul 2026 19:22:01 -0500 Subject: [PATCH] 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 --- engine/MUNGA/AUDREND.cpp | 2 +- engine/MUNGA/AUDWTHR.cpp | 1 + engine/MUNGA/SIMULATE.cpp | 11 +++++++++++ engine/MUNGA/SIMULATE.h | 15 ++++++++++++++- engine/MUNGA/STATE.h | 7 +++++++ engine/MUNGA_L4/L4AUDRND.cpp | 13 +++++++++++-- game/reconstructed/mech.cpp | 30 +++++++++++++++++++++++------- game/reconstructed/mech2.cpp | 11 +++++++++++ game/reconstructed/mechdmg.cpp | 22 ++++++++++++++++++++++ 9 files changed, 101 insertions(+), 11 deletions(-) diff --git a/engine/MUNGA/AUDREND.cpp b/engine/MUNGA/AUDREND.cpp index ea5fb94..4ec8dd2 100644 --- a/engine/MUNGA/AUDREND.cpp +++ b/engine/MUNGA/AUDREND.cpp @@ -250,7 +250,7 @@ void return; } if (getenv("BT_AUDIO_SPATIAL") && message->controlID == StartAudioControlID) { - static int s_st=0; if (s_st++<40) + static int s_st=0; if (s_st++<5000) DEBUG_STREAM << "[spatial] START request src=" << (void*)audio_source << " vol=" << audio_source_volume_scale << "\n" << std::flush; } diff --git a/engine/MUNGA/AUDWTHR.cpp b/engine/MUNGA/AUDWTHR.cpp index 7f6183e..2bf0f62 100644 --- a/engine/MUNGA/AUDWTHR.cpp +++ b/engine/MUNGA/AUDWTHR.cpp @@ -1083,6 +1083,7 @@ void DEBUG_STREAM << "[statefire] trigState=" << triggerState << " old=" << old_state << " new=" << new_state << " comp=" << (void*)audioComponentSocket.GetCurrent() + << " compClass=" << (int)audioComponentSocket.GetCurrent()->GetClassID() << " ctl=" << (int)controlID << "/" << controlValue << "\n" << std::flush; } #if 1 diff --git a/engine/MUNGA/SIMULATE.cpp b/engine/MUNGA/SIMULATE.cpp index 0d7d1ef..dc69576 100644 --- a/engine/MUNGA/SIMULATE.cpp +++ b/engine/MUNGA/SIMULATE.cpp @@ -157,6 +157,17 @@ void { Check(this); Verify(new_state < stateCount); + // #78 DIAG: trap ANY 3/4 -> <=1 transition on ANY indicator, with the + // caller's return address (symbolize via tools/symcrash.py). Catches + // writers that bypass Simulation::SetSimulationState. + if (getenv("BT_AUDIO_SPATIAL") + && (currentState == 3 || currentState == 4) && new_state <= 1) + { + static int s_it=0; if (s_it++<40) + DEBUG_STREAM << "[indstomp] " << currentState << "->" << new_state + << " ind=" << (void*)this + << " ra=" << _ReturnAddress() << "\n" << std::flush; + } // //-------------------------------------------------------------------------- diff --git a/engine/MUNGA/SIMULATE.h b/engine/MUNGA/SIMULATE.h index 314fcdf..c333054 100644 --- a/engine/MUNGA/SIMULATE.h +++ b/engine/MUNGA/SIMULATE.h @@ -2,6 +2,7 @@ #include "state.h" #include "receiver.h" +#include // _ReturnAddress (the #78 simstomp diag) #include "time.h" #include "resource.h" @@ -221,7 +222,19 @@ public: {Check(this); return simulationState.GetOldState();} void SetSimulationState(unsigned new_state) - {Check(this); simulationState.SetState(new_state);} + {Check(this); + // #78 DIAG: trap the gimp-cell downgrade (3/4 -> <=1) with the + // caller's return address (symbolize via tools/symcrash.py). + if (getenv("BT_AUDIO_SPATIAL")) { + unsigned _cur = simulationState.GetState(); + if ((_cur == 3 || _cur == 4) && new_state != _cur) { + static int s_ds=0; if (s_ds++<40) + DEBUG_STREAM << "[simstomp] " << _cur << "->" << new_state + << " this=" << (void*)this + << " ra=" << _ReturnAddress() + << "\n" << std::flush; } + } + simulationState.SetState(new_state);} StateIndicator simulationState; diff --git a/engine/MUNGA/STATE.h b/engine/MUNGA/STATE.h index fb720d0..50ffbcd 100644 --- a/engine/MUNGA/STATE.h +++ b/engine/MUNGA/STATE.h @@ -47,6 +47,13 @@ public: Check(&gaugeWatcherSocket); gaugeWatcherSocket.Add(watcher); } + int DebugAudioWatcherCount() // DEBUG (#78 audio flake): registered audio watchers + { + int n = 0; + SChainIteratorOf it(audioWatcherSocket); + while (it.ReadAndNext() != NULL) ++n; + return n; + } private: SChainOf audioWatcherSocket; diff --git a/engine/MUNGA_L4/L4AUDRND.cpp b/engine/MUNGA_L4/L4AUDRND.cpp index 057dd28..f5b082a 100644 --- a/engine/MUNGA_L4/L4AUDRND.cpp +++ b/engine/MUNGA_L4/L4AUDRND.cpp @@ -593,13 +593,19 @@ void { Check(this); Check(audio_source); - + + if (getenv("BT_AUDIO_SPATIAL")) { static int s_sr=0; if (s_sr++<5000) + DEBUG_STREAM << "[startreq] src=" << (void*)audio_source + << " state=" << (int)audio_source->GetAudioSourceState() + << " class=" << (int)audio_source->GetClassID() + << "\n" << std::flush; } + // //-------------------------------------------------------------------------- // Verify that the source is stopped //-------------------------------------------------------------------------- // - Verify(audio_source->GetAudioSourceState() == StoppedAudioSourceState); + Verify(audio_source->GetAudioSourceState() == StoppedAudioSourceState); // //-------------------------------------------------------------------------- @@ -619,6 +625,9 @@ void ); if (!resources_available) { + if (getenv("BT_AUDIO_SPATIAL")) { static int s_rf=0; if (s_rf++<40) + DEBUG_STREAM << "[startreq] RESOURCE FAIL src=" << (void*)audio_source + << "\n" << std::flush; } // Audio-dropout fix: a failed acquisition can leave a PARTIAL set // (alGenSources succeeded for some slots before the pool ran dry). // A dropped transient never plays and nothing else ever released it, diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 7506876..2327d07 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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 diff --git a/game/reconstructed/mech2.cpp b/game/reconstructed/mech2.cpp index 59032e8..13395ae 100644 --- a/game/reconstructed/mech2.cpp +++ b/game/reconstructed/mech2.cpp @@ -250,6 +250,17 @@ void // StateIndicator::SetState's Verify(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); } diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index 0de6b69..6025527 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -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