Ladder rungs, each run-verified on the pod rig: ReportLeak (HeatSink),
GeneratorOn (Generator -- member existed, publication missing),
ConfigureActivePress (MechSubsystem -- likewise), AmmoState +
FireCountdownStarted + the rest of the AmmoBin table.
THE PINNED RANGE IS NOW THE AUTHENTIC SHAPE. Publishing ReportLeak on
HeatSink and ConfigureActivePress on MechSubsystem shifts the chain down two,
and MechWeapon's bridge to its binary-pinned PercentDone (0x12) collapses
from THREE guessed pads to ONE -- which is exactly the arithmetic the shipped
string pool predicts (MechSubsystem 1, HeatableSubsystem 3, HeatSink 6,
PoweredSubsystem 5). Three pads of guesswork replaced by a real attribute
and a real base-class row. HeatableSubsystem and Torso rebase onto
MechSubsystem::AttributeIndex; MechControlsMapper does not (it derives from
Subsystem directly).
Types were chosen deliberately this time, per the AudioWatcher families the
engine instantiates (Motion / Hinge / Scalar / StateIndicator): every *State
name resolves to a StateIndicator, ReportLeak is a Scalar.
RESULT: the pod run now gets past every authored watcher and DRAWS THE FULL
COCKPIT -- sensor cluster, myomers, cooling, the weapon panels, kills/deaths
-- with the board booted and audio running. It then exits without flushing
its redirected stdout, so the exit reason is not yet known; next step is a
run with the redirect removed so the console is readable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The linker map named the faulting function: the crash address minus the CODE
base (0x410000) looked up in btl4opt.map's Publics-by-Value landed inside
AudioStateWatcher::AudioStateWatcher +0x2D.
AudioStateWatcher is AudioWatcherOf<StateIndicator> and its ctor immediately
runs Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this).
So every authored *State name must be published as a StateIndicator --
AlarmIndicator counts, it derives from one -- and pointing one at a plain int
sends that member call through garbage.
That explains both earlier failures: the AlarmIndicator attempt was the right
type but was tested with other bugs still in the batch, and the plain-int
attempt was simply the wrong type and crashed further along.
Published as state objects: Reservoir/ReservoirState -> reservoirAlarm,
Generator/GeneratorState -> stateAlarm, plus new StateIndicator members for
Condenser/CondenserState and Torso/MotionState. StateIndicator has no
Initialize(); the default ctor suffices because the watcher only needs the
object to exist.
Verified on the pod rig: no crash, ladder advanced to ReportLeak.
Technique worth keeping: on an extender fault, subtract 0x410000 from the
dumped address and look it up in btl4opt.map -- it names the engine function,
and for this family of work that names the watcher class and hence the
required member type.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Published and verified green on the pod rig: Condenser/CondenserState,
Generator/GeneratorState, Emitter/LaserOn, ControlsMapper/TargetRangeExponent,
plus the Torso and Myomers tables from the previous pass.
Reservoir/ReservoirState CRASHES the pod on the DOS extender, and a bisect
pins it to exactly that change: revert it and the run returns to a clean
Fail, re-apply it alone and the crash returns. It is reverted; the tree is
green and the ladder is blocked there.
The important lesson is general: AttributeWatcherOf<T> does
currentValue = *(T*)attributePointer AT CONSTRUCTION, so a published name is
read the moment its watcher is built. My earlier staging note claimed the
provisional types could not matter because nothing drives the values yet --
that is wrong, and this is the counterexample.
Ruled out by measurement and recorded so they are not retried: the
AlarmIndicator-vs-int type (a plain int got further, 232 -> 749 bytes of log,
but still crashed), static-init order (reservr.obj sorts last, after heat),
an id gap (contiguous at HeatSink::NextAttributeID), and the gauge rig (same
binary runs clean there -- only the pod/arena context faults).
Crash signature for whoever picks it up: 0044B4AD, mov eax,[edx+0x18] then
call [eax+4], EAX=0x15, fault at 0x19 -- a small integer called through as an
object, i.e. the AttributeIndexSet::Build uninitialised-slot pattern.
Condenser is the control: same base class, plain int, no crash.
Also fixed on the way: staged members must be appended at the END of a class
(offset-sensitive readers exist -- condenserNumber is reached as
master+0x1d4) and never added to a resource struct, which is a wire format.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gauges now read the real simulation: heat/power/sensor/mech attribute
tables published under the 1995 L4GAUGE.CFG spellings the widgets already
bind by name. Temperatures, coolant mass/capacity/leak rate, condenser
valve settings, generator voltages and numbers, radar percent, and the
mech's radar/speed rows all resolve to live members.
THE NUMBERING IS PINNED: the chain publishes 2..0x0E so
PoweredSubsystem::NextAttributeID lands on the authentic 0x0F -- the
surviving SENSOR.HPP numbers RadarPercent off it and MechWeapon's
binary-pinned table starts at 0x12, so its pads shrank 16 -> 3
(0x0F..0x11) exactly as the 5.3.x header comment predicted. Sensor's
authentic enum finally has its table defined; MechWeapon/Sensor rechain to
PoweredSubsystem and Reservoir/AggregateHeatSink to HeatSink so the whole
family is visible where the cfg expects it.
Verified with BT_GAUGE_ATTR_LOG: 48 OK / 2 NULL (was 33/17, and 0/50 at
the block's birth). The two remaining have no member to bind --
HeatSink/AmbientTemperature (a sim constant) and Searchlight/LightOn (a
memberless subclass) -- and fall to the documented zero cell. Gauge
fight (15/15 rounds, 127 zone hits), smoke and novice all clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full circuit, workflow-researched (5 parallel dossiers over BT411 + the
engine) and adversarially reviewed (3 lenses) before landing:
Mech side: the once-per-death transition in Simulate (freeze, motion kill,
DeathShutdown sweep, MASTER-only VehicleDead(-1) to the player link);
Mech::Reset @0049fb74 (reposition + heal every hull zone + the roster
DeathReset sweep + PreRun -- the reset-based respawn that REUSES the entity);
dead-owner terms in both weapon hard gates (wrecks fall silent).
Player side: the VehicleDead(-1) branch (deathPending dedup, deathCount
bump+stamp, scenario-role life debit, +5s re-post -> the engine drop-zone
hunt) and the DropZoneReply respawn branch (reset the dead mech at the
replied drop zone; probes on a live mech are moot).
Subsystem side: the engine base virtual DeathReset implemented across the
family -- MechSubsystem (zone heal + DestroyedState clear), MechWeapon (full
powered/thermal restore + fresh Loading cycle), AmmoBin (restock from the
ctor-cached count), HeatSink/HeatWatcher/PowerWatcher/Generator/Sensor/
MissileLauncher.
Review catches fixed before commit: AmmoBin restocked through the FREED
padBuffer pointer (use-after-free -> initialAmmoCount; MechSubsystem::
resource is now documented as never-deref-post-ctor); died-hot weapons
respawned at FailureHeat (now full thermal re-init); Generator tap counts
desynced across reset (preserved); Sensor skipped the base heal; PowerWatcher
inherited a statically-bound reset; cooling toggle + connect mode restored.
Verified: kill -> wreck (0 shots while dead) -> +5s -> drop-zone hunt ->
HEALED + placed at a real drop zone -> 134 shots after respawn incl. 12 SRM
(restock proof); unowned enemy wreck settles silently; fight/smoke/novice
regressions green, zero faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The central heat bank is now its real class: the binary's 0x0BBE
AggregateHeatSink (the value our VDATA enum named HeatSinkClassID -- there is
no streamed plain HeatSink; the segment walk now builds the bank).
- Ctor (@4ae8d0): heatSinkCount from res +0xFC (bhk1 = 6, matching its six
condensers); thermalConductance x 0.1 x count (231000 -> 138600); ambient
setpoint 300 (the mission [mission] temperature overwrite joins the
Mech-PlayerLink wave).
- RadiatorSimulation (@4ae73c) replaces the base heat step on the bank -- THE
system's only heat exit: relax toward the ambient target with rate
k = conductance x (1-damage) x (coolant/capacity) x flowScale / mass; tail
tops the bank's coolant from the attached store via the DrawCoolant virtual
(base 0 until the reservoir-attach wave). VERIFIED signed-correct: the bank
warms 77 -> 300 from the cold start, then flips to actively radiating
(-48K..-167K/step) once fired heat pushes it past ambient. Until now heat
only ever POOLED in the central sink; the mech now genuinely sheds it.
- Reservoir master path: capacity = 0.05 x bankCount x streamed = 6 (the
authentic tank), refilled.
Zero Fail; the expert economy regression stays green (202 heat events under
forced spam).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subsystem-family cockpit buttons now work through the authentic Receiver
dispatch -> per-class handler-table path. The id space decodes cleanly:
Receiver::NextMessageID == 3, so ToggleCooling = 3 on the HeatSink chain and
the per-class id 4 is MoveValve on a Condenser / InjectCoolant on the
Reservoir -- same number, different class, the binary's per-receiver-class
convention.
- HeatSink::ToggleCoolingMessageHandler (@004ad6f8, id 3): novice-locked,
press-only; toggles coolantAvailable + coolantFlowScale together.
- Condenser::MoveValveMessageHandler (@4ae464, id 4): novice-locked; cycles
the valve 1->5->50->0->1 and calls Condenser::RecomputeValves (@0049f788):
every condenser's coolantFlowScale = valve / sum-of-valves. The ctor now
streams the AUTHENTIC flowScale=0; the Mech ctor seeds equal shares once at
spawn -- the flowScale=1 interim is retired.
- Reservoir::InjectCoolantMessageHandler (@4aee70, id 4): novice-locked;
press arms the flush when the tank holds charge, release drops it.
- MechSubsystem::NoviceLockout() (@4ac9c8): owner -> playerLink -> experience
== novice; unlinked mechs read unlocked.
- DEV harness BT_PRESS_VALVE / BT_PRESS_FLUSH: dispatch the REAL messages ~6s
into the mission from Mech::Simulate.
VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1
valve=5 flow=0.5, others 0.1 (valve/sum exact); flush arms via the real
button message; NOVICE locks both presses (valve lines stay spawn-only, zero
flush). Zero Fail throughout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The player-experience system now gates the entire heat/jam economy, verified in
BOTH directions with a one-token egg edit (TESTNOV.EGG, experience=novice):
NOVICE = 212 fire cycles all pinned at T=77, zero jams, zero shutdowns (the
heat model authentically absent); EXPERT = the full economy (duty cycles, 119
shutdowns + 2 authentic jams under forced fire). Zero Fail in both.
- BTPLAYER: the flag block renamed to its TRUE semantics (simLive @0x25c,
heatModelOn @0x260 -- the FUN_004ad7d4 master switch, advancedDamageOn pair,
levelFlag26c/270, experienceLevel); the ctor rows were already
binary-accurate (nov 0000 / std 1011 / vet 1111 / exp 1101); accessors +
[exp] sentinel.
- HeatSink::HeatModelActive() + ProjectileWeapon::LiveFireEnabled(): owner
mech -> Entity::GetPlayerLink() -> the BTPlayer flags, NULL-permissive.
Gates: both HeatSinkSimulation phases, every weapon fire-heat dump, and
CheckForJam is now the AUTHENTIC form (LiveFireEnabled + heatLoad<=0
early-outs + the minJamChance floor; the interim heat-degraded gate retired).
- THE LOAD-BEARING FIX: Mech ctor SetValidFlag(). Every 1995 entity ctor tail
marks itself valid; ours didn't -- Entity::Dispatch routes messages to an
INVALID entity into the deferred event queue, so the PlayerLink bind (and
every directly-dispatched mech message) silently never landed and the gates
read a NULL player forever.
- THE COOLANT SYSTEM (authentic bodies, byte-verified constants: HeatLoadScale
0.002 -> heatLoad now in [0,1]; equalize eps 1e-4; draw floor/ON
0.0025/0.003): UpdateCoolant (damage-scaled draw -- an undamaged mech leaks
nothing), BalanceCoolant (full clamp chain from ConductHeat), DrawCoolant
base=0 with the RESERVOIR override as THE SOURCE; Reservoir reconstructed
(capacity overlays thermalCapacity, CoolantSimulation + the full
InjectCoolant flush distribution, BT_FORCE_FLUSH dev hook); Condenser
RefrigerationSimulation (massScale = (1-damage)*refrigerationFactor >= 1 --
the heat pump that chills the bank; valveState inits 1; digit-suffix number
fix). Deferred: AggregateHeatSink family, cockpit-button handlers
(MoveValve/ToggleCooling/InjectCoolant), TrackSeekVoltage charge model.
Research driven by a 4-agent workflow dossier over the BT411 RE (verbatim
bodies for every function above + the egg experience plumbing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Weapons dump firing heat into their own sinks, sinks conduct through the
Condenser bank into the central HeatSink, temperatures drive the degradation/
failure alarms, and every powered subsystem tracks its generator through the
electrical state machine. Verified: the authentic fire-discipline game -- a PPC
spikes 77->709K per shot, relaxes to 441K across its 5s reload, and sustained
fire climbs 441->674->838->960K, brushing the authored 1000K degradation
threshold. All calibration values match the BT411 audit exactly (central bank
1.39e6, PPC sink 174000, thresholds 77/1000/2000).
- HEAT: HeatSinkSimulation (@004ad924 absorb->T->load->conduct->alarm),
ConductHeat/ComputeHeatFlow (@004ad8ac/@004ad9ec two-body equilibrium
relaxation; flow==0 exactly at uniform T), UpdateHeatLoad (15-sample filter);
heat-state enum + accessors; installed by the HeatSink ctor.
- WIRE-VERIFIED: HeatSink resource gains linkedSinkIndex -- THE missing
ancestry int that shifted every descendant block +1 (voltageSourceIndex had
been reading the linked-sink index: "power sources" appeared to be
Condensers). Conduction topology wired from it at construction:
weapons->Condensers1-6->central; Generators->Condensers; Reservoir->central.
- MECHWEAP resource: authentic pip tail (pipPosition int + pipColor 3 floats +
pipExtendedRange int) per the BT411 verified overlay; with linkedSinkIndex
this closes the whole +3 alignment mystery (ProjectileWeapon pad deleted).
True bhk1 reads: PPC recharge 5.0s (not 1.0), discharge 0.99s, range 900.
- POWERSUB: ctor resolves voltageSourceIndex -> GeneratorA-D (wire-verified),
taps via Generator::TapVoltageSource (-1 when full); PoweredSubsystemSimulation
(@004b0bd0) electrical FSM (Starting/NoVoltage/Shorted/GeneratorOff/Ready);
GeneratorSimulation partial (start/short-recovery timers).
- Weapons: power step at sim head; Loading recharge gated on electrical Ready
(authentic @4bbdf5); FireWeapon dumps firing heat. HEAT UNITS: the chain is
1e7-native with TWO authoring conventions -- energy weapons store small
(PPC=11, x1e7 from the energy algebra), ballistics store native (SRM6=5.06e7,
dumped raw; double-scaling it was the bring-up runaway-temperature bug).
- SENSOR: authentic gating (@004b1c4c) -- power step, electrical-Ready gate,
heat-state switch (Degradation x0.5 / Failure 0). Verified Ready + 100%.
Deferred: coolant depletion/venting, the novice HeatModelOff gate, the charge
model (TrackSeekVoltage/voltage sag/I2R), weapon gate 1 + jam roll, the central
sink's forward-linked drain. Zero Fail across fire + neutral runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HeatSink : public HeatableSubsystem (confirmed hierarchy) -- a thermal mass with
a coolant loop. Added to HEAT.HPP/.CPP: the class + its SubsystemConnection slot
helper + HeatSink__SubsystemResource. Ctor chains HeatableSubsystem and seeds the
thermal state from the resource (startingTemperature/degradation/failure/
thermalConductance/thermalMass), heatAlarm(3) = AlarmIndicator, heatFilter =
AverageOf<Scalar>(15). Statics, dtor, ResetToInitialState, TestClass/TestInstance
real; the per-frame thermal sim (HeatSinkSimulation/DrawCoolant) staged.
Also reconciled HeatableSubsystem to the 4.10 fields: its resource struct now
carries startingTemperature/degradationTemperature/failureTemperature/
thermalConductance/thermalMass (was the BT411-mislabeled thermalMass/degrade/fail/
coolingLoop set), and degradationTemperature/failureTemperature are base members
(HeatSink reads them). Verified slice now: MechSubsystem -> HeatableSubsystem ->
HeatSink. Compile-verified (BT: 25 ok); tree links clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HeatableSubsystem (HEAT.CPP) reconstructed + compile-verified (BT: 25 ok):
statics, ctor (chains MechSubsystem resource form + ResetToInitialState ->
currentTemperature=300, heatLoad=0), dtor, TestClass/TestInstance,
CreateStreamedSubsystem staged. Verified slice now: Subsystem -> MechSubsystem
-> HeatableSubsystem.
Also mapped the roster hierarchy from BT411, which is DEEPER than the staged
headers (recorded in MECHSUB.NOTES.md):
MechSubsystem -> HeatableSubsystem -> HeatSink -> PoweredSubsystem -> {Sensor,
Generator, ...}
Findings for the next steps: (1) HeatSink is MISSING entirely from the staged
tree and must be added between HeatableSubsystem and PoweredSubsystem;
(2) staged POWERSUB.HPP wrongly derives PoweredSubsystem from HeatableSubsystem
(real base = HeatSink) -- fix it; (3) PoweredSubsystem has inter-subsystem wiring
(resolves its voltage-source generator from the owner mech's subsystem roster),
imposing a segment-walk ordering constraint for the phase-4 ctor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>