User report from the first live 2-window session: throttle worked, A/D turn
did nothing. [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).
Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe). Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.
ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it. In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0. MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode. This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).
Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits). Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).
VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-reported recurrence of "legs stutter and lose sync". Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking. Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:
- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
frames; a demand change landing between them picks DIFFERENT next states
(one channel winds down, the other keeps walking) -> opposite-phase churn
(observed bs=6 ls=7 -- body on walk-R, leg on walk-L).
Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.
DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
- master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
-> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
(writing last) the displayed pose;
- IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
the BODY is the dead-reckoning PROJECTOR -- locally invisible.
v4's reading of FUN_004ab430 as the travel source was wrong.
FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split. Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).
Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes. KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry). New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dpl2d API fully recovered from the binary recorders (@487f34-488630):
opcode model (points/lines/polyline/circle/color/width/matrix/push-pop),
CallList = INLINE include (state persists to caller), centered coordinate
frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework.
- BTReticleRenderable ctor @004cc40c transcribed with the authentic
calibration (originX .35, originY .25, scaleY .5, 0..1200m right range
ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings,
turn arrows); range caret slides from the live target range fed by the
mech4 targeting step (BTSetHudTargetRange).
- Weapon pips: the binary gate is IsDerivedFrom(0x511830 =
MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing
weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL
7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A
(lit, authored PipColor) on TargetWithinRange, else dark ring B.
- AddWeapon @004cdac0 store map corrected to the verified order
(part_014.c:4827-4837); both state attrs are literally named
"SimulationState" (strings @51d526/51d577) -> weapon simulationState.
- Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster
(0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag
table added to context/decomp-reference.md.
- Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary
Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked
in context/open-questions.md with the blx_cop canopy + PNAME pip meshes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels. Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.
- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets. The mapper's
FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
[1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
field -> perpetual 0). FIX per "if it doesn't work, fix it": read the real
deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).
Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot). Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.
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>