The emitter charge cycle is now the authentic electrical model end to end.
- WIRE-VERIFIED resource fix: the Emitter block is graphicLength /
dischargeTime / seekVoltage[5] (FRACTIONS of the generator's rated voltage,
-1 sentinel) / seekVoltageRecommendedIndex. The old two-field guess read
seekVoltage[3]=0.99 as "dischargeTime 0.99s" -- the true PPC discharge is
0.2s, and the calibration reads seekV={6000,7000,8000,9900} x rated 10000,
recommended gear 2: the documented curve exactly.
- Ctor calibration (@004bb120): energyTotal=(dmg+heat)x1e7; EC pins
E=0.5V^2EC to energyTotal at the recommended gear; voltageScale calibrates
the exponential charge to reach seekV[rec] in the authored RechargeRate
seconds cold; damageFraction = the damage share.
- TrackSeekVoltage (@004ba838) + ChargeTimeScale (@004b0d50): the charge
integrates from the generator through the heat-stretched time scale, and
the I2R loss heats the GENERATOR -- verified GeneratorA at 477.9K charging
its PPC (BT411 predicted ~+480K) while the avionics generator idles at 77.
Hot generators charge slower: the heat/firepower feedback loop is CLOSED.
- Emitter::ComputeOutputVoltage override (now virtual, as in the binary
vtable): dial = level/seekV[gear], 0.01 snap, over-1 clamp.
- Sub-stepped Loading (1/60s slices -- the BT411 weapon-brick fix) + the
documented-divergence overcharge rescue. VERIFIED: the PPC loads at level
~7920, inside the binary's exact [7920,8080] snap window.
- FireWeapon energy algebra (@004bace8): damage/heat = energy shares x
chargeRatio^2. VERIFIED dmg=11.78 / heat=1.079e8 at ratio 0.9906. The
heatCost x 1e7 partial is retired -- heat now comes from the energy.
Zero Fail; jams/shutdowns regressions stay live under spam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 KiB
HEAT.CPP / POWERSUB.CPP — the power/heat wave (Phase 5.3.10, 2026-07-21)
The mech's thermal + electrical economy is LIVE: weapons dump firing heat into their own sinks, the 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.
The heat topology (wire-verified on TEST.EGG)
linkedSinkIndex (the previously-missing HeatSink resource int — see below)
wires the authored conduction graph at construction:
weapons / equipment -> Condenser1..6 (slots 4-9) -> central HeatSink (slot 2)
GeneratorA..D (10-13) -> Condensers
Reservoir (3) -> central HeatSink
Authored values match the BT411 calibration audit EXACTLY: central bank mass
1.39e6, PPC's own sink 174000, thresholds 77 start / 1000 degradation / 2000
failure, PPC conductance 86800. (The central sink's own linkedSinkIdx=5 is a
FORWARD reference — Condenser2 isn't constructed yet at slot 2's ctor — so its
drain is unwired; a post-walk fixup is a small future item. Everything drains
INTO it fine.)
Reconstructed
- HeatSink::HeatSinkSimulation (@004ad924): absorb pendingHeat → T = E/mass → UpdateHeatLoad (15-sample filter) → ConductHeat → alarm thresholds. Installed by the HeatSink ctor (derived classes override and chain it).
- ConductHeat / ComputeHeatFlow (@004ad8ac/@004ad9ec): the two-body
equilibrium relaxation —
(T·massScale − (E_other+pending+E_own)/denom)·tau· (1−exp(−dt·conductance·(coolant/capacity)·flowScale/denom)). Verified: flow == 0 exactly at uniform temperature; a fired PPC bleeds ~170K units/step into Condenser4. - PoweredSubsystem ctor + PoweredSubsystemSimulation (@004b0f74/@004b0bd0):
resolves
voltageSourceIndexfrom the roster (GeneratorA-D — wire-verified), attaches the tap (Generator::TapVoltageSource, −1 when full), and runs the electrical FSM (Starting=0 / NoVoltage=1 / Shorted=2 / GeneratorOff=3 / Ready=4) watching the generator state. Deferred: the AutoConnect replacement-generator hunt (needs the status flags / damage wave). - Generator::GeneratorSimulation (PARTIAL): heat step + start/short-recovery timers; the load model (voltage sag, I²R self-heat feeding the charge integration) joins the electrical-charge wave (TrackSeekVoltage).
- Sensor::SensorSimulation upgraded to the authentic gating (@004b1c4c): power step first, radar = 1 − damage, electrical-Ready gate (badVoltage), heat-state switch (Degradation ×0.5, Failure → 0). Verified: voltState=4, radar 100% on the healthy mech.
- Weapons: Emitter/Projectile sims run the power step at their head; the Loading recharge is gated on electrical Ready (authentic @4bbdf5); FireWeapon dumps the firing heat.
Heat units (1e7-native) — TWO authoring conventions
The heat chain is 1e7-unit-native (BT411 audit). The stream stores
heatCostToFire in TWO conventions:
- Energy weapons: small units (PPC = 11) — the ×1e7 comes from the energy closed form ((1−dF)·E ≈ heatCost·1e7 at full charge). Our partial multiplies by 1e7 pending the charge model.
- Ballistics: native units (SRM6 = 5.06e7) — dumped RAW. (Double-scaling this to 5e14 was the runaway-temperature bug during bring-up.) Both land ~+640K on the weapon's own sink — the consistent design magnitude.
The resource-alignment resolution (wire-verified, closes the +3 mystery)
The raw-stream dumps pinned the FULL ancestry alignment:
HeatSink__SubsystemResource.linkedSinkIndex— THE missing int that shifted every descendant block +1 (PoweredSubsystem's voltageSourceIndex previously read the linked-sink index — hence "power sources" appearing to be Condensers).- The MechWeapon pip tail is
pipPosition(int) + pipColor(3 floats) + pipExtendedRange(int)(+2) — matches BT411's verified overlay exactly. - +1 +2 = the +3 the interim ProjectileWeapon pad compensated; the pad is gone.
- True bhk1 reads: PPC recharge 5.0 s (not the misaligned 1.0), discharge 0.99 s, range 900, damage 12, heatCost 11, pipColor (0,0,1); SRM6 recharge 5 s, range 800, salvo damage 35, missileCount 6.
VERIFIED (BT_FORCE_FIRE + BT_POWER_LOG)
PPC: 77° → fires → ~709° → relaxes to 441° across its 5 s reload → sustained fire climbs the residual 441 → 674 → 838 → 960 — brushing the 1000° degradation threshold: the authentic fire-discipline game. SRM salvos spike +641K and cross degradation after three. Condensers absorb and pass to the central bank. Neutral run: sensor Ready/100%, mech holds, zero Fail.
Heat consequences (Phase 5.3.11, 2026-07-22)
The weapon-side consequences are wired and VERIFIED under sustained fire:
- Ballistic gate 1 (@4bbd36): weapon destroyed-state or its own sink at FailureHeat pins full recoil + the unavailable alarm (the NoAmmo roach-motel re-asserts). (Mech-disabled half joins the damage wave.)
- CheckForJam (@4bbfcc):
p = 0.41·T/failTclamped [minJamChance, 1.0], rolled per granted shot against the MUNGA uniformRandom. Interim gate: heat-degraded only (stands in for the deferredLiveFireEnablednovice switch AND the trivially-trueheatLoad>0early-out — the exact spurious- cold-jam trap the BT411 port documented). Verified: SRM6s jam at T≈1200-1320 (24%+ rolls past the 1000° threshold), stay jammed. - Emitter hard gate (@4baab9): FailureHeat drops the beam state + charge each frame; SELF-RECOVERING — conduction cools the sink below 2000° and the weapon resumes from Loading. Verified: the PPC settles into an emergent thermal duty cycle (fire at ~1930° → spike 2562° → shutdown → cool → refire); the lasers oscillate around ~2200°.
The experience gates + the coolant wave (Phase 5.3.12, 2026-07-22)
The player-experience gates are LIVE (the workflow dossier recovered every body from the BT411 RE):
HeatSink::HeatModelActive()(binary FUN_004ad7d4): owner mech →Entity::GetPlayerLink()→BTPlayer::heatModelOn— ON only for veteran / expert. Gates BOTH phases of HeatSinkSimulation (absorb/conduct AND UpdateCoolant) and every weapon fire-heat dump. Standard mode has NO heat consequences — authentic. NULL player link reads ON (dev-permissive).ProjectileWeapon::LiveFireEnabled(): playerLink →simLive— 0 only for novice, whose ballistics never jam.CheckForJamis now the AUTHENTIC form: LiveFireEnabled early-out + theheatLoad <= 0early-out (with the model off heatLoad stays 0, so standard mode never jams either) + the minJamChance floor (a veteran+ launcher carries its authored ~5% cold jam chance — the interim heat-degraded gate is retired).- BTPlayer's flag block renamed to its TRUE semantics (simLive @0x25c, heatModelOn @0x260, advancedDamageOn/2, levelFlag26c/270, experienceLevel) — the ctor rows were already binary-accurate; only the names/types were the old "display toggles" misread.
THE LOAD-BEARING FIX found on the way: Mech ctor SetValidFlag().
Every 1995 entity ctor tail marks itself valid; ours didn't — and
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. The experience gates read a NULL player
forever (permissive ON) until this one authentic line was restored.
The coolant system (all bodies authentic from the dossier, constants byte-verified: HeatLoadScale 0.002 → heatLoad now lives in the [0,1] band, HeatEqualizeEpsilon 1e-4, draw floor 0.0025 / ON 0.003, draw gate 1e-4):
UpdateCoolant(@004adbf8): coolantDraw = own structural damage × heatLoad (an UNDAMAGED mech leaks nothing — the bars stay full), drain, top-up via the virtualDrawCoolant, and the active-flag hysteresis.BalanceCoolant(@004ada94): full clamp chain, called from ConductHeat.DrawCoolantbase = 0; Reservoir overrides it as THE SOURCE (@4af3b0).- Reservoir reconstructed (@4af408): capacity overlays thermalCapacity
(streamed 20), squirtMass 0.5,
CoolantSimulation+InjectCoolant(@4aefa4, the full flush distribution: condensers → weapons → heatables → master with intentional duplicate weighting, squirts only leave the tank, the chill only cools).BT_FORCE_FLUSH=1dev hook (the cockpit InjectCoolant button joins the message wave). Deferred: the AggregateHeatSink master-bank attach + the 0.05×count capacity rescale. - Condenser refrigeration (@4ae4d8): valveState inits 1, massScale = (1 − own damage) × refrigerationFactor (streamed 3.0) clamped ≥ 1 each frame — the condenser pumps heat as if 3× hotter, chilling itself below ambient; that chill is what cools the weapons. INTERIM: coolantFlowScale stays 1.0 (authentic streams 0 + the MoveValve/BTRecomputeCondenserValves share assignment — the cockpit-button wave; zeroing it now would strand condenser heat).
VERIFIED headlessly, both directions of the experience lever (TESTNOV.EGG
= TEST.EGG with experience=novice):
- NOVICE: 212 fire cycles ALL at T=77, zero jams, zero shutdowns, coolant static — the heat model authentically absent.
- EXPERT: playerLink resolves, the full economy runs (PPC duty cycle now sawing ~650-1200° — the refrigeration pump drains harder than before — 119 shutdowns + 2 authentic jams under forced sustained fire). Zero Fail in both.
Still deferred
The electrical charge model (TrackSeekVoltage / voltage sag / I²R), the mech-disabled halves of the weapon gates (damage wave), the AggregateHeatSink family (the central bank's count/radiator loop + reservoir attach + capacity rescale + its forward-linked drain), the MoveValve / ToggleCooling / InjectCoolant cockpit-button message handlers + BTRecomputeCondenserValves, Myomers movement heat, jam recovery via ResetToInitialState (mission reset).
The cockpit-button message layer (Phase 5.3.13, 2026-07-24)
The subsystem-family button messages are LIVE 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 -- the same number, different class, exactly the binary's per-receiver-class convention):
- HeatSink::ToggleCoolingMessageHandler (@004ad6f8, id 3): novice-locked, press-only; toggles coolantAvailable + coolantFlowScale together (OFF strands this sink's heat until switched back).
- Condenser::MoveValveMessageHandler (@4ae464, id 4): novice-locked, press-only; cycles the valve 1 -> 5 -> 50 -> 0 -> 1 and calls Condenser::RecomputeValves (@0049f788): every condenser's coolantFlowScale = valve / sum-of-valves. The Condenser ctor now streams the authentic flowScale = 0; the Mech ctor seeds the shares once at spawn (all valves 1 -> equal shares) -- retiring the flowScale=1 interim. (The condenser-alarm change pulse joins the gauge wave.)
- Reservoir::InjectCoolantMessageHandler (@4aee70, id 4): novice-locked; press raises the inject alarm when the tank holds charge (flush-cloud psfx deferred), 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 ~6 s in (Mech::Simulate).
VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1 valve=5 flow=0.5, the others 0.1 (valve/sum exactly); the flush arms via the real button message; NOVICE: both presses swallowed by the lockout (valve lines stay spawn-only, zero flush). Zero Fail throughout.
The heat-sink BANK (Phase 5.3.14, 2026-07-24)
AggregateHeatSink reconstructed -- the binary's 0x0BBE class (the value our VDATA enum names HeatSinkClassID; there is NO streamed plain-HeatSink -- the 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-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, then top 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 (shed positive, absorbing toward ambient), then flips negative (-48K.. -167K/step) as fired heat pushes it past ambient -- actively radiating the mech's heat away.
Reservoir master rescale: capacity = 0.05 x bankCount x streamed (0.05 x 6 x 20 = 6 -- the authentic tank), coolant refilled to it.
Zero Fail; the full expert economy regression stays green.
The electrical charge model (Phase 5.3.15, 2026-07-24)
The emitter charge cycle is REAL (all bodies authentic from the BT411 RE):
- Resource fix (wire-verified): the Emitter block is graphicLength, dischargeTime, seekVoltage[5] (FRACTIONS of the generator's rated voltage, -1 sentinel), seekVoltageRecommendedIndex. The earlier two-field guess had read seekVoltage[3] = 0.99 as "dischargeTime 0.99 s" -- the TRUE PPC discharge is 0.2 s, and the calibration reads seekV = {6000, 7000, 8000, 9900} x rated 10000, rec gear 2, exactly the documented curve.
- Ctor calibration (@004bb120): energyTotal = (damage + heat) x 1e7 (PPC 2.3e8); EC pins E = 0.5 V^2 EC to energyTotal at the recommended gear; voltageScale calibrates the exponential charge to reach seekV[rec] in the authored RechargeRate seconds on a COLD generator; damageFraction = the damage share.
- TrackSeekVoltage (@004ba838): seekRate = (genV - level)/ChargeTimeScale; level += rate/EC x dt; and the charging I^2R loss lands in the GENERATOR's pendingHeat -- recharging weapons is what heats generators. VERIFIED: GeneratorA at 477.9 K charging its PPC (BT411 predicted ~+480K), GeneratorD (avionics only) idle at 77.
- ChargeTimeScale (@004b0d50, PoweredSubsystem): the base voltageScale stretched by (1 + thermalResistivity x generator temp rise) -- hot generators charge slower. The feedback loop is CLOSED.
- Emitter::ComputeOutputVoltage override (@004ba738, now virtual as in the binary vtable): dial = level/seekV[gear], snap to 1.0 within 0.01, over-1 clamp zeroes.
- Sub-stepped Loading (the BT411 weapon-brick fix): the charge integrates in 1/60 s slices so the +-0.01 Loaded snap window can't be jumped by a dt spike; plus the DOCUMENTED-DIVERGENCE overcharge rescue (the binary deadlocks a weapon whose level lands above a re-geared snap window). VERIFIED: the PPC loads at level ~7920 -- inside the exact [7920, 8080] window from the binary's disasm.
- FireWeapon energy algebra (@004bace8): damage/heat = the two energy shares x chargeRatio^2 (level/seekV[rec]). VERIFIED: dmg=11.78, heat=1.079e8 at ratio 0.9906 -- ~98% of authored at the snap edge. The heatCost x 1e7 partial is retired; the heat now comes from the ENERGY.
Deferred: the ToggleSeekVoltage gear button (needs the energy-weapon message table ids 4..0xb with pads), ServiceDischarge (replicant beam path), the beam build + damage submission (targeting/render waves).