diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index d16a357a..70484296 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -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; diff --git a/restoration/source410/BT/HEAT.CPP b/restoration/source410/BT/HEAT.CPP index 3eaebf6a..08d6df84 100644 --- a/restoration/source410/BT/HEAT.CPP +++ b/restoration/source410/BT/HEAT.CPP @@ -19,6 +19,8 @@ # include #endif +#include + // //############################################################################# // 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 << ""; + } + 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 diff --git a/restoration/source410/BT/HEAT.HPP b/restoration/source410/BT/HEAT.HPP index 0d8a72ee..e4eb6703 100644 --- a/restoration/source410/BT/HEAT.HPP +++ b/restoration/source410/BT/HEAT.HPP @@ -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); diff --git a/restoration/source410/BT/HEAT.NOTES.md b/restoration/source410/BT/HEAT.NOTES.md new file mode 100644 index 00000000..1afcb523 --- /dev/null +++ b/restoration/source410/BT/HEAT.NOTES.md @@ -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. diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index e12f1445..45266633 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -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; } } diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index 85674dbc..7a447132 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -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; }; //########################################################################### diff --git a/restoration/source410/BT/MISLANCH.CPP b/restoration/source410/BT/MISLANCH.CPP index 875319ba..e7f436bc 100644 --- a/restoration/source410/BT/MISLANCH.CPP +++ b/restoration/source410/BT/MISLANCH.CPP @@ -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; } } diff --git a/restoration/source410/BT/POWERSUB.CPP b/restoration/source410/BT/POWERSUB.CPP index 896d0583..db8de654 100644 --- a/restoration/source410/BT/POWERSUB.CPP +++ b/restoration/source410/BT/POWERSUB.CPP @@ -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 << ""; + } + 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(); } //########################################################################### diff --git a/restoration/source410/BT/POWERSUB.HPP b/restoration/source410/BT/POWERSUB.HPP index ee782cd4..6443991e 100644 --- a/restoration/source410/BT/POWERSUB.HPP +++ b/restoration/source410/BT/POWERSUB.HPP @@ -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); diff --git a/restoration/source410/BT/PROJWEAP.CPP b/restoration/source410/BT/PROJWEAP.CPP index 0cda83bc..0bde9cca 100644 --- a/restoration/source410/BT/PROJWEAP.CPP +++ b/restoration/source410/BT/PROJWEAP.CPP @@ -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; + } } } } diff --git a/restoration/source410/BT/PROJWEAP.HPP b/restoration/source410/BT/PROJWEAP.HPP index 14bc196e..7b005eb5 100644 --- a/restoration/source410/BT/PROJWEAP.HPP +++ b/restoration/source410/BT/PROJWEAP.HPP @@ -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; diff --git a/restoration/source410/BT/SENSOR.CPP b/restoration/source410/BT/SENSOR.CPP index 598a2e94..cc5be8f5 100644 --- a/restoration/source410/BT/SENSOR.CPP +++ b/restoration/source410/BT/SENSOR.CPP @@ -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; } }