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<Component*> 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<Component*> 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) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-15 21:38:15 -05:00
co-authored by Claude Opus 4.8
parent 03f0c21484
commit 5ba541ed56
9 changed files with 149 additions and 18 deletions
+8 -3
View File
@@ -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;
}
}
+3 -1
View File
@@ -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;
+6
View File
@@ -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
+50
View File
@@ -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<Component*> iterator(audioWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute();
}
{
SChainIteratorOf<Component*> iterator(videoWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute();
}
{
SChainIteratorOf<Component*> 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(),
+47 -13
View File
@@ -39,6 +39,8 @@
#include <average.hpp> // AverageOf<T>
#include <scalar.hpp> // Scalar
#include <alarm.hpp> // GaugeAlarm
#include <SCHAIN.hpp> // SChainOf<Component*> (GaugeAlarm54 watcher sockets)
class Component; // fwd -- AddAudioWatcher(Component*) defined in heat.cpp
#include <string.h>
#include <stdlib.h>
@@ -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<Component*> audioWatcherSocket; // +0x18
SChainOf<Component*> videoWatcherSocket; // +0x2c
SChainOf<Component*> 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
//
+19
View File
@@ -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(),
+13
View File
@@ -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
+2 -1
View File
@@ -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&
+1
View File
@@ -342,6 +342,7 @@ class Generator;
public:
enum {
OutputVoltageAttributeID = HeatSink::NextAttributeID,
GeneratorStateAttributeID, // audio binds an AudioStateWatcher -> stateAlarm (0x54 StateIndicator)
NextAttributeID
};
private: