Commit Graph
17 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 4fbc911f55 Gauges: fix temp/status bar colours (black remainder, not green)
Follow-up to the tiled-render fix: the two colour params were swapped.  The
binary caller (@004c8269) pushes the colours 0xff then 0; they land at
[this+0xA0]=fillColor (0xff green) and [this+0xA4]=backgroundColor (0 black).
The binary uses the GREEN for zone 1 (tile SetColor / dots) and zone 2 (the
over-degrade fill), and BLACK only for zone 3 (the unfilled remainder
[valPix,width]).

The port had them reversed -- zone 3 used fillColor -- so the whole remainder
of the bar (most of it, since valPix is small when cold) rendered solid green
instead of black.  Swapped: zone 1 + zone 2 = fillColor (green), zone 3 =
backgroundColor (black).  Render-verified: black background + green dotted tick
scale + a green hatch fill growing from the left, matching the reference.
Both builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:19:05 -05:00
arcattackandClaude Opus 4.8 4d4436ed03 Gauges: temp/status bar tiles the striped pattern from x=0 (HorizTwoPartBar)
The per-weapon TEMP/STATUS bar (HorizTwoPartBar) rendered as a solid block
starting at the warn-threshold pixel -- it "started in the middle" and was
never striped.  The port's Execute had been rewritten with DrawFilledRectangle
and never used the interned tile image, unlike the (correct) VertTwoPartBar
which tiles via DrawTiledBitmap.

Restored the binary's three-zone render (disasm @004c4340), along X:
  [0, warnPix)      DrawTiledBitmap(tileImage) -- the striped/dotted pattern
  [warnPix, valPix) backgroundColor solid (only when value > low)
  [valPix, width)   fillColor solid
So the bar now fills the striped tile from the beginning and matches the
reference (hatched fill block on the left + dotted tick scale).  Render-
verified vs the DOSBox reference capture.  Both builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:03:41 -05:00
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
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>
2026-07-12 17:24:15 -05:00
arcattackandClaude Opus 4.8 d68284ede2 gauge wave P2a: build the condenser LeakGauge + valve slider (heat panel)
The gauge-widget-decode workflow reconstructed the unbuilt cockpit widgets from
the binary.  First two (the heat MFD's missing SET/LEAK columns):

- LeakGauge (BitMapInverseWipe, keyword "LeakGauge"): the class body already
  existed + is byte-faithful; it was just UNREGISTERED so every LeakGauge(...)
  config line was parse-skipped.  Added the Make factory + methodDescription +
  the BTL4MethodDescription[] registration.  Value @6 = Condenser/CoolantMassLeakRate
  (already published P1); 0 undamaged -> the leak wipe is authentically empty.

- VertNormalSlider (keyword "vertNormalSlider"): the condenser VALVE slider @2 was
  PROSE-ONLY.  Reconstructed all 7 functions (Make/ctor/dtor/TestInstance/
  BecameActive/Execute/Draw) from part_013.c:14051-14175 (Make @004c4b08 by
  disassembly) -- an XOR indicator row=Round(span*value) over the track; fixed the
  jumbled header member layout to the byte-exact order (sizeof 0xB0).  Published
  ValveSetting -> coolantFlowScale@0x15C on the HeatSink table so Condenser/ValveSetting
  resolves (verified: all 6 condensers OK).  The valve indicator renders (condenser 1
  shows it near the top = coolantFlowScale 1.0 = valve open).

Both /FORCE-safe (every vtable slot has a real body; link log grep clean, no
unresolved VertNormalSlider/BitMapInverseWipe).  Verified DBASE+dev gauges: the
heat panel now shows all 3 columns per condenser (TEMP scales, SET valve indicator,
LEAK track), coolant bar stays full, combat un-regressed (TARGET DESTROYED), 0 crashes.
The player-valve-toggle -> coolantFlowScale drive (SetValveSetting vtable+0x48) is
the remaining Phase-3 valve-mechanism work; the slider shows the static valve value today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:19:04 -05:00
arcattackandClaude Opus 4.8 4ab6748563 vehicleSubSystems follow-up: reconstruct BitMapInverseWipe::Execute (LeakGauge)
Replace the minimal-safe Execute stub with the authentic wipe (@004c5d08): round
the level, repaint `frames` segments stacked vertically, each picking its strip
frame (empty/half/full via colorA/colorB) from the level.  The coolant LeakGauge in
every SubsystemCluster now renders faithfully from its CoolantMassLeakRate value.
Build clean, 7 panels, 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:34:12 -05:00
arcattackandClaude Opus 4.8 fbf5118c9c vehicleSubSystems follow-up: reconstruct SegmentArc270 recharge dial
Reconstruct SegmentArc270 (@004c6244): engine SegmentArc base + a Scalar connection
driving currentValue + the |n|/(|n|-1)*0.75 segment-span factor.  Wire it into
WeaponCluster as the recharge dial (was a deferred NULL), reading the weapon's real
PercentDone attribute.  Render-verified (BT_DEV_GAUGES_DOCK): the green segmented
recharge circles draw in the weapon panels; 7 panels build, 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:30:44 -05:00
arcattackandClaude Opus 4.8 6fceb58439 vehicleSubSystems Phase 2: authentic aux-screen assignment -> panels build
The engineering-screen cluster panels now build for each subsystem on its real
aux-screen position.  Two pieces:

1. Aux-screen bridge (powersub.cpp BTGetSubsystemAuxScreen): the assignment lives
   on PoweredSubsystem (auxScreenNumber/Placement/Label, resource +0x104/108/10C)
   which our ctor already populates -- but the reconstructed heat-leaf branch is
   NOT byte-exact, so the Make's raw sub[0x1dc]/[0x1e0]/[0x1e4] reads were garbage
   (0xCDCDCDCD).  The bridge casts through the real PoweredSubsystem type + returns
   the NAMED fields (and is the FUN_0041a1a4/0x50f4bc type filter).  Make + Subsystem
   Cluster now read via the bridge.

2. Reconstruct the 4 child gauges the clusters build that were declared-but-undefined
   (only VertTwoPartBar/OneOfSeveral/PixInt existed): HorizTwoPartBar (full, mirrors
   Vert), OneOfSeveralInt (ctor), OneOfSeveralStates (ctor + BecameActive + State
   Connection @004c3324), BitMapInverseWipe/LeakGauge (full) + SeekVoltageGraph's
   Execute/BecameActive/dtor -- their /FORCE-stubbed ctors were segfaulting when a
   cluster built them.

VERIFIED (BT_DEV_GAUGES + BT_VSS_LOG): 7 authentic panels build -- Sensor(scr5,
qsensors) HeatSinkCluster, Myomers(scr6,qmyomers) MyomerCluster, PPC(scr1,10) +
Emitter(scr4,8,7) Energy/Ballistic clusters -- with real labels + placements, 0
crashes, combat un-regressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:10:47 -05:00
arcattackandClaude Opus 4.8 a538d6438e gauges: reconstruct + register oneOfSeveralPixInt -- cockpit button-state lamps
The OneOfSeveral family was declared but undefined.  Reconstruct the shared base
(OneOfSeveral: ctor @004c4d88, BecameActive @004c4f14, Execute @004c4f28, dtor
@004c4e7c) and the OneOfSeveralPixInt subclass (Make @004c5204, ctor @004c52d8,
dtor @004c5364) + its methodDescription, and register "oneOfSeveralPixInt".

OneOfSeveral is a multi-frame strip selector: the base holds a BitMap or PixMap8
strip of columns*rows frames + the per-frame size (source size / columns,rows);
Execute picks frame `selected` (col=selected%columns, row=selected/columns) and
blits that sub-rectangle -- DrawPixelMap8(opaque) for a pixmap strip (its own
palette, no SetColor), or SetColor(bg)+DrawBitMapOpaque(fg) for a bitmap strip.
Added the missing OneOfSeveral::foregroundColor field (@0x9C -- the DrawBitMap
Opaque background arg).  OneOfSeveralPixInt forces the PixMap path (fromImageStrip
=0, bg/fg=0) and wires one GaugeConnectionDirectOf<int> to the frame index.
methodDescription = (rate,modeMask,string strip,integer columns,integer rows,
attribute state) matching oneOfSeveralPixInt(rate,mode,strip.pcc,cols,rows,attr).
Strip resource via warehouse->pixelMap8Bin (WRHOUS.h) .Get/.Release.

The config binds the duck / searchlight / display-mode / piloting-mode buttons;
ControlsMapper/DisplayMode already resolves (MechControlsMapper table), and
DuckState/Searchlight/LightOn degrade to frame 0 (safe) until their tables land.

Verified live (BT_DEV_GAUGES): the cockpit button-lamp PixMap8 icons now render
(were absent).  No /FORCE unresolved externals.  Combat un-regressed (TARGET
DESTROYED after 8 hits), 0 crashes, no missing-image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:33:16 -05:00
arcattackandClaude Opus 4.8 a2ee1181e6 gauges: reconstruct + register segmentArcRatio -- the cockpit SPEED arc
SegmentArcRatio (config keyword "segmentArcRatio") was declared but undefined.
It is a thin subclass of the engine SegmentArc gauge: two Scalar connections
(numerator=value, denominator=max) and an Execute override that computes the
[0,1] fill fraction into the base's currentValue, then delegates drawing to
SegmentArc::Execute (which lights numerator/denominator of the segments).

Reconstruct Make (@004c62fc) / ctor (@004c6394) / Execute (@004c6488) / dtor +
the methodDescription (rate,modeMask,scalar inner,scalar outer,scalar deg0,
scalar deg1,integer segs,color bg,color fg,attribute numerator,attribute
denominator -- matching the engine SegmentArcNormalized param types; the config
gives inner/outer once and the Make duplicates them into inner0==inner1 /
outer0==outer1).  Execute recovered by disassembling the vtable override
@004c6488 (Ghidra dropped the x87):
  if (denominator < 1) currentValue = 0;
  else currentValue = clamp(|numerator/denominator * segmentSpan|, 0, 1);
  SegmentArc::Execute();
where FUN_004dcd00 = fabs and the ctor precomputes
  segmentSpan = (Scalar)(|n|/(|n|-1)) * 0.75f    // int division => 0.75 for n=36
(_DAT_004c6484 = 0.75f, read live from the binary).  Fixed the header field type
int segmentSpan -> Scalar (the ctor fstp's it and Execute fmul's it).

The config binds numerator=LinearSpeed, denominator=MaxRunSpeed -- both now
resolved by the Mech attribute table -- so the arc sweeps with speed.

Verified live (BT_DEV_GAUGES + drive): the radar-surface SPEED dial now shows a
green segmented arc filling with speed (was a plain ring).  No /FORCE unresolved
externals.  Combat un-regressed (TARGET DESTROYED after 8 hits), 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:50:35 -05:00
arcattackandClaude Opus 4.8 18d58f38f6 gauges: reconstruct + register vertBar (VertTwoPartBar) -- cockpit coolant bars
VertTwoPartBar was declared in btl4gaug.hpp (ctor/field offsets/decomp addrs)
but never defined.  Reconstruct Make (@004c462c) / ctor (@004c4724) /
BecameActive (@004c48e0) / Execute (@004c48fc) / dtor + the "vertBar"
methodDescription, and register it in BTL4MethodDescription[].

The Execute pixel math is x87 that Ghidra dropped (FUN_004dcd94 is just
(int)ROUND(ST0); the operands are on the FPU stack) -- recovered by disassembling
@004c4940/@004c4960:
  warnPix = clamp(round(height*low/high  + 0.5), 0, height)
  valPix  = clamp(round(height*value/high + 0.5), 0, height)
value is first clamped to [0,high]; on a change it tile-blits the bitmap over
[0,warnPix), fills [warnPix,valPix) with fillColor, clears [valPix,height) with
extraColor (reusing the existing DrawTiledBitmap helper = FUN_004c2ff8; view
methods SetPositionWithinPort/SetColor/MoveToAbsolute/DrawFilledRectangleTo
Absolute = vtbl +0x08/+0x18/+0x24/+0x48).  methodDescription param types
(rate,modeMask,vector,string,color,color,color,attribute,attribute,attribute)
match the config vertBar(rate,mode,(w,h),tile,bg,fill,extra,current,warn,max).

For the coolant bars the config binds current=CoolantMass, warn=max=Coolant
Capacity, so the warn line sits at full and the bar is a simple level gauge that
empties as CoolantMass drops -- now driven by the HeatSink attribute table
committed earlier.

Verified live (BT_DEV_GAUGES): the Heat surface COOLANT/RES bars now render the
btwarn.pcc hatch fill (was empty), reading the real coolant attributes (full
hatch = coolantLevel~=capacity).  No /FORCE unresolved externals on the new
symbols.  Combat un-regressed (TARGET DESTROYED after 8 hits), 0 crashes, and
the gauge build + mission load are heap-clean under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:42:04 -05:00
arcattackandClaude Opus 4.8 d006b40927 gauges: reconstruct cmCrit -- subsystem-critical tint; ColorMapper family complete [widget recon 5]
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>
2026-07-06 17:14:54 -05:00
arcattackandClaude Opus 4.8 fb8c1d6ace gauges: colorMapperMultiArmor (worst-of-8-zones) + fix the interpreterTable overflow [widget recon 4]
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>
2026-07-06 16:56:13 -05:00
arcattackandClaude Opus 4.8 1d2ddd8399 gauges: reconstruct cmArmor -- the per-zone ARMOR DAMAGE schematic [widget recon 3]
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>
2026-07-06 16:30:45 -05:00
arcattackandClaude Opus 4.8 e869b00181 gauges: reconstruct headingPointer -- the rotating compass + heading readout [widget recon 2]
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>
2026-07-06 16:05:47 -05:00
arcattackandClaude Opus 4.8 547f25e043 gauges: reconstruct + register the first BT gauge widget (cmHeat) [widget recon 1]
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>
2026-07-06 15:05:47 -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