BT410 Phase 5.3.10: power/heat wave -- the thermal + electrical economy is LIVE

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>
This commit is contained in:
Cyd
2026-07-21 23:55:48 -05:00
co-authored by Claude Fable 5
parent ab7191dc38
commit 57507cb15c
12 changed files with 691 additions and 69 deletions
+31 -7
View File
@@ -126,11 +126,24 @@ void
recoil = rechargeRate; // full recoil -> dial 0, decays in Loading
ComputeOutputVoltage();
//
// Dump the firing heat into our own thermal accumulator; the HeatSink step
// absorbs it next frame and conducts it toward the linked Condenser bank.
// The heat chain is 1e7-unit-native (the BT411 calibration audit): the
// authored heatCostToFire is the closed form's full-charge value / 1e7
// (PPC: 11 -> 1.1e8 units -> +632 K on its own 174000-mass sink).
// (PARTIAL: the (1-dF)*E*chargeRatio^2 charge-scaling joins with the
// electrical-charge wave.)
//
AddPendingHeat(heatCostToFire * 10000000.0f);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[fire] '" << GetName()
<< "' FIRED (discharge=" << dischargeTime
<< "s recharge=" << rechargeRate << "s)" << endl << flush;
<< "s recharge=" << rechargeRate
<< "s heat+=" << heatCostToFire
<< " T=" << CurrentTemperatureOf() << ")" << endl << flush;
}
}
@@ -171,6 +184,12 @@ void
{
Check(this);
//
// The PoweredSubsystem step first (binary @004baa88 head): the HeatSink
// thermal absorb/conduct + the electrical state machine.
//
PoweredSubsystem::PoweredSubsystemSimulation(time_slice);
{
static int forceFire = -1;
if (forceFire < 0)
@@ -221,13 +240,17 @@ void
weaponAlarm.SetLevel(LoadingState);
}
//
// Recharge: decay the recoil over the authored RechargeRate seconds;
// the dial (rechargeLevel) rises 0 -> 1 with it.
// Recharge only while the electrical supply is Ready (the authentic
// charge integration gates on the voltage state): decay the recoil
// over the authored RechargeRate seconds; the dial rises 0 -> 1.
//
recoil -= time_slice;
if (recoil <= 0.0f)
if (GetVoltageState() == Ready)
{
recoil = 0.0f;
recoil -= time_slice;
if (recoil <= 0.0f)
{
recoil = 0.0f;
}
}
ComputeOutputVoltage();
if (recoil == 0.0f)
@@ -236,7 +259,8 @@ void
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[fire] '" << GetName()
<< "' LOADED" << endl << flush;
<< "' LOADED (T=" << CurrentTemperatureOf() << ")"
<< endl << flush;
}
}
break;
+163 -4
View File
@@ -19,6 +19,8 @@
# include <mech.hpp>
#endif
#include <math.h>
//
//#############################################################################
// Shared data support -- reuses the base Subsystem sets (no boot-critical
@@ -186,6 +188,56 @@ HeatSink::HeatSink(
pendingHeat = 0.0f;
radiatedHeat = 0.0f;
//
// Wire the heat-conduction link: the resource names the roster slot of the
// sink this one drains into (weapons/equipment -> the Condenser bank, the
// Condensers -> the central HeatSink). The shipped stream orders sinks so
// the target is already constructed; an unresolvable index leaves the sink
// standalone (its own thermal mass only).
//
{
Subsystem *linked = NULL;
if (subsystem_resource->linkedSinkIndex >= 0
&& subsystem_resource->linkedSinkIndex < owner->GetSubsystemCount())
{
linked = owner->GetSubsystem(subsystem_resource->linkedSinkIndex);
}
if (linked != NULL)
{
linkedSinks.Add(linked);
}
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[heat] '" << GetName()
<< "' linkedSinkIdx=" << subsystem_resource->linkedSinkIndex
<< " -> ";
if (linked != NULL)
{
DEBUG_STREAM << linked->GetName();
}
else
{
DEBUG_STREAM << "<none>";
}
DEBUG_STREAM << " thermalMass=" << thermalMass
<< " T0=" << currentTemperature
<< " degrade=" << degradationTemperature
<< " fail=" << failureTemperature
<< " conduct=" << thermalConductance << endl << flush;
}
}
//
// Install the per-frame thermal Performance (replicant copies are driven by
// console updates instead). Derived classes (PoweredSubsystem, Generator,
// the weapons) override with their own Performance in their ctors, each of
// which chains this step.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&HeatSink::HeatSinkSimulation);
}
Check_Fpu();
}
@@ -234,14 +286,121 @@ Logical
//
//#############################################################################
// Per-frame thermal simulation (heat conduction, coolant draw, radiation).
// Not yet reconstructed -- fires only once the mech is ticking.
// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924).
// Absorb the pending heat into the thermal mass, recompute the temperature and
// the smoothed heat-load reading, conduct into the linked sink, then drive the
// degradation / failure alarm from the authored thresholds.
//
// PARTIAL: the heat model runs unconditionally -- the authentic gate is the
// player experience level (HeatModelActive: novice mode disables the heat
// model / jams; joins with the player-link accessor wave). UpdateCoolant
// (coolant depletion / venting) is deferred with the coolant wave; the
// coolant level stays at capacity, which holds the conduction term at its
// full-coolant value.
//#############################################################################
//
void
HeatSink::HeatSinkSimulation(Scalar)
HeatSink::HeatSinkSimulation(Scalar time_slice)
{
Fail("HeatSink::HeatSinkSimulation -- heat.cpp not yet reconstructed");
Check(this);
heatEnergy += pendingHeat;
currentTemperature = heatEnergy / thermalMass;
UpdateHeatLoad();
pendingHeat = 0.0f;
ConductHeat(time_slice);
//
// Drive the degradation / failure alarm.
//
if (currentTemperature > failureTemperature)
{
heatAlarm.SetLevel(FailureHeat);
}
else if (currentTemperature > degradationTemperature)
{
heatAlarm.SetLevel(DegradationHeat);
}
else
{
heatAlarm.SetLevel(NormalHeat);
}
Check_Fpu();
}
//
//#############################################################################
// UpdateHeatLoad -- recompute the radiated heat and feed it through the
// 15-sample running-average filter to produce the smoothed heatLoad reading
// (binary @004ad7f0; the HeatLoadScale / min / max shaping constants join with
// the gauge-calibration wave).
//#############################################################################
//
void
HeatSink::UpdateHeatLoad()
{
Check(this);
radiatedHeat = currentTemperature * coolantLevel;
heatFilter.Add(radiatedHeat);
heatLoad = heatFilter.CalculateAverage();
}
//
//#############################################################################
// ConductHeat -- conduct heat into the linked sink (binary @004ad8ac). The
// coolant rebalance (BalanceCoolant) is deferred with the coolant wave.
//#############################################################################
//
void
HeatSink::ConductHeat(Scalar time_slice)
{
Check(this);
HeatSink *other = (HeatSink *)linkedSinks.Resolve();
if (other != NULL && coolantAvailable != 0)
{
Scalar flow = ComputeHeatFlow(other, time_slice);
other->pendingHeat += flow;
pendingHeat -= flow;
}
}
//
//#############################################################################
// ComputeHeatFlow -- conductive heat exchange between this sink and 'other'
// (binary @004ad9ec):
// tau = thermalMass / massScale
// denom = tau + other->thermalMass
// q = (currentTemperature*massScale
// - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom)
// * tau
// * (1 - exp( -dt * thermalConductance
// * (coolantLevel / thermalCapacity)
// * coolantFlowScale / denom ))
//#############################################################################
//
Scalar
HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice)
{
Check(this);
Check(other);
Scalar tau = thermalMass / massScale;
Scalar denom = tau + other->thermalMass;
Scalar equilibrium =
currentTemperature * massScale
- (other->heatEnergy + other->pendingHeat + heatEnergy) / denom;
Scalar response = 1.0f - (Scalar)exp(
-time_slice * thermalConductance
* (coolantLevel / thermalCapacity)
* coolantFlowScale / denom
);
return equilibrium * tau * response;
}
Scalar
+44
View File
@@ -126,6 +126,15 @@
struct HeatSink__SubsystemResource:
public HeatableSubsystem__SubsystemResource
{
//
// WIRE-VERIFIED (raw-stream dump): the roster index of the sink this
// one conducts its heat into -- the weapons/equipment link to the
// Condenser bank (slots 4-9), the Condensers to the central HeatSink.
// This was THE missing ancestry int that shifted every descendant
// resource block +1 (PoweredSubsystem voltageSourceIndex, the MechWeapon
// block, ...).
//
int linkedSinkIndex;
};
//###########################################################################
@@ -209,12 +218,47 @@
void
ResetToInitialState(Logical powered);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Heat state (carried in heatAlarm, 3 levels). The thresholds are the
// authored degradation/failure temperatures.
//
public:
enum {
NormalHeat = 0,
DegradationHeat,
FailureHeat,
HeatStateCount
};
unsigned
GetHeatState() { Check(this); return heatAlarm.GetLevel(); }
Scalar
CurrentTemperatureOf() { Check(this); return currentTemperature; }
void
AddPendingHeat(Scalar heat)
{ Check(this); pendingHeat += heat; }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
typedef void
(HeatSink::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
HeatSinkSimulation(Scalar time_slice);
void
UpdateHeatLoad();
void
ConductHeat(Scalar time_slice);
Scalar
ComputeHeatFlow(HeatSink *other, Scalar time_slice);
virtual Scalar
DrawCoolant(Scalar requested);
+89
View File
@@ -0,0 +1,89 @@
# 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.
## Still deferred
UpdateCoolant / BalanceCoolant (coolant depletion + venting), the HeatModelOff
experience gate (novice mode), the electrical charge model (TrackSeekVoltage /
voltage sag / I²R), gate 1 + the heat-scaled jam roll in the weapon FSMs, the
central sink's forward-linked drain, Condenser MoveValve handling.
+14
View File
@@ -364,6 +364,20 @@ Mech::Mech(
++i;
}
DEBUG_STREAM << "[skel] segments=" << i << endl << flush;
//
// The subsystem roster map (slot -> name), for cross-referencing the
// streamed index fields (voltage source / linked sink / ammo bin).
//
for (int r = 2; r < subsystemCount; ++r)
{
if (subsystemArray[r] != NULL)
{
DEBUG_STREAM << "[roster] slot " << r << " = "
<< subsystemArray[r]->GetName() << endl;
}
}
DEBUG_STREAM << flush;
}
}
+9 -3
View File
@@ -34,6 +34,12 @@
//###################### MechWeapon Model Resource ######################
//###########################################################################
//
// WIRE-VERIFIED layout (raw-stream dump vs the BT411 verified overlay):
// eleven fields; the pip tail is pipPosition(int) + pipColor(3 floats) +
// pipExtendedRange(int). bhk1 PPC reads: recharge 5.0s, range 900,
// damage 12, type 4, heatCost 11, pipColor (0,0,1).
//
struct MechWeapon__SubsystemResource:
public PoweredSubsystem::SubsystemResource
{
@@ -44,9 +50,9 @@
Scalar damageAmount;
int damageType;
Scalar heatCostToFire;
Scalar pipPositionX;
Scalar pipPositionY;
int rearFiring;
int pipPosition;
Scalar pipColor[3];
int pipExtendedRange;
};
//###########################################################################
+8 -1
View File
@@ -102,11 +102,18 @@ void
// the entity-spawn + targeting waves; the view/target gate, ammo pull and
// recoil all live in the CALLER (ProjectileWeaponSimulation's Loaded case).
//
// Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6
// reads 5.06e7 = +641K on its own sink -- the same design magnitude as the
// PPC's +632K); dump it raw. (The EMITTER's authored value is small and
// its 1e7 comes from the energy algebra -- see EMITTER.CPP.)
AddPendingHeat(heatCostToFire);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[fire] '" << GetName()
<< "' FIRED (salvo of " << missileCount
<< ", recharge=" << rechargeRate << "s)" << endl << flush;
<< ", recharge=" << rechargeRate
<< "s heat+=" << heatCostToFire << ")" << endl << flush;
}
}
+186 -16
View File
@@ -40,14 +40,13 @@ PoweredSubsystem::SharedData
//
//#############################################################################
// A HeatSink that draws electrical power from a generator.
//
// The voltage-source resolution (indexing the owner mech's SUBSYSTEM ROSTER to
// find the powering generator) and the master-instance electrical simulation
// need the roster populated -- which happens during the Mech ctor's
// segment-table walk -- so they are deferred to that phase (see MECHSUB.NOTES.md).
// The ctor here chains HeatSink and primes the electrical state; the subsystem
// constructs with no attached source (inputVoltage 0) until the wiring lands.
// A HeatSink that draws electrical power from a generator (binary ctor
// @004b0f74). Resolves the "VoltageSource" roster index to the powering
// generator, attaches the tap, and primes the electrical state machine. The
// voltageSourceIndex indexes the owner mech's SUBSYSTEM ROSTER (the same
// index space the AmmoBin link uses) -- the roster slots ahead of this
// subsystem are already constructed by the segment walk, and the shipped
// stream orders the generators first.
//#############################################################################
//
PoweredSubsystem::PoweredSubsystem(
@@ -68,6 +67,53 @@ PoweredSubsystem::PoweredSubsystem(
outputVoltage = 0.0f;
ratedVoltage = 0.0f;
thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient;
startTime = subsystem_resource->startTime;
startTimer = startTime;
voltageScale = 1.0f;
//
// Resolve the voltage source from the roster and attach the tap.
//
Subsystem *source = NULL;
if (subsystem_resource->voltageSourceIndex >= 0
&& subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount())
{
source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex);
}
if (source != NULL)
{
AttachToVoltageSource(source);
}
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[power] '" << GetName()
<< "' srcIdx=" << subsystem_resource->voltageSourceIndex << " -> ";
if (source != NULL)
{
DEBUG_STREAM << source->GetName();
}
else
{
DEBUG_STREAM << "<none>";
}
DEBUG_STREAM << " startTime=" << startTime << endl << flush;
}
electricalStateAlarm.SetLevel(Ready);
modeAlarm.SetLevel(Connected);
//
// A master (non-replicant) instance runs the per-frame electrical
// simulation. Derived subsystems (the weapons, Sensor, ...) override with
// their own Performance in their ctors, each of which chains this step.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation);
}
Check_Fpu();
}
@@ -112,14 +158,97 @@ Logical
//
//#############################################################################
// AttachToVoltageSource Link this subsystem to its powering generator.
// Deferred with the segment-walk (needs the populated roster).
// AttachToVoltageSource Link this subsystem to its powering generator
// (binary @004b0dd8): take a tap on the generator (-1 when every tap is
// taken) and hold the live connection.
//#############################################################################
//
int
PoweredSubsystem::AttachToVoltageSource(Subsystem *source)
{
Check(this);
Check(source);
Generator *generator = (Generator *)source;
if (generator->TapVoltageSource() != 0)
{
return -1;
}
voltageSource.Add(source);
inputVoltage = generator->MeasuredVoltage();
return 0;
}
//
//#############################################################################
// PoweredSubsystemSimulation -- the per-frame electrical step (binary
// @004b0bd0). Runs the HeatSink thermal step, then advances the electrical
// state machine from the state of the powering generator.
//
// PARTIAL: the AutoConnect replacement-generator hunt (modeAlarm AutoConnect +
// the status-flag gate) joins with the damage wave.
//#############################################################################
//
void
PoweredSubsystem::AttachToVoltageSource(Subsystem *)
PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice)
{
Fail("PoweredSubsystem::AttachToVoltageSource -- powersub.cpp not yet reconstructed");
Check(this);
HeatSink::HeatSinkSimulation(time_slice);
Generator *source = (Generator *)voltageSource.Resolve();
if (source == NULL)
{
electricalStateAlarm.SetLevel(NoVoltage);
}
else
{
if (source->GeneratorStateOf() == Generator::GeneratorShorted)
{
electricalStateAlarm.SetLevel(Shorted);
}
if (source->GeneratorStateOf() == Generator::GeneratorStarting
|| source->GeneratorStateOf() == Generator::GeneratorFailed)
{
electricalStateAlarm.SetLevel(GeneratorOff);
}
}
switch (electricalStateAlarm.GetLevel())
{
case Starting:
startTimer += time_slice;
if (startTime <= startTimer)
{
electricalStateAlarm.SetLevel(Ready);
}
break;
case NoVoltage:
if (source != NULL)
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
case Shorted:
case GeneratorOff:
if (source != NULL
&& source->GeneratorStateOf() == Generator::GeneratorReady)
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
}
if (source != NULL)
{
inputVoltage = source->MeasuredVoltage();
}
Check_Fpu();
}
//###########################################################################
@@ -181,6 +310,14 @@ Generator::Generator(
const char *name = GetName();
generatorNumber = name[strlen(name) - 1] - 0x40;
//
// Install the generator's per-frame electrical Performance.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Generator::GeneratorSimulation);
}
Check_Fpu();
}
@@ -228,14 +365,47 @@ void
//
//#############################################################################
// Per-frame electrical simulation (voltage, short recovery). Not yet
// reconstructed -- fires only once the mech is ticking.
// GeneratorSimulation -- the generator's per-frame step (PARTIAL). Runs the
// HeatSink thermal step and the start/short-recovery timers. The authentic
// load model (output voltage sag under tap load / I^2R self-heat feeding the
// charge integration) joins with the electrical-charge wave
// (TrackSeekVoltage). A healthy generator holds GeneratorReady at its rated
// voltage.
//#############################################################################
//
void
Generator::GeneratorSimulation(Scalar)
Generator::GeneratorSimulation(Scalar time_slice)
{
Fail("Generator::GeneratorSimulation -- powersub.cpp not yet reconstructed");
Check(this);
HeatSink::HeatSinkSimulation(time_slice);
switch (stateAlarm.GetLevel())
{
case GeneratorStarting:
startTimer += time_slice;
if (startTime <= startTimer)
{
stateAlarm.SetLevel(GeneratorReady);
outputVoltage = ratedVoltage;
}
break;
case GeneratorShorted:
shortTimer -= time_slice;
if (shortTimer <= 0.0f)
{
shortTimer = shortRecoveryTime;
stateAlarm.SetLevel(GeneratorStarting);
startTimer = 0.0f;
}
break;
default:
break;
}
Check_Fpu();
}
//###########################################################################
+69 -1
View File
@@ -63,14 +63,51 @@
void
ResetToInitialState(Logical powered);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Electrical state machine (carried in electricalStateAlarm, 5 levels) and
// the connection-mode indicator (modeAlarm, 3 levels).
//
public:
enum ElectricalState {
Starting = 0, // powering up; startTimer counts toward startTime
NoVoltage = 1, // voltage source missing / unresolvable
Shorted = 2, // source generator shorted
GeneratorOff = 3, // source generator off / not ready
Ready = 4 // powered and operating
};
enum ConnectMode {
ManualConnect = 0,
Connected = 1,
AutoConnect = 2
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Power support
//
public:
Subsystem*
ResolveVoltageSource() { return voltageSource.Resolve(); }
void
int
AttachToVoltageSource(Subsystem *source);
unsigned
GetVoltageState() { Check(this); return electricalStateAlarm.GetLevel(); }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Per-frame simulation (heat step + the electrical state machine).
//
public:
typedef void
(PoweredSubsystem::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
PoweredSubsystemSimulation(Scalar time_slice);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
@@ -108,6 +145,10 @@
SubsystemConnection voltageSource;
AlarmIndicator electricalStateAlarm;
AlarmIndicator modeAlarm;
Scalar thermalResistivityCoefficient;
Scalar startTime;
Scalar startTimer;
Scalar voltageScale;
};
//###########################################################################
@@ -216,6 +257,33 @@
Scalar MeasuredVoltage() { Check(this); return outputVoltage; }
Scalar RatedVoltageOf() { Check(this); return ratedVoltage; }
int GetGeneratorNumber(){ Check(this); return generatorNumber; }
unsigned
GeneratorStateOf() { Check(this); return stateAlarm.GetLevel(); }
//
// Tap the generator for one load (a PoweredSubsystem attaching): -1 when
// every tap is taken, else 0.
//
int
TapVoltageSource()
{
Check(this);
if (currentTapCount >= maxTapCount)
{
return -1;
}
++currentTapCount;
return 0;
}
typedef void
(Generator::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
GeneratorSimulation(Scalar time_slice);
+29 -11
View File
@@ -144,10 +144,17 @@ void
{
Check(this);
// Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6
// reads 5.06e7 = +641K on its own sink -- the same design magnitude as the
// PPC's +632K); dump it raw. (The EMITTER's authored value is small and
// its 1e7 comes from the energy algebra -- see EMITTER.CPP.)
AddPendingHeat(heatCostToFire);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[fire] '" << GetName()
<< "' FIRED (ballistic, recharge=" << rechargeRate << "s)"
<< "' FIRED (ballistic, recharge=" << rechargeRate
<< "s heat+=" << heatCostToFire << ")"
<< endl << flush;
}
}
@@ -179,6 +186,12 @@ void
{
Check(this);
//
// The PoweredSubsystem step first (binary @4bbd12): the HeatSink thermal
// absorb/conduct + the electrical state machine.
//
PoweredSubsystem::PoweredSubsystemSimulation(time_slice);
{
static int forceFire = -1;
if (forceFire < 0)
@@ -259,20 +272,25 @@ void
weaponAlarm.SetLevel(LoadingState);
}
//
// Recoil bleeds down; at zero (and a round chambered) -> Loaded.
// Recoil bleeds ONLY while the electrical supply is Ready (binary
// @4bbdf5/@4bbe04); at zero (and a round chambered) -> Loaded.
//
recoil -= time_slice;
if (recoil < 0.0f)
if (GetVoltageState() == Ready)
{
recoil = 0.0f;
if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoLoadedState)
recoil -= time_slice;
if (recoil < 0.0f)
{
weaponAlarm.SetLevel(LoadedState);
if (getenv("BT_MECH_LOG"))
recoil = 0.0f;
if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoLoadedState)
{
DEBUG_STREAM << "[fire] '" << GetName()
<< "' LOADED (rounds=" << bin->GetAmmoCount() << ")"
<< endl << flush;
weaponAlarm.SetLevel(LoadedState);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[fire] '" << GetName()
<< "' LOADED (rounds=" << bin->GetAmmoCount()
<< " T=" << CurrentTemperatureOf() << ")"
<< endl << flush;
}
}
}
}
+8 -11
View File
@@ -25,20 +25,17 @@
//################ ProjectileWeapon Model Resource #####################
//###########################################################################
//
// WIRE-VERIFIED layout (raw-stream dump, TEST.EGG SRM6s): tracerInterval
// reads 1, ammoBinIndex the true bin roster slot (27/29), minTimeOfFlight
// 0.5, minVoltagePercentToFire 0.3, minJamChance 0.05, and
// MissileLauncher's missileCount lands on 6 (an SRM6!). (The interim
// alignment pad is gone -- the ancestry shortfall was the HeatSink
// linkedSinkIndex + the MechWeapon pip tail, both now real fields.)
//
struct ProjectileWeapon__SubsystemResource:
public MechWeapon::SubsystemResource
{
//
// WIRE-VERIFIED alignment (raw-stream dump, TEST.EGG SRM6s): the
// MechWeapon resource ancestry above runs THREE ints short of the wire
// (the pip-family fields -- pipColor/pipExtendedRange -- are not yet
// broken out; the full overlay verification is its own wave). The pad
// re-aligns this struct's fields to their true stream offsets:
// tracerInterval reads 1, ammoBinIndex the true bin roster slot (27/29),
// minTimeOfFlight 0.5, minVoltagePercentToFire 0.3, minJamChance 0.05,
// and MissileLauncher's missileCount lands on 6 (an SRM6!).
//
int resourceAlignPad[3];
int tracerInterval;
int ammoBinIndex;
Scalar minTimeOfFlight;
+41 -15
View File
@@ -117,30 +117,55 @@ void
//#############################################################################
//
void
Sensor::SensorSimulation(Scalar)
Sensor::SensorSimulation(Scalar time_slice)
{
Check(this);
//
// Minimal safe per-frame sensor update (PARTIAL reconstruction). The
// authentic SensorSimulation (BT411 @004b1c4c) first runs
// PoweredSubsystem::PoweredSubsystemSimulation, then gates radarPercent /
// selfTest / badVoltage on the heat state (Normal / Degradation / Failure)
// and the electrical Ready state -- both of which live in the power/heat
// per-frame sim chain that is not yet reconstructed (the powersub/heat
// *Simulation methods are staged). Until that wave, derive radarPercent
// from the one reconstructed input -- this sensor's own structural damage
// level ([0,1], 0 intact .. 1 destroyed) -- and report the sensor healthy.
// This keeps the engine's roster tick path (Entity::PerformAndWatch) safe
// while producing a real radar-capability value. See SENSOR.NOTES.md.
// The authentic per-frame sensor update (binary @004b1c4c): the
// PoweredSubsystem step first, then radarPercent = baseline - structural
// damage, gated by the electrical Ready state and the heat state.
// (Still deferred: the novice-mode HeatModelOff gate -- the player
// experience switch -- joins with the player-link accessor wave.)
//
PoweredSubsystemSimulation(time_slice);
radarPercent = 1.0f - GetSubsystemDamageLevel(); // RadarBaseline - zoneDamage
if (radarPercent < 0.0f)
{
radarPercent = 0.0f;
}
selfTest = True;
badVoltage = False;
selfTest = True;
if (GetVoltageState() == Ready)
{
badVoltage = False;
}
else
{
badVoltage = True;
radarPercent = 0.0f;
}
switch (GetHeatState())
{
case NormalHeat:
selfTest = True;
break;
case DegradationHeat:
radarPercent *= 0.5f; // HeatDegradationScale
selfTest = True;
break;
case FailureHeat:
radarPercent = 0.0f;
selfTest = False;
break;
default:
break;
}
{
//
@@ -153,7 +178,8 @@ void
{
firstTick = 1;
DEBUG_STREAM << "[tick] roster live (first Sensor frame), radarPercent="
<< radarPercent << endl << flush;
<< radarPercent << " voltState=" << GetVoltageState()
<< endl << flush;
}
}