- 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>
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>
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>
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>