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>
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>
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>
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>
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>
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>
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>