19 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 7fc4acb89f gauge-complete P4g: MessageBoard reconstructed + registered -> LAST parse-skip cleared (0 unregistered gauges)
The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker,
L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This
was the last unregistered gauge primitive.

Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive
@4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed
(decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at
+0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED
(BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c).
Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription
= 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[].

DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource
has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62)
is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe
no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real
/FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer
TU, dodging the databinding trap). The name-cell path is a marked structural simplification to
restore when the feed lands.

VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive
registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET
DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:39:47 -05:00
arcattackandClaude Opus 4.8 c0d2eb0aae gauge-complete P4f: PrepEngrScreen reconstructed + registered -> 12 engineering-screen label overlays build
The "prepEngr" cockpit primitive (the per-engineering-screen static label overlay,
12 CFG calls at L4GAUGE.CFG:4595-4759) was a MISLABELED stub (base Gauge, an Execute
override, mech/screenNumber SWAPPED) with no methodDescription -> parse-SKIPPED, none built.

Reconstructed byte-verified (ctor @4c7bf0, BecameActive @4c7e48, Make @4c7b30; vtable
0051a06c). THREE corrections vs the stub (decode workflow + vtables.tsv proof):
(1) base = GraphicGaugeBackground, not Gauge -- vtable-identical shape to the engine's
BackgroundBitmap (only dtor + BecameActive overridden); (2) the overridden slot is
BecameActive (a paint-on-activation), NOT Execute -- GraphicGaugeBackground has no Execute
virtual, so the Gauge::Execute->Fail->abort hazard doesn't apply; (3) screenNumber@0x6C /
mech@0x70 were swapped. sizeof 0x90.

BecameActive walks the mech roster (databinding-safe GetSubsystemCount/GetSubsystem +
BTGetSubsystemAuxScreen bridge, like VehicleSubSystems::Make), finds the subsystem whose
auxScreenNumber == this screen (1..12), and paints the screen-number numeric + subsystem
label + type-specific label cells dispatched on GetClassID (Sensor/Myomers/Emitter/PPC/
Projectile/Missile/GaussRifle). methodDescription = 9 params (mode, screen, 7 .pcc names).
Registered in BTL4MethodDescription[]. Two raw subsystem reads (heat-sink #, +0x224 label)
are guarded fail-soft best-efforts (marked; the aux-screen bridge label64 substitutes).

VERIFIED: the skip list drops from {prepEngr x12, messageBoard} to just {messageBoard} --
all 12 prepEngr gauges build; no screen-range/crash; boot+sim+combat run; gauge composite
renders identically (COOLANT 300/77, clusters, radar all un-regressed). The prepEngr labels
paint on the Eng (ModeMFD*Eng*) screens, not the default composite view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:32:12 -05:00
arcattackandClaude Opus 4.8 4a4ec6855c gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z
coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/
methodDescription/registration) -> the config line was parse-SKIPPED and never built.

Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c,
methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute
reads the linked mech's world position and shows two 100-unit sector numerics:
  numericA = Round(-localOrigin.z * 0.01) + 500
  numericB = Round( localOrigin.x * 0.01) + 500
(rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong
"truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm).
Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating),
Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked
(static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling;
the config image name is copied (nameCopy) since Execute reads it per-frame.
Registered in BTL4MethodDescription[].

VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1;
Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504
-- authentic 100-unit sectors from live mech position). Gauge composite renders full,
no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard).

Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each
unregistered gauge primitive the dev-parse skips -- the tool that pinned this down
(earlier "not built" runs were killed before the lazy gauge-renderer init).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:25:48 -05:00
arcattackandClaude Opus 4.8 88cd68aaa1 gauge wave P2d: GeneratorCluster un-gated -- the 4 generator panels render
Root-caused the P2c abort with a cdb attach to the frozen abort dialog: the stack
was Gauge::Execute (GAUGE.cpp:598) -> abort, via GuardedExecute <- Update <-
ProcessOneActiveGauge.  The engine base Gauge::Execute is Fail("not overridden")
-> abort, so an ACTIVE container that doesn't override Execute aborts by design;
GuardedExecute's SEH can't catch an abort().  The decode's "GeneratorCluster
overrides ONLY the dtor / renders via its children" was wrong -- the 1995 base
Gauge::Execute was a no-op, the 2007 WinTesla engine's aborts, so the container
MUST override Execute (the sibling SubsystemCluster does).

FIX: give GeneratorCluster the two container overrides (mirroring SubsystemCluster):
- BecameActive() -- blit the panel's static background pixmap (DrawPixelMap8) and,
  by overriding the default, DON'T self-inactivate so the panel stays active.
- Execute() -- no-op (the 5 self-registered child gauges draw the dynamic content;
  the override exists only to avoid the base Fail->abort).

Re-registered in BTL4MethodDescription[].  Verified DBASE+dev gauges: all 4
generator panels (A/B/C/D) render -- blue OutputVoltage bars (the @004c72ac
ScalarBarGauge Scalar* variant), labels, status lamps -- 0 "BecameActive not
defined" warnings, NO abort, combat un-regressed (TARGET DESTROYED).  Phase 2 is
now COMPLETE (4/4 widgets: LeakGauge, VertNormalSlider, PilotList, GeneratorCluster).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:57:20 -05:00
arcattackandClaude Opus 4.8 b44e908101 gauge wave P2c: GeneratorCluster reconstructed + infra (gated -- parent aborts)
Reconstructed the 4th unbuilt widget (the 4 generator engineering panels, buttons
9-12) + its companions, but left it UNREGISTERED pending a runtime-abort fix:

- Generator OutputVoltage attribute table (powersub) -> outputVoltage@0x1DC, chained
  to HeatSink's dense index (temps already reach it).  Correct + reusable.
- ScalarBarGauge @004c72ac plain-Scalar variant (btl4gau2) -- the DIRECT-Scalar* bar
  the GeneratorCluster voltage bar needs (the existing @004c721c takes a resolved
  source object).  Correct + harmless.
- GeneratorCluster Make/ctor/dtor + 15-param methodDescription reconstructed from
  part_014.c:891-1013 (replacing the prose + placeholder Make + the bogus DAT pool);
  builds the 5 children (temp bar / voltage bar / leak wipe / 2 lamps).  Header fixed
  to the binary ctor param order (secondaryColor@0x94, +_reserved0xB0 -> sizeof 0xB4).
  ChildRate forward-declared.

⚠ REGISTRATION GATED (commented out in BTL4MethodDescription[]): registering it
aborts at runtime -- the 4 panels build (the 4 "BecameActive not defined" warnings
fire) then abort().  ISOLATED to the PARENT lifecycle: the abort PERSISTS with all 5
children nulled; there are no pure virtuals; Execute is SEH-guarded under BT_DEV_GAUGES
so the fault is in the unguarded ctor/lifecycle path.  The sibling SubsystemCluster
overrides BecameActive/Execute/TestInstance whereas the decode has GeneratorCluster
overriding ONLY the dtor -- that "inherits only the dtor" claim is the prime suspect.
Deferred; the 3 other P2 widgets (LeakGauge/VertNormalSlider/PilotList) ship clean.

Verified: unregistered -> parse-skips -> no abort; combat un-regressed (DESTROYED),
0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:43:47 -05:00
arcattackandClaude Opus 4.8 0060a3e1ca gauge wave P2b: build the Comm KILLS/DEATHS pilot roster (PilotList)
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>
2026-07-07 17:24:24 -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 674c172cf8 vehicleSubSystems Phase 1: reconstruct + register the cluster-panel family
Reconstruct the whole engineering-screen (MFD) subsystem cluster-panel system in
btl4gau2 (from the Ghidra decomp of BTL4OPT.EXE) and register the vehicleSubSystems
config primitive:

- Sub-gauges: AnimatedSubsystemLamp/AnimatedSourceLamp (OneOfSeveral + cooling/
  power connections), ScalarBarGauge (BarGraphBitMapScalar + generator-voltage
  connection), ConfigMapGauge, plus the 3 file-private GaugeConnection classes
  (@004c3134/31ec/3288 samplers).
- SubsystemCluster base (ctor builds ~11 child gauges across the MFD + engineering
  ports; Execute/dtor/frame-draw/enable helpers) + the 5 subclasses HeatSink/
  Myomer/Weapon/Energy/Ballistic (ctors + Executes).
- VehicleSubSystems::Make dispatcher (@004cbaf0): per-subsystem factory keyed on
  classID (HeatSink 0xBC3 / Myomer 0xBC6 / Energy 0xBC8,0xBD4 / Ballistic
  0xBCD,0xBD0), placed via the 12-row aux-screen geometry table, + registered in
  BTL4MethodDescription[].

SAFE + gated: gauges only build under BT_DEV_GAUGES/pod, and until Phase 2
populates the subsystem aux-screen field (subsystem[0x1dc]) the Make hits its
"aux screen = zero" skip -> combat untouched.  Two minor children deferred as
marked NULLs (SegmentArc270 recharge dial + BitMapInverseWipeScalar eject wipe --
their ctors are btl4gaug placeholders).  Raw-offset subsystem reads (0x1dc/0x1e0/
0x1e4/0x31c/0x334/0x364/0x43c...) marked BEST-EFFORT pending Phase 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:43:32 -05:00
arcattackandClaude Opus 4.8 b6918632fc gauges: reconstruct + register PlayerStatus (mission-review scoreboard)
PlayerStatus (btl4gau3) had only a BUGGY Make defined (it read raw DAT_ pools,
passed (int)entity/(int)gauge_renderer as x/y, and DROPPED the graphics_port_number
param -- the header ctor was one int short); the ctor/Execute/dtor/BecameActive and
the PlayerStatusMappingGroup + CreateMutantPixelmap8 helpers were comment-only
stubs.  Reconstructed the whole family (mapped by the playerstatus-decomp-map
workflow, 6 agents):

* methodDescription (8 params: rate,modeMask,integer player#,string font,4x color)
  + rewired Make reading parameterList[] with the port/x/y bug fixed.
* ctor (@004cb1a8): GraphicGauge base (owner folded to 0), store colours+port,
  SetOrigin, playerIndex=player_number-1, build a score NumericDisplay (fmt 2 =
  signedBlankedZeros).  Fixed the header layout: nameImage is a BitMap* (not a
  Pixmap), and two missing members (dirty@0x2F, previousStatus@0x30).
* dtor / BecameActive / TestInstance.
* PlayerStatusMappingGroup (@004c9bd0): 28 ColorMapperArmor zone tints over the
  mech-outline schematic, one per dz_* damage zone (mech->GetDamageZoneIndex),
  using the EXISTING 10-arg ColorMapperArmor ctor (per the workflow's correction --
  changing it would break cmArmor); added ColorMapper/Armor::SetColor (FUN_004c3c38,
  stores @0x6C).
* Execute (@004cb358): resolve the player (WinTesla-clean via GetMissionPlayer for
  the local player, instead of the binary's raw App+0x24 "Players"-node dictionary
  walk), then draw the score + name box + alive/dead status box; null-guarded.

KEY FINDING: PlayerStatus lives in the config's `cameraInit` block (the MISSION-
REVIEW / spectator camera cockpit), NOT `MechInit` -- so it is NOT part of the mech
cockpit and does not build during normal mech play (which is why the mech test shows
it un-regressed and its Execute never runs).  It is the post-mission scoreboard
(all 8 players' name/score/mech-status), rendered by the BTCameraDirector game
model.  Registered in BTL4MethodDescription[] so `cameraInit` builds it instead of
parse-skipping.

STATE: complete reconstruction; compiles, links (no new /FORCE unresolved), and the
mech cockpit is un-regressed (TARGET DESTROYED, 0 crashes).  NOT runtime-verified --
cameraInit is only built by the mission-review camera model, which a normal
`vehicle=<mech>` egg does not trigger.  BRING-UP STUB (marked): CreateMutantPixelmap8
returns NULL (the mech-outline recolor needs the DynamicMemoryStream read API +
Pixmap pixel-copy mapped) -> the score/name-box/status-box render but not the
recoloured mech schematic.  BT_PS_LOG traces the resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:13:32 -05:00
arcattackandClaude Opus 4.8 48ed3a8b73 gauges: register + wire the map (radar) -- the tactical display renders
MapDisplay (the radar/threat-map gauge, btl4rdr.cpp) was fully reconstructed
(ctor + Execute + DrawViewWedge/DrawStatic/DrawMoving/DrawNames) but never
registered and its data attributes never published.  Wire it up end-to-end:

DATA (the attributes the map binds):
* Mech table extended to the full 0x15..0x38 (was the 0x21 prefix): add
  RadarRange@0x2f -> radarRange, RadarLinearPosition@0x30 -> radarLinearPosition
  (Point3D* -> &localOrigin.linearPosition), RadarAngularPosition@0x31 ->
  radarAngularPosition (Quaternion* -> &localOrigin.angularPosition), DuckState
  @0x37 -> duckState; the rest bind the shared attrPad (kept dense so Find can't
  strcmp a gap).  New members set in the Mech ctor; radarRange defaults to 500
  (SetTargetRange is still stubbed) so nearby contacts are on-scale.  The map's
  position/angle attributes resolve to Point3D**/Quaternion** (AttributePointer
  is int Simulation::*, so the ATTRIBUTE_ENTRY reinterpret binds pointer members
  fine -- verified live: posPtr/anglePtr resolve).
* Sensor (named "Avionics") publishes RadarPercent/SelfTest/BadVoltage: define
  Sensor::AttributePointers[] + build the previously-empty Sensor::AttributeIndex
  chained to PoweredSubsystem::GetAttributeIndex() (= HeatSink's dense index).

WIDGET (the registration glue -- NOT in the assert-anchored decomp, so
reconstructed from the config + ctor):
* MapDisplay::methodDescription (config "map") + MapDisplay::Make.  The
  offset_position keyword "center"/"bottom" is typed as a STRING and converted
  in Make, avoiding a ModeManager named-constant registration.  Registered in
  BTL4MethodDescription[] (btl4grnd.cpp now includes btl4rdr.hpp).

Three crash fixes exposed once the map ran (each a pre-existing stub the map is
the first consumer of):
* Sensor radarPercent went hugely negative (RadarBaseline - HeatMasterEnergy(),
  where the inherited heatEnergy is un-normalized in the not-byte-exact heat-leaf
  branch) -> the radar reported non-operational.  Guard: treat an out-of-[0,1]
  heat term as no penalty (marked bring-up; real overheat penalty needs the heat
  normalization).
* ResolveOperatorEntity was a stub returning NULL -> EntityPosition(NULL) crashed
  CalculateBounds.  Fixed to the renderer linked-entity + application viewpoint
  fallback (same as HeadingPointer).
* MapName::ExtractFromEntity didn't guard a NULL entity (the binary's Verify(False)
  iterator-mismatch path compiles out at DEBUG_LEVEL 0) -> EntityPosition(NULL).
  Guarded.

Verified live (BT_DEV_GAUGES): the radar now draws the view wedge (the mech's
180-deg FOV cone) and completes the full wedge/static/moving/names cycle with
operating=1, cap=1, scale=500, position/angle resolved, 0 crashes.  Combat un-
regressed (TARGET DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK.
Permanent gated diagnostic: BT_MAP_LOG traces the phase/draw pipeline.  Contacts
show 0 (the spatial-query entity classification is a follow-up); DuckState now
resolves for the duck button too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:16:29 -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