Files
TeslaRel410/restoration/source410/BT/HEAT.NOTES.md
T
CydandClaude Fable 5 b772e716fe BT410 Phase 5.3.15: the electrical charge model -- real charge, closed feedback loop
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>
2026-07-24 09:14:56 -05:00

267 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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·
(1exp(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 `voltageSourceIndex` from 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 ((1dF)·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/failT` clamped [minJamChance, 1.0],
rolled per granted shot against the MUNGA uniform `Random`. Interim gate:
heat-degraded only (stands in for the deferred `LiveFireEnabled` novice
switch AND the trivially-true `heatLoad>0` early-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. `CheckForJam` is now the AUTHENTIC form:
LiveFireEnabled early-out + the `heatLoad <= 0` early-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 virtual `DrawCoolant`, and the active-flag hysteresis.
- `BalanceCoolant` (@004ada94): full clamp chain, called from ConductHeat.
- `DrawCoolant` base = 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=1` dev 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).