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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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·
|
||||
(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 `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 ((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.
|
||||
|
||||
## 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.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user