From 5ba541ed56d8d4803b701c81a0b57061653ca5d8 Mon Sep 17 00:00:00 2001 From: arcattack Date: Wed, 15 Jul 2026 21:38:15 -0500 Subject: [PATCH] Audio: subsystem STATE sounds -- GaugeAlarm54 IS a StateIndicator; register Generator/Condenser/Reservoir state (task #50) The audio subsystem binds AudioStateWatchers (class 28) to per-subsystem state attrs (GeneratorState/CondenserState/ReservoirState) BY NAME. Recovered the watcher TYPE per attribute via a MakeObjectImplementation ClassID trace. Root cause of the subsystem-state crash: the binary's 0x54-byte subsystem alarm (FUN_0041b9ec, reconstructed as GaugeAlarm54) IS a StateIndicator -- its "three sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, and level@0x14 is currentState. The reconstruction had modeled that tail as an opaque `_tail`, so the sockets were never constructed -> AddAudioWatcher AV'd on 0xCDCDCDCD. - GaugeAlarm54: give it the three REAL, constructed SChainOf sockets + AddAudioWatcher/Video/Gauge; SetLevel now fires the watchers on a level CHANGE (empty sockets = no-op, so the ~all other alarms are unaffected). levelB (weapon LevelCountB "reset source") is left untouched -- weapons unchanged. sizeof stays 0x54 (static_assert holds -> SChainOf is 0x14, layout exact). - Register the state attrs -> the subsystem's own alarm (own AttributePointers[] chained to the parent): Generator.GeneratorState->stateAlarm, Condenser. CondenserState->condenserAlarm, Reservoir.ReservoirState->reservoirAlarm. Those alarms are already SetLevel'd by the sim, so the state audio fires on transition. - AudioStateWatcher guard now validates the +0x18 SOCKET (what AddAudioWatcher touches), not the +0 vtable -- GaugeAlarm54 is non-polymorphic at +0 (raw header) but has a real socket at +0x18. audiostate skips 85 -> 10 (only AmmoBin AmmoState/SimulationState left). The Logical/Scalar/Enum subsystem attrs (ReportLeak/GeneratorOn/ConfigureActivePress/ MotionState/SpeedOfTorsoHorizontal/TargetRangeExponent) stay inert (read 0, silent, non-crashing) -- they need backing members in size-locked layouts (a polish wave). Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/MUNGA/AUDWTHR.cpp | 11 +++-- engine/MUNGA/WATCHER.cpp | 4 +- engine/MUNGA_L4/L4AUDRES.cpp | 6 +++ game/reconstructed/heat.cpp | 50 +++++++++++++++++++ game/reconstructed/heat.hpp | 60 ++++++++++++++++++----- game/reconstructed/heatfamily_reslice.cpp | 19 +++++++ game/reconstructed/heatfamily_reslice.hpp | 13 +++++ game/reconstructed/powersub.cpp | 3 +- game/reconstructed/powersub.hpp | 1 + 9 files changed, 149 insertions(+), 18 deletions(-) diff --git a/engine/MUNGA/AUDWTHR.cpp b/engine/MUNGA/AUDWTHR.cpp index 5f605bc..1f3c0e0 100644 --- a/engine/MUNGA/AUDWTHR.cpp +++ b/engine/MUNGA/AUDWTHR.cpp @@ -883,14 +883,19 @@ AudioStateWatcher::AudioStateWatcher( // at +0x18 -- and AddAudioWatcher would AV in SChainOf::Add. Skip the register // (that subsystem's state audio stays silent) until the subsystem is built; this // is SELF-CLEARING (a real StateIndicator passes). See docs: audio subsystem wave. + // Validate the AUDIO SOCKET at +0x18 (what AddAudioWatcher touches), NOT the + // object's +0 vtable: a real StateIndicator has a vtable at +0, but the binary's + // 0x54 subsystem alarm (GaugeAlarm54) is non-polymorphic there (a raw header) yet + // has a real, constructed SChainOf socket at +0x18. A skip means the socket is + // unconstructed (null / debug-fill 0xCDCDCDCD) -- the inert pad or a subsystem not + // yet reconstructed. Registering there would AV in SChainOf::Add. { - unsigned vt = *(unsigned*)attributePointer; unsigned chain = *(unsigned*)((char*)attributePointer + 0x18); - if (vt == 0 || vt == 0xCDCDCDCD || chain == 0xCDCDCDCD) + if (chain == 0 || chain == 0xCDCDCDCD) { if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audiostate] skip watcher on unbuilt StateIndicator " - << attributePointer << " (vt=" << (void*)vt << ")\n" << std::flush; + << attributePointer << " (chain=" << (void*)chain << ")\n" << std::flush; return; } } diff --git a/engine/MUNGA/WATCHER.cpp b/engine/MUNGA/WATCHER.cpp index 83cd080..7acb2c0 100644 --- a/engine/MUNGA/WATCHER.cpp +++ b/engine/MUNGA/WATCHER.cpp @@ -98,7 +98,9 @@ AttributeWatcher::AttributeWatcher( if (getenv("BT_ATTRBIND_LOG")) { - DEBUG_STREAM << "[attrbind] subsys=[" << subsystem_name + extern int g_curAudioWatcherClass; + DEBUG_STREAM << "[attrbind] class=" << g_curAudioWatcherClass + << " subsys=[" << subsystem_name << "] attr=[" << attribute_name << "] -> ptr=" << attributePointer << " vtbl=" << (attributePointer ? *(void**)attributePointer : (void*)0) << "\n" << std::flush; diff --git a/engine/MUNGA_L4/L4AUDRES.cpp b/engine/MUNGA_L4/L4AUDRES.cpp index 3599765..d9b19c6 100644 --- a/engine/MUNGA_L4/L4AUDRES.cpp +++ b/engine/MUNGA_L4/L4AUDRES.cpp @@ -21,6 +21,7 @@ ALuint *g_buffers; int g_numBuffers; +int g_curAudioWatcherClass = 0; // // Minimal canonical-PCM WAV reader. The repo's libsndfile-1.dll is a STUB @@ -140,6 +141,11 @@ RegisteredClass* Check(this); Verify(class_ID != RegisteredClass::NullClassID); + // DIAG (BT_ATTRBIND_LOG): record which audio-object ClassID is being built so the + // AttributeWatcher trace can print the watcher TYPE alongside the bound attribute. + extern int g_curAudioWatcherClass; + g_curAudioWatcherClass = (int)class_ID; + // //----------------------------------------------------------------------- // HACK - Constructors should be referenced through registration, for diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index c373d2b..923a195 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -80,6 +80,37 @@ static const Scalar CoolantDrawFloor = 0.0025f; // _DAT_0050e3d8 (zero floor static const Scalar CoolantActiveOn = 0.003f; // _DAT_0050e3d4 (ON threshold) +//########################################################################### +// GaugeAlarm54 watcher sockets. The binary's 0x54 alarm (FUN_0041b9ec) IS a +// StateIndicator -- its three sub-indicators @0x18/0x2c/0x40 are the audio/ +// video/gauge watcher chains, level@0x14 is currentState. These out-of-line +// bodies let an AudioStateWatcher bind to any subsystem alarm BY NAME +// (GeneratorState / CondenserState / ReservoirState / ...); SetLevel fires the +// registered watchers on a level change, so the state audio plays on transition. +// Empty sockets iterate to nothing, so alarms with no audio watchers are a no-op. +//########################################################################### +void GaugeAlarm54::AddAudioWatcher(Component *watcher) { audioWatcherSocket.Add(watcher); } +void GaugeAlarm54::AddVideoWatcher(Component *watcher) { videoWatcherSocket.Add(watcher); } +void GaugeAlarm54::AddGaugeWatcher(Component *watcher) { gaugeWatcherSocket.Add(watcher); } + +void GaugeAlarm54::NotifyWatchers() +{ + Component *watcher; + { + SChainIteratorOf iterator(audioWatcherSocket); + while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); + } + { + SChainIteratorOf iterator(videoWatcherSocket); + while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); + } + { + SChainIteratorOf iterator(gaugeWatcherSocket); + while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); + } +} + + //########################################################################### //########################################################################### // HeatableSubsystem @@ -236,6 +267,25 @@ int //############################################################################# // Shared Data Support // +// CondenserState -> condenserAlarm (@0x1DC). Chained to HeatSink so the inherited +// coolant/temp attrs stay reachable; dense from HeatSink::NextAttributeID. +const Condenser::IndexEntry + Condenser::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Condenser, CondenserState, condenserAlarm) // @0x1DC (0x54 StateIndicator-compatible alarm) +}; + +Condenser::AttributeIndexSet& + Condenser::GetAttributeIndex() +{ + static Condenser::AttributeIndexSet attributeIndex( + ELEMENTS(Condenser::AttributePointers), + Condenser::AttributePointers, + HeatSink::GetAttributeIndex() + ); + return attributeIndex; +} + Condenser::SharedData Condenser::DefaultData( Condenser::GetClassDerivations(), diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index bc3dbb3..b1959b0 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -39,6 +39,8 @@ #include // AverageOf #include // Scalar #include // GaugeAlarm +#include // SChainOf (GaugeAlarm54 watcher sockets) +class Component; // fwd -- AddAudioWatcher(Component*) defined in heat.cpp #include #include @@ -91,26 +93,42 @@ class Mech; class GaugeAlarm54 { public: - GaugeAlarm54(int levels = 0) { levelA = levelB = levels; level = 0; } + GaugeAlarm54(int levels = 0) + : audioWatcherSocket(0), videoWatcherSocket(0), gaugeWatcherSocket(0) + { levelA = levelB = levels; level = 0; } void Initialize(int levels) { levelA = levelB = levels; level = 0; } - void SetLevel(int n) { level = n; } + void SetLevel(int n) { if (n != level) { level = n; NotifyWatchers(); } } int GetLevel() const { return level; } int Level() const { return level; } int LevelCountB() const { return levelB; } // +0x10 (the weapon update-record "reset" source) // ReconAlarm/AlarmIndicator API aliases (callers that predate the retype use these): - void SetState(unsigned n){ level = (int)n; } + void SetState(unsigned n){ SetLevel((int)n); } unsigned GetState() const { return (unsigned)level; } + // The binary's 0x54 alarm (FUN_0041b9ec) IS a StateIndicator: its "three + // sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher sockets, + // and level@0x14 is currentState. So an AudioStateWatcher can bind to any + // subsystem alarm BY NAME (GeneratorState/CondenserState/ReservoirState/...) and + // AddAudioWatcher registers on the +0x18 socket; SetLevel fires it on change. + // (Defined out-of-line in heat.cpp so this header stays free of Component/iterator.) + void AddAudioWatcher(Component *watcher); + void AddVideoWatcher(Component *watcher); + void AddGaugeWatcher(Component *watcher); +private: + void NotifyWatchers(); // fire audio/video/gauge watchers (empty sockets == no-op) protected: - // Interior mirrors FUN_0041b9ec: base GaugeAlarm header (+0x00), three count - // words at +0x0c/+0x10/+0x14, three sub-indicators at +0x18/+0x2c/+0x40. The - // STATUS level the binary reads (HeatSink+0x184 == alarm+0x14, GetStatusFlags - // @004add30 / PrintState @004ae050) is param_1[5] at +0x14, so `level` lands - // there -- a raw `subsystem+0x184` read now returns GetLevel(). - char _hdr[0x0c]; // +0x00 base GaugeAlarm (vtable + FUN_004178cc header) - int levelA; // +0x0c param_1[3] (level count) - int levelB; // +0x10 param_1[4] - int level; // +0x14 param_1[5] -- the status level (Normal/Deg/Fail) - char _tail[0x54 - 0x18]; // +0x18 three sub-indicators to 0x54 + // Interior mirrors FUN_0041b9ec / StateIndicator: base header (+0x00, a Node in + // the binary), three state words @+0x0c/+0x10/+0x14 (== stateCount/oldState/ + // currentState), three watcher sockets @+0x18/+0x2c/+0x40. level@+0x14 is the + // STATUS level the binary reads (HeatSink+0x184 == alarm+0x14). levelB@+0x10 is + // left as the weapon "reset source" (LevelCountB) -- SetLevel does NOT touch it, + // so weapon alarms are unchanged; only the watcher sockets are now real. + char _hdr[0x0c]; // +0x00 base header (vtable + FUN_004178cc header) + int levelA; // +0x0c stateCount + int levelB; // +0x10 oldState slot / weapon LevelCountB + int level; // +0x14 currentState (status level) + SChainOf audioWatcherSocket; // +0x18 + SChainOf videoWatcherSocket; // +0x2c + SChainOf gaugeWatcherSocket; // +0x40 }; // The 8-byte modeling type kept ONLY for HUD::statusAlarm (still on the old @@ -543,6 +561,22 @@ inline int static Receiver::MessageHandlerSet& GetMessageHandlers(); static SharedData DefaultData; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Attribute Support -- audio binds an AudioStateWatcher to CondenserState; + // it resolves to condenserAlarm (@0x1DC, a 0x54 StateIndicator-compatible + // GaugeAlarm54). RefrigerationSimulation SetLevel's it (2/1/0), so the + // condenser cycling audio fires on the flow-change pulse. Chained to HeatSink. + // + public: + enum { + CondenserStateAttributeID = HeatSink::NextAttributeID, + NextAttributeID + }; + private: + static const IndexEntry AttributePointers[]; + public: + static AttributeIndexSet& GetAttributeIndex(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support // diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index 5fa2f1f..4931bfe 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -494,6 +494,25 @@ int //############################################################################# // Shared Data Support // +// ReservoirState -> reservoirAlarm (@0x1D0). Chained to HeatSink so the inherited +// coolant/temp attrs stay reachable; dense from HeatSink::NextAttributeID. +const Reservoir::IndexEntry + Reservoir::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Reservoir, ReservoirState, reservoirAlarm) // @0x1D0 (0x54 StateIndicator-compatible alarm) +}; + +Reservoir::AttributeIndexSet& + Reservoir::GetAttributeIndex() +{ + static Reservoir::AttributeIndexSet attributeIndex( + ELEMENTS(Reservoir::AttributePointers), + Reservoir::AttributePointers, + HeatSink::GetAttributeIndex() + ); + return attributeIndex; +} + Reservoir::SharedData Reservoir::DefaultData( Reservoir::GetClassDerivations(), diff --git a/game/reconstructed/heatfamily_reslice.hpp b/game/reconstructed/heatfamily_reslice.hpp index aec53ff..af79695 100644 --- a/game/reconstructed/heatfamily_reslice.hpp +++ b/game/reconstructed/heatfamily_reslice.hpp @@ -224,6 +224,19 @@ static Derivation *GetClassDerivations(); // name "Reservoir" static SharedData DefaultData; + // Attribute Support -- audio binds an AudioStateWatcher to ReservoirState; + // it resolves to reservoirAlarm (@0x1D0, a 0x54 StateIndicator-compatible + // GaugeAlarm54; level@0x1e4 is the inject flag). CoolantSimulation SetLevel's + // it, so the coolant-inject audio fires on the inject on/off transition. + enum { + ReservoirStateAttributeID = HeatSink::NextAttributeID, + NextAttributeID + }; + private: + static const IndexEntry AttributePointers[]; + public: + static AttributeIndexSet& GetAttributeIndex(); + typedef Reservoir__SubsystemResource SubsystemResource; typedef void diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 95a1a03..b16eb41 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -885,7 +885,8 @@ int const Generator::IndexEntry Generator::AttributePointers[]= { - ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage) // @0x1DC + ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage), // @0x1DC + ATTRIBUTE_ENTRY(Generator, GeneratorState, stateAlarm) // @0x1FC (0x54 StateIndicator-compatible alarm; SetLevel fires audio) }; Generator::AttributeIndexSet& diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index d764bb3..9c56f86 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -342,6 +342,7 @@ class Generator; public: enum { OutputVoltageAttributeID = HeatSink::NextAttributeID, + GeneratorStateAttributeID, // audio binds an AudioStateWatcher -> stateAlarm (0x54 StateIndicator) NextAttributeID }; private: