Commit Graph
8 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 e058b6acb7 #99: trace the coolant-leak alarm end to end -- it IS implemented; found the real defect
Oracle: "alarm is not sounding despite an active leak" ... "was sounding,
resolved a leak, but there was another".

RETRACTION FIRST.  I previously told the user the coolant alarm was "genuinely
unbuilt -- there is no alarm implementation to starve".  That was wrong, and it
was asserted without checking.  The alarm is fully implemented and the entire
chain works.  Traced live, in order:

  Condenser6.ReportLeak (= HeatSink::coolantActive) changes 0 -> 1
    -> [watchpoll] CHANGE on that exact address
    -> [matchfire] val=1 -> ctl 1  (StartAudioControlID)
    -> [seqstart] the alarm AudioControlSequence, events=25 looped=1
    -> [seqsend] ctl 8/16 (select), 6/1 (volume), 1/0 (START), 2/0 (STOP), ...
       a three-part looping alarm: two chirps then an 8s sustained tone.

The authored design, read from BTL4.RES: 19 subsystems per mech each bind TWO
AudioLogicalTriggers to their ReportLeak flag -- match 1 -> Start, match 0 ->
Stop -- and ALL of them drive ONE shared alarm sequence.

SO WHY WAS IT SILENT IN .674?  Almost certainly the audio source pool, fixed
after that build in ad9dfad.  Every player log from .674 is saturated with
ACQUIRE FAILED (3k-6.5k lines each, starting ~10% in and never recovering); an
alarm that cannot acquire an OpenAL source is silent.  The bench here shows 0
acquire failures on the current build.  ⚠ NOT PROVEN: this bench has no audio
device (pool census reads live=0 pooled=0), so the control chain is verified but
final playback is not.  Field confirmation needed.

THE SECOND SYMPTOM IS A REAL, REPRODUCED DEFECT AND IT IS IN THE AUTHORED DATA.
All 19 subsystems -- Condenser1-6, GeneratorA-D, Myomers, PPC_1/2, ERMLaser_1-3,
SRM6_1/2, Avionics -- share the single alarm sequence, and EACH one's leak-clear
sends an UNCONDITIONAL Stop.  So the moment any one of them stops leaking the
alarm goes silent, even while others are still leaking.  That is exactly what was
reported.  Since both the authoring and the MUNGA watcher code are authentic,
this is 1995 behaviour, and changing it is a deliberate divergence -- flagged for
a fidelity call rather than "fixed" unilaterally.

Diagnostics added (all under the existing BT_ATTRBIND_LOG gate):
  [seqcfg]  extended: the sequence's authored event list
  [seqstart] extended: events / looped / tempo / divisionsPerBeat
  [seqrun]  RunSequence entry state (isRunning / iterator / current event)
  [seqwait] an event exists but is not yet ready, with the time delta
  [seqsend] the sequence actually emitting a control
  [idlewatch] which components receive the idle tick that advances sequences
  [hsparm] extended with &ReportLeak so audio bindings can be correlated
plus scratchpad/night8/leakaudio.sh.

NOTE ON THE PROBES, because it cost real time twice today: [seqrun] first used a
single shared static counter, which the busy sequences consumed so the alarm
sequence never printed -- reading as "RunSequence is never called".  It is now
throttled PER SEQUENCE (runProbeCount).  A capped diagnostic that is silent is
not evidence of absence; the same trap produced a false "seqsend x0" and a false
"the trigger never fires" earlier in this investigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:07:40 -05:00
Joe DiPrimaandClaude Fable 5 1671b7d4b2 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>
2026-07-29 19:22:01 -05:00
Joe DiPrimaandClaude Fable 5 ed844456af audio: gated [statefire] diagnostic on AudioStateTrigger (BT_AUDIO_SPATIAL)
Logs animation-range state-trigger fires (trigState>=5, capped 60).  Used to
prove the #78 limp audio end-to-end: entering the gimp states fires the
authored strained-servo component (statefire trigState=23 old=7 new=23,
ctl Start) -- the 1995 wounded-leg sound, reachable for the first time now
that the limp gait lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 17:40:19 -05:00
arcattackandClaude Opus 4.8 9dcb4752de Audio Phase 4c (AUDIO_FIDELITY F17/F19): impact scaling + the authored footstep feed
F17 CollisionSpeed (binary id 24 @0x4B4): real member captured as
|worldLinearVelocity| when the contact accumulator arms (0->1) -- the authored
AttackVolume [0.9,1] / Brightness [0.7,1] scales over impact speed [0,25] now
make harder hits sound louder and brighter.  ReduceButton (id 46 @0x340): real
watchable member (the keyboard rig never presses it).  UnstablePercentage
stays deferred: its sway/overspeed model @0x3F0 is the known gyro-ledger gap
(live writer unexported); binding without the model would be a stand-in.

F19 footstep feed (the invention is dead, long live the authored chain):
new [motionscalecfg/motiontrigcfg] traces recovered the authored configs --
EVERY motion watcher extracts |linearMotion| (motionValue=3); the footstep
volume mixer is fed by LocalAcceleration [0,10] -> ctl100 (per-stride kick)
+ LocalVelocity [0,0.6] -> ctl101 (0.4 base while moving).  The port never
wrote Mover::localAcceleration, so ctl100 read 0 and the old mech2.cpp
step-intensity broadcast (patch-sniffing, invented curve) fought the live
authored scale.  Now: localAcceleration.linear = d(published velocity)/dt --
EXACTLY the binary's derivation ((avgVel - prev)/avgDt into +0x1e4,
part_012.c:15186-15195) -- and the broadcast is REMOVED.

Regression (30s, walk throttle): stable; footfalls deliver through the
wholly-authored chain with per-stride VARYING gains (0.61/0.72/0.62 -- real
step dynamics, impossible under the old constant-curve invention).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:11:36 -05:00
arcattackandClaude Opus 4.8 99757a947d Audio: footstep chain traced to the END -- gait VINDICATED; audio clock 18x unit mismatch found (task #50)
MAJOR RESULT -- THE GAIT IS CORRECT, the enum was mislabeled:
LoadLocomotionClips (binary-verified slot map) proves namedClip==&animationClips[5]:
clips 6/7 = wwr/wwl (WALK cycle), 12/13 = rrr/rrl (RUN cycle -- what autodrive
uses at full throttle), 10/11 = wrr/wrl, 14/15 = rwr/rwl, 16-21 = reverse set.
The mech2.cpp "MechAnimationState" enum (0x3c-stride name table) does NOT match
the runtime clip ids -- it is the mislabel (12/13 are NOT StandToReverse).
The authored AudioStateTriggers on AnimationState (statecfg dump: states
1,2,5,8,9,10,11,14,15,16,17,20,21,22,23,26,27 -> Start) align EXACTLY with the
TRANSITION clips under the true map; the CYCLING strides (6/7, 12/13) have no
state triggers because the FootStep pulse attribute serves them -- our FootStep
reconstruction is the authentic design.  DO NOT renumber the gait.

Footstep chain, fully mapped (all empirtical):
pulse -> AudioMatchOf(match=1) -> Start on the FootFall DirectPatchSource ->
DROPPED at volume 0.  Volume path: an AudioControlMixer (2 inputs, never fed)
<- an AudioControlSplitter <- AudioControlSequence timeline events.  The
sequences DO fire -- but only value-0 events (the tick-0 initializers); the
later (real-volume) events never land.

NEW ROOT-CAUSE CANDIDATE [T3]: audio clock unit mismatch.  The renderer is
constructed with DefaultRendererRate=30 (calibrationRate: Seconds_To_Frames =
s*30) but AudioHead::Execute sets audioFrameCount = Now().ticks which advances
~550/s (measured via the [audioclock] probe) -- sequence event scheduling runs
~18x off the authored timing.  Fix candidate: calibrate the audio renderer to
the actual Now().ticks rate (or drive audioFrameCount at the calibrated 30/s).

Also corrected en route: FUN_004b9550/95b8 are MechWeapon ConfigureMappables/
ChooseButton (mapper EnterConfiguration +0x38), NOT Myomers::ConnectToMover;
+0x31c there is fireImpulse.  Myomers::speedEffect has NO binary writer found
beyond the ctor 1.0f (its 'mover coupling' stand-in is spurious).

Diagnostics added: [statecfg] AudioStateTrigger ctor dump, [split] splitter
forwards, [seqev] sequence timeline events, [audioclock] frame-rate probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 00:25:18 -05:00
arcattackandClaude Opus 4.8 5ba541ed56 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>
2026-07-15 21:38:15 -05:00
arcattackandClaude Opus 4.8 7b1e469ffa Audio: in-game TRIGGERING works -- real StateIndicator attrs + audio-watcher databinding (task #50)
Enabling audio unlocked L4AudioRenderer::CreateEntityAudioObjects, which binds
AudioStateWatchers to entity/subsystem state attrs BY NAME (attribute-pointer
databinding) and calls AddAudioWatcher on them.  The reconstruction pointed the
Mech's state attrs at the scalar read-pad `attrPad` (fine for gauges that READ a
value, fatal for a state watcher that calls a METHOD) -> AV in SChainOf::Add on
0xCDCDCDCD.  Root-caused via cdb + a BT_ATTRBIND_LOG trace in AttributeWatcher.

Mech-level state audio now REAL + driven (verified: walking fires SetupPatch +
PlayNote/alSourcePlay, patches 78/80/116):
- AnimationState (0x1f) / ReplicantAnimationState (0x20): real StateIndicators
  (0x21 states covering MechAnimationState 0..0x20), driven from SetBodyAnimation
  so footstep/gait/transition sounds fire on animation change.
- CollisionState (0x16): real 4-state StateIndicator driven from ProcessCollision
  via collisionTemporaryState (the deferred @4aa741 per-frame zero + the
  part_012.c:15406-15413 contact tail, InitialHit accumulation; the 1-vs-2 Slide
  split awaits the 0x240/0x244 floats still stubbed by fieldAt()).
- @0x44c CORRECTED: "ammoState (0/1 leaking/2 dry)" was a mislabel -> it is
  collisionTemporaryState (never read as ammo).

Bring-up guards [T3, temporary, self-clearing] so audio-on is STABLE while the
subsystem state models are still stubs (Generators/Condensers/Myomers/Torso/
Avionics/Reservoir/ControlsMapper -- ~15 subsystems, ~40 attrs: GeneratorState,
CondenserState, ReportLeak, ...): a NULL subsystem attr redirects to an inert pad
(was a fatal Fail); an AudioStateWatcher on an unconstructed StateIndicator (null/
0xCDCDCDCD) skips instead of AV.  Both pass automatically once the subsystem is
reconstructed.  Those subsystem sounds stay silent until then (their audio wave).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:40:08 -05:00
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00