Replace the return-0 stubs with the real functions:
- ResolveLink (FUN_00417ab4) = SubsystemConnection::Resolve (two-level deref of the
plug's bound link); an unbound plug resolves to 0, the authentic no-source branch.
- FindLinkedSubsystem (FUN_0041bf44) = GetAttributePointer(index) via the engine
Simulation attribute-index system (the cooling-loop master at slot 3).
- IsGeneratorDerived (FUN_0041a1a4) = the real IsDerivedFrom(PoweredSubsystem
ClassDerivations 0x50f4bc) via a new powersub.cpp bridge BTIsPoweredSubsystem
(shared with the aux-screen filter) -> the generator stateLamp now builds.
So the cooling/power/voltage connections resolve real data when the plug is bound,
instead of always reading 0. Build clean.
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>
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>
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>
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>
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>
Add a BT_VSS_LOG diagnostic (Make entry + per-subsystem classID/auxScreen dump +
the ConfigureForModel path). Verified under BT_DEV_GAUGES: the gauge renderer
builds, ConfigureForModel(Init) runs bhk1Init, and VehicleSubSystems::Make is
called with the full 33-subsystem roster -- the dispatcher iterates every
subsystem and its classID switch resolves the displayable types (0xBC3 Sensor,
0xBC6 Myomers, 0xBC8/0xBD4 Emitter/PPC, 0xBCD/0xBD0 Proj/Missile).
CONFIRMED (empirically) the Phase-2 blocker: subsystem[0x1dc] (the aux-screen
position the Make gates on) reads 0xCDCDCDCD (uninitialized) / garbage for the
displayable subsystems -> every panel hits the "Illegal aux screen position"
skip, so nothing renders yet. Phase 2 = populate the aux-screen field from the
subsystem resource. Combat remains un-regressed (the skip path is inert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
The previous commit found+drew contacts but they landed off-radar. Debugged with
BT_MAP_LOG entity/blip tracing: the reads were CORRECT all along -- the enemy mech
(cls 0xBB9, own=0) sits at dsq=14400 (exactly 120u) from the player (own=1, dsq=0),
and the grid also carries ~311 cls-0x5E vehicle props per cycle (the map's
scattered vehicles) which the 0xBB9 blip filter correctly skips.
The bug was the PROJECTION: worldToView is a camera (view->world) matrix and
Point3D::Multiply bakes a scale that is wrong for a point transform -- a 120u
contact at ppm=0.366 projected to |167px| instead of |44px| (~3.8x), and with a
spurious rotation. (The earlier "377-828m" reading was this wrong scale amplifying
the varying player-drift distance, not a distance error.) Fixed by projecting
delta = (entity - viewpoint) DIRECTLY: rotate by -heading (from the orientation
quaternion's yaw, top-down heading-up) and scale by pixelsPerMeter. Now |screen|
== |delta|*ppm exactly (blip @(0,44) for the 120u enemy), on-radar inside the view
wedge. Blip is a cross + box so it reads as a distinct contact marker.
Verified live (BT_DEV_GAUGES): the enemy contact renders as a box marker INSIDE
the FOV wedge (render-confirmed via a 4x radar crop); combat un-regressed (TARGET
DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK. BT_MAP_LOG traces blip
coords + scale + delta. Remaining map polish: blip-vs-wedge rotation convention
(the enemy reads ahead/in-wedge, which is correct); the authentic pip symbol/name
infrastructure (still stubbed); SetTargetRange (radar zoom, 1km default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-caused why the radar showed 0 contacts: the gauge renderer's moving/static
entity grids (queried by GetMoving/StaticEntitiesWithinBounds) were NEVER
populated -- the 1995 game fed them from ExecuteImplementation's InterestingEntity
iterator, which the WinTesla port dropped.
Fix (engine): add GaugeRenderer::RebuildEntityGrid() -- Clear + repopulate
movingEntities from the world's DynamicMaster + DynamicReplicant entities (the
vehicles/mechs; AllEntity would flood the grid with ~300 props/effects/terrain).
Called from the map's Execute phase 0 (under the gauge's guarded Execute, so a
fault disables only the map). Verified: contacts are now found, combat un-
regressed (TARGET DESTROYED), heap-clean under BT_HEAPCHECK.
Fix (map): DrawMoving now draws a cross blip for mech-class contacts (0xBB9) when
the authentic pip raster set is absent (the BT pip table is stubbed in this engine
build). KEY PROJECTION FIX: worldToView is a camera (view->world) matrix and
Point3D::Multiply applies only rotation+scale (NOT translation), so projecting the
absolute entity position left the viewpoint un-subtracted and the blip landed
off-screen (verified: blip @(580,-1154)). Project the RELATIVE position (delta =
entity - viewpoint, already computed for the range check) instead -> the blip
projects viewpoint-relative + heading-rotated correctly (@(164,221)).
radarRange default 500->1000 (1km zoom) so contacts within ~500m show on the
radar (SetTargetRange, the real player zoom, is still stubbed). BT_MAP_LOG traces
the blip coords.
STATE: the grid population + viewpoint-relative blip projection are correct and
combat-safe. A reliable on-screen contact demo still needs the display-scale vs
spawn-distance reconciliation (the BT_SPAWN_ENEMY dummy sits at a varying, large
distance -- 377-828m -- beyond the display radius across runs) + the authentic pip
symbol/name/video-object infrastructure (still stubbed).
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>
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>
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>
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>
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>
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>
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>
mech.cpp shipped an EMPTY, unchained Mech::AttributeIndex (a default-constructed
AttributeIndexSet passed to DefaultData), so Simulation::GetAttributePointer
severed the whole parent chain -> EVERY bare Mech gauge binding (LinearSpeed /
MaxRunSpeed / DuckState / RadarRange / ...) resolved to NullAttribute.
Add the Mech AttributeID enum (full binary set 0x15..0x38, VA 0x50be84) + a
Mech::AttributePointers[] table + Mech::GetAttributeIndex() chained to
JointedMover::GetAttributeIndex(), and change DefaultData to pass it.
DENSE-TABLE HAZARD (systemic): AttributeIndexSet::Build (SIMULATE.cpp:565) sizes
the built index to max(id) and Find (SIMULATE.cpp:663) strcmps EVERY slot, and
Build does NOT zero gap slots -> an unpublished id between the parent's
NextAttributeID and the max published id holds a garbage entryName -> AV on the
next name lookup. So the table is a DENSE PREFIX 0x15..0x21 (JointedMover's
NextAttributeID through LinearSpeed): the ids the BLH cockpit doesn't bind
(collision/eyepoint/reticle/footstep/anim-state) point at one shared read-only
attrPad member; CurrentSpeed/MaxRunSpeed bind to the existing gait members
legCycleSpeed/reverseStrideLength; LinearSpeed binds a new linearSpeed member
populated each frame in PerformAndWatch (|adv|/dt = forward ground speed). Ids
0x22..0x38 stay declared in the enum for later extension (RadarRange/DuckState
when the map/duck widgets land) but are NOT published yet. New members appended
to Mech's own region (no locked base/gait offset shifts); inited in the ctor.
Verified live (BT_DEV_GAUGES + drive): the radar-surface SPEED readout
(numericSpeed(LinearSpeed), a base engine primitive already drawing NULL) now
shows 225 (~63 u/s in the widget's display units) instead of 0, tracking the
mech's motion. Heat surface CurrentTemperature=77 un-regressed. Combat un-
regressed (TARGET DESTROYED after 8 hits), 0 attribute-not-found, and Mech
construction + attribute-index build is heap-clean under BT_HEAPCHECK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cockpit gauge config binds Subsystem/Attribute names (e.g.
HeatSink/CurrentTemperature) resolved by ParseAttribute -> FindSubsystem ->
GetAttributePointer -> the class's activeAttributeIndex. HeatSink wired
GetAttributeIndex() into DefaultData but published an EMPTY table (only the
inherited SimulationState id 1), so every HeatSink/* numeric read NULL ->
guarded to 0.
Add HeatSink::AttributePointers[] + GetAttributeIndex() publishing CoolantMass
(coolantLevel @0x12C), CoolantCapacity (thermalCapacity @0x128), and
CurrentTemperature (inherited currentTemperature @0x114), chained to the parent
index so SimulationState is preserved and the built index stays dense (ids run
contiguously from HeatableSubsystem::NextAttributeID -- a gap would make
AttributeIndexSet::Find strcmp a garbage slot). Condenser and Reservoir derive
from HeatSink with no override, so they inherit this table for free.
Verified live (BT_DEV_GAUGES): the Heat surface heatsink-temperature readout
(numeric HeatSink/CurrentTemperature, a base engine primitive that was already
drawing NULL) now shows 77 (= startTemp) instead of 0. Combat un-regressed
(TARGET DESTROYED after 8 hits), 0 crashes. The coolant attributes are ready
for the vertBar widget (next increment).
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>
Rewrite the build/run/debug recipe (§10a) and the "where inputs live" section
(§10a-bis) for the consolidated bt411 layout; remap the reconstruction-ledger
citations (btbuild/*.md -> docs/*.md); flag §6's port/ viewer as archive-only.
The deep decomp-history sections still cite the original nick-games paths as
provenance -- the orientation banner at the top maps them onto the new tree.
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>