Commit Graph
22 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 c32d02b3cc the heat schematic was lit at every spawn: a dropped x87 tail, and the last #47 derivation
Playtest night 5: "heat damage @ launch is lit up like a Christmas tree",
confirmed by two players at ANY spawn -- so not the respawn-reset bug it was
first filed as.  Two stacked causes, both fixed here.

1. THE RATIO WAS NEVER RECONSTRUCTED.  HeatConnection::Transfer @004c3720 sets
   the colour index a ColorMapper pushes into the palette slot.  The port wrote

       *currentColorIndex = HeatRound(heat->currentTemperature);

   straight into an index whose range is 0..99.  A stone-cold mech sits at ~77
   and the generators idle near 260, so every one of the 24 cmHeat mappers in
   GAUGE/L4GAUGE.CFG (GeneratorA-D, Condenser1-6, HUD, Avionics, Gyroscope,
   Torso, GAUSS, the lasers, SRM6, Myomers) saturated at the hot end from the
   first frame and stayed there.

   The real computation was INVISIBLE in the decomp.  Ghidra renders the tail as
   `uVar1 = FUN_004dcd94();` -- an arg-less __ftol, exactly the carve artifact
   reconstruction-gotchas §19 documents: the x87 expression that left the value
   in ST0 is dropped from the export.  The previous author reconstructed the
   only thing visible and flagged the scaling as unreconciled in a comment.
   Raw disasm @0x4c379a-0x4c37c1 recovers it:

       fld [num] ; fdiv [den]        ; ratio
       fcomp 1.0f ; jbe -> ratio=1   ; clamp
       fmul 99.0f ; call __ftol      ; -> 0..99

   i.e. "how close to failure am I", not a raw temperature.  At spawn that is
   77/2000 -> 4, and the schematic reads cold.

   The two operands come from one of two branches, chosen by a class flag at
   +0x14 that the port did not model at all (ctor disasm @0x4c3682-0x4c36c2):
     HeatableSubsystem (0x50e3ec) -> own temp@0x114 / own failureTemperature@0x11C
     HeatWatcher       (0x50e604) -> WATCHED subsystem's temp@0x114
                                     / the WATCHER's failureTemperature@0x124
     neither                      -> source NULLed, Transfer writes 100
   Note the watcher asymmetry: temperature from the watched subsystem, reference
   from the watcher itself.  Bridged as BTHeatWatcherSample so the gauge TU need
   not include the heat family's headers.

2. THE WATCHER BRANCH COULD NEVER BE SELECTED.  With (1) fixed, HUD, Gyroscope
   and Torso still pinned at 99 reading temp=1.6369e-35 failAt=0 -- uninitialised
   bytes.  HeatWatcher's C++ base had been re-based to MechSubsystem, but its
   DERIVATION chain still said HeatableSubsystem, so every watcher answered "yes,
   I am heat-bearing", took branch one, and read its own watchedLink@0x114 as a
   temperature.  This is the last surviving instance of the #47 bug -- in the
   file that fix's own comment cites as already correct.  The two families are
   disjoint in the binary and the branch test depends on it.

VERIFIED LIVE (solo, F6 to the Heat schematic, BT_HEATGAUGE_LOG=1): mode mask
reached 0x510421 (ModeSecondaryHeat) and the gauge tracks per subsystem rather
than saturating -- GeneratorA 259.5->13, Condenser3 202.0->10, SRM6 182.8->9,
lasers 90-97->4-5, and AmmoBinSRM6_1 (a watcher) resolves its link and reads
182.7->9 through the branch that previously read 1.6369e-35.  Nothing pinned at
99.  Operator confirms the panel is blue, not red.

BT_HEATGAUGE_LOG kept as a permanent env-gated diagnostic: bind-time
classification plus per-sample temp/failAt/ratio/index.  A fresh spawn reading
99 is this regression returning.

Not covered: ColorMapperCritical (cmCrit / ModeSecondaryCritical) already
computes a proper damageLevel*100 ratio and was read, not measured -- if the
Critical page specifically still misbehaves that is a separate lead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:28:11 -05:00
arcattackandClaude Opus 5 e28dcbc161 the #35-class sweep + the games-night load crash: four lifecycle-window null-derefs guarded
THE CLASS (established by Gitea #35's 515 fix, confirmed growing tonight):
code the binary could run bare because a pod's mission world always existed,
crashing in the port's MP join/teardown/respawn windows where mission, player,
or a roster link is briefly NULL.  Swept every GetMissionPlayer()/
GetPlayerVehicle()/GetCurrentMission() chain in the tree (23+7 sites).

GUARDED (the 515 pattern -- authentic behavior whenever the object exists,
skip/degrade + loud log when mid-teardown):

1. DPLRenderer::SortAndReloadNameBitmaps -- runs in the round-STOP path (the
   end-of-round circle), raw mission+player+entity-manager derefs.
2. DPLRenderer::LoadOrdinalBitmaps -- called from (1); raw
   GetMissionPlayer()->GetInstance() evaluated on EVERY machine (the camera-
   director test derefs before it branches).
3. DPLRenderer::LoadNameBitmaps -- same window, raw GetPlayerCount() chain.
4. Reservoir::Reservoir master-gate block -- THE CAPTURED GAMES-NIGHT CRASH
   ([crash] btl4+0x4990d = this ctor inlined into CreateReservoirSubsystem,
   AV reading NULL+0x1d0):  linkedSinks.Resolve() derefed raw; +0x1d0 is
   exactly the masterScale FILD of the resolved master heat-sink bank
   (heatSinkCount @0x1D0 -- the offset the crash named).  Observed trigger: a
   DUPLICATE-PILOT egg (the seat-ghost bug put one identity in two seats)
   poisoning the ID registry, so Resolve() nulled during viewpoint-entity
   construction and at least two pods died loading the same egg.  Now: loud
   '[spawn] FATAL-AVOIDED' + degrade to the inactive-copy shape.  The
   duplicate-pilot ROOT CAUSE is the seat-ghost fix (tracked separately);
   this guard makes the failure survivable and self-identifying either way.

CLASSIFIED SAFE, not touched: btl4mppr/btplayer/mechmppr/btl4gau3 chains
(guarded or null-tolerant by design); APP.cpp's five Check(player) sites
(mission state machine -- player exists by construction, original engine
ordering); CAMMGR/CULTURAL GetCurrentMission chains (construction-time).

VERIFIED: build clean (0 errors, no new unresolved externs); solo smoke on the
guarded exe -- zero FATAL-AVOIDED lines (guards silent on the healthy path),
subsystems simulating, no faults.  STILL OWED: a full launch->stop rig cycle
(exercises the guarded round-stop path live) and the duplicate-pilot-egg repro
-- both blocked on port 1500 while the operator's console session is up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 00:03:31 -05:00
arcattackandClaude Opus 4.8 86e387b6c7 Valve forensics + KB: the boost mechanic is real; the field cook-off was a detent question
Measured A/B/C (Blackhawk, sustained autofire, 150s soaks): SRM6 on
Condenser2 plateaus ~531 and COOLS at share 0.91 (boost 2), climbs to
the jam band at balanced (0.167), and runs at the 2000 failure line
starved (0.018).  MoveValve reruns the redistribute every press;
veteran passes the novice guard.  The field 'boosted loop 2 still
cooked' therefore means the valve was NOT at the 50 detent during the
fight -- the 1->5->50->0 cycle turns the loop OFF one press past max.
MoveValve presses now log always-on ('[valve] <name> -> detent N' +
a CLOSED warning) so the next field session records the detents.

Also: solo mission clock verified live (60s egg self-ends,
'[mission] solo game clock expired' -> RunMissions returns).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:59:04 -05:00
arcattackandClaude Opus 4.8 e9db161404 Issue #20: wire Mech BalanceCoolant (id 0x16) -- the auto-cooling-balance button
Third instance of the silent-swallow pattern (unregistered handler id ->
Receiver NullHandler).  The Mech handler table @0x50BDF8 binds
{0x16, "BalanceCoolant" -> @0049f728}; the reconstruction never registered it.

Disasm-verified body (@0049f728): press-only guard (msg+0xc > 0; the binary
has NO novice lockout on this one), set EVERY condenser's valveState@0x1D0
to 1 (equal weights), then the shared redistribute @0049f788 -- which the
port ALREADY had faithfully as BTRecomputeCondenserValves (flow =
valve/total + condenserAlarm pulse; MoveValve calls it).  So the fix is:
- mech.hpp/mech.cpp: BalanceCoolantMessageID=0x16 + handler + MESSAGE_ENTRY
- heatfamily_reslice.cpp: BTBalanceCondenserValves bridge (Condenser is a
  complete type there; sets valves to 1 + redistributes)
- mech4.cpp: BT_BALTEST scripted harness (MoveValve press alternating with
  a Balance press)

VERIFIED headless (BT_BALTEST=1 BT_VALVE_LOG=1):
  boot     -> all valves 1, flows 0.1667 each
  MoveValve-> condenser#1 valve 5, flows 0.5 / 0.1x5 (the priority boost)
  BALANCE  -> all valves 1, flows 0.1667 each (equalized)
Awaiting the tester's live ADV-mode confirmation.

KB: decomp-reference.md Mech-table row marked wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:31:46 -05:00
arcattackandClaude Opus 4.8 cbc5ff9532 Wire ToggleCooling (msg 3): restore the coolant on/off button + handler chain
Coolant priority (Lynx: weapon MFD -> Display -> Coolant) was unreachable: the
ToggleCooling handler (@004ad6f8, HeatableSubsystem table @0x50E41C id 3) was
undefined AND the handler chain skipped it -- PoweredSubsystem::GetMessageHandlers
chained straight to the Receiver root, bypassing HeatSink/HeatableSubsystem, so a
weapon's dispatch never saw id 3.

Reconstructed ToggleCooling from the disassembly (tools/disas2.py 0x4ad6f8): a
per-subsystem coolant on/off TOGGLE -- novice-locked (owner->BTPlayer->
roleClassIndex+0x274==0, the same guard as MoveValve), press-only, then flip
coolantAvailable(+0x134) 0<->1 and coolantFlowScale(+0x15C) 0.0f<->1.0f.  Cutting a
system's cooling frees the shared loop for the rest -- the emergent "coolant
priority" (the mechanism is a toggle, not a multi-level cycle).

- heat.hpp / heatfamily_reslice.cpp: HeatSink::ToggleCoolingMessageHandler (id 3)
  + HeatSink::GetMessageHandlers.  Registered at HeatSink (the abstract
  HeatableSubsystem never instantiates; every concrete heatable subsystem is a
  HeatSink, where the coolant fields live) -- identical coverage to the binary's
  base-table registration, no downcast.
- powersub.cpp + Condenser/Reservoir: chain their handler sets through
  HeatSink::GetMessageHandlers so id 3 reaches every heatable subsystem.
- mech4.cpp: BT_COOLTOGGLE_TEST scripted inject (dispatch id 3 to the first weapon)
  for verification.

Verified (BT_COOLTOGGLE_TEST + BT_COOL_LOG, expert egg): "[cool] PPC_1 ToggleCooling
reached" then coolant OFF(flow 0.0) <-> ON(flow 1.0) each press -- chain routes,
handler toggles, novice guard honored.  Both build/ + build-glass/ compile clean;
existing ids 4-8 unaffected (found before id 3).  NEXT: the #2 MFD button routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:04:04 -05:00
arcattackandClaude Fable 5 2ae9bd43ae Coolant Flush end-to-end (Gitea #7): InjectCoolant id-4 handler + drain + FLUSH.PFX cloud + 'H' key
The playtest report (sound plays, no gauge drop, no cloud) traced to the
Reservoir's InjectCoolant chain being dead in three places:

- The handler was never REGISTERED: the binary's Reservoir handler table
  @0x50e680 has one entry {4, "InjectCoolant", @4aee70}; added
  Reservoir::GetMessageHandlers + the press/release handler (press starts
  the flush gated on coolantLevel@0x12C > 0 -- the old body misread +0x12C
  as currentTemperature; release stops it; novice lockout via FUN_004ac9c8).

- Reservoir::InjectCoolant (@4aefa4, 1019 bytes) was an empty stub -- the
  drain the coolant gauge reads.  Reconstructed in full: work list =
  condenser/weapon/heatable chains (+0x7cc/+0x7bc/+0x7ac, roster-walk
  emulation with the binary's duplicate-visit weighting; HeatSink-filtered
  [T2 guarded]) + the linked master sink; per sink with flowScale != 0 move
  squirtMass x flowScale x dt (clamped to the tank / sink capacity) and
  credit pendingHeat with the negative carried-heat delta capped at
  sinkMass x reservoir startingTemperature -- the set%-biased flush of the
  manual (p24), riding the existing heat model.

- Two latent ctor decode bugs surfaced and fixed: the master gate @4af408
  (and @4aeb40 HeatWatcher, swept) reads the OWNER MECH's simulationFlags
  (*(param_2+0x28)), not the resource's subsystemFlags (the misread left
  the CoolantSimulation Performance unregistered); and the capacity scale
  FILDs the bank's INTEGER HeatSinkCount ((float10)*(int*)(link+0x1d0)) --
  the float reinterpret gave ~1e-44 -> a permanently empty tank.
  Capacity = 0.05 x heatSinkCount x streamed CoolantCapacity (BLH: 6.0).

Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built
for classID 0xBC0 on "ReservoirState", part_014.c:5439; tick @part_007.c:
8780) starts psfx 19 = FLUSH.PFX ("Coolant flush", BTDPL.INI) when the
state changes to 1 -- BTSpawnFlushCloud (mech4.cpp) spawns it on the same
alarm edge as an attached emitter at torso height.

Input: new CONTROLS.MAP action "Flush" on 'H' (HELD; press+release both
dispatch, the held-button payload).  Diags: BT_FLUSH_LOG, BT_FLUSH_TEST.

Verified live (FOGDAY): [flush] Reservoir level 6 -> 0.13 -> 0 over ~0.6 s
held (= the manual's 3-4 punches to empty), the coolant vertBar source
Reservoir/CoolantMass drains, and the bluish condensation cloud rises from
the mech (scratchpad/flush_cloud.png vs flush_before.png).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:15:44 -05:00
arcattackandClaude Fable 5 f4cf1e631a Spec cross-check vs the 1995 manual: reads CORRECT, numbers are 4.0-vs-4.10 drift
Audited our live streamed subsystem config against the manual's per-mech
stat sheets (new BT_SPEC_LOG dump: torso speed/limits, heat-sink count,
reservoir capacity, at subsystem ctor).

RESULT: structure + semantics align perfectly; the tuning NUMBERS
differ -- because the manual is Tesla 4.0 and our content is release
4.10.  Point-release balance drift, NOT a reconstruction bug:
- Owens + Blackhawk are the two fixed-torso mechs in BOTH (speed 0,
  limit ~0, torsoHorizontalEnabled=0); the other four twist-enabled --
  our reads correctly identify every case
- torso speed/limit + heat-sink count exist per-mech exactly as the
  manual documents; only the values were retuned 4.0->4.10 (e.g. torso
  speed MadCat 80->50, heat sinks Loki 38->15)
- reservoir coolantCapacity reads a flat 20 = the coolant THERMAL
  capacity (game units), NOT the manual's per-mech 'liters' (a
  display/flavor quantity with no single subsystem field) -- different
  measures, not a bug

CONCLUSION: do NOT retune content to the manual -- BTL4.RES is the
authentic 4.10 shipping data; the 4.0 manual's numbers are an earlier
pass.  The manual CONFIRMS our structural fidelity.

BT_SPEC_LOG retained for re-auditing.  Follow-up (structural, less
drift-prone): the per-mech coolant-loop weapon/generator assignments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:28:43 -05:00
arcattackandClaude Fable 5 4459262b9c Camera seat LIVE + Mech::PlayerLinkMessageHandler reconstructed (@0049f624)
The spectator/broadcast seat works end-to-end: a hostType=1 +
vehicle=camera pilot page boots a CameraShip whose 62-camera arena1
network loads from the BTL4.RES type-27 resource (the engine's
CreateStreamedCameraInstances existed all along -- the file probe falls
back to it), the BTCameraDirector locks onto the first live mech, and
the ship TRACKS it (sees=1, goal advancing) and CUTS between authored
camera positions (screenshot-verified: wide arena shot -> close
tracking shot).

The missing piece was the Mech override of PlayerLinkMessageHandler
(@0049f624), which the reconstruction never filled.  The engine base
resolves mech->player; BT's override adds:
  1. the REVERSE link player->playerVehicle = this on EVERY node
     (replicants included) -- how the camera director and scoreboard
     find a REMOTE player's mech; without it a spectator parks forever
     ('NO goal entity')
  2. clears NonScoringPlayerFlag (0x4000 = bit 14): a pilot with a
     vehicle is a SCORING player -- this admits REPLICATED players to
     the ranking pass, so cross-node rank/score displays work
  3. master only: seeds the heat bank's ambientTemperature (bank
     @0x1d4) from the mission [mission] temperature= (BTMission+0xf4)
     -- correcting the heat family's 'frozen 300' deviation note (it
     was never frozen; THIS is the writer)

Bridge BTSetBankAmbientTemperature lives in heatfamily_reslice.cpp
(mech.cpp cannot include subsystem headers -- local-stub collision).
BT_CAM_LOG diagnostics: camera-network count, director pick + Players
group census, ship follow state, link dispatch/receive.

Verified live: 2-seat (mech + camera) relay session -- replicated
player shows +veh, director goal locked w/ 30s timer, ship tracking a
moving mech through camera cuts; standard 2-node mech smoke PASS
(un-regressed).  Lesson re-learned: a 'clean' build filtered on 'error
C' missed a linker file-lock failure -- one whole test cycle ran
against a stale exe (the /FORCE gotcha's cousin; grep -i error, not
error C).

Remaining (task open): ranking-window overlay draw (L4VIDRND stubs),
operator-app camera-seat row, shot polish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:10:29 -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 4e72f0c4f4 Diag: BT_VALVE_BOOST=<condenserNumber> (route coolant priority to one condenser)
Extends the coolant/jam probe: boost one condenser's valve to 50 and starve the rest
to 1 (share ~0.9 vs the ~1/N baseline) -- the authentic 'coolant priority' scenario,
for measuring the per-weapon boost effect. No behavior change (env-gated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 07:11:15 -05:00
arcattackandClaude Fable 5 3acab416ee Diag: BT_JAM_LOG (per-shot weapon temp/jam roll) + BT_VALVE (force condenser detent)
Probes for verifying the autocannon jam/coolant mechanic against QA ground-truth
(AFCs jam on ~3rd shot uncooled). BT_JAM_LOG logs currentTemp/degradT/failT/alarm/p
at each fire attempt; BT_VALVE=<n> forces every condenser valveState so cooling can
be tested at a known level. No behavior change (both gated on env).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:34:37 -05:00
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00
arcattackandClaude Fable 5 80cadc98c7 Cockpit: the COOLANT VALVE control -- the heat-management triad completes (task #13)
The pilot's third heat tool: Condenser::MoveValve (id 4, the Condenser handler
table @0x50E52C -- PE-verified, exactly one entry) registered via the
order-proof function-local accessor and GUARDED by the real FUN_004ac9c8
(task #12's BTPlayerRoleLocksAdvanced rookie-role bridge -- the old deferral
note blamed the 0xBD3 messmgr, a stale misattribution, swept).  Each press
cycles the valve 1 -> 5 -> 50 -> 0 -> 1 and RecomputeCondenserValves re-shares
the fixed total coolant flow across all six condensers -- opening one valve
wide starves the others: the pilot's cooling-triage lever.

Desktop: 'C' cycles the selected condenser's valve (BT_VALVE_SLOT picks the
roster slot; default Condenser1).  BT_VALVE_TEST scripts a headless press.

Verified live: press -> "[valve] condenser#1 valveState=5 flow=0.5 (total=10)"
-- exactly the authored redistribution math (5/(5+5x1)), through the real
role gate.

The heat-management triad is now complete and all-authentic:
  G + fire buttons  = regroup weapons across buttons     (task #6)
  F5-F8 / F9        = reassign weapon generators / mode  (task #12)
  C                 = coolant valve triage               (task #13)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:33:27 -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 aae3ce29d8 gauge-complete P4d: Condenser::MoveValve reconstructed from the REAL @0x4ae464 (was a mislabel)
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>
2026-07-07 20:29:25 -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 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 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 8b36440d05 subsystems: un-stub Wave 4 readouts + Wave 6 Myomers (10->15 factory cases)
WAVE 4 (standalone readouts) -- Sensor(0xBC3)/Searchlight(0xBD8)/
ThermalSight(0xBDE)/AmmoBin(0xBCB) un-stubbed via Create<Class>Subsystem
bridges + Torso-style de-shim (drop cross-family shadow fields, redirect
accessors to the real inherited base, static_assert layout locks).
- FIX: Searchlight/ThermalSight ctors gated their Performance on the shadow
  segmentFlags(=0) so it NEVER installed; switched to owner->simulationFlags.
- AmmoBin: retype ammoAlarm HeatAlarm->WatcherGaugeAlarm(0x54) + drop the
  statusState@0x40 shadow -> exact 0x22C layout.
- Guard HeatWatcher::WatchSimulation against the unresolved watchedLink
  (null-deref exposed once these sims run; faithful fix = resolve the link).
- Heat-leaf branch (Sensor) is not byte-exact -> overflow-lock only.

WAVE 6 (Myomers 0xBC6, mover-coupled) -- structural un-stub, gated
BT_MYOMERS (default on; =0 -> Actuator stub). Wired INERT: MyomersSimulation
early-returns (advanced-damage gate stubbed) + no-op mover feed, so the live
JointedMover is untouched and the gait cannot regress. De-shim drops the
owner*/segmentFlags shims to fit the exact-0x358 alloc. Authentic mover/heat
coupling deferred (needs messmgr 0xBD3 + reconciling the mover feed with the
gait cutover).

Verified: BLH tick 20->27, Mad Cat 24, combat DESTROYED un-regressed,
locomotion un-regressed, 0 crashes, 0 heap detections under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 23:01:31 -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