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>
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>
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>
The reconstructed Condenser::MoveValve carried FUN_004afbe0's body -- a CONTROLS-mapper
display-mode cycler (cycles field@0x190 through 0/1/2 + repoints owner HUD gauges), NOT
the condenser valve. The real valve handler @0x4ae464 was not captured by the
assert-anchored exporter; disassembled it (tools/disas2.py) and reconstructed faithfully:
cycle valveState (@0x1D0) 1 -> 5 -> 50 -> 0 -> 1 (byte-verified @4ae480-4ae4bf), then
RecomputeCondenserValves(owner) to redistribute flow so every ValveSetting gauge updates.
DORMANT by design: the binary guards on owner->messageManager(+0x190)+0x274 (FUN_004ac9c8,
the 0xBD3 SubsystemMessageManager) and the message that invokes MoveValve arrives through
that same manager -- both DEFERRED to WAVE 8. Reproducing the raw owner+0x190 deref would
be a databinding trap in our layout, so the handler is faithful-but-uninvoked; the valve
gauge shows the authentic static 1/N (RecomputeCondenserValves at ctor) until 0xBD3 lands.
No V-key bring-up stand-in added.
Build green; combat TARGET DESTROYED, un-regressed (MoveValve is not yet routed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale
@0x15C) reads its authentic value instead of garbage/zero.
STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/
TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in
heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real
ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD.
#if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1,
coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the
sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated)
stay in heat.cpp.
STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op
template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale
was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264):
distribute coolant flow across the mech's condensers, coolantFlowScale_i =
valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse
(2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering
Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc
condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the
Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read).
Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6)
for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED,
un-regressed. Diagnostic BT_VALVE_LOG added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0xBBE heat-sink BANK was built as a plain HeatSink, so the numeric-R
cockpit gauge binding HeatSink/AmbientTemperature (L4GAUGE.CFG:4552) had
no publisher -> the LAST unresolved config attribute.
Reconstruct AggregateHeatSink : HeatSink (ctor @4ae8d0, own GUID 0x50e590),
byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature
@0x1D4=300, helper@0x1D8 0xC link node, sizeof 0x1E4 == factory alloc @9993).
Publish HeatSinkCount + AmbientTemperature via a dense-prefix attribute table
chained to HeatSink::NextAttributeID (shared HeatSink table unchanged, so
CoolantMass/CoolantCapacity keep resolving). Move CreateHeatSinkBankSubsystem
into heatfamily_reslice.cpp (needs the class def) and build the real class at
factory case 0xBBE; mech.cpp unchanged.
DELIBERATE DEVIATION (documented in the class): keep the base HeatSinkSimulation
the HeatSink ctor installs; do NOT reimplement the authentic Performance @4ae73c
-- it derefs a raw self+0xE0 -> [+0x158] that does not map in our compiled layout
(AV/NaN, and runs for EVERY mech), and ambientTemperature is a frozen constant so
the gauge reads 300 either way. Authentic relaxation model deferred.
Verified: [attr] HeatSink/AmbientTemperature OK; all 50 config attribute
bindings resolve (0 NULL); no every-mech crash; combat TARGET DESTROYED,
FIRED #41+, un-regressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gauge-complete-decode workflow found the "deep heat-leaf byte-exact re-base" is
STALE -- the leaf layout already shipped byte-exact (alarm-unification/P7), and the
real work is a small misidentified-field data fix (highest value, lowest risk):
- Sensor radarPercent read the INHERITED heatEnergy (~1.3e7) -> hugely negative ->
needed a bring-up guard forcing it to 1.0. The binary (@004b1c4c:1829) reads
*(this[0x38]+0x158) = the subsystem's OWN DamageZone::damageLevel [0,1] (0 intact ..
1 destroyed). Fixed: radarPercent = RadarBaseline - GetSubsystemDamageLevel(); guard
DELETED (damageLevel is engine-clamped). So the radar authentically degrades with
sensor structural damage (RadarBaseline-damage, x0.5 on DegradationHeat, 0 on Failure).
- New databinding-SAFE accessor MechSubsystem::GetSubsystemDamageLevel()/Set... reads the
ENGINE DamageZone::damageLevel NAMED member (NOT the ReconDamageZone proxy, whose
offset-0 structureLevel aliases the vtable ptr as a float = garbage).
- HeatSink::HandleMessage msg==1: same misidentification -> SetSubsystemDamageLevel(0.5)
(was linkedSinks->heatEnergy=0.5, wrong object+field).
- MyomerCluster seek lamp: subsys+0x800 (OOB for 0x358 Myomers) -> currentSeekVoltageIndex
@0x320 (the Emitter+0x3f0 analog; INFERRED -- ctor not in the assert-anchored export).
- numericSpeed (~225 vs 61.5): verified NOT A BUG (NumericDisplaySpeed applies x3.6; the
feed is correct u/s) -- closed, no code.
No layout change (already byte-exact); pure data-read retargets. Verified DBASE+dev
gauges: no crash, combat un-regressed (TARGET DESTROYED). (radarPercent degrade is only
OBSERVABLE once per-subsystem damage routes to each engine DamageZone -- a separate task;
today the sensor is undamaged so radarPercent = full 1.0, which is correct.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RANK=-1: Player::DefaultFlags creates every player NonScoring (CalcRanking only
ranks IsScoringPlayer()==true, else -1), and the bring-up never cleared it. Call
SetScoringPlayerFlag() in DropZoneReplyMessageHandler's mech branch (a piloted mech
IS an active scoring combatant; the camera-ship branch stays non-scoring). -> RANK=0.
DEATHS runaway (climbed ~1/s, not the "1" first seen): @004c012c -- which our
MESSAGE_ENTRY binds as the VehicleDead handler -- is actually the drop-zone
RESPAWN-RETRY helper. The engine's RequestDropZone (PLAYER.cpp:400) posts a
2s-delayed VehicleDead(deathCount=-2) as a "did the drop zone reply?" timer; our
handler ++deathCount + RE-POSTED it UNCONDITIONALLY -> an infinite loop, because the
drop-zone handshake never "completes" in the bring-up (no console/drop-zone system).
Traced it to a pure boot artifact (13 fires with zero combat; the only VehicleDead
producers are the re-post + my kill-producer, and the kill fires AFTER the loop
starts). FIX (correct respawn-retry semantics): if the player already has a vehicle
the drop zone WAS acquired (the DropZoneReply created it), so the retry is moot --
return without counting a death or re-posting. -> DEATHS=0 in solo, loop gone.
Verified DBASE+dev gauges: the Comm roster shows KILLS=1 / DEATHS=0; the KILL log
reads killCount=1 deaths=0 score=164 rank=0; 0 crashes. (MP real-death DEATHS -- a
destroyed vehicle via the separate @004c05c4 path -- remains deferred per the plan;
solo never dies so the retry gate is correct there.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Comm roster + score gauges read 0 for 4 independent reasons (none was handler
logic -- the BTPlayer ScoreMessage/VehicleDead/ScoreInflicted handlers were already
reconstructed). Mapped by the scoring-feed-decode workflow; fixed all 4:
- NO PRODUCER: combat only dispatched Entity::TakeDamageMessage, never a scoring
message. Added producers (btplayer.cpp bridges, called from mech4.cpp): per-hit
ScoreInflicted at the beam (:2171) + projectile (:831) damage dispatch -> SCORE;
KillScore at the TARGET-DESTROYED edge (:2198) -> KILLS. senderMechID = the
VICTIM (so the local player is credited via the !=our-mech branch, not the
suicide-negate branch); dispatched to application->GetMissionPlayer().
- CROSS-FAMILY MECH OFFSETS: MECH_OWNING_PLAYER/TONNAGE/DAMAGE_BIAS read raw binary
offsets (garbage in our 0x638 Mech). MECH_OWNING_PLAYER -> Entity::GetPlayerLink()
(NULL for the ownerless dummy); tonnage/bias stubbed 1.0/0.0 (bring-up).
- NULL scenarioRole: the BTMission role registry has no WinTesla analog, so every
award path NULL-deref'd. CalcInflictedScore returns the neutral (damage+bias)
when scenarioRole==0; DamageReceived guarded.
- NULL-owner dummy: the KillScore sender-owner increment + StatusMessage now guard
GetPlayerLink() (the dummy has none) so a solo kill credits only the local player.
- DATABINDING: the PilotList read KILLS/DEATHS at raw offsets (pilot+0x27c/+0x200)
that don't match our compiled layout -> silent 0. Repointed to the compiled
members via bridges (BTPilotKills/BTPilotDeaths -> BTPlayer::GetKillCount/GetDeaths).
⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a
raw-offset deref (*(local+0x284) objectiveMech, then *(tgt+0x190)) that AV'd on our
layout -- caught silently by the SEH GuardedExecute, aborting Execute BEFORE the
KILLS/DEATHS draw. Replaced with BTPilotIsSelected (accessors + GetPlayerLink).
- SCORE persistence: PlayerSimulation's 10s flush zeroed currentScore (the binary's
console delta); now only flushes/zeros when a console host exists (solo keeps the
running score for the gauge + CalcRanking).
Verified DBASE+dev gauges (BT_SCORE_LOG): SCORE climbs +5.83/hit -> 164; on the
kill KILLS 0->1, and the Comm roster RENDERS KILLS=1 (screenshot). No crash.
Follow-ups (noted): DEATHS=1 is a pre-existing spawn/respawn-handshake artifact
(not from the producers -- the dummy's GetPlayerLink is NULL so no VehicleDead is
posted; confirmed killCount=1 not 2); RANK=-1 is CalcRanking's non-scoring mark
(Plasma serial surface, invisible on dev); MP VehicleDead/tonnage/role registry
remain per the plan. Diagnostics kept: BT_SCORE_LOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
- Searchlight publishes "LightOn" -> lightState@0x1D8 (the button-5 searchlight
lamp; the empty default-constructed index resolved NOTHING). The config binds
the name "LightOn", so the enum id is renamed LightOnAttributeID and the table
is chained to PowerWatcher's dense index. Verified: LightOn resolves OK.
- FIX a genuine reconstruction bug the heatmodel decode caught (heat.cpp
UpdateCoolant + heatfamily_reslice RefrigerationSimulation): both read
linkedSinks.Resolve()->heatEnergy where the binary reads *(this[0x38]+0x158) =
the subsystem's OWN DamageZone.damageLevel (the engine base zone @0xE0).
Consequences of the misread: coolantDraw = master.heatEnergy(~1.3e7) * heatLoad
would SLAM the live CoolantMass bars to empty whenever the link resolved; and
RefrigerationSimulation clamped massScale permanently to 1.0 (minimum) instead
of the authentic 3.0, plus null-deref'd a Condenser whose linkedSinks is skipped.
Now both read this->Subsystem::damageZone->damageLevel (qualified past the
MechSubsystem shadow, per mechweap.cpp:252): undamaged 0 -> coolantDraw 0 (no
leak, coolant bars stay full = authentic pristine) / massScale = 3.0, rising
only as the sink/condenser itself takes battle damage.
After this only HeatSink/AmbientTemperature remains NULL (the #if 0'd aggregate
HeatSink bank -> P3). Verified DBASE+dev gauges: no crash, combat un-regressed
(TARGET DESTROYED). (Also noted: the Condenser dup-ctor links to the REAL
heatfamily_reslice definition -- LNK4006 keeps the first/real one; the heat.cpp
stub is ignored -- so no bug today; #if 0 cleanup deferred to the Condenser table.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gauge-databinding-map workflow found most cockpit gauges resolve NULL
because the reconstructed subsystems publish only a fraction of the attributes
the config binds. First publishing batch (attribute tables are read-only static
data; ids kept a dense prefix from each parent's NextAttributeID):
- HeatSink table dense-append: DegradationTemperature/FailureTemperature (the
condenser temp-bar warn/max endpoints -- were NULL, so the two-part bars could
not scale), NormalizedPressure/DegradationPressure/CoolantMassLeakRate, and the
HeatSink link. Condenser/Reservoir inherit this -> all 6 condenser temp bars
now resolve current/warn/max (verified: BT_GAUGE_ATTR_LOG all OK).
- PoweredSubsystem::GetAttributeIndex() (new) publishes InputVoltage->voltageSource
-- the cluster power-branch gate (the power-lamp/generator-voltage/state-lamp
sub-branch is skipped when it resolves NULL). Flows to Sensor/Myomers/weapons.
- MechWeapon::GetAttributeIndex() (new) publishes OutputVoltage/PercentDone->
rechargeLevel; Emitter/PPC/ProjectileWeapon/MissileLauncher/GaussRifle DefaultData
re-pointed at it (they carried an EMPTY default-constructed index -> resolved
NOTHING). Verified: the ER MED LASER / PPC / STREAK weapon clusters now render
live recharge dials (were blank TEMP/STATUS).
- Mech::SetTargetRange un-stubbed (radarRange = range) -> the radar map scale +
overlay range readout track the mapper's zoom (was frozen at 1000).
- GAUGREND ParseAttribute: env-gated per-binding resolution trace (BT_GAUGE_ATTR_LOG)
-- durable diagnostic infra for the wave.
Verified DBASE+dev gauges: no startup/gauge-construction crash (dense chain intact),
combat un-regressed (TARGET DESTROYED), clusters build with InputVoltage resolving.
Remaining config-binding NULLs: HeatSink/AmbientTemperature (aggregate bank, P3) +
Searchlight/LightOn (P1b).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues the user hit driving the Mad Cat: (1) projectiles appeared to launch from the
mech's FEET -- MechWeapon::GetMuzzlePoint falls back to the mech origin (localOrigin) when the
weapon's mount segment (this+0xdc) doesn't resolve, so the passed muzzle was at ground level;
(2) they rendered as a thin laser-like line, not a missile.
Fix: BTPushProjectile now takes the SHOOTER mech and resolves the real launch port by NAME
(sitermissleport / sitelmissleport, alternating L/R for a salvo look; then torso ports, then
gun ports; then a raised fallback) -- the same segment-name -> world-transform mechanism the
visible laser beams use for the gun ports. The tracer is now a 3-part missile look (dim smoke
trail behind + orange body + hot flame tip) instead of a single thin beam. FireWeapon (both
ProjectileWeapon and MissileLauncher) passes owner as the shooter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Interactive repro: fire one missile salvo, then only lasers. Cause: ProjectileWeapon::
CheckForJam gates the heat-scaled jam roll on `heatLoad > 0` (true whenever firing), and
the roll uses currentTemperature -- which the reconstructed heat model pins at a constant
~77 (thermalMass is huge, so temp doesn't rise/fall with fire yet). So the roll fired
spuriously on ~every shot, and since JammedState clears only on ResetToInitialState, a
weapon that jammed stayed jammed for the whole mission. (The auto-fire harness hid this --
it kills the target before jams accumulate.)
Fix: gate the roll on the weapon being in a degraded HEAT STATE (heatAlarm level >=
DegradationHeat) instead of raw heatLoad -- the authentic "jam risk scales with heat" rule.
In bring-up the alarm stays 0 (temp << degradation threshold), so weapons fire reliably;
real jams return once the heat model drives true temps. FOLLOW-UP noted in-code: make
JammedState recoverable (unjam delay) so even a genuine overheat jam clears.
Verified: 31 projectile impacts sustained over 18s, 0 crashes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368)
is infeasible on the 2007 engine: measured sizeof(engine Entity)=0x1BC vs the 1995 binary's
0x300, so the reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250)
read GARBAGE on the engine (the Mech 0x638-vs-0x854 gap, but the entity integrator DEPENDS on
those offsets). So -- like the mech drive and the beam renderer -- flying projectiles are a PORT
reconstruction (BTPushProjectile/BTUpdateProjectiles in mech4.cpp, a static array + stack
messages, ZERO heap ops): seeded from the launcher's fire with the decomp's real muzzle
(GetMuzzlePoint) / launch speed (|launchVelocity|) / per-shot damage (damageData, split across
missileCount), they fly to the target (tracer via BTPushBeam) and deliver the weapon's damage on
impact through the SAME Entity::TakeDamage path as the beam (aim Mech::FirstVitalZone()).
THREE bring-up fixes were needed to make a projectile weapon fire at all (found by tracing):
1. TRIGGER: fireImpulse was only driven for the Emitter; ProjectileWeaponSimulation now sets
fireImpulse = gBTWeaponTrigger too (else CheckFireEdge never sees an edge).
2. AMMO BIN: OwnerSubsystemCount/OwnerSubsystem were stubbed ->0, so ammoBinLink never resolved
and ConsumeRound always failed; redirected to the real roster (owner->GetSubsystemCount()/
GetSubsystem(i) -- the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0).
3. JAM ROLL: UniformRandom() was stubbed `return 0.0f`, so CheckForJam's `0 < jamChance` ALWAYS
jammed (a projectile weapon could NEVER fire); replaced with a real LCG [0,1) (fires
~1-jamChance of the time -- authentic occasional jams).
Also: the mech's own target slot (owner+0x388) isn't populated in bring-up (the visible fire
targets the gEnemyMech global), so BTPushProjectile falls back to gEnemyMech.
Verified: Mad Cat PUSH=62 / IMPACT=31 (LRM missiles 3.33 dmg each split across the salvo + AFC100
autocannon 25 dmg), TARGET DESTROYED, 0 crashes, construction heapcheck-clean; BLH un-regressed
(also fires its ballistic weapon now). Diagnostics BT_PROJ_LOG ([projectile] PUSH/IMPACT).
REMAINING (deferred): the byte-exact world-entity Missile (Projectile : Mover, MP-replicable via
Registry::MakeEntity) -- the port projectile is master-local only (no MP replication); the real
per-weapon fire-rate/heat wiring off the subsystem sim (mech4 beam path is still the bring-up harness).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An 8-agent decomp-mapping workflow mapped every weapon-family ctor + fire/spawn path
(scratchpad/wave7_maps.txt). KEY IDENTITY CORRECTION via VDATA.h enum (base 3000=0xBB8):
the factory ctor-address comments were right but the built CLASS names were stubs/base --
0xBCD == ProjectileWeapon (was building the base MechWeapon stub)
0xBCE == GaussRifle (NOT MissileLauncher -- was a MissileLauncher stub)
0xBD0 == MissileLauncher (NOT BallisticWeapon -- was a BallisticWeapon stub)
Un-mislabeled + wired via Create<Class>Subsystem bridges (mirroring CreateEmitterSubsystem),
each static_assert-locked byte-exact on the now-locked MechWeapon(0x3F0)/Emitter(0x478) bases:
* ProjectileWeapon (0xBCD, @4bc3fc : MechWeapon, sizeof 0x448)
FIX: AmmoBinConnection was an empty 1-byte struct -> retyped to the binary's 0xC
SharedData::Connection (was making the object 8 bytes short, sliding
MissileLauncher's missileCount off 0x448).
* MissileLauncher (0xBD0, @4bcff0 : ProjectileWeapon, sizeof 0x44C)
FIX: deleted muzzleVelocity (ALIAS of inherited ProjectileWeapon launchVelocity@0x410)
and salvoCount (SHADOW of inherited MechWeapon damageData.burstCount@0x3d4),
keeping the single own field missileCount@0x448.
* GaussRifle (0xBCE, @4bdcb4 : Emitter, sizeof 0x484 = Emitter + Vector3D muzzleVelocity)
DEDICATED bridge (not CreateEmitter): its FireWeapon is a no-op (mov [eax+0x414],0) --
a non-functional weapon in this 1995 build. Also fixed GAUSS.CPP's discharge write
rechargeLevel -> currentLevel@0x414 (the binary writes this+0x414).
Phase A safe-stubs FireWeapon (consume round + recoil, NO spawn): the reconstructed
Projectile (@4be1bc, 0x340) + Missile (@4bf5b4, 0x368) ENTITY classes are NOT spawn-ready
(Entity-base phantom members overflow their New() alloc -> heap corruption; New() takes
(int)this instead of the 0xD4 descriptor; Entity base size unconfirmed 2007-vs-1995, the
Mech 0x638-vs-0x854 problem). The workflow's adversarial verify flagged the live spawn as a
heap-overflow hazard and recommended exactly this phasing. Phase B = the entity byte-exactness
+ descriptor build + New(MakeMessage*) so a fired shot becomes a flying entity that damages.
Factory now 18 of 20 cases wired to real ticking classes (remaining: SubsystemMessageManager
0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). Verified: Mad Cat (LRM/ballistic) + BLH construct
the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes,
heapcheck-clean through construction. Energy weapons already damage via the mech4 beam path,
so combat is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset. Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact. An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation. static_assert-locked chain (verified vs the raw ctors):
HeatSink 0x1D0
-> PoweredSubsystem 0x31C retype electricalStateAlarm@0x264 + modeAlarm@0x2B8 @004b0f74
-> MechWeapon 0x3F0 retype weaponAlarm@0x350; delete 5 phantom tail fields @004b99a8
-> Emitter 0x478 delete outputVoltage/beamLengthRatio/firingArmed aliases
+ beamHit*/beamColor/beamHitData/energyRampTime phantoms;
retype beamOrientation EulerAngles->Quaternion(16B) @004bb120
-> PPC 0x478 (no own fields) @004bb888
PoweredSubsystem -> Sensor 0x328 (no alarm; 3 own fields) @004b1d18
PoweredSubsystem -> Myomers 0x358 (no alarm) @004b8fec
New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
* alias field - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
* phantom field - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
(past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
or the value is a method local)
Non-layout fixes required in the same wave:
* outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
* MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
* DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
writes the 0x264 alarm)
The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).
Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed). LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reconstruction modeled the binary's shared alarm/connection types with undersized
stand-ins, sliding every field above them low (the 72-byte auxScreenNumber gap). Fix the
foundational heat-leaf classes byte-exact + static_assert-lock them, from the ctor decomp:
Shared types corrected:
* SubsystemConnection 4 -> 0xC (binary link node FUN_004af9cf; FUN_00417ab4 derefs +8)
* GaugeAlarm54 = 0x54 (real AlarmIndicator FUN_0041b9ec; STATUS level at +0x14,
so subsystem+0x184 == heatAlarm+0x14 == GetLevel())
WatcherGaugeAlarm now typedefs GaugeAlarm54 (Watcher branch locks stay valid).
Byte-exact + locked (ctor-verified):
* HeatSink heatEnergy@0x158 linkedSinks@0x164 heatAlarm@0x170 resource@0x1C4
pendingHeat@0x1C8, sizeof 0x1D0 (@004adda0)
* Condenser valveState@0x1D0 condenserAlarm@0x1DC (@004ae568)
* Reservoir reservoirAlarm@0x1D0 ... squirtEfficiency@0x22C, sizeof 0x230 (@4aef78)
* Generator stateAlarm@0x1FC, sizeof 0x250 (@004b225c)
* Myomers phantom moverConnection tail removed (fits 0x358)
Three systemic bug classes fixed (added to the checklist in CLAUDE.md / HARD_PROBLEMS.md):
* alias field - a subclass member re-declaring an inherited slot the ctor reuses
(Condenser refrigerationOutput==massScale@0x160; Reservoir
coolantCapacity==thermalCapacity@0x128) -> use the inherited name
* alarm-interior - a value read at alarm+0x14 modeled as a separate member
(HeatSink heatState@0x184, Reservoir injectActive@0x1e4)
-> route to alarm.GetLevel()
* phantom field - a member past the object end (Generator shortFlag@0x25C is really
*(owner+0x190)+0x25c the msg-manager, @004b0efc; Myomers
moverConnection@0x110 a write-only base slot) -> remove it
Heat conduction now reads the REAL heatEnergy=1.34e7 (not garbage); combat DESTROYED-in-8,
0 crashes, heapcheck-clean through construction.
REMAINING (measured; a distinct larger task): making PoweredSubsystem byte-exact grows it
+0x98 and cascades into MechWeapon/Emitter/PPC/Sensor/Myomers -- all model the 0x54
AlarmIndicator with 4-byte ReconAlarm / 8-byte HeatAlarm stand-ins and are short +
phantom-tailed; retyping without byte-exacting them overflows the Emitter alloc (heap
corruption). PoweredSubsystem kept on HeatAlarm(8) stand-ins (marked) pending a
subsystem-tree ALARM UNIFICATION. See docs/HARD_PROBLEMS.md P7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the measured/decompiled diagnosis of the systemic heat-leaf layout gap
(auxScreenNumber compiles to 0x194 vs binary 0x1DC, -72): duplicate temps +
spurious heatState/heatModelFlag (really alarm-internal) + SubsystemConnection
4->0xC + HeatAlarm 8->0x54 (AlarmIndicator confirmed 0x54; HeatFilter already
byte-exact). Includes the offsetof-probe measurement technique.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>