Full 5-path audit of the damage economy vs the decomp (5-finder +
adversarial-verify workflow), resolving the KB self-contradiction the
binary-coverage audit flagged.
CONFIRMED AUTHENTIC as-is (no change needed):
- Energy beam (emitter.cpp): damagePortion = authored DamageAmount x
(charge/seekV)^2; the ctor x1e7 and fire x1e-7 cancel (_DAT_004bafbc
dumped from the exe = x87 80-bit 1e-7 exactly).
- Autocannon (projweap.cpp): full authored DamageAmount from resource
+0x19C delivered unmodified; the 0.0625 at :667 is the shooter's own
gyro recoil, not the round.
- Zone-armor BASE model: damageLevel += amount x damageScale[type]
(engine DAMAGE.cpp:379, called mechdmg.cpp:427), legs x0.5, 1.0=dead.
3 STAND-INS FIXED (all byte-verified against the decomp):
- A. mechdmg.cpp:451 -- read the phantom `stance` member (no binary
offset, zero writers -> perma-0), so the leg-shot-out -> fall/death
branch was DEAD. Now MovementMode() (mech+0x40, @part_012.c:6910).
- B. mechdmg.cpp:458 -- guarded the leg partial-failure graphic on the
always-0 IsAirborne() stub where the binary calls IsDisabled()
(@0049fb54 = movementMode 2||9). On a wreck the binary SUPPRESSES the
write; the stub let it corrupt graphicAlarm 9->4/3 -- the task-#52
wreck-graphic bug, now fixed AT SOURCE (was only masked by the
IsMechDestroyed latch).
- C. mech4.cpp:1551 -- flat kShotDamage=12 fed as the kill-score
damageAmount (the KB self-contradiction: task #8 claimed it retired,
but it was live). The score handler @0x4c02e4 derives the whole kill
award from it, so every kill scored identically regardless of weapon.
Now lastInflictingDamage -- the real killing-blow magnitude, latched
in TakeDamageMessageHandler (mech.cpp:624), mirroring the per-hit path
(mech4.cpp:1207). The phantom `int stance` slot is reused for the new
Scalar member (size-neutral, no layout shift); init 0 in the ctor.
DEFERRED (task #60-D, documented): the missile CLUSTER model -- the port
fires N flying rounds (net armor total authentic) vs the binary's ONE
missile with a random burstCount cluster roll (loses cluster variance +
single-zone concentration). Blocked on an OPEN decomp semantic (does
burstCount multiply armor or only the gyro kick? settle at FUN_004bef78
-> FUN_004be078 -> EXPLODE.cpp:209-210).
VERIFIED live: clean build; 2-node fight -> clean center-mass kill (no
crash, kills 0->1); [zone-armor] dump confirms per-zone armor 50-140 +
legs x0.5. NB the displayed POINTS score still reads 0 -- a SEPARATE
open gap (scoreAward + role/team/tonnage multipliers unwired); fix C
corrected the damage INPUT to that formula.
KB swept: open-questions.md (self-contradiction resolved + task #60
summary + deferred missile item), combat-damage.md (damageScale is
type-indexed not even/odd; task-#52 source fix; kill-score section),
RECONCILE.md (missile = ONE spawn not N), stale comments in mechweap.cpp
(SendDamageMessage is LIVE), mislanch.hpp, mechdmg.cpp (FUN_0049fb54 =
IsDisabled).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:
COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).
MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.
RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px. Gotcha #17 (verify the FUN_ body, not its call shape).
WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed). Polarity chain corrected end-to-end (failedState, fed by real
damage saturation). Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched). The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.
COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.
DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TASK #17 -- AUTHENTIC RADAR SYMBOLOGY (the cross-blip stand-in retired).
The recon proved the 'missing pip raster set' never existed as BT game code:
pips are the ENGINE's L4GaugeImage vector-stroke system (T0 source in tree,
L4GAUIMA.cpp == FUN_0046f0c0 line-for-line), the 'pip table' is L4Warehouse::
gaugeImageBin keyed by Entity::resourceID, and BTL4.RES ships 110 type-0x12
shapes (every mech/vehicle/building/tree). The six btl4rdr stubs are wired
to the real facilities: contacts draw their authentic model silhouettes with
LOD selection, the target gets the binary's 4px-inflated box highlight, and
player-name labels resolve via Mission::GetSmallNameBitmap (the prebuilt
64x16 egg rasters keyed by the player's bitmapindex). DECODED: the invented
'VideoObject' was Entity::owningPlayer all along (+0x190; nameID =
playerBitmapIndex@0x1E0, target = BTPlayer::objectiveMech@0x284 -- new
bridge BTPlayerObjectiveMechOf); the 'LabelledEntity' class is Landmark
(cultural.h; label path dormant -- no landmark content ships, no runtime
landmarkID writer). The L4GREND BT_DEV_GAUGES warehouse guard is removed
(its AV had a different culprit, below); resource type 18 corrected to
GaugeImageStream in decomp-reference (was mislabeled 'ModelList').
TASK #14 -- the Myomers ODR duplicate ELIMINATED: the powersub.cpp/hpp
'Myomers' (classID 0xBC3 -- actually Sensor) is retired whole; it duplicated
?DefaultData@Myomers@@ against the real class (dumpbin-verified) and /FORCE
picked the winner by link order. The real Myomers (0xBC6) now chains
PoweredSubsystem's handler set (ids 4-8) and publishes its SEVEN binary
attributes (@00511588: SpeedEffect/Current/Recommended/Min/MaxSeekVoltage-
Index/SeekVoltage/OutputVoltage) -- the old empty unchained index starved
the Myomer engineering panel of every resolve.
TASK #16 -- attribute parity: MechWeapon publishes the FULL binary table
@0x511890 (11 entries; ids renumbered to binary truth -- the port aliases
had squatted the binary's DistanceToTarget/TargetWithinRange ids; the
streamed TriggerState 0x13 binding unchanged). Binary names resolved two
TODO members: pipState -> estimatedReadyTime (attr 0x1A), and the EXT-model
flag is the binary's RearFiring (0x1B). ThermalSight LightState published.
HUD (offset conflicts) + missile-side tables (id encoding suspect)
documented for a re-dump instead of publishing blind.
THE CRASH THIS EXPOSED [T2, cdb-verified]: gotcha #11's dense-table gap
is a LATENT AV, not a guaranteed one -- the old table's 0x0D..0x12 gap
(task #5) survived on heap luck; the renumber reshuffled allocations and
Find() AV'd on a garbage entryName in WeaponCluster's PercentDone resolve.
Fixed with five named PAD entries (the mech.cpp attrPad idiom) + a
static_assert locking the pad base to PoweredSubsystem::NextAttributeID.
Gotcha #11 amended with the proof.
TASK #15 -- stale-ledger sweep: GAUGE_COMPOSITE ('composite not yet built',
Reservoir shadow, PlayerStatus/vehicleSubSystems 'remaining', valve-dormant-
until-0xBD3, sensor guard, the superseded 'Heat MFD near-static' reframe --
all banner-corrected), gauges-hud frontmatter, L4VB16 + powersub comments.
Verified live: 50/50 config attribute bindings resolve, 0 NULLs, 0 parse
skips, mech spawns and simulates 31/31 subsystems, no cross-blip fallbacks,
the pip cache fills through entity registration without the old guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
@004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
+ the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
+0x40,+0x44 AddOrErase evt/dir. No "secondary vtable @0050f498", no
"CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
(ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
was wrong). Finding [T1]: the shipped binary NEVER enables this gauge (no
SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
BT_CONFIG_TEST scripts a headless verify. @004afbc4 corrected to its real
Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
(PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.
TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
authentic recharges landed. Retired (the real impact visual flows from
each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
(+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
and the weapon sticks in Loading at level ~10000 forever (observed live).
Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
-- also fixes the I^2R integral over-heating generators on big steps.
NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
never trips solo, PPCs sustain ~11.8-damage full-charge fire. The slow
kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
counters (this regrouping UI, generator reassignment, coolant valves) are
the reconstruction queue.
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>
The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset. Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact. An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation. static_assert-locked chain (verified vs the raw ctors):
HeatSink 0x1D0
-> PoweredSubsystem 0x31C retype electricalStateAlarm@0x264 + modeAlarm@0x2B8 @004b0f74
-> MechWeapon 0x3F0 retype weaponAlarm@0x350; delete 5 phantom tail fields @004b99a8
-> Emitter 0x478 delete outputVoltage/beamLengthRatio/firingArmed aliases
+ beamHit*/beamColor/beamHitData/energyRampTime phantoms;
retype beamOrientation EulerAngles->Quaternion(16B) @004bb120
-> PPC 0x478 (no own fields) @004bb888
PoweredSubsystem -> Sensor 0x328 (no alarm; 3 own fields) @004b1d18
PoweredSubsystem -> Myomers 0x358 (no alarm) @004b8fec
New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
* alias field - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
* phantom field - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
(past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
or the value is a method local)
Non-layout fixes required in the same wave:
* outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
* MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
* DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
writes the 0x264 alarm)
The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).
Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed). LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reconstruction modeled the binary's shared alarm/connection types with undersized
stand-ins, sliding every field above them low (the 72-byte auxScreenNumber gap). Fix the
foundational heat-leaf classes byte-exact + static_assert-lock them, from the ctor decomp:
Shared types corrected:
* SubsystemConnection 4 -> 0xC (binary link node FUN_004af9cf; FUN_00417ab4 derefs +8)
* GaugeAlarm54 = 0x54 (real AlarmIndicator FUN_0041b9ec; STATUS level at +0x14,
so subsystem+0x184 == heatAlarm+0x14 == GetLevel())
WatcherGaugeAlarm now typedefs GaugeAlarm54 (Watcher branch locks stay valid).
Byte-exact + locked (ctor-verified):
* HeatSink heatEnergy@0x158 linkedSinks@0x164 heatAlarm@0x170 resource@0x1C4
pendingHeat@0x1C8, sizeof 0x1D0 (@004adda0)
* Condenser valveState@0x1D0 condenserAlarm@0x1DC (@004ae568)
* Reservoir reservoirAlarm@0x1D0 ... squirtEfficiency@0x22C, sizeof 0x230 (@4aef78)
* Generator stateAlarm@0x1FC, sizeof 0x250 (@004b225c)
* Myomers phantom moverConnection tail removed (fits 0x358)
Three systemic bug classes fixed (added to the checklist in CLAUDE.md / HARD_PROBLEMS.md):
* alias field - a subclass member re-declaring an inherited slot the ctor reuses
(Condenser refrigerationOutput==massScale@0x160; Reservoir
coolantCapacity==thermalCapacity@0x128) -> use the inherited name
* alarm-interior - a value read at alarm+0x14 modeled as a separate member
(HeatSink heatState@0x184, Reservoir injectActive@0x1e4)
-> route to alarm.GetLevel()
* phantom field - a member past the object end (Generator shortFlag@0x25C is really
*(owner+0x190)+0x25c the msg-manager, @004b0efc; Myomers
moverConnection@0x110 a write-only base slot) -> remove it
Heat conduction now reads the REAL heatEnergy=1.34e7 (not garbage); combat DESTROYED-in-8,
0 crashes, heapcheck-clean through construction.
REMAINING (measured; a distinct larger task): making PoweredSubsystem byte-exact grows it
+0x98 and cascades into MechWeapon/Emitter/PPC/Sensor/Myomers -- all model the 0x54
AlarmIndicator with 4-byte ReconAlarm / 8-byte HeatAlarm stand-ins and are short +
phantom-tailed; retyping without byte-exacting them overflows the Emitter alloc (heap
corruption). PoweredSubsystem kept on HeatAlarm(8) stand-ins (marked) pending a
subsystem-tree ALARM UNIFICATION. See docs/HARD_PROBLEMS.md P7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the measured/decompiled diagnosis of the systemic heat-leaf layout gap
(auxScreenNumber compiles to 0x194 vs binary 0x1DC, -72): duplicate temps +
spurious heatState/heatModelFlag (really alarm-internal) + SubsystemConnection
4->0xC + HeatAlarm 8->0x54 (AlarmIndicator confirmed 0x54; HeatFilter already
byte-exact). Includes the offsetof-probe measurement technique.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark SegmentArc270 (recharge dial) and BitMapInverseWipe::Execute (LeakGauge) done;
refine the remaining follow-up list with the honest blocker/scope for each (seek
plot blocked on attribute + sampler; eject wipe needs a new base; secondary lamp/
counter raw reads = the heat-leaf-branch re-base; ConfigMapGauge overlay needs the
ModeManager).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark the engineering-screen cluster-panel system complete in the spec + CLAUDE.md
§10. Render-verified (BT_DEV_GAUGES_DOCK): 7 authentic panels build + draw --
SENSOR CLUSTER, MYOMERS, ER MED LASER RANGE 500M x3, PPC RANGE 500M x2 -- with bar
gauges, recharge dials, and lamps; 0 crashes, combat un-regressed. Records that
the aux-screen blocker resolved cleanly via the PoweredSubsystem bridge (the fields
were already populated; no MechSubsystem core re-base was needed) and lists the
non-blocking follow-ups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverse-engineered the vehicleSubSystems config primitive end to end (Ghidra
headless, all ~28 functions). It is NOT a widget: it is the engineering-screen
(MFD) subsystem-panel system. Its Make (FUN_004cbaf0) is a per-subsystem factory
that builds a SubsystemCluster-family status panel onto one of 12 auxiliary MFD
positions, dispatching on subsystem classID (HeatSink/Myomer/Energy/Ballistic
clusters). The whole cluster family (base + 5 subclasses) + 4 btl4gau2 sub-gauges
(CoolingLoop/PowerSource/ScalarBarGauge/ConfigMapGauge) are declared in btl4gau2.hpp
but not reconstructed.
BLOCKER: the Make reads base subsystem fields subsystem[0x1dc] (aux-screen position),
[0x1e4]/[0x1e0]/[0x224] that our MechSubsystem reconstruction (ends 0x114) does not
have or populate -- so the panels render nothing until the core subsystem layout is
extended + populated from the resource parse, which touches the working combat/heat
subsystem code (regression risk). Checkpointed at full spec pending go/no-go on the
large core-touching implementation.
- docs/VEHICLE_SUBSYSTEMS.md: complete reconstruction spec (dispatch table, geometry
table, class family map, sub-gauge inventory, engine-primitive reuse, the blocker,
the Phase-1/Phase-2 plan).
- reference/ghidra_scripts/DecompVSS.java: headless address-list decompiler (reusable
for any function the assert-anchored exporter skipped).
- CLAUDE.md: record the finding in the gauge-widget notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark PlayerStatus reconstructed + registered in the remaining-widget scope note;
record the key finding that it lives in cameraInit (the mission-review camera
cockpit) not MechInit, so it is build/link/mech-cockpit-safe but not runtime-
verifiable via a normal mech egg. vehicleSubSystems (from-scratch, 26 uses)
remains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both need a full multi-function reconstruction (not the registration-glue the
other widgets were): PlayerStatus's ctor/Execute/helpers (CreateMutantPixelmap8,
PlayerStatusMappingGroup, App Players-node lookup) are comment-only stubs and its
existing Make/header ctor are buggy (drop the graphics_port_number param, pass
entity/renderer as x/y); vehicleSubSystems is undeclared/from-scratch. Records
the decomp addresses + the ctor arg order for a focused follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update GAUGE_COMPOSITE.md + CLAUDE.md: the radar plots the enemy contact inside
the FOV wedge. Record the two fixes (engine RebuildEntityGrid populating the
dropped entity feed; the delta-relative blip projection replacing the wrong
camera-matrix point transform) and narrow the remaining map work to the authentic
pip/name infrastructure + SetTargetRange zoom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GAUGE_COMPOSITE.md + CLAUDE.md: add the map/MapDisplay radar (increment 6, the
view wedge renders) to the done list; note the systemic pattern (a newly-consumed
reconstructed gauge is the first reader of stubbed helpers -- the map exposed 3);
narrow the remaining gauge widgets to PlayerStatus + vehicleSubSystems, and list
the map follow-ups (contact classification / stubbed pip+name infra, SetTargetRange
zoom).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GAUGE_COMPOSITE.md + CLAUDE.md: add the OneOfSeveral/oneOfSeveralPixInt button-
lamp reconstruction to the done list; narrow the remaining widgets to map (radar
marquee), PlayerStatus, vehicleSubSystems, plus the follow-ups to make the
duck/searchlight buttons dynamic (extend the Mech table to 0x37 + a Searchlight
table).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the cockpit-gauge attribute-pointer system in GAUGE_COMPOSITE.md
(§Attribute wave) and CLAUDE.md §10: how a Subsystem/Attribute or bare-Attribute
binding resolves (ParseAttribute -> FindSubsystem/GetName -> GetAttributePointer
-> activeAttributeIndex), what a class must publish (AttributeID enum +
AttributePointers[] + GetAttributeIndex chained + DefaultData wiring), the
DENSE-TABLE HAZARD (Build leaves gap slots uninitialized, Find strcmps every
slot -> tables must be a dense prefix), and the base-primitive vs BT-specific
widget split. Records the 4 committed increments (HeatSink table, Mech table,
vertBar, segmentArcRatio) and the follow-ups (oneOfSeveralPixInt/map/PlayerStatus/
vehicleSubSystems, the #if0'd HeatSink-bank AmbientTemperature, the Reservoir
coolantCapacity shadow).
Promote the two reverse-engineering helpers from the session scratchpad into
tools/ (durable): disas2.py (capstone + PE parse -- recovers x87 float math
Ghidra drops; FUN_004dcd94=round, FUN_004dcd00=fabs) and vtdump.py (dump a class
vtable to find an override the assert-anchored decomp never exported, e.g.
SegmentArcRatio::Execute). Doc references updated scratchpad/ -> tools/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconstructs ColorMapperCritical -- tints a schematic colour by a subsystem's
operational state (the "critical" secondary display mode):
- CriticalConnection (@004c3598/@004c3610): src==0 -> 0; src->simulationState==1
(DestroyedState) -> 100; else the subsystem's own damage-zone damageLevel*100.
- ColorMapperCritical::Make/ctor (@004c3ddc/@004c3e40): FindSubsystem by name +
wire the CriticalConnection.
Resolved the subsystem damageZone@0xE0 shadow: MechSubsystem::damageZone is
declared ReconDamageZone*, but the assignment (mechsub.cpp: damageZone =
(ReconDamageZone*)new DamageZone(...)) shows the pointer IS a real engine
DamageZone -- so cast it back and read damageLevel. Added public accessors
GetSimulationState()/GetDamageZoneProxy() to MechSubsystem (those fields are
protected; mirrors how HeatConnection reads the public currentTemperature).
Completes the ColorMapper family (cmHeat, cmArmor, colorMapperMultiArmor,
cmCrit). Verified: parses, builds, all subsystems resolve (0 warnings), no
/FORCE unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes), stable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconstructs colorMapperMultiArmor -- tints one schematic colour (a whole torso
section) by the WORST of up to 8 damage zones:
- MultiArmorConnection (@004c346c/@004c34f4): scans 8 zones via
entity->damageZones[idx], keeps the max damageLevel, count==0 ? 100 :
Round(worst*100).
- ColorMapperMultiArmor::Make/ctor (@004c3c48/@004c3d60): resolves 8 named
zones to indices via Entity::GetDamageZoneIndex (13-param methodDescription).
ALSO fixes a latent HEAP CORRUPTION that registering this widget exposed (and
that would have blocked every further widget): GaugeInterpreter's bytecode
buffer is a fixed interpreterTableSize=46864 (GAUGREND.h) tuned for RP's config,
and its Insert() bounds guard is a Verify() that compiles out at DEBUG_LEVEL 0.
As each BT gauge widget is registered, more of BT's larger L4GAUGE.CFG resolves
into bytecode (vs being parse-skipped); colorMapperMultiArmor's 13 params x
hundreds of calls pushed the total past 46864 -> silent overflow past the char[]
-> heap smash detected later in mission bitmap loading (not the gauge code).
Fix: interpreterTableSize -> 262144 (sized for BT's full config).
Verified: parses, builds, all zones resolve, no /FORCE unresolved, combat
un-regressed (TARGET DESTROYED, 0 crashes), stable. Details: docs/GAUGE_COMPOSITE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each armor zone on the cockpit schematic is now tinted by that zone's live
damage. Render-verified: the schematic shows an all-green Blackhawk (all zones
damageLevel=0 at spawn), replacing the static green+red it showed before cmArmor
was registered -- i.e. cmArmor now owns those zone colors and shows the real
undamaged state; damaged zones shift toward red.
Reuses the ColorMapper base (from cmHeat) + two pieces reconstructed from the
binary (Ghidra dropped the x87, recovered by disassembly):
- ArmorZoneConnection (@004c33a4/@004c3430): resolves one zone from the owner's
inherited Entity::damageZones[zone_index]; per-frame feed = zone==NULL ? 100
: Round(zone->damageLevel * 100) (the 0..1 damage ratio -> 0..100 percentage
-> the colour index ColorMapper::Execute pushes into the palette slot).
- ColorMapperArmor::Make/ctor (@004c3aa4/@004c3b98): Make resolves the CFG zone
name (dz_ltorso etc., the 6th param) to an index via Entity::GetDamageZoneIndex
(== the binary's FUN_0042076c, scanning damageZones[] by name); the ctor wires
the ArmorZoneConnection.
Decomp fact (corrects the cmHeat note): the ColorMapper base ctor takes NINE
args -- an owner_ID sits between renderer and graphics_port_number. Our 8-arg
ColorMapper::ColorMapper folds owner_ID=0 in (gauges have owner 0), so it is
equivalent; cmArmor's mapping matches cmHeat plus the zone name.
Verified: parses, builds, all dz_* zones resolve (0 "not found"), no /FORCE
unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes). The dynamic
red-on-damage needs the player to take damage (the passive spawn dummy never
hits back). Details: docs/GAUGE_COMPOSITE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The visible money-shot: a green compass needle that rotates with the mech's
facing plus a numeric heading in whole degrees (render-verified: the needle
swung and the number went 247 -> 182 as the mech turned).
Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/
ShowInstance/BecameActive/Execute). Ghidra dropped the x87 float math feeding
Execute's needle endpoints and the ctor's NumericDisplay centering, so it was
recovered by disassembling BTL4OPT.EXE with capstone (scratchpad/disas_hp.py):
the needle is a radial line from innerRadius(20) to outerRadius(39) at the
heading angle, endpoints rounded half-up; the readout is round(360 - deg).
Two corrections vs the map:
- The header ctor was 12 args; the binary's is 14 -- widened it. Fields @0x98/
@0x9C are the needle's inner/outer RADIUS (Execute multiplies them by sin/cos),
not "color/spacing" as the old field names implied; renamed accordingly.
- ENGINE-CONVENTION FIX: the binary read the heading from EulerAngles index [0],
but the WinTesla EulerAngles(Quaternion) decomposition is ambiguous for a
yawing mech (the quaternion double-cover flips it to a pitch=roll=pi branch as
yaw sweeps past +/-pi -> the needle spins erratically). Switched to
YawPitchRoll, whose yaw-first .yaw is the clean continuous heading
(pitch=roll~0). Faithful to the binary's intent (heading), correct for this
engine's decomposition.
renderer->GetLinkedEntity() resolves the viewpoint mech (a GetViewpointEntity
fallback is kept belt-and-braces). Deps (NumericDisplay/GraphicsViewRecord/
GraphicGauge) are real engine classes -> no /FORCE risk; verified no unresolved
HeadingPointer externals, no parse desync, combat un-regressed (TARGET
DESTROYED, 0 crashes). Details + the disasm technique: docs/GAUGE_COMPOSITE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Begins the BT gauge WIDGET reconstruction -- making the cockpit instruments show
live data instead of just frame art. The BT gauge method table
(BTL4MethodDescription[]) was a chain-only stub with zero registered
methodDescriptions, so every BT-specific gauge keyword was parse-skipped.
Registers the first widget, cmHeat (ColorMapperHeat -- tints the heat/critical
schematic palette by a named subsystem's live temperature):
- Added ColorMapperHeat::methodDescription (keyword "cmHeat", param list
R,Md,C,St,St,St matching the binary .data) and registered it in
BTL4MethodDescription[] (chain link stays last).
- Rewired ColorMapperHeat::Make to read methodDescription.parameterList[]
(the interpreter restores each instance's parsed params there) instead of the
earlier unrecovered DAT_* placeholders -- so it uses the real CFG rate/mode/
colour-slot/palette/subsystem-name, and the runtime port arg as the graphics
port.
Registering it exposed the /FORCE trap: two REAL functions in the chain were
undefined (silently stubbed -> would AV once the gauge builds). Reconstructed
both from the decomp:
- ColorMapper::BecameActive (@004c395c): invalidate the cached colour index +
last-written RGB so the next Execute re-pushes the hardware palette.
- ColorMapperHeat::~ColorMapperHeat: empty -- the base chain (~ColorMapper ->
~Gauge) releases the HeatConnection + palettes.
Verified: register->parse->Make->ctor->FindSubsystem("GeneratorA") (resolves,
no "does not exist")->HeatConnection->Execute->palette-push runs end-to-end
under BT_DEV_GAUGES; no parse desync; combat un-regressed (TARGET DESTROYED,
0 crashes). Establishes the registration recipe + the /FORCE-check discipline
for every remaining widget. Details + priority order: docs/GAUGE_COMPOSITE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalizes the single-'sec'-inset (Milestone B) into a full 6-surface
compositor presented in its own top-level window -- the default under
BT_DEV_GAUGES (BT_DEV_GAUGES_DOCK=1 docks it into the main window instead).
A 960x384 window tiles all six pod instrument screens: Heat (COOLANT/BALANCE/
RES + condenser gauges), Comm (KILLS/DEATHS/SELECT TARGET), Mfd1/2/3 (DISPLAY/
PROGRAM/NEAREST frames), and the color radar (SCALE grid + dials + ARMOR
DAMAGE). Main 800x600 3D view is un-occluded; default DEV un-regressed
(TARGET DESTROYED after 8 hits, 0 crashes).
Design (mapped by the mfd-multisurface-map workflow):
- All 6 surfaces are bit-plane MASKS over the ONE shared SVGA16/pixelBuffer
(sec=palette low byte; Heat=0x4000 UL, Mfd2=0x0400 UC, Comm=0x8000 UR,
Mfd1=0x0100 LL, Mfd3=0x1000 LR), so the compositor reaches the SVGA16 once
and extracts each by mask. No port-name reconcile needed on the dev path --
fetch the BT names directly; the RP aux* names only matter to the pod's own
SVGA16::Update demux (a deferred pod-only fallback).
- SVGA16::DrawDevSurface: two kernels -- palette-LUT (sec) + mono bit-plane->
tint ((word&mask)?tint:0). BTDrawGaugeSurfaces iterates a 6-entry table.
- Separate window = one CreateAdditionalSwapChain on the existing device (no
2nd D3D device). BTGaugeWindowRenderAndPresent (after the main EndScene):
SetRenderTarget -> SetDepthStencilSurface(NULL) -> Clear -> BeginScene ->
6 tiles -> EndScene -> restore -> swap->Present.
- KEY BUG fixed: the main 800x600 depth surface stays bound when rendering to
the 960x384 gauge backbuffer; a bound depth smaller than the RT is invalid
-> all draws silently fail (clear color, no geometry). Unbind depth (Z is
off), restore after.
Pod SVGA16::Update/BuildWindows path byte-unchanged; all gated BT_DEV_GAUGES.
Content is still the authored cockpit frames + base-table gauges -- the live
MFD widgets need the BTL4MethodDescription reconstruction (this window is now
the live viewer for that work). Details: docs/GAUGE_COMPOSITE.md.
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>
PLATFORM PROFILE (-platform pod|dev / BT_PLATFORM / run.cmd pod; default dev):
an env-preset selector, NOT a code fork -- it picks which environment the
existing video/gauge/input code reads. dev = single 800x600 window + keyboard
(the working build); pod = RIO input + (on real hardware) the multi-surface
gauges/MFDs, mirroring content/SETENV.BAT (the pod path -- FindBestAdapterIndices
/ SVGA16 -- is left untouched). The multi-surface needs the pod's 2 video cards,
so -platform pod does NOT auto-enable L4GAUGE on a dev box (stays bootable,
single-window). Also fixes a real engine bug: SVGA16::BuildWindows
PostQuitMessage'd on a CreateDevice failure but fell through to a null-device
deref (segfault) -- now breaks (inert on the pod).
GAUGE DEV-COMPOSITE, Milestone A (option B; opt-in BT_DEV_GAUGES, default OFF):
wake the (dormant) gauge renderer so its CPU-rastered pixelBuffer can later be
composited into the dev window, WITHOUT touching the pod SVGA16 output path.
SVGA16 does no per-surface D3D in this mode (a DevGaugeComposite() gate forces
the no-surface path + short-circuits Update/Refresh). Waking the never-exercised
gauge subsystem exposed 4 latent reconstruction bugs, each guarded:
SVGA16::Update / Refresh (empty mSurfaces[]), LBE4ControlsManager::MakeLinkedLamp
(NULL lamp manager), L4GaugeRenderer::NotifyOfNewInterestingEntity (garbage
warehouse chain). It now boots STABLY; the gauge reconstruction is incomplete
(shadowed lamp-manager + warehouse members) -- Milestone B (the composite pass)
will reveal whether the content is real. See docs/GAUGE_COMPOSITE.md.
Verified: default DEV un-regressed (combat DESTROYED, 0 crashes); pod path
untouched; BT_DEV_GAUGES boots stable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WAVE 4 (standalone readouts) -- Sensor(0xBC3)/Searchlight(0xBD8)/
ThermalSight(0xBDE)/AmmoBin(0xBCB) un-stubbed via Create<Class>Subsystem
bridges + Torso-style de-shim (drop cross-family shadow fields, redirect
accessors to the real inherited base, static_assert layout locks).
- FIX: Searchlight/ThermalSight ctors gated their Performance on the shadow
segmentFlags(=0) so it NEVER installed; switched to owner->simulationFlags.
- AmmoBin: retype ammoAlarm HeatAlarm->WatcherGaugeAlarm(0x54) + drop the
statusState@0x40 shadow -> exact 0x22C layout.
- Guard HeatWatcher::WatchSimulation against the unresolved watchedLink
(null-deref exposed once these sims run; faithful fix = resolve the link).
- Heat-leaf branch (Sensor) is not byte-exact -> overflow-lock only.
WAVE 6 (Myomers 0xBC6, mover-coupled) -- structural un-stub, gated
BT_MYOMERS (default on; =0 -> Actuator stub). Wired INERT: MyomersSimulation
early-returns (advanced-damage gate stubbed) + no-op mover feed, so the live
JointedMover is untouched and the gait cannot regress. De-shim drops the
owner*/segmentFlags shims to fit the exact-0x358 alloc. Authentic mover/heat
coupling deferred (needs messmgr 0xBD3 + reconciling the mover feed with the
gait cutover).
Verified: BLH tick 20->27, Mad Cat 24, combat DESTROYED un-regressed,
locomotion un-regressed, 0 crashes, 0 heap detections under BT_HEAPCHECK.
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>