The pre-launch host drop/rejoin re-stream re-creates every mech at mission
launch; the LOBBY-built gauge tree kept attribute pointers into the freed
first-stream mechs, every widget Execute AV'd, and the BT_DEV_GAUGES SEH
guard (Gauge::GuardedExecute, GAUGE.cpp:618) disabled each one PERMANENTLY
(rate=0, no rebind) -- the frozen dev-gauges window of incident #12. The
authentic engine flow only tears the tree down at mission transitions
(Application::Shutdown -> gaugeRenderer->Shutdown() -> ShutdownImplementation
-> Remove(0), APP.cpp:787 / GAUGREND.cpp:3264); the re-stream bypassed it.
Fix: BTL4GaugeRenderer::TearDownForViewpointRestream() performs the
ENTITY-BOUND half of that same ShutdownImplementation sequence, in order
(gaugeAlarmManager->RemoveAllAlarms() -> Remove(0) -> moving/static entity
grid Clear), keeping the mission-scoped state (warehouse, graphics ports,
interpreter, controls-owned L4Lamps -- RemoveAllLamps would delete them
behind the buttonGroup's &lamp->automaticValue registrations). Called from
BTL4Application::MakeViewpointEntity before ConfigureForModel("Init") on any
viewpoint RE-make -- that handler runs once per stream (the incident log's
[ctrlmap] installing x2) -- so the tree rebuilds bound to the NEW mech via
the renderer's existing lazy build. Sentinel: "[gauge] viewpoint re-stream:
tearing the gauge tree down for rebuild".
Verified with the 2-instance relay harness (port 15600, MP_RELAY.EGG,
the incident sequence): join A (dev gauges) + B, kill B by exact PID at
WAITING FOR OPERATOR LAUNCH, rejoin B, operator launch. Pod A re-streamed
(2x ctrlmap install, 4x zonebuild), logged the sentinel, and post-launch the
docked composite is fully live (mission clock/heading/radar advancing between
BT_SHOT frames) with ZERO [gauge-fault] DISABLED lines (the incident had 11).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both surfaced today (dormant ~1 day) the moment the pod launch flags
(BT_DEV_GAUGES=1 BT_START_INSIDE=1, tools/mp_launch.sh) were used to drive in
multiplayer -- which made the same-day paint change look guilty. Confirmed on
the pre-paint build too; the trigger was the Jul-16 audio attribute work.
1) BT_DEV_GAUGES crash (cdb: CoolingLoopConnection::Update, ~15s in as the gauge
builds lazily). The dev-gauge cooling-loop lamp reached the cooling master by
RAW attribute index -- GetAttributePointer(3) + *(master+0x1d4). The audio
commits (cc2b109 ReportLeak etc.) inserted rows into the chained attribute
tables, so index 3 shifted onto a scalar, the resolve walked garbage, and the
background pass AV'd. Fix: route through a complete-type bridge reading NAMED
members (heat.cpp BTCoolingLoopFrame: linkedSinks.Resolve() +
Condenser::condenserNumber) -- the databinding rule; never a raw numeric
attribute index. Removed the now-orphaned GetAttributePointer-by-index
helper. Name-keyed samplers (InputVoltage) were unaffected.
2) -net dead controls mapper (mech wouldn't walk; turning/weapons still worked).
The RIO mapper is built from a stack SubsystemResource in btl4app.cpp that set
only name/classID/modelSize -- leaving subsystemFlags as stack GARBAGE, which
Subsystem::Subsystem copies into simulationFlags. A stray DontExecuteFlag
(0x2) froze the mapper (speedDemand stuck at 0). Config-dependent: clean in
SP, dirty in -net; the audio commits shifted the stack and flipped the bit.
Fix: memset each hand-built control-mapper resource to 0 (flags 0 =
AlwaysExecute, the faithful value -- a mapper must tick every frame).
Verified with the pod launch flags: SP alive 32s no crash; MP mech walks
(speedDemand 61.5 at full throttle, ~430u traveled), both nodes alive, no crash.
KB: reconstruction-gotchas.md gains gotcha 18 (uninitialized stack-resource
flags + the raw-attribute-index-into-a-growing-table variant) and a
verify-under-the-user's-launch-flags note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F3 distance: alDistanceModel(AL_NONE); restored the commented authored
distance multiply in Dynamic3D::CalculateSourceVolumeScale, added the same
override to Static3D, dropped the (now inert) AL_MAX_DISTANCE writes. Far
battle audio follows the authored knee/rolloff curve again, and the volume
cull / voice steal / ducking chains are distance-aware.
F4 volume law: AL_GAIN = volume_scale^2 at all 3 per-frame sites (Direct/
Dyn3D/Static3D) -- the GM CC7 squared curve; linear was ~+6 dB at mids.
F10 doppler: alDopplerFactor(0) (AL's model ran wrong constants + sign-
inverted velocity: approaching sources pitched DOWN); the AUTHORED
dopplerCents (AUDIO.INI range/speed-of-sound model, decomp-proven consumer
part_008.c:7466) now adds into the Dynamic3D pitch chain.
F15 ConfigureActivePress: published at the MechSubsystem BASE (binary id 2,
descriptor @0x50de5c -> +0x110); renamed the misnamed vitalSubsystemIndex
member (the weapon ConfigureMappables handler already drives it 0/-1).
Removed the invented Sensor/Myomers duplicates and RESTORED their byte-
exact layouts (0x328/0x358 allocs + asserts). MechWeapon's pinned-id pad
absorbed the +1 chain shift -- which matches the binary's own numbering.
F18 cook-off warning: AmmoBin FireCountdownStarted -> the existing
cookOffArmed @0x18C (binary table @0x512600); the countdown klaxon can fire.
F20 zoom blip: L4MechControlsMapper publishes TargetRangeExponent -> the live
@0x1a4 zoom member (own table chained to MechControlsMapper).
KB: replaced the bogus divisionParameters+0x10 rate read with
SystemClock::GetTicksPerSecond() (FUN_0044e19c is GetFrameRate -- original
audio frames were CLOCK TICKS).
Regression (35s drive+fire): stable; ConfigureActivePress binds real on all 9
subsystems (idle -1, zero pad redirects); FireCountdownStarted +
TargetRangeExponent bind live members; attrnull 53 -> 41; chirp still dead;
footfalls still fire (gain now correctly squared).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two real bugs found + fixed chasing the footstep volume:
1. BTL4Application::MakeAudioRenderer read sample_rate from a raw 1995-layout
offset (divisionParameters+0x10) -- the databinding trap: it yielded
-1.6e14 (measured), poisoning Renderer::calibrationRate for the ENTIRE
audio system. Every AudioTime conversion (sequence event scheduling,
compression curve durations, Seconds_To_Frames) was garbage. Sanity-clamp
to the authored 1000 frames/sec default (BT_AUDIO_LOG logs the value).
2. AudioHead::Execute fed audioFrameCount raw OS ticks; now converts
ticks -> calibrated frames via SystemClock::GetTicksPerSecond (1000ms
fallback when the static isn't measured yet), so AudioTime consumers see
the rate they were calibrated for. With rate=1000 + ms ticks the units
now align end-to-end.
Footstep status: chain verified through pulse -> matcher -> Start -> renderer;
the volume mixer's two inputs receive only value-0 AudioControlSequence events
even with correct timing -- the authored event VALUES need decoding next
(sequence tempo/divisions ctor dump; possibly Verify()-stripped tempo garbage
or the events are resets and the true volume rides another control id).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User reported never hearing any audio. Root-caused: the KB "Audio: DONE" was wrong.
The OpenAL playback chain IS fully implemented (device/buffers/sources; PlayNote
really calls alSourcePlay), but three gaps kept it silent:
1. GATED OFF (the "no sound" root cause): BTL4Application::MakeAudioRenderer returned
NULL unless the pod's AWE_FRONT/AWE_REAR AWE32-card env vars were set -- authentic
1995 pod behavior, dead on modern hardware, so the renderer was NEVER created.
FIXED: default audio ON (BT_NO_AUDIO=1 restores silence; AWE vars still force-on).
Verified: the OpenAL device now opens with no env vars ([audio] device OPENED).
2. Soundbank STUB: allPresets[2][100] (btstubs.cpp) is zero-init -> PRESET_isImplemented
false -> 0 buffers load. Sample data exists (AUDIO1/2.RES: EnginePower/LaserAFire/...)
but the event->sample map is gone (not in the decomp). STILL OPEN.
3. No triggering: the reconstructed game never creates AudioEntities on events. STILL OPEN.
So the device opens but nothing loads/plays yet -- enabling real sound needs the
soundbank reconstructed + the game triggers wired (a proper audio wave). Added a
BT_AUDIO_LOG trace harness across L4AUDRND/L4AUDRES/L4AUDLVL. KB corrected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three root causes, all fixed:
1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling
the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2
(closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35).
The observed "0.25" was the degenerate EC=1 fallback, never authored data.
2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the
release value (-65) is a negative NaN. The binary's x87 unordered compare
read it as "released"; IEEE-correct float compares latched the edge
detector shut after the first release -- the reason the emitter discharge
chain NEVER fired in-game. Fixed with bit-pattern sign compares.
3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData
(amount + damageForce=target-muzzle [the gyro directional-bounce feed] +
impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) ->
messmgr consolidation with per-weapon records + explosion bundling.
The mech4 bring-up damage block + flat kShotDamage retired to diag hooks.
Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread). Zone model
verified byte-exact (no change). Solo end-to-end: 5-record volleys (2 PPC
+ 3 ERML with authored amounts), per-weapon zone granularity, explosions,
kill. Heat stays bring-up scale pending the heat-calibration audit [T3].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a BT_VSS_LOG diagnostic (Make entry + per-subsystem classID/auxScreen dump +
the ConfigureForModel path). Verified under BT_DEV_GAUGES: the gauge renderer
builds, ConfigureForModel(Init) runs bhk1Init, and VehicleSubSystems::Make is
called with the full 33-subsystem roster -- the dispatcher iterates every
subsystem and its classID switch resolves the displayable types (0xBC3 Sensor,
0xBC6 Myomers, 0xBC8/0xBD4 Emitter/PPC, 0xBCD/0xBD0 Proj/Missile).
CONFIRMED (empirically) the Phase-2 blocker: subsystem[0x1dc] (the aux-screen
position the Make gates on) reads 0xCDCDCDCD (uninitialized) / garbage for the
displayable subsystems -> every panel hits the "Illegal aux screen position"
skip, so nothing renders yet. Phase 2 = populate the aux-screen field from the
subsystem resource. Combat remains un-regressed (the skip path is inert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pod's secondary/radar cockpit surface now renders as an inset in the
800x600 dev window under BT_DEV_GAUGES: radar/tactical grid, SPEED/HEADING/
MAGNETIC/PROP dials, and the color-coded ARMOR DAMAGE mech schematic -- real
gauge content (nzSec=27247), composited from the shared CPU pixelBuffer.
Composite pass (engine): SVGA16::DrawDevInset palette-expands the secondary
plane into a MANAGED texture on the MAIN device and draws an XYZRHW inset quad;
BTDrawGaugeInset() reaches the gauge renderer's 'sec' port and is called from
DPLRenderer::ExecuteImplementation as the last draw before EndScene.
Three reconstruction bugs fixed to get real content:
1. BTL4Application::MakeGaugeRenderer signature mismatch (REAL fix): the
reconstructed no-arg override only HID the 2007-engine's widened 3-arg
virtual MakeGaugeRenderer(int*,int*,int*), so the engine built a base
L4GaugeRenderer whose ctor never parsed gauge/l4gauge.cfg -> empty symbol
table -> "undefined label 'bhk1Init'" -> no ports/gauges. Matched the 3-arg
signature so it truly overrides (args ignored; BT is fullscreen). Same bug
class as the BTL4GaugeRenderer(false,NULL,NULL,NULL) ctor fix.
2. Parse hung on undefined primitives (gated dev accommodation): the BT gauge
primitive table (BTL4MethodDescription) is still a stub, so an unknown
primitive -> ReportParsingError -> Fail() -> a MODAL dialog freezing the
parse. Under BT_DEV_GAUGES, skip the unknown primitive's params so labels
register and the base "configure" primitive builds the ports.
3. Gauge widgets AV on NULL data bindings (gated): NumericDisplayScalar's NULL
value_pointer -> GaugeConnectionDirectOf ctor deref (bind NULL->static zero);
RankAndScore::Execute derefs unreconstructed game state (Gauge::GuardedExecute
SEH wrapper -> Disable(True) on first fault).
All guards gated on BT_DEV_GAUGES: default DEV un-regressed (TARGET DESTROYED,
0 crashes) and the pod path is byte-unchanged (strict Fail, no guards).
Remaining (docs/GAUGE_COMPOSITE.md): the real gauge WIDGET reconstruction
(BTL4MethodDescription method table + gauge->game-state data bindings), then
Step 2 (RP<->BT MFD port-name reconcile) + Step 3 (2-window layout).
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>