Commit Graph
27 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 ac94ce6a31 #96: DISSIPATION AUDIT -- byte-faithful end to end; the cooling-too-weak hypothesis is DEAD
"no guesses. we want to decompile and find the right answer" -- done.  Every
function in the heat-removal chain read from the decomp and diffed against our
reconstruction:

  HeatSinkSimulation @004ad924   E += pending; T = E/mass; alarms       EXACT
  UpdateHeatLoad     @004ad7f0   radiated = T x coolant; clamp(0.002x)  EXACT
  ConductHeat        @004ad8ac   exchange into linked + rebalance       EXACT
  ComputeHeatFlow    @004ad9ec   (T1*mScale - (E1+E2+pend2)/den) * tau
                                 * (1 - e^(-dt*k*(cool/cap)*flow/den))  EXACT
  BalanceCoolant     @004ada94   radiated-equalising move, 3 clamps     EXACT
  Condenser sim      @004ae4d8   mScale = clamp>=1((1-zoneDmg)*refrig)  EXACT
  Reservoir sim      @004aef78   inject-only; thermally passive         EXACT

Every term is properly dt-scaled -- the dissipation side is rate-INDEPENDENT.
The only rate-dependent energy term in the whole thermal system remains the
myomer kinetic generation, i.e. the calibration knob is pointed at the right
factor and nothing on the cooling side can absorb the blame.

LIVE chain verification (new [heatflow] census, incl. a RADIATOR tag on the
bank's own Performance, which the base-sim census could not see):
  Myomers 490 -> Condenser5 420 -> bank 337 -> ambient (relaxing onto the
  300K setpoint).  The exit exists, runs, and holds the bank just above 300.

AND ONE REAL FINDING -- my night-8 "no per-chassis cooling variation" claim was
WRONG, and Oracle's stated mechanism is vindicated to the digit:
  bank k = 0.1 x HeatSinkCount x 231000:   Owens count=5  -> k=115,500
                                           Thor  count=13 -> k=300,300
The Thor sheds 2.6x the Owens' heat through the mech's ONLY ambient exit --
"a thor could run seek 4 for extended periods because of the number of
heatsinks" is exactly the authored model.  My hsparm chassis comparison had
printed RESOURCE constants BEFORE the ctor's count scaling; the dump now
carries a warning to stop the next person misreading it the same way.
(The KB was never wrong -- open-questions.md:593 documented the radiator
scaling when task #9 landed it.  The wrong claim lived in my #96 comment.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:12:46 -05:00
Joe DiPrimaandClaude Opus 5 e058b6acb7 #99: trace the coolant-leak alarm end to end -- it IS implemented; found the real defect
Oracle: "alarm is not sounding despite an active leak" ... "was sounding,
resolved a leak, but there was another".

RETRACTION FIRST.  I previously told the user the coolant alarm was "genuinely
unbuilt -- there is no alarm implementation to starve".  That was wrong, and it
was asserted without checking.  The alarm is fully implemented and the entire
chain works.  Traced live, in order:

  Condenser6.ReportLeak (= HeatSink::coolantActive) changes 0 -> 1
    -> [watchpoll] CHANGE on that exact address
    -> [matchfire] val=1 -> ctl 1  (StartAudioControlID)
    -> [seqstart] the alarm AudioControlSequence, events=25 looped=1
    -> [seqsend] ctl 8/16 (select), 6/1 (volume), 1/0 (START), 2/0 (STOP), ...
       a three-part looping alarm: two chirps then an 8s sustained tone.

The authored design, read from BTL4.RES: 19 subsystems per mech each bind TWO
AudioLogicalTriggers to their ReportLeak flag -- match 1 -> Start, match 0 ->
Stop -- and ALL of them drive ONE shared alarm sequence.

SO WHY WAS IT SILENT IN .674?  Almost certainly the audio source pool, fixed
after that build in ad9dfad.  Every player log from .674 is saturated with
ACQUIRE FAILED (3k-6.5k lines each, starting ~10% in and never recovering); an
alarm that cannot acquire an OpenAL source is silent.  The bench here shows 0
acquire failures on the current build.  ⚠ NOT PROVEN: this bench has no audio
device (pool census reads live=0 pooled=0), so the control chain is verified but
final playback is not.  Field confirmation needed.

THE SECOND SYMPTOM IS A REAL, REPRODUCED DEFECT AND IT IS IN THE AUTHORED DATA.
All 19 subsystems -- Condenser1-6, GeneratorA-D, Myomers, PPC_1/2, ERMLaser_1-3,
SRM6_1/2, Avionics -- share the single alarm sequence, and EACH one's leak-clear
sends an UNCONDITIONAL Stop.  So the moment any one of them stops leaking the
alarm goes silent, even while others are still leaking.  That is exactly what was
reported.  Since both the authoring and the MUNGA watcher code are authentic,
this is 1995 behaviour, and changing it is a deliberate divergence -- flagged for
a fidelity call rather than "fixed" unilaterally.

Diagnostics added (all under the existing BT_ATTRBIND_LOG gate):
  [seqcfg]  extended: the sequence's authored event list
  [seqstart] extended: events / looped / tempo / divisionsPerBeat
  [seqrun]  RunSequence entry state (isRunning / iterator / current event)
  [seqwait] an event exists but is not yet ready, with the time delta
  [seqsend] the sequence actually emitting a control
  [idlewatch] which components receive the idle tick that advances sequences
  [hsparm] extended with &ReportLeak so audio bindings can be correlated
plus scratchpad/night8/leakaudio.sh.

NOTE ON THE PROBES, because it cost real time twice today: [seqrun] first used a
single shared static counter, which the busy sequences consumed so the alarm
sequence never printed -- reading as "RunSequence is never called".  It is now
throttled PER SEQUENCE (runProbeCount).  A capped diagnostic that is silent is
not evidence of absence; the same trap produced a false "seqsend x0" and a false
"the trigger never fires" earlier in this investigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:07:40 -05:00
Joe DiPrimaandClaude Opus 5 e6103d02b2 #97: leak gauge reaches all THREE levels -- the normalisation was dropped with an x87 expression
Players: "you can get up to three traingles which drains really quickly" ...
"only seeing one level (lowest) right now" (Oracle), corroborated by Draco
("3 triple delta being the top level").

The display was never the problem: BitMapInverseWipe is already built with
frames=3 and stacks three segments at two levels each (0..6).  The LEVEL
computation was wrong.

Our Execute rounded the raw leak rate:
    level = round(value)
The binary's @004c5d08 does NOT.  Ghidra renders the round as a bare
`FUN_004dcd94()` because it drops the x87 expression feeding __ftol -- exactly
KB gotcha 19, which we already had written down.  The real prologue, disassembled:

    fild  dword ptr [ebp-0x14]    ; ST0 = (float)fullWidth   (frames*2 = 6)
    fmul  dword ptr [ebx+0xb4]    ; ST0 *= value             (CoolantMassLeakRate)
    fdiv  dword ptr [ebx+0xb0]    ; ST0 /= third             (full-scale divisor)
    call  0x4dcd94                ; level = round(ST0)

i.e. level = round(fullWidth * leakRate / fullScale).

Without the normalisation the gauge rounded a value that never exceeds ~1.0
(coolantDraw = zoneDamage * heatLoad, and heatLoad is clamp(0.002*T, 0, 1) --
constants re-verified from the image, including the 80-bit extended 0.002).  So
level could only ever be 0, or 1 via the >0.0025 floor: ONE triangle, always, no
matter how bad the leak.  Three of the six condensers being mis-lamped (#98) hid
how systematic this was.

Also corrected on the way:
  * `third` was declared int but the binary FDIVs it -- it is a float.  The two
    call sites passed *(int *)(subsystem+0x150), i.e. the BIT PATTERN of a float
    read from a RAW OFFSET into our own layout (the databinding trap): at +0x150
    our layout has filterDecay, not the binary's field.  Now resolved through a
    complete-type bridge, BTHeatSinkLeakFullScale.
  * filterDecay was initialised 0.4f; @004b8fec writes 0.15f (param_1[0x54] =
    0x3e19999a).  It is written-once-never-read elsewhere in the port, so the
    correction is behaviourally safe and it IS the gauge's full scale.

Verified live: Condenser6 at 10% zone damage -> draw 0.0358 -> level 1 (one
triangle); the same formula reaches all three at heavier damage, and the mapping
spans draw 0.0026 (half of one) to 0.15 (three full).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:54:13 -05:00
Joe DiPrimaandClaude Opus 5 9dd7d48b41 #96: instrument the myomer heat model -- per-mech profile CONFIRMED, overheat cause LOCATED
Two player claims, both now answered from the decomp + measurement rather than
inference.

THE EQUATION (FUN_004b8d18, constants read from the image: _DAT_004b8ee4=0.5,
_DAT_004b8ee8=0.0 (the fabs), _DAT_004b8eec=1.0):

  heat += ratio^2 * (1+damageLevel) *
          [ (1-accEff)*|vy|*m*g*dt        climb   POWER
          + (1-velEff)*(0.5*m*|v|^2)      kinetic ENERGY -- NO dt
          + (1-accEff)*|v|*|a|*m*dt ]     accel   POWER

Our implementation already reproduces this verbatim, dt-less term included.

CLAIM 1 -- "each mech has a unique heating profile".  TRUE, and working, but NOT
by the mechanism the player described.  Measured across thr1/own1/mad1/vul1:
  * the myomer record is IDENTICAL on every chassis
    (velEff 0.995, accEff 0.8, gears 3000/5000/7000/9999, rec 2,
     degradeT 1000, failT 2000, thermalMass 250000)
  * the heat-family COUNT is identical too -- 6 Condensers, 1 HeatSinkBank,
    1 Reservoir, 4 Generators on all four
  * every cooling parameter is byte-identical Thor vs Owens (condenser
    conductance 315000 / mass 420000, bank 231000 / 1.39e6, reservoir
    190000 / 3.42e6)
So there is NO authored per-chassis cooling variation.  The profile emerges from
the equation instead: heat ~ m*v^2, and light mechs are faster.  Measured at
seek 4, flat out:
    thr1  mass 70000  |v| 11.34  kinetic/tick 49026
    own1  mass 35000  |v| 17.22  kinetic/tick 57350
The Owens is HALF the mass and generates 17% MORE drive heat, because v^2 beats
m.  That reproduces the player's OUTCOME (a Thor sustains seek 4, a light
chicken-walker cannot) via speed, not heatsink count.

CLAIM 2 -- "it runs too hot".  The kinetic term carries NO dt: it adds an ENERGY
every TICK, so its contribution per SECOND scales with the tick rate.  Measured
dt here is ~0.017 (~59Hz) and variable.  The other two terms are power terms and
are rate-independent.  On flat ground the climb term is additionally dead --
gravity reads 0 (the carried "environment gravity unwired" open), so hills do not
heat at all right now.
NOT yet established: the 1995 tick rate the dt-less term was calibrated against.
Until that is pinned the OVERHEAT FACTOR is unquantified -- flagged, not guessed.

Adds three diagnostics, all under BT_MYO_LOG:
  [myoheat] now splits climb/kinetic/accel + dt + the kinetic share
  [myoparm] one line per myomer: efficiencies, gears, thermal thresholds
  [hsparm]  one line per heat subsystem: conductance + thermal mass
and three benches (myoheat/myoparm/myocmp) that produced the tables above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:14:55 -05:00
Joe DiPrimaandClaude Fable 5 31552c0444 #88 part 2: the central coolant pull-through -- HeatSink::DrawCoolant was a
return-0 stub; the Reservoir BAR now drops when a damaged mech leaks

The user watched the coolant panel's level bar during the leak bench and it
never moved -- correctly: a leaking subsystem drained only its own loop,
because the slot-14 top-up (DrawCoolant) was stubbed.  Disassembled the real
base @0x4add00: the draw RECURSES UP the sink linkage, scaling by
coolantFlowScale@0x15C per hop, terminating at the Reservoir's supply
(@0x4af3b0 -- clamp to [0, coolantLevel], drain the tank, return granted --
already faithfully ported as Reservoir::DrawCoolant).  The binary's terminal
hop is the bank's slot-14 override @0x4ae8b0 resolving its reservoir
connection @0x1D8 (the port's 'helper' member -- not vestigial after all);
the port's existing Reservoir-ctor Attach + Reservoir override terminate the
same chain equivalently (the extra bank hop scales by ctor-default 1.0).

Implemented the base recursion (null-guarded -- the binary calls through the
resolved link unguarded; authored topology always links) + a BT_COOL_LOG
[resdraw] probe at the supply.

Verified live: a destroyed myomer's damage leak pulled the central tank
6.0 -> 5.58 over ~30s ([resdraw] granted lines) -- Reservoir/CoolantMass,
the cockpit coolant bar, now visibly drops as a damaged mech bleeds
coolant, and DeathReset refills it on respawn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 08:47:55 -05:00
Joe DiPrimaandClaude Fable 5 a8a0042f28 #88: coolant leaks LIVE -- the #64 damageZone shadow was the single break
in the leak chain

HeatSink::UpdateCoolant prices the damage-driven leak from the QUALIFIED
engine member (Subsystem::damageZone @0xE0): coolantDraw = zoneDamage *
heatLoad, with the coolantActive hysteresis (= the ReportLeak attribute
the 19 authored leak watchers ride).  But the MechSubsystem ctor only
ever filled its re-declared SHADOW member (gitea #64, gotcha #1), so the
engine member stayed NULL forever, zoneDamage pinned 0, and a coolant
leak was STRUCTURALLY IMPOSSIBLE no matter how much damage landed --
Oracle's night-7 report exactly.  The #80 crit fix and the #83 collision
rattle write real subsystem-zone damage, but into an object the leak
reader could never see.

Fix: alias the ENGINE base member to the same zone in both MechSubsystem
ctors.  Every shadow reader keeps working (same object); the engine base
Subsystem::TakeDamage latent null-AV (#64 consequence 2) is disarmed;
the full #64 de-shadow sweep remains the long-term cleanup.

Verified live (solo, BT_COOL_LOG/BT_CRIT_LOG): collision rattle drove a
damaged Myomers to [cool] draw = dmg*heatLoad with the level draining;
sustained weapon fire rolled real crits ([critroll]) and produced 588
leak-pricing lines.  Authored routing extracted from BTL4.RES: collision
rattle targets HeatSinkBank 0.3 / Gyro 0.35 / Torso 0.25 / Myomers 0.35
(Condenser+Reservoir authored 0 for collisions); weapon crits select via
the zone's crit-entry list.  Known-remaining fidelity item (documented):
HeatSink::DrawCoolant (slot 14, central top-up) is still a return-0 TODO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 08:26:40 -05:00
Joe DiPrimaandClaude Fable 5 14ff3519ac #83: the COLLISION DIVERT -- type-0 damage prices as internal rattle, never armor
The crash self-damage reconstruction (9c83a46) exposed its missing second
half: the binary's TakeDamage hub DIVERTS damageType 0 (@0x4a0368) into
FUN_0049ffcc -- an export-gap distributor recovered by raw disasm -- so
collision damage NEVER reaches the zone/armor loop.  Without the divert the
mover's raw kinetic-energy figure (60-ton mech: ~1000+ per wall tap, 90k+ on
a hard slam) fell into the WEAPON loop: 'tapped a wall and died instantly'.

The distributor, byte-anchored: gate on the owning player's advancedDamage
copy (+0x268 -- the manual's 'splash/collision damage' technician setting);
scale = (2000/moverMass) / (100 km/h in u/s)^2 / (1 - elasticity^2)
(tbyte 1/3.6 @0x4a0148; mass @+0x20c; elasticity @+0x244); scaled < 0.5 is
FREE; else Round(2x) sub-hits of 0.5 land on random INTERNAL subsystems
(HeatSink family / Gyroscope / Torso, drawn by collisionCriticalHitWeight
@0x10C) via ApplyDamageAndMeasure (@0x4ac07c).  Bridges: family derivations
(heat/gyro/torso.cpp), BTPlayerAdvancedDamageOn (btplayer.cpp).

Verified: bhk1 wall test -- 94k slam = 4.2 rattle pts in 8 sub-hits, taps
free (0.049 < 0.5), armor untouched.  A/B: arena wall-grind now 0 deaths,
27 rattle events, the limper STAYS GIMPED and peers' replicants re-enter the
gimp cycle between bounded staggers; grass circling unchanged (198 replicant
gimp entries).  [crashdmg]/[colldmg] probes document the pricing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:04:24 -05:00
arcattackandClaude Opus 5 ef8e449a17 #55 steps 0/1/2/4: the 1995 DEATH pass restored, the arg gate made real, and 5 subsystems the respawn sweep never reached
THE HEADLINE: the port's death sweep was a total no-op, and fixing it required
fixing the arg gate in the same commit -- otherwise corpses refill their ammo.

STEP 2 -- the authentic dispatch shape:
  * engine/MUNGA/SUBSYSTM.h: Subsystem::DeathShutdown gains the binary's
    universal base body { DeathReset(c) } (@004ad10e).  It was empty, and NO
    port class overrode it, so everything the 1995 game does at death was
    skipped entirely.
  * mech4.cpp death sweep now passes 0, not 1 (the binary's @0049fe0c arg) --
    the wreck shape: alarms/state settle, nothing refills.
  * ARG PROPAGATION (mandatory once the sweep runs): AmmoBin::DeathReset is now
    arg-gated exactly as @004bd26c -- refill only when arg != 0, ammoAlarm
    unconditional.  Ignoring the arg was harmless only while the sweep was
    dead; with it restored, every corpse would have re-armed.  Also forwarded
    in PoweredSubsystem / HeatSink / Condenser / HeatableSubsystem / Generator
    (each binary body forwards it -- verified addresses in the plan).

STEP 4 -- coverage for 5 classes that had an authentic slot-10 body but no
DeathReset, so the respawn sweep fell through to the empty base:
  Torso (this is Gitea #70 -- "loosing torso twist function after a death"),
  Gyroscope, Myomers, HUD, Seeker.

STEPS 0/1 -- observability + the David chain:
  * Three unconditional matchlog rows: DEAD_NOTIFY (mech + resolved link),
    PLAYER_LINK (player <-> vehicle), RESPAWN (mode/alive/zones/subsys/pos).
    A respawn was previously INVISIBLE in the matchlog -- night 3's analysis
    had to infer them from ammo arithmetic.
  * Mech::PlayerLinkMessageHandler + the death dispatch site: when the engine's
    one-shot registry lookup misses (no null check, no retry -> the whole
    death/respawn cycle silently swallowed), recover the SAME object via the
    reverse link the binary's own respawn branch walks (player+0x1FC ==
    playerVehicle).  Complete-type TU, no raw offsets.
  * deathPending cleared in the first-spawn branch (a latch carried in would
    permanently kill every later respawn -- the #57 class).

VERIFIED on the 2-pod rig (madcat vs thor, forced kills, 5 death/respawn cycles
per pod): build clean, ZERO new /FORCE unresolved externs from the 5 new
overrides; refill lines appear ONLY immediately before a Mech::Reset and NEVER
between a death and the next respawn (the corpse-refill regression this commit
had to pre-empt); all three probe rows present in both pods' matchlogs; every
DEAD_NOTIFY carried a non-null link.

DELIBERATELY DEFERRED (documented, not forgotten): the mechsub.cpp rename pair
(ResetToInitialState -> GenerateFault, ClearStatus -> the root reset @004ac22c),
deleting HeatableSubsystem::ResetToInitialState, and deleting RespawnRepair.
Those need the HeatableSubsystem vtable-slot-10 pre-flight the plan calls for,
and we have no binary image here to dump the vtable from -- guessing at it
risks the vptr-alias trap.  Next session with the decomp shards open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 08:09:31 -05:00
arcattackandClaude Fable 5 f3179ef020 #47 follow-up: restore the DAMAGE TIER to the heat branch -- HeatableSubsystem::GetStatusFlags was a stub returning 0
Found while auditing today's vtable change before release.  The binary's
HeatSink::GetStatusFlags @004add30 OPENS with `call 0x4ac144` --
MechSubsystem::GetStatusFlags, the damage tier (damageLevel >= 1.0 -> bit 0
Destroyed; > 0 -> bit 1 Damaged).  There is no zero-returning
HeatableSubsystem override anywhere in the image.

Our port routed that call through `HeatableSubsystem::GetStatusFlags()
{ return 0; }` -- a "neutral default" that silently dropped bits 0 and 1 for
the ENTIRE heat branch (every weapon, sensor, emitter, PPC, generator,
powered subsystem).  Two live consequences:

  * Destroyed / Damaged could never reach MechTech's annunciator scan --
    gutting half the shipped mechalrm table (Destroyed -> gotoEngineering +
    engCooling + engBusMode) hours after #47 shipped the flash;
  * PoweredSubsystem's AutoConnect gate (`GetStatusFlags() == 0`,
    powersub.cpp:358) read a DAMAGED subsystem as pristine.

One-line fix: chain the real tier, reproducing @004add30 exactly.  The call
site's own comment already said `// FUN_004ac144` -- the implementation had
simply never matched it.

Also corrected a stale comment on MechSubsystem::GetStatusFlags that read the
field as a STRUCTURE level ("1.0 = intact, 0 = dead") -- backwards.  The
engine member is damageLevel and ACCUMULATES to 1.0 = destroyed (mechdmg's
crit cascade tests `level >= StructureMax` for exactly that), which is what
makes bits 0/1 line up with TechStatusType Destroyed/Damaged.  Behaviour was
always right; the comment was a leftover of the same structureLevel misreading
#46 retired.

VERIFIED (3-pod combat sim, BT_LAMP_LOG): the annunciator is genuinely live --
Jammed x7, AmmoBurning x5, BadPower x20, Overheating x207 (Overheating
correctly maps to NO lamp: the shipped table has no entry for it).  The lamp
resolution is per-subsystem as designed -- different weapons flash their own
panel buttons (lamps 0xf/0x5/0xb/0x3 under distinct mode masks), 32 flash
writes across a whole battle, no thrash.  Zero crashes, zero plane/OOB
tripwire hits across all three pods.

FILED, deliberately NOT fixed tonight (open-questions.md): vtable slot +0x40
is misattributed.  The binary calls it `(this, 0)` in AutoConnect's outer gate
and `(this, candidateGenerator)` in the roster walk -- a one-arg voltage query
(HasVoltage(source)), not GetStatusFlags().  Modelling both as the no-arg
GetStatusFlags makes the outer gate demand "no flags" and the inner "some
flag" with nothing between them, so AutoConnect's loop body can never execute.
Pre-existing before and after this change; it is a live power-bus behaviour
change and deserves its own playtest cycle, not a release-eve patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 17:58:20 -05:00
arcattackandClaude Fable 5 6f5a264835 Gitea #47 COMPLETE: the ENG-button attention FLASH is live (jam / bay fire) -- MechTech status scan + BTL4GaugeAlarmManager + lamp chain, all from the binary
The pod behaviour Cyd described -- "on a jam or bay fire the display eng button
for the system flashes" -- is authored data + a five-stage chain, now running:

  MechTech::TechnicalAssistance (@004ad33c, per frame)
    edge-scans every monitored subsystem's GetStatusFlags() 7-bit condition
    mask (TechStatusType: Destroyed 0, Damaged 1, CoolantLeaking 2,
    Overheating 3, AmmoBurning 4, Jammed 5, BadPower 6)
  -> Start/StopEntityAlarmMessage (@00436688 id 7 size 0x20 / @004366b8 id 8
     size 0x1C; broadcast @004364e4; port shape: direct
     Start/StopEntityAlarmImplementation calls on the gauge renderer)
  -> GaugeAlarmManager::Activate(alarmModel) -- alarmModel = MechTech+0x100 =
     the 'mechalrm' ModelList (id 83) -> SearchList(83, type 31) -> the baked
     GaugeAlarmStream (id 331, 11 items {condition, lampCode}), decoded:
         Destroyed      -> gotoEngineering + engCooling + engBusMode
         CoolantLeaking -> gotoEngineering + engCooling
         AmmoBurning    -> gotoEngineering + engEject
         Jammed         -> gotoEngineering + engEject
         BadPower       -> gotoEngineering + engBusMode
  -> BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- THE REAL BODY, from
     @004cc2fc + helpers @004cc108/148/1a0/264/27c + tables @0051cf1c..0x51d084
     (gotoEngineering 0x80 = the subsystem's QUAD-SELECT bezel button via
     lamp[aux]/mode[aux] tables; 0x81+ descend the eng-page bank; Condenser /
     Generator specials on CoolantLeaking; <0x80 = the heat-bank fixed map).
     btl4galm.cpp's old bodies were admitted fabrications and its provenance
     note ("no override body exists in the image") was wrong -- corrected.
  -> LampManager::FindLamp (@00444c80) -> Lamp::SetAlertState (@00444e64, a
     COUNTER so stacked alarms hold the flash) -> L4Lamp::NotifyOfStateChange
     emits RIO flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239) -> the
     pod's physical lamps AND the glass panels (PadRIO IS rioPointer there).

FOUR load-bearing defects found and fixed en route -- each independently fatal
to the feature:

  1. HeatableSubsystem's Derivation chained Subsystem directly, SKIPPING
     MechSubsystem (written before the WAVE-1 re-basing) -- so EVERY subsystem
     on both family branches failed IsDerivedFrom(MechSubsystem), which is
     exactly MechTech's monitor filter: the status scan watched NOTHING.
  2. MechSubsystem::GetStatusFlags was non-virtual (binary: vtable slot 12) --
     the scan's MechSubsystem* call bound statically to the base tier and the
     weapon bits could never surface.
  3. ProjectileWeapon::GetStatusFlags sat in a Ghidra export gap -- raw-disasm
     @004bbf88: base | 0x20 (weaponAlarm==5 Jammed) | 0x10 (linked bin
     cookOffArmed@0x18C AmmoBurning).  The port had "defer to base".
  4. Mech::Reset's respawn sweep blanket-cast every roster entry to
     MechSubsystem and called RespawnRepair -- wrong for the Subsystem-level
     entries (MechTech 0xBDC, SubsystemMessageManager 0xBD3).  On MechTech the
     recon damageZone slot lands on its subsystemMonitors chain head: NULL
     while the scan was broken (fix #1's bug MASKED this one), but the moment
     the monitors populated, RespawnRepair virtual-called through a
     SubsystemMonitor as if it were a DamageZone -> load-time crash in
     StateIndicator::SetState (caught with cdb; call [edx+14h] on code bytes).
     The sweep now filters IsDerivedFrom(MechSubsystem) before the cast.

Also: GUID identities pinned -- 0x50f4bc = PoweredSubsystem::ClassDerivations
(via @004b1208 = its TestInstance; btl4gau2's two "Generator" comments were
wrong, swept), 0x50fb60 = Generator's.  A shadow-field instance documented for
the deferred de-shadow: MechSubsystem::damageZone re-declares the PUBLIC engine
Subsystem::damageZone (SUBSYSTM.h:159) -- mechtech's naive `sub->damageZone`
read the never-written engine member (0 monitors again); now reads the recon
member via GetDamageZoneProxy().  Logged in open-questions.

Wiring: BTL4GaugeRenderer now constructs + assigns the BTL4GaugeAlarmManager
(the base ctor NULLs it and Activate Check()s it); MechTech's Report*/
StatusMessageSink stubs are real; alarmModel confirmed baked (= 83) at runtime.

VERIFIED LIVE (scratchpad/lampflash.py, BT_LAMP_LOG chain trace):
    [techstat] MechTech id 32 monitors 29 subsystems, alarmModel 83
    [techstat] LRM15_1 condition 4 SET (alarmModel 83)      <- bay fire armed
    [galarm] condition 4 code 0x80 -> lamp 0xd mode 0x1 FLASH
    [lamp] 0xd <- 0x37  (FLASHING)                          <- the select button
    [galarm] condition 4 code 0x85 -> lamp 0xb mode 0x4 FLASH   <- engEject
    [techstat] LRM15_1 condition 4 CLEARED                  <- detonation clears
    [techstat] AmmoBinLRM15_1 condition 0 SET               <- bin Destroyed
Regressions: baytest PASS (bay fire kills), baypurge PASS (purge extinguishes),
sim3 3-pod brawl PASS with ZERO crashes (6 kills, organic heat-route bay fires,
respawns clean through the new sweep filter).

Env: BT_LAMP_LOG ([techstat]/[galarm]/[lamp]).  KB: gauges-hud (the flash
section), decomp-reference (the GaugeAlarm closure + tables + GUIDs),
open-questions (built-note + the de-shadow deferred item), btl4gau2 comment
sweep.  checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:04:34 -05:00
arcattackandClaude Opus 5 08977ff128 Gitea #55: respawn now restores COOLANT / heat / generator state (+ a Generator reset re-transcribed from the binary)
THE GAP: Mech::Reset sweeps every subsystem with the virtual DeathReset
(mech4.cpp:1789), but only 7 weapon/ammo classes overrode it -- the entire
heat/coolant/power family fell through to the empty Subsystem::DeathReset base
(SUBSYSTM.h:161), so the respawn reset was a SILENT NO-OP for them.  All the
ResetToInitialState bodies already existed; nothing ever called them.  Hence the
field report: 'respawned with drained coolant'.

Added DeathReset forwarders (ResetToInitialState is not virtual, so each class
with its own body needs one): HeatableSubsystem, HeatSink, Condenser,
PoweredSubsystem, Generator.  HeatSink's also serves Reservoir -- the coolant
tank -- which has no body of its own; that is the one that refills coolant.

TWO MIS-TRANSCRIPTIONS FIXED, both verified against the binary with capstone:

1. HeatSink::ResetToInitialState chained HeatableSubsystem::ResetToInitialState
   with the comment 'FUN_004ac22c'.  But @004ac22c is Subsystem's terminus --
   disassembled: it reads the damage zone at this+0xe0, clears zone+0x158, and
   Set_Alarm_Levels zone+0x10 and this+0x2c to 0.  And the binary's HeatSink
   @004ad760 calls only @004ad884 (ClearHeatFilter), @004ad7f0 (UpdateHeatLoad)
   and @004ac22c -- never HeatableSubsystem.  Worse, HeatableSubsystem's body sets
   currentTemperature = 300.0f, CLOBBERING the startingTemperature that HeatSink
   assigned four lines earlier.  Dropped the call; the terminus work is already
   done for every subsystem by MechSubsystem::RespawnRepair (mech4.cpp:1790).

2. Generator::ResetToInitialState chained HeatableSubsystem and set
   outputVoltage = 0 -- which matches GNRATOR.TCP, but BT's BINARY DIVERGES from
   the TCP source, and the binary is what shipped.  Disassembled @004b215c
   instruction by instruction: it chains HeatSink (so the coolant refill DOES run
   for a generator), then generatorOn=1, startTimer=0, stateAlarm 0 then 2,
   **outputVoltage = ratedVoltage**, coolantAvailable=1, coolantFlowScale=1.0,
   startTimer=startTime.  Every offset matches our declared members exactly.
   The old version brought generators back from a respawn at ZERO output voltage,
   so energy weapons could never charge -- no recharge ring, no ready dot, nothing
   damaged.  That is a strong candidate for #54 (David's three dead lasers).

LIVE VERIFICATION (sim3.py, 3 mechs + force damage): 4 death/respawn cycles, and
every heat sink logged coolant == capacity on every respawn, with temp restored to
startingTemperature (77) rather than the clobbered 300.  Left as a silent
regression guard that only speaks if a respawn leaves coolant short.

KNOWN REMAINING [T3, noted in code]: Condenser's body chains HeatableSubsystem
rather than HeatSink, so a coolant loop's VALVE DETENT may still persist across a
respawn -- the binary's Condenser reset is not yet decompiled.  Do not claim
valves are fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-25 09:19:10 -05:00
arcattackandClaude Opus 4.8 e634709e5d Gauges: fix blank per-weapon loop/generator lamps (4A/1B/5D boxes)
The SubsystemCluster per-weapon MFD panels render two image-strip lamps in
the TEMP/STATUS area: the cooling-loop number (btploop, 1..6) and the
generator letter (btpbus, A..D). Both drew as completely empty boxes.
Three stacked databinding bugs, all now fixed:

1. Color drop (btl4gau2.cpp): AnimatedSubsystemLamp/AnimatedSourceLamp
   ctors dropped the bg/fg color params the binary passes (bg=0xff,fg=0,
   same as the neighboring temp bar) and hardcoded 0,0 -> OneOfSeveral::
   Execute did SetColor(0)+DrawBitMapOpaque(0), i.e. black-on-black.
   Restored 0xff,0 (verified vs @004c70a4 / caller SubsystemCluster
   @004c8140).

2. Shadow-field trap (btl4gau2.hpp, gotcha #2): both lamp classes
   re-declared `int selected;` while already inheriting it from
   OneOfSeveral (@0xAC). The CoolingLoop/PowerSource connection wrote the
   derived shadow copy while OneOfSeveral::Execute read the base @0xAC
   (always 0) -> every lamp stuck on frame 0 ("OFF"). Removed the
   redeclarations so &selected binds the inherited member. Loop numbers
   now render (verified: PPC->4, Myomers->5, ERMLaser->1/6, etc.).

3. Attribute-table shift (powersub.cpp + btl4gau2.cpp, gotcha #8): the
   generator-letter lamp resolved its source via
   ResolveLink(AttributePointerOf(subsystem,"InputVoltage")), but the
   BT_DEV_GAUGES audio attribute rows shifted the chained attribute ids so
   AttributePointerOf no longer landed on voltageSource@0x1D0 -> the link
   always resolved to 0 (OFF) even though the master PoweredSubsystem ctor
   DID bind voltageSource to its Generator. New BTPowerSourceFrame bridge
   reads the NAMED member via PoweredSubsystem::ResolveVoltageSource() and
   returns Generator::generatorNumber (@0x1E0), bypassing the attribute
   table -- the same pattern as the BTCoolingLoopFrame fix. Generator
   letters now render (verified: PPC_1->GeneratorA="A", Myomers->
   GeneratorD="D", SRM6->GeneratorB="B").

Result: the "4 A / 1 B / 5 D" boxes render with both the loop number and
generator letter, matching the reference. Kept BT_LOOP_LOG-gated
[loopfeed]/[busfeed] feed diagnostics; both pod + glass builds clean.

Awaiting live playtest verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 00:08:58 -05:00
arcattackandClaude Opus 4.8 63250aee41 Fix two Jul-16 regressions: BT_DEV_GAUGES crash + -net dead controls mapper
Both surfaced today (dormant ~1 day) the moment the pod launch flags
(BT_DEV_GAUGES=1 BT_START_INSIDE=1, tools/mp_launch.sh) were used to drive in
multiplayer -- which made the same-day paint change look guilty.  Confirmed on
the pre-paint build too; the trigger was the Jul-16 audio attribute work.

1) BT_DEV_GAUGES crash (cdb: CoolingLoopConnection::Update, ~15s in as the gauge
   builds lazily).  The dev-gauge cooling-loop lamp reached the cooling master by
   RAW attribute index -- GetAttributePointer(3) + *(master+0x1d4).  The audio
   commits (cc2b109 ReportLeak etc.) inserted rows into the chained attribute
   tables, so index 3 shifted onto a scalar, the resolve walked garbage, and the
   background pass AV'd.  Fix: route through a complete-type bridge reading NAMED
   members (heat.cpp BTCoolingLoopFrame: linkedSinks.Resolve() +
   Condenser::condenserNumber) -- the databinding rule; never a raw numeric
   attribute index.  Removed the now-orphaned GetAttributePointer-by-index
   helper.  Name-keyed samplers (InputVoltage) were unaffected.

2) -net dead controls mapper (mech wouldn't walk; turning/weapons still worked).
   The RIO mapper is built from a stack SubsystemResource in btl4app.cpp that set
   only name/classID/modelSize -- leaving subsystemFlags as stack GARBAGE, which
   Subsystem::Subsystem copies into simulationFlags.  A stray DontExecuteFlag
   (0x2) froze the mapper (speedDemand stuck at 0).  Config-dependent: clean in
   SP, dirty in -net; the audio commits shifted the stack and flipped the bit.
   Fix: memset each hand-built control-mapper resource to 0 (flags 0 =
   AlwaysExecute, the faithful value -- a mapper must tick every frame).

Verified with the pod launch flags: SP alive 32s no crash; MP mech walks
(speedDemand 61.5 at full throttle, ~430u traveled), both nodes alive, no crash.

KB: reconstruction-gotchas.md gains gotcha 18 (uninitialized stack-resource
flags + the raw-attribute-index-into-a-growing-table variant) and a
verify-under-the-user's-launch-flags note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:58:32 -05:00
arcattackandClaude Opus 4.8 cc2b109cc8 Audio Phase 4a (AUDIO_FIDELITY F6/F7): coolant-leak warning + missile alarm
F6 ReportLeak (the largest dead authored block -- 38 of 54 match watchers):
recovered the binary HeatSink attribute table (16-byte {id,name,off+1} rows
@0x50e438..0x50e4c8, ids 3..12) [T1] -- it confirms EVERY existing heat
binding (CurrentTemperature@0x114 .. CoolantMassLeakRate@0x130, HeatSink@
0x164) and adds the one row we never published: ReportLeak (id 12) ->
+0x138 = coolantActive, the INT leak hysteresis flag UpdateCoolant
(@004adbf8) drives 1/0 around draw 0.003/0.0025.  PoweredSubsystem derives
from HeatSink, so the single row serves all 19 authored leak watchers
through the chained index, exactly like the binary.  MechWeapon's pinned-id
pad absorbed the +1 chain shift (0x0E -> 0x0F, tripwire fired as designed).

F7 IncomingLock/DistanceToMissile (missile alarm): binary Mech table walked
in full (ids 21..56 [T1] -- also settles FootStep@0x394, CollisionSpeed@
0x4B4, UnstablePercentage@0x3F0, ReduceButton@0x340 for the next findings).
IncomingLock id 54 @0x3fc, DistanceToMissile id 56 @0x400; the old
"maxSpeed @0x400 = FLT_MAX" member was a MISREAD of the far default and is
retired (its 1000.0f "override" was RadarRange id 47 @0x404, already
published).  Real members + accumulators: Missile::MoveAndCollide reports
target + range each tick (BTReportIncomingMissile bridge); PerformAndWatch
latches per frame.  The authored beeper (match 1/0) + range->TEMPO scale
(100..800 -> 600..10, accelerating as the missile closes) read them
unchanged.  Drive is intent-level [T3]; init 0/FLT_MAX matches the binary
reset (part_012.c:9446-9447).

Regression (30s): stable; ReportLeak binds real on every subsystem (0 pad
redirects); DistanceToMissile binds with FLT_MAX live; attrnull 41 -> 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:58:08 -05:00
arcattackandClaude Opus 4.8 5ba541ed56 Audio: subsystem STATE sounds -- GaugeAlarm54 IS a StateIndicator; register Generator/Condenser/Reservoir state (task #50)
The audio subsystem binds AudioStateWatchers (class 28) to per-subsystem state
attrs (GeneratorState/CondenserState/ReservoirState) BY NAME.  Recovered the
watcher TYPE per attribute via a MakeObjectImplementation ClassID trace.

Root cause of the subsystem-state crash: the binary's 0x54-byte subsystem alarm
(FUN_0041b9ec, reconstructed as GaugeAlarm54) IS a StateIndicator -- its "three
sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, and
level@0x14 is currentState.  The reconstruction had modeled that tail as an opaque
`_tail`, so the sockets were never constructed -> AddAudioWatcher AV'd on 0xCDCDCDCD.

- GaugeAlarm54: give it the three REAL, constructed SChainOf<Component*> sockets +
  AddAudioWatcher/Video/Gauge; SetLevel now fires the watchers on a level CHANGE
  (empty sockets = no-op, so the ~all other alarms are unaffected).  levelB (weapon
  LevelCountB "reset source") is left untouched -- weapons unchanged.  sizeof stays
  0x54 (static_assert holds -> SChainOf<Component*> is 0x14, layout exact).
- Register the state attrs -> the subsystem's own alarm (own AttributePointers[]
  chained to the parent): Generator.GeneratorState->stateAlarm, Condenser.
  CondenserState->condenserAlarm, Reservoir.ReservoirState->reservoirAlarm.  Those
  alarms are already SetLevel'd by the sim, so the state audio fires on transition.
- AudioStateWatcher guard now validates the +0x18 SOCKET (what AddAudioWatcher
  touches), not the +0 vtable -- GaugeAlarm54 is non-polymorphic at +0 (raw header)
  but has a real socket at +0x18.

audiostate skips 85 -> 10 (only AmmoBin AmmoState/SimulationState left).  The
Logical/Scalar/Enum subsystem attrs (ReportLeak/GeneratorOn/ConfigureActivePress/
MotionState/SpeedOfTorsoHorizontal/TargetRangeExponent) stay inert (read 0, silent,
non-crashing) -- they need backing members in size-locked layouts (a polish wave).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:38:15 -05:00
arcattackandClaude Fable 5 530bbeb959 Fix BT_SPAWN_AT terrain embed + add BT_COOL_LOG probe
BT_SPAWN_AT teleport set only X/Z, keeping the old (dropzone) Y -- so teleporting to
a new spot embedded the mech in sloped terrain (user-reported).  Lift the teleport
+500 above the old elevation so the authentic per-frame ground SNAP (BoxTree
FindBoundingBoxUnder, no gravity) settles it onto the surface next frame (the snap
only lowers, so it must start above the surface).  Diagnostic only.

Also BT_COOL_LOG (heat.cpp): logs a heat subsystem's damageLevel/heatLoad/coolantDraw
when it carries damage -- for the 'do damaged mechs leak coolant' investigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:46:00 -05:00
arcattackandClaude Fable 5 4e63a7b6c3 Heat: THE AUTHENTIC ELECTRICAL MODEL -- weapons recharge from generators (task #10)
Task #10 set out to fix "scrambled linked-sink routing"; a [heat-link] attach
log proved the routing was NEVER scrambled (every subsystem links its authored
condenser exactly; the "pools in Condenser1" read was the diagnostic-sampler
aliasing trap).  The real defect: emitter.cpp's local FUN_00417ab4 stub
returned NULL, so the whole electrical model was inert and the E7 force-charge
recharged every emitter in ONE frame (~0.3s cycle, 1501 fires/90s, ~1.7e9
heat/s -- the "runaway").

Landed authentically [T1: disasm + byte-verified constants]:
- Emitter ctor @004bb120: seekVoltage = authored fraction x generator
  ratedVoltage (10000); EC = energyTotal/(seekV^2 x 0.5); voltageScale@0x310 =
  (RechargeRate / -ln(1 - 1e-4 x seekV)) / EC -- charge reaches seekV[rec] in
  EXACTLY the authored RechargeRate (PPC 5s, ERL 4s, SRM 3s, ERM 2s) cold.
  Owner-flags ctor gate (the usual gotcha; the this-flags read never armed).
- PoweredSubsystem::ChargeTimeScale (@004b0d50, was a =1.0 stub): voltageScale
  x (1 + thermalResistivity x srcTempRise) -- hot generators charge slower.
  ("voltageScale is never read back" was wrong; corrected + swept.)
- TrackSeekVoltage @004ba838: charging I^2R -> the GENERATOR's pendingHeat
  (~3.5e8/full PPC charge) -- generators self-heat, conduct to their authored
  condensers, and throttle further charging.  The feedback economy closes.
- FailureHeat consumers found: this+0x184 == 2 gates BOTH weapon families
  (@004baa88 emitter: reset firing + hold charge 0; @004bbd36 ballistic:
  recoil=rechargeRate + alarm 7).  Emitter::GetFaultState un-stubbed.
- ProjectileWeaponSimulation @004bbd04 opens with call 0x4b0bd0 (disasm) --
  launchers now run the powered/heat step (their firing heat previously
  accumulated in pendingHeat forever).
- heat.cpp: per-instance BT_HEAT_LOG census (the old shared-static 1-Hz
  sampler aliased); [heat-link] attach log; the stolen-else Verify restored.

Verified live (120s max-rate autofire): PPC ~470-500 (was 55,000), bank
plateaus ~600 and sheds to ambient, generators 1100-1400, ERMLaser
self-regulates at the authored 2000 failure threshold (shutdown-cool-resume).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:59:29 -05:00
arcattackandClaude Fable 5 4ed2bbc293 Heat: authentic 1e7-unit heat + the ambient radiator land (task #9)
- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2
  (PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat
  adds (raw -- resources author pre-scaled 1e7 units).
- The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat
  exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) +
  the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the
  inversion blocked condenser->bank links and closed the system).
- Constant corrections (all byte-verified float80s): coolant epsilons
  0.0025/0.003/1e-4 (was a single 1e-4 serving three sites);
  CoolantCapacityScale 0.05 (was 1.738).
- Verified: heat flows + exits; max-rate autofire overheats weapons into the
  authentic heatLoad range-cutout (thermal spam unsustainable by design).
- KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1;
  authored map says PPC->C4/C6) -- the heat-stream offset audit, filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:21:37 -05:00
arcattackandClaude Opus 4.8 b91057aadc gauge-complete P4c: Condenser dedup + RecomputeCondenserValves -> ValveSetting gauge reads authentic 1/N
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>
2026-07-07 20:22:01 -05:00
arcattackandClaude Opus 4.8 16f5f545ce gauge-complete P4b: AggregateHeatSink 0xBBE -> HeatSink/AmbientTemperature bound (last config NULL cleared)
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>
2026-07-07 20:10:11 -05:00
arcattackandClaude Opus 4.8 12c4254aae gauge-complete P4a: heat-leaf data fixes (radar heat-degradation authentic)
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>
2026-07-07 20:03:07 -05:00
arcattackandClaude Opus 4.8 d68284ede2 gauge wave P2a: build the condenser LeakGauge + valve slider (heat panel)
The gauge-widget-decode workflow reconstructed the unbuilt cockpit widgets from
the binary.  First two (the heat MFD's missing SET/LEAK columns):

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:19:04 -05:00
arcattackandClaude Opus 4.8 64975ec22f gauge wave P1b: Searchlight/LightOn + fix coolant/refrigeration source misread
- 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>
2026-07-07 16:27:57 -05:00
arcattackandClaude Opus 4.8 070af409f7 gauge wave P1a: publish heat/power/weapon attributes + radar zoom
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>
2026-07-07 16:22:45 -05:00
arcattackandClaude Opus 4.8 1356870e56 P7: byte-exact re-base of the CORE heat leaf (HeatSink/Condenser/Reservoir/Generator/Myomers)
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>
2026-07-07 09:47:37 -05:00
arcattackandClaude Opus 4.8 9ea4bdaf50 gauges: publish HeatSink attribute table (CoolantMass/CoolantCapacity/CurrentTemperature)
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>
2026-07-06 18:19:09 -05:00
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00