BT410 Phase 5.3.12: experience gates + coolant system -- novice/expert modes REAL
The player-experience system now gates the entire heat/jam economy, verified in BOTH directions with a one-token egg edit (TESTNOV.EGG, experience=novice): NOVICE = 212 fire cycles all pinned at T=77, zero jams, zero shutdowns (the heat model authentically absent); EXPERT = the full economy (duty cycles, 119 shutdowns + 2 authentic jams under forced fire). Zero Fail in both. - BTPLAYER: the flag block renamed to its TRUE semantics (simLive @0x25c, heatModelOn @0x260 -- the FUN_004ad7d4 master switch, advancedDamageOn pair, levelFlag26c/270, experienceLevel); the ctor rows were already binary-accurate (nov 0000 / std 1011 / vet 1111 / exp 1101); accessors + [exp] sentinel. - HeatSink::HeatModelActive() + ProjectileWeapon::LiveFireEnabled(): owner mech -> Entity::GetPlayerLink() -> the BTPlayer flags, NULL-permissive. Gates: both HeatSinkSimulation phases, every weapon fire-heat dump, and CheckForJam is now the AUTHENTIC form (LiveFireEnabled + heatLoad<=0 early-outs + the minJamChance floor; the interim heat-degraded gate retired). - THE LOAD-BEARING FIX: Mech ctor SetValidFlag(). Every 1995 entity ctor tail marks itself valid; ours didn't -- Entity::Dispatch routes messages to an INVALID entity into the deferred event queue, so the PlayerLink bind (and every directly-dispatched mech message) silently never landed and the gates read a NULL player forever. - THE COOLANT SYSTEM (authentic bodies, byte-verified constants: HeatLoadScale 0.002 -> heatLoad now in [0,1]; equalize eps 1e-4; draw floor/ON 0.0025/0.003): UpdateCoolant (damage-scaled draw -- an undamaged mech leaks nothing), BalanceCoolant (full clamp chain from ConductHeat), DrawCoolant base=0 with the RESERVOIR override as THE SOURCE; Reservoir reconstructed (capacity overlays thermalCapacity, CoolantSimulation + the full InjectCoolant flush distribution, BT_FORCE_FLUSH dev hook); Condenser RefrigerationSimulation (massScale = (1-damage)*refrigerationFactor >= 1 -- the heat pump that chills the bank; valveState inits 1; digit-suffix number fix). Deferred: AggregateHeatSink family, cockpit-button handlers (MoveValve/ToggleCooling/InjectCoolant), TrackSeekVoltage charge model. Research driven by a 4-agent workflow dossier over the BT411 RE (verbatim bodies for every function above + the egg experience plumbing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,41 +155,51 @@ BTPlayer::BTPlayer(
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
// Master: derive the game-mode flags from the mission experience
|
// Master: derive the game-mode flags from the mission experience level
|
||||||
// level (novice / standard / veteran / expert) and advanced-damage
|
// (the egg's per-pilot "experience" token). The binary rows for
|
||||||
// technician flag. See BTPLAYER.NOTES.md for the flag mapping.
|
// (simLive, heatModelOn, levelFlag26c, levelFlag270):
|
||||||
|
// nov 0,0,0,0 / std 1,0,1,1 / vet 1,1,1,1 / exp 1,1,0,1.
|
||||||
|
// Standard mode is LIVE but has no heat model -- authentic.
|
||||||
//
|
//
|
||||||
Check(bt_mission);
|
Check(bt_mission);
|
||||||
switch (bt_mission->ExperienceLevel())
|
experienceLevel = bt_mission->ExperienceLevel();
|
||||||
|
switch (experienceLevel)
|
||||||
{
|
{
|
||||||
case BTMission::NoviceMode:
|
case BTMission::NoviceMode:
|
||||||
showDamageReceived = 0;
|
simLive = 0;
|
||||||
showKills = 0;
|
heatModelOn = 0;
|
||||||
roleReturnDelay2 = 0;
|
levelFlag26c = 0;
|
||||||
showScore = 0;
|
levelFlag270 = 0;
|
||||||
break;
|
break;
|
||||||
case BTMission::StandardMode:
|
case BTMission::StandardMode:
|
||||||
showDamageReceived = 1;
|
simLive = 1;
|
||||||
showKills = 0;
|
heatModelOn = 0;
|
||||||
roleReturnDelay2 = 1;
|
levelFlag26c = 1;
|
||||||
showScore = 1;
|
levelFlag270 = 1;
|
||||||
break;
|
break;
|
||||||
case BTMission::VeteranMode:
|
case BTMission::VeteranMode:
|
||||||
showDamageReceived = 1;
|
simLive = 1;
|
||||||
showKills = 1;
|
heatModelOn = 1;
|
||||||
roleReturnDelay2 = 1;
|
levelFlag26c = 1;
|
||||||
showScore = 1;
|
levelFlag270 = 1;
|
||||||
break;
|
break;
|
||||||
case BTMission::ExpertMode:
|
case BTMission::ExpertMode:
|
||||||
showDamageReceived = 1;
|
simLive = 1;
|
||||||
showKills = 1;
|
heatModelOn = 1;
|
||||||
roleReturnDelay2 = 0;
|
levelFlag26c = 0;
|
||||||
showScore = 1;
|
levelFlag270 = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
showDamageInflicted = bt_mission->AdvancedDamageOn();
|
advancedDamageOn = bt_mission->AdvancedDamageOn();
|
||||||
roleReturnDelay = bt_mission->AdvancedDamageOn();
|
advancedDamageOn2 = advancedDamageOn;
|
||||||
roleClassIndex = bt_mission->ExperienceLevel();
|
|
||||||
|
if (getenv("BT_MECH_LOG"))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[exp] master player experience=" << experienceLevel
|
||||||
|
<< " simLive=" << (int)simLive
|
||||||
|
<< " heatModelOn=" << (int)heatModelOn
|
||||||
|
<< " advDamage=" << (int)advancedDamageOn << endl << flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
killCount = 0;
|
killCount = 0;
|
||||||
@@ -403,6 +413,12 @@ void
|
|||||||
playerVehicle->Dispatch(&player_link_message);
|
playerVehicle->Dispatch(&player_link_message);
|
||||||
playerVehicle->DispatchToReplicants(&player_link_message);
|
playerVehicle->DispatchToReplicants(&player_link_message);
|
||||||
|
|
||||||
|
if (getenv("BT_MECH_LOG"))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[link] InitializePlayerLink -> vehicle playerLink="
|
||||||
|
<< (void *)playerVehicle->GetPlayerLink() << endl << flush;
|
||||||
|
}
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,24 +173,46 @@
|
|||||||
suppressConsole;
|
suppressConsole;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Game-mode flags derived in the constructor from the mission's
|
// EXPERIENCE-LEVEL game-mode flags, derived in the constructor from the
|
||||||
// experience level (novice / standard / veteran / expert) and
|
// mission's experience level (novice / standard / veteran / expert) and
|
||||||
// advanced-damage technician flag. See BTPLAYER.NOTES.md.
|
// advanced-damage technician flag. TRUE SEMANTICS (the early "display
|
||||||
|
// toggles" reading of this block was wrong -- see BTPLAYER.NOTES.md):
|
||||||
|
// binary rows for (simLive, heatModelOn, levelFlag26c, levelFlag270) =
|
||||||
|
// nov 0,0,0,0 / std 1,0,1,1 / vet 1,1,1,1 / exp 1,1,0,1.
|
||||||
|
// simLive -- the novice lockout: jams, searchlight, powersub
|
||||||
|
// short events (CheckForJam's LiveFireEnabled gate);
|
||||||
|
// heatModelOn -- THE heat-model master switch (HeatModelActive):
|
||||||
|
// weapon-fire heat, the heat sim, coolant, movement
|
||||||
|
// heat. Standard mode has NO heat consequences --
|
||||||
|
// authentic, not a gap;
|
||||||
|
// advancedDamageOn/2 -- the egg's technician flag (an int pair in the
|
||||||
|
// binary; NOT part of the level switch);
|
||||||
|
// experienceLevel -- the raw 0..3 (the ==0 novice-lockout predicate).
|
||||||
//
|
//
|
||||||
Logical
|
Logical
|
||||||
showDamageReceived;
|
simLive;
|
||||||
Logical
|
Logical
|
||||||
showKills;
|
heatModelOn;
|
||||||
Logical
|
Logical
|
||||||
showDamageInflicted;
|
advancedDamageOn;
|
||||||
Logical
|
Logical
|
||||||
showScore;
|
advancedDamageOn2;
|
||||||
Scalar
|
Logical
|
||||||
roleReturnDelay;
|
levelFlag26c;
|
||||||
Scalar
|
Logical
|
||||||
roleReturnDelay2;
|
levelFlag270;
|
||||||
int
|
int
|
||||||
roleClassIndex;
|
experienceLevel;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Logical
|
||||||
|
IsSimLive() const { Check(this); return simLive; }
|
||||||
|
Logical
|
||||||
|
IsHeatModelOn() const { Check(this); return heatModelOn; }
|
||||||
|
Logical
|
||||||
|
IsAdvancedDamageOn() const { Check(this); return advancedDamageOn; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
int
|
int
|
||||||
killCount;
|
killCount;
|
||||||
|
|||||||
@@ -147,3 +147,17 @@ marker), zero Fail. The mech BODY is still `DoNothingOnce` (its real
|
|||||||
3. Score / ScoreInflicted / ScoreUpdate handlers (scoring wave); the console
|
3. Score / ScoreInflicted / ScoreUpdate handlers (scoring wave); the console
|
||||||
SCORE-delta flush inside PlayerSimulation.
|
SCORE-delta flush inside PlayerSimulation.
|
||||||
4. DropZoneReply respawn tail (`Mech::Reset` in-place heal/move).
|
4. DropZoneReply respawn tail (`Mech::Reset` in-place heal/move).
|
||||||
|
|
||||||
|
## Experience flags renamed to TRUE semantics (2026-07-22, Phase 5.3.12)
|
||||||
|
|
||||||
|
The `show*` / `roleReturn*` block was the old "per-role display toggles"
|
||||||
|
misread (the memory note flagged it). Renamed + retyped to the binary truth:
|
||||||
|
`simLive` @0x25c (novice lockout: jams/lights/powersub short events),
|
||||||
|
`heatModelOn` @0x260 (THE heat-model master switch, FUN_004ad7d4),
|
||||||
|
`advancedDamageOn`/`advancedDamageOn2` (the egg technician flag pair),
|
||||||
|
`levelFlag26c` (0/1/1/0, consumer unlocated), `levelFlag270` (0/1/1/1),
|
||||||
|
`experienceLevel` (raw 0..3). The ctor rows were already binary-accurate.
|
||||||
|
Public accessors IsSimLive()/IsHeatModelOn()/IsAdvancedDamageOn(); a gated
|
||||||
|
`[exp]` sentinel prints the derived flags per master player. Consumers:
|
||||||
|
HeatSink::HeatModelActive() (HEAT.CPP), ProjectileWeapon::LiveFireEnabled()
|
||||||
|
(PROJWEAP.CPP) -- both walk mech->GetPlayerLink(), NULL-permissive.
|
||||||
|
|||||||
@@ -127,15 +127,19 @@ void
|
|||||||
ComputeOutputVoltage();
|
ComputeOutputVoltage();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Dump the firing heat into our own thermal accumulator; the HeatSink step
|
// Dump the firing heat into our own thermal accumulator, under the
|
||||||
// absorbs it next frame and conducts it toward the linked Condenser bank.
|
// heat-model experience gate (novice / standard fire generates no heat --
|
||||||
// The heat chain is 1e7-unit-native (the BT411 calibration audit): the
|
// authentic); the HeatSink step absorbs it next frame and conducts it
|
||||||
// authored heatCostToFire is the closed form's full-charge value / 1e7
|
// toward the linked Condenser bank. The heat chain is 1e7-unit-native
|
||||||
// (PPC: 11 -> 1.1e8 units -> +632 K on its own 174000-mass sink).
|
// (the BT411 calibration audit): the authored EMITTER heatCostToFire is
|
||||||
// (PARTIAL: the (1-dF)*E*chargeRatio^2 charge-scaling joins with the
|
// the closed form's full-charge value / 1e7 (PPC: 11 -> 1.1e8 units ->
|
||||||
// electrical-charge wave.)
|
// +632 K on its own 174000-mass sink). (PARTIAL: the
|
||||||
|
// (1-dF)*E*chargeRatio^2 charge-scaling joins the electrical-charge wave.)
|
||||||
//
|
//
|
||||||
AddPendingHeat(heatCostToFire * 10000000.0f);
|
if (HeatModelActive())
|
||||||
|
{
|
||||||
|
AddPendingHeat(heatCostToFire * 10000000.0f);
|
||||||
|
}
|
||||||
|
|
||||||
if (getenv("BT_MECH_LOG"))
|
if (getenv("BT_MECH_LOG"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,8 +19,26 @@
|
|||||||
# include <mech.hpp>
|
# include <mech.hpp>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(BTPLAYER_HPP)
|
||||||
|
# include <btplayer.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// Tuning constants (byte-verified against the shipped image in the BT411 RE:
|
||||||
|
// .data / literal-pool reads).
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
static const Scalar HeatLoadScale = 0.002f; // heat-load normalising scale -> band [0,1]
|
||||||
|
static const Scalar HeatLoadMinimum = 0.0f;
|
||||||
|
static const Scalar HeatLoadMaximum = 1.0f;
|
||||||
|
static const Scalar HeatEqualizeEpsilon = 1.0e-4f; // BalanceCoolant no-op band
|
||||||
|
static const Scalar CoolantDrawGate = 1.0e-4f; // DrawCoolant |amount| gate
|
||||||
|
static const Scalar CoolantDrawFloor = 0.0025f; // draw zero-floor + ACTIVE-off hysteresis
|
||||||
|
static const Scalar CoolantActiveOn = 0.003f; // coolantActive ON threshold
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Shared data support -- reuses the base Subsystem sets (no boot-critical
|
// Shared data support -- reuses the base Subsystem sets (no boot-critical
|
||||||
@@ -286,17 +304,39 @@ Logical
|
|||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924).
|
// HeatModelActive -- the heat-model master switch (binary FUN_004ad7d4):
|
||||||
// Absorb the pending heat into the thermal mass, recompute the temperature and
|
// owner mech -> Entity::playerLink -> BTPlayer::heatModelOn, ON only for
|
||||||
// the smoothed heat-load reading, conduct into the linked sink, then drive the
|
// veteran / expert experience. Standard mode has NO heat consequences --
|
||||||
// degradation / failure alarm from the authored thresholds.
|
// authentic, not a gap. A NULL player link (unlinked dev mech / target dummy)
|
||||||
|
// reads ON, matching the permissive dev-rig behavior; the binary derefs
|
||||||
|
// unguarded (a linked player always exists in pod missions).
|
||||||
|
//#############################################################################
|
||||||
//
|
//
|
||||||
// PARTIAL: the heat model runs unconditionally -- the authentic gate is the
|
Logical
|
||||||
// player experience level (HeatModelActive: novice mode disables the heat
|
HeatSink::HeatModelActive()
|
||||||
// model / jams; joins with the player-link accessor wave). UpdateCoolant
|
{
|
||||||
// (coolant depletion / venting) is deferred with the coolant wave; the
|
Check(this);
|
||||||
// coolant level stays at capacity, which holds the conduction term at its
|
|
||||||
// full-coolant value.
|
if (owner == NULL)
|
||||||
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink());
|
||||||
|
if (player == NULL)
|
||||||
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
return player->IsHeatModelOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924).
|
||||||
|
// Under the heat-model experience gate: absorb the pending heat into the
|
||||||
|
// thermal mass, recompute the temperature and the smoothed heat-load reading,
|
||||||
|
// conduct into the linked sink, and run the coolant draw. The degradation /
|
||||||
|
// failure alarm drive is OUTSIDE the gate (authentic -- with the model off the
|
||||||
|
// temperature never moves, so the alarm just holds Normal).
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
@@ -304,11 +344,19 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
heatEnergy += pendingHeat;
|
if (HeatModelActive())
|
||||||
currentTemperature = heatEnergy / thermalMass;
|
{
|
||||||
UpdateHeatLoad();
|
heatEnergy += pendingHeat;
|
||||||
pendingHeat = 0.0f;
|
currentTemperature = heatEnergy / thermalMass;
|
||||||
ConductHeat(time_slice);
|
UpdateHeatLoad();
|
||||||
|
pendingHeat = 0.0f;
|
||||||
|
ConductHeat(time_slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HeatModelActive())
|
||||||
|
{
|
||||||
|
UpdateCoolant(time_slice);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Drive the degradation / failure alarm.
|
// Drive the degradation / failure alarm.
|
||||||
@@ -326,15 +374,32 @@ void
|
|||||||
heatAlarm.SetLevel(NormalHeat);
|
heatAlarm.SetLevel(NormalHeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// BT_HEAT_LOG: a 5-second per-sink census (temperature / coolant / load).
|
||||||
|
//
|
||||||
|
if (getenv("BT_HEAT_LOG"))
|
||||||
|
{
|
||||||
|
static Scalar censusAccum = 0.0f;
|
||||||
|
censusAccum += time_slice;
|
||||||
|
if (censusAccum >= 5.0f)
|
||||||
|
{
|
||||||
|
censusAccum = 0.0f;
|
||||||
|
DEBUG_STREAM << "[heat-t] " << GetName()
|
||||||
|
<< " T=" << currentTemperature
|
||||||
|
<< " cool=" << coolantLevel << "/" << thermalCapacity
|
||||||
|
<< " load=" << heatLoad << endl << flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// UpdateHeatLoad -- recompute the radiated heat and feed it through the
|
// UpdateHeatLoad -- recompute the radiated heat and feed the normalised sample
|
||||||
// 15-sample running-average filter to produce the smoothed heatLoad reading
|
// through the 15-sample running-average filter to produce the smoothed
|
||||||
// (binary @004ad7f0; the HeatLoadScale / min / max shaping constants join with
|
// heatLoad reading in the [0,1] band (binary @004ad7f0; the shaping constants
|
||||||
// the gauge-calibration wave).
|
// are byte-verified from the image).
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
@@ -343,14 +408,25 @@ void
|
|||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
radiatedHeat = currentTemperature * coolantLevel;
|
radiatedHeat = currentTemperature * coolantLevel;
|
||||||
heatFilter.Add(radiatedHeat);
|
|
||||||
|
Scalar sample = HeatLoadScale * radiatedHeat;
|
||||||
|
if (sample < HeatLoadMinimum)
|
||||||
|
{
|
||||||
|
sample = HeatLoadMinimum;
|
||||||
|
}
|
||||||
|
else if (sample > HeatLoadMaximum)
|
||||||
|
{
|
||||||
|
sample = HeatLoadMaximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
heatFilter.Add(sample);
|
||||||
heatLoad = heatFilter.CalculateAverage();
|
heatLoad = heatFilter.CalculateAverage();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// ConductHeat -- conduct heat into the linked sink (binary @004ad8ac). The
|
// ConductHeat -- conduct heat into the linked sink, then rebalance coolant so
|
||||||
// coolant rebalance (BalanceCoolant) is deferred with the coolant wave.
|
// the hotter side sheds load (binary @004ad8ac).
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
@@ -364,6 +440,116 @@ void
|
|||||||
Scalar flow = ComputeHeatFlow(other, time_slice);
|
Scalar flow = ComputeHeatFlow(other, time_slice);
|
||||||
other->pendingHeat += flow;
|
other->pendingHeat += flow;
|
||||||
pendingHeat -= flow;
|
pendingHeat -= flow;
|
||||||
|
BalanceCoolant(time_slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// BalanceCoolant -- move coolant between this sink and its linked sink so that
|
||||||
|
// the hotter side sheds load (binary @004ada94). Clamped on both ends so
|
||||||
|
// neither sink goes below empty or above its capacity.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
HeatSink::BalanceCoolant(Scalar time_slice)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
HeatSink *other = (HeatSink *)linkedSinks.Resolve();
|
||||||
|
if (other == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar spread = radiatedHeat - other->radiatedHeat;
|
||||||
|
if (spread < 0.0f)
|
||||||
|
{
|
||||||
|
spread = -spread;
|
||||||
|
}
|
||||||
|
if (spread <= HeatEqualizeEpsilon)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Clamp delta to +/- (thermalCapacity * dt).
|
||||||
|
//
|
||||||
|
Scalar limit = thermalCapacity * time_slice;
|
||||||
|
if (delta < -limit)
|
||||||
|
{
|
||||||
|
delta = -limit;
|
||||||
|
}
|
||||||
|
else if (delta > limit)
|
||||||
|
{
|
||||||
|
delta = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Clamp so this sink stays within [0, thermalCapacity].
|
||||||
|
//
|
||||||
|
Scalar hi = thermalCapacity - coolantLevel;
|
||||||
|
Scalar lo = -coolantLevel;
|
||||||
|
if (delta < lo) delta = lo;
|
||||||
|
else if (delta > hi) delta = hi;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Clamp so the other sink stays within its own [0, thermalCapacity].
|
||||||
|
//
|
||||||
|
Scalar otherLo = -(other->thermalCapacity - other->coolantLevel);
|
||||||
|
if (delta < otherLo) delta = otherLo;
|
||||||
|
else if (delta > other->coolantLevel) delta = other->coolantLevel;
|
||||||
|
|
||||||
|
delta = coolantFlowScale * delta;
|
||||||
|
coolantLevel += delta;
|
||||||
|
other->coolantLevel -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// UpdateCoolant -- consume coolant proportional to the current load, request a
|
||||||
|
// top-up from the central cooling system, and update the draw state machine
|
||||||
|
// (binary @004adbf8). The draw scales with THIS subsystem's own structural
|
||||||
|
// damage: an undamaged subsystem leaks nothing (the coolant bars stay full on
|
||||||
|
// a pristine mech); the draw rises only as the sink itself takes battle
|
||||||
|
// damage.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
HeatSink::UpdateCoolant(Scalar time_slice)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
coolantDraw = GetSubsystemDamageLevel() * heatLoad;
|
||||||
|
if (coolantDraw < CoolantDrawFloor)
|
||||||
|
{
|
||||||
|
coolantDraw = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar amount = coolantDraw * time_slice;
|
||||||
|
if (coolantLevel < amount)
|
||||||
|
{
|
||||||
|
amount = coolantLevel;
|
||||||
|
}
|
||||||
|
coolantLevel -= amount;
|
||||||
|
|
||||||
|
if (amount > CoolantDrawGate || amount < -CoolantDrawGate)
|
||||||
|
{
|
||||||
|
coolantLevel += DrawCoolant(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The draw state machine (hysteresis: ON above 0.003, OFF below 0.0025).
|
||||||
|
//
|
||||||
|
if (coolantActive == 0 && coolantDraw > CoolantActiveOn)
|
||||||
|
{
|
||||||
|
coolantActive = 1;
|
||||||
|
}
|
||||||
|
else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor)
|
||||||
|
{
|
||||||
|
coolantActive = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,10 +589,17 @@ Scalar
|
|||||||
return equilibrium * tau * response;
|
return equilibrium * tau * response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// DrawCoolant -- ask the central cooling system for coolant and return how
|
||||||
|
// much was actually supplied. Base sinks supply nothing on their own; the
|
||||||
|
// Reservoir (the coolant store) overrides this as the source.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
Scalar
|
Scalar
|
||||||
HeatSink::DrawCoolant(Scalar)
|
HeatSink::DrawCoolant(Scalar)
|
||||||
{
|
{
|
||||||
Fail("HeatSink::DrawCoolant -- heat.cpp not yet reconstructed");
|
Check(this);
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,14 +712,41 @@ Condenser::Condenser(
|
|||||||
Check(owner);
|
Check(owner);
|
||||||
Check_Pointer(subsystem_resource);
|
Check_Pointer(subsystem_resource);
|
||||||
|
|
||||||
valveState = 0;
|
//
|
||||||
|
// The authentic ctor (binary @4ae568): the valve opens at 1, the
|
||||||
|
// refrigeration output rides the inherited massScale slot, and the
|
||||||
|
// condenser's own coolantFlowScale streams 0 (the valve recompute --
|
||||||
|
// BTRecomputeCondenserValves, the cockpit MoveValve wave -- assigns each
|
||||||
|
// condenser its share of the total valve opening). INTERIM: flow scale is
|
||||||
|
// left at the inherited 1.0 until the valve-recompute wave lands -- a zero
|
||||||
|
// flow scale would zero the condenser->bank conduction exponent and strand
|
||||||
|
// the heat in the condensers with no way to reach the central sink.
|
||||||
|
//
|
||||||
|
valveState = 1;
|
||||||
refrigerationFactor = subsystem_resource->refrigerationFactor;
|
refrigerationFactor = subsystem_resource->refrigerationFactor;
|
||||||
|
massScale = refrigerationFactor;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Condenser number from the segment-name suffix ('A' -> 1 ...).
|
// Condenser number from the segment-name DIGIT suffix ("Condenser1" -> 1;
|
||||||
|
// the letter form -0x40 is the GENERATOR convention).
|
||||||
//
|
//
|
||||||
const char *name = GetName();
|
const char *name = GetName();
|
||||||
condenserNumber = name[strlen(name) - 1] - 0x40;
|
condenserNumber = name[strlen(name) - 1] - '0';
|
||||||
|
|
||||||
|
if (getenv("BT_POWER_LOG"))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[cond] '" << GetName()
|
||||||
|
<< "' refrigFactor=" << refrigerationFactor
|
||||||
|
<< " #" << condenserNumber << endl << flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Install the per-frame refrigeration Performance.
|
||||||
|
//
|
||||||
|
if (owner->GetInstance() != Entity::ReplicantInstance)
|
||||||
|
{
|
||||||
|
SetPerformance(&Condenser::RefrigerationSimulation);
|
||||||
|
}
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -546,3 +766,27 @@ Logical
|
|||||||
{
|
{
|
||||||
return IsDerivedFrom(ClassDerivations);
|
return IsDerivedFrom(ClassDerivations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// RefrigerationSimulation -- the condenser's per-frame step (binary @4ae4d8):
|
||||||
|
// recompute the refrigeration output as (1 - own structural damage) *
|
||||||
|
// refrigerationFactor, clamped >= 1, into the inherited massScale slot -- an
|
||||||
|
// undamaged condenser behaves refrigerationFactor-times "hotter" in the
|
||||||
|
// conduction exchange, actively pumping heat toward the central bank and
|
||||||
|
// chilling itself below ambient -- then run the base HeatSink step.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
Condenser::RefrigerationSimulation(Scalar time_slice)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
massScale = (1.0f - GetSubsystemDamageLevel()) * refrigerationFactor;
|
||||||
|
if (massScale < 1.0f)
|
||||||
|
{
|
||||||
|
massScale = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeatSink::HeatSinkSimulation(time_slice);
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
public HeatableSubsystem
|
public HeatableSubsystem
|
||||||
{
|
{
|
||||||
friend class Condenser;
|
friend class Condenser;
|
||||||
|
friend class Reservoir;
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Shared Data Support
|
// Shared Data Support
|
||||||
//
|
//
|
||||||
@@ -251,6 +252,14 @@
|
|||||||
activePerformance = (Simulation::Performance)performance;
|
activePerformance = (Simulation::Performance)performance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The heat-model master switch (binary FUN_004ad7d4): the owning
|
||||||
|
// BTPlayer's heatModelOn experience flag, reached through the mech's
|
||||||
|
// playerLink. ON only for veteran / expert; a NULL player reads ON.
|
||||||
|
//
|
||||||
|
Logical
|
||||||
|
HeatModelActive();
|
||||||
|
|
||||||
void
|
void
|
||||||
HeatSinkSimulation(Scalar time_slice);
|
HeatSinkSimulation(Scalar time_slice);
|
||||||
void
|
void
|
||||||
@@ -259,6 +268,10 @@
|
|||||||
ConductHeat(Scalar time_slice);
|
ConductHeat(Scalar time_slice);
|
||||||
Scalar
|
Scalar
|
||||||
ComputeHeatFlow(HeatSink *other, Scalar time_slice);
|
ComputeHeatFlow(HeatSink *other, Scalar time_slice);
|
||||||
|
void
|
||||||
|
UpdateCoolant(Scalar time_slice);
|
||||||
|
void
|
||||||
|
BalanceCoolant(Scalar time_slice);
|
||||||
virtual Scalar
|
virtual Scalar
|
||||||
DrawCoolant(Scalar requested);
|
DrawCoolant(Scalar requested);
|
||||||
|
|
||||||
@@ -340,6 +353,26 @@
|
|||||||
);
|
);
|
||||||
~Condenser();
|
~Condenser();
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Per-frame refrigeration (binary @4ae4d8): the condenser actively pumps
|
||||||
|
// heat toward the central bank -- massScale is recomputed each frame as
|
||||||
|
// (1 - own damage) * refrigerationFactor, clamped >= 1, so an undamaged
|
||||||
|
// condenser behaves refrigerationFactor-times "hotter" in the conduction
|
||||||
|
// exchange and chills itself below ambient (which is what cools the
|
||||||
|
// weapons equalizing against it). Damage degrades it toward a plain sink.
|
||||||
|
//
|
||||||
|
public:
|
||||||
|
typedef void
|
||||||
|
(Condenser::*Performance)(Scalar time_slice);
|
||||||
|
void
|
||||||
|
SetPerformance(Performance performance)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
activePerformance = (Simulation::Performance)performance;
|
||||||
|
}
|
||||||
|
void
|
||||||
|
RefrigerationSimulation(Scalar time_slice);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int valveState;
|
int valveState;
|
||||||
int condenserNumber;
|
int condenserNumber;
|
||||||
|
|||||||
@@ -100,11 +100,71 @@ The weapon-side consequences are wired and VERIFIED under sustained fire:
|
|||||||
thermal duty cycle (fire at ~1930° → spike 2562° → shutdown → cool → refire);
|
thermal duty cycle (fire at ~1930° → spike 2562° → shutdown → cool → refire);
|
||||||
the lasers oscillate around ~2200°.
|
the lasers oscillate around ~2200°.
|
||||||
|
|
||||||
|
## The experience gates + the coolant wave (Phase 5.3.12, 2026-07-22)
|
||||||
|
|
||||||
|
**The player-experience gates are LIVE** (the workflow dossier recovered every
|
||||||
|
body from the BT411 RE):
|
||||||
|
|
||||||
|
- `HeatSink::HeatModelActive()` (binary FUN_004ad7d4): owner mech →
|
||||||
|
`Entity::GetPlayerLink()` → `BTPlayer::heatModelOn` — ON only for veteran /
|
||||||
|
expert. Gates BOTH phases of HeatSinkSimulation (absorb/conduct AND
|
||||||
|
UpdateCoolant) and every weapon fire-heat dump. Standard mode has NO heat
|
||||||
|
consequences — authentic. NULL player link reads ON (dev-permissive).
|
||||||
|
- `ProjectileWeapon::LiveFireEnabled()`: playerLink → `simLive` — 0 only for
|
||||||
|
novice, whose ballistics never jam. `CheckForJam` is now the AUTHENTIC form:
|
||||||
|
LiveFireEnabled early-out + the `heatLoad <= 0` early-out (with the model off
|
||||||
|
heatLoad stays 0, so standard mode never jams either) + the minJamChance
|
||||||
|
floor (a veteran+ launcher carries its authored ~5% cold jam chance — the
|
||||||
|
interim heat-degraded gate is retired).
|
||||||
|
- BTPlayer's flag block renamed to its TRUE semantics (simLive @0x25c,
|
||||||
|
heatModelOn @0x260, advancedDamageOn/2, levelFlag26c/270, experienceLevel) —
|
||||||
|
the ctor rows were already binary-accurate; only the names/types were the
|
||||||
|
old "display toggles" misread.
|
||||||
|
|
||||||
|
**THE LOAD-BEARING FIX found on the way: `Mech` ctor `SetValidFlag()`.**
|
||||||
|
Every 1995 entity ctor tail marks itself valid; ours didn't — and
|
||||||
|
`Entity::Dispatch` routes messages to an INVALID entity into the deferred
|
||||||
|
event queue, so the PlayerLink bind (and every directly-dispatched mech
|
||||||
|
message) silently never landed. The experience gates read a NULL player
|
||||||
|
forever (permissive ON) until this one authentic line was restored.
|
||||||
|
|
||||||
|
**The coolant system** (all bodies authentic from the dossier, constants
|
||||||
|
byte-verified: HeatLoadScale 0.002 → heatLoad now lives in the [0,1] band,
|
||||||
|
HeatEqualizeEpsilon 1e-4, draw floor 0.0025 / ON 0.003, draw gate 1e-4):
|
||||||
|
- `UpdateCoolant` (@004adbf8): coolantDraw = own structural damage × heatLoad
|
||||||
|
(an UNDAMAGED mech leaks nothing — the bars stay full), drain, top-up via
|
||||||
|
the virtual `DrawCoolant`, and the active-flag hysteresis.
|
||||||
|
- `BalanceCoolant` (@004ada94): full clamp chain, called from ConductHeat.
|
||||||
|
- `DrawCoolant` base = 0; **Reservoir overrides it as THE SOURCE** (@4af3b0).
|
||||||
|
- **Reservoir reconstructed** (@4af408): capacity overlays thermalCapacity
|
||||||
|
(streamed 20), squirtMass 0.5, `CoolantSimulation` + `InjectCoolant`
|
||||||
|
(@4aefa4, the full flush distribution: condensers → weapons → heatables →
|
||||||
|
master with intentional duplicate weighting, squirts only leave the tank,
|
||||||
|
the chill only cools). `BT_FORCE_FLUSH=1` dev hook (the cockpit
|
||||||
|
InjectCoolant button joins the message wave). Deferred: the
|
||||||
|
AggregateHeatSink master-bank attach + the 0.05×count capacity rescale.
|
||||||
|
- **Condenser refrigeration** (@4ae4d8): valveState inits 1, massScale =
|
||||||
|
(1 − own damage) × refrigerationFactor (streamed 3.0) clamped ≥ 1 each
|
||||||
|
frame — the condenser pumps heat as if 3× hotter, chilling itself below
|
||||||
|
ambient; that chill is what cools the weapons. INTERIM: coolantFlowScale
|
||||||
|
stays 1.0 (authentic streams 0 + the MoveValve/BTRecomputeCondenserValves
|
||||||
|
share assignment — the cockpit-button wave; zeroing it now would strand
|
||||||
|
condenser heat).
|
||||||
|
|
||||||
|
**VERIFIED headlessly, both directions of the experience lever** (TESTNOV.EGG
|
||||||
|
= TEST.EGG with `experience=novice`):
|
||||||
|
- NOVICE: 212 fire cycles ALL at T=77, zero jams, zero shutdowns, coolant
|
||||||
|
static — the heat model authentically absent.
|
||||||
|
- EXPERT: playerLink resolves, the full economy runs (PPC duty cycle now
|
||||||
|
sawing ~650-1200° — the refrigeration pump drains harder than before — 119
|
||||||
|
shutdowns + 2 authentic jams under forced sustained fire). Zero Fail in
|
||||||
|
both.
|
||||||
|
|
||||||
## Still deferred
|
## Still deferred
|
||||||
|
|
||||||
UpdateCoolant / BalanceCoolant (coolant depletion + venting), the
|
The electrical charge model (TrackSeekVoltage / voltage sag / I²R), the
|
||||||
HeatModelOff / LiveFireEnabled experience gates (novice mode), the electrical
|
mech-disabled halves of the weapon gates (damage wave), the AggregateHeatSink
|
||||||
charge model (TrackSeekVoltage / voltage sag / I²R), the mech-disabled halves
|
family (the central bank's count/radiator loop + reservoir attach + capacity
|
||||||
of the weapon gates (damage wave), the central sink's forward-linked drain,
|
rescale + its forward-linked drain), the MoveValve / ToggleCooling /
|
||||||
Condenser MoveValve handling, jam recovery via ResetToInitialState (mission
|
InjectCoolant cockpit-button message handlers + BTRecomputeCondenserValves,
|
||||||
reset path).
|
Myomers movement heat, jam recovery via ResetToInitialState (mission reset).
|
||||||
|
|||||||
@@ -462,6 +462,16 @@ Mech::Mech(
|
|||||||
//
|
//
|
||||||
SetPerformance(&Mech::Simulate);
|
SetPerformance(&Mech::Simulate);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The entity is complete -- mark it VALID, like every 1995 entity ctor tail
|
||||||
|
// (CamShip / DoorFrame / DropZone / ...). LOAD-BEARING: Entity::Dispatch
|
||||||
|
// routes messages to an INVALID entity into the deferred event queue, so
|
||||||
|
// without this the mech never receives a directly-dispatched message --
|
||||||
|
// the PlayerLink bind (and with it every player-experience gate) silently
|
||||||
|
// never lands.
|
||||||
|
//
|
||||||
|
SetValidFlag();
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -355,3 +355,13 @@ invalidates every bucket; newest BT/BT_L4 header mtime additionally invalidates
|
|||||||
the game buckets) so header edits force the dependent recompiles. CODE/ is
|
the game buckets) so header edits force the dependent recompiles. CODE/ is
|
||||||
immutable and needs no stamp. When in doubt: purge `build410/obj/*` for a
|
immutable and needs no stamp. When in doubt: purge `build410/obj/*` for a
|
||||||
clean baseline.
|
clean baseline.
|
||||||
|
|
||||||
|
## PHASE 5.3.12: the missing SetValidFlag (2026-07-22)
|
||||||
|
|
||||||
|
The Mech ctor tail now calls `SetValidFlag()` -- every 1995 entity ctor does
|
||||||
|
(CamShip/DoorFrame/DropZone/...), and ours didn't. LOAD-BEARING:
|
||||||
|
`Entity::Dispatch` routes messages to an INVALID entity into the deferred
|
||||||
|
event queue, so the mech never received a directly-dispatched message -- the
|
||||||
|
PlayerLink bind silently never landed and every player-experience gate read a
|
||||||
|
NULL player (dev-permissive heat-always-on) until this line was restored.
|
||||||
|
Found while verifying the novice-mode heat lockout end-to-end.
|
||||||
|
|||||||
@@ -104,9 +104,13 @@ void
|
|||||||
//
|
//
|
||||||
// Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6
|
// 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
|
// 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
|
// PPC's +632K); dump it raw, under the heat-model experience gate (novice /
|
||||||
// its 1e7 comes from the energy algebra -- see EMITTER.CPP.)
|
// standard fire generates no heat -- authentic). (The EMITTER's authored
|
||||||
AddPendingHeat(heatCostToFire);
|
// value is small and its 1e7 comes from the energy algebra.)
|
||||||
|
if (HeatModelActive())
|
||||||
|
{
|
||||||
|
AddPendingHeat(heatCostToFire);
|
||||||
|
}
|
||||||
|
|
||||||
if (getenv("BT_MECH_LOG"))
|
if (getenv("BT_MECH_LOG"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
# include <random.hpp>
|
# include <random.hpp>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(BTPLAYER_HPP)
|
||||||
|
# include <btplayer.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
Derivation
|
Derivation
|
||||||
ProjectileWeapon::ClassDerivations(
|
ProjectileWeapon::ClassDerivations(
|
||||||
MechWeapon::ClassDerivations,
|
MechWeapon::ClassDerivations,
|
||||||
@@ -150,9 +154,14 @@ void
|
|||||||
|
|
||||||
// Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6
|
// 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
|
// 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
|
// PPC's +632K); dump it raw, under the heat-model experience gate (novice /
|
||||||
// its 1e7 comes from the energy algebra -- see EMITTER.CPP.)
|
// standard fire generates no heat -- authentic). (The EMITTER's authored
|
||||||
AddPendingHeat(heatCostToFire);
|
// value is small and its 1e7 comes from the energy algebra -- see
|
||||||
|
// EMITTER.CPP.)
|
||||||
|
if (HeatModelActive())
|
||||||
|
{
|
||||||
|
AddPendingHeat(heatCostToFire);
|
||||||
|
}
|
||||||
|
|
||||||
if (getenv("BT_MECH_LOG"))
|
if (getenv("BT_MECH_LOG"))
|
||||||
{
|
{
|
||||||
@@ -165,18 +174,39 @@ void
|
|||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// CheckForJam -- the heat-scaled jam roll (binary @4bbfcc):
|
// LiveFireEnabled -- the "sim live" novice lockout: owner mech ->
|
||||||
// p = 0.41 * currentTemperature / failureTemperature,
|
// Entity::playerLink -> BTPlayer::simLive, 0 only for NOVICE experience. A
|
||||||
// clamped to [minJamChance, 1.0], rolled against a uniform random.
|
// NULL player link reads LIVE (the unlinked dev mech / target dummy).
|
||||||
// A jammed launcher clears only on ResetToInitialState (mission reset).
|
//#############################################################################
|
||||||
//
|
//
|
||||||
// TWO interim gates stand in for deferred systems (the BT411 port hit exactly
|
Logical
|
||||||
// this): the authentic leading gate is LiveFireEnabled() -- the player
|
ProjectileWeapon::LiveFireEnabled()
|
||||||
// experience switch (novice mode = no jams), which needs the player-link
|
{
|
||||||
// accessor wave -- and the binary's own `heatLoad <= 0` early-out is trivially
|
Check(this);
|
||||||
// true once the heat model runs. Until LiveFireEnabled lands, gate the roll
|
|
||||||
// on the sink actually being heat-degraded, so cold weapons fire reliably and
|
if (owner == NULL)
|
||||||
// jams appear exactly when the heat economy says the weapon is cooking.
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink());
|
||||||
|
if (player == NULL)
|
||||||
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
return player->IsSimLive();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// CheckForJam -- the heat-scaled jam roll (binary @4bbfcc), AUTHENTIC form:
|
||||||
|
// novice never jams (LiveFireEnabled); a cold heat model never jams (heatLoad
|
||||||
|
// stays 0 with the model off -- UpdateHeatLoad only runs under the experience
|
||||||
|
// gate); otherwise
|
||||||
|
// p = 0.41 * currentTemperature / failureTemperature,
|
||||||
|
// clamped to [minJamChance, 1.0], rolled against the uniform random. Note the
|
||||||
|
// floor: even a cool launcher at veteran+ carries the authored minimum jam
|
||||||
|
// chance per shot (SRM6: 5%) -- authentic pod behavior. A jammed launcher
|
||||||
|
// clears only on ResetToInitialState (mission reset).
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
Logical
|
Logical
|
||||||
@@ -184,7 +214,11 @@ Logical
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
if (GetHeatState() < DegradationHeat) // interim LiveFireEnabled stand-in
|
if (!LiveFireEnabled())
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
if (heatLoad <= 0.0f)
|
||||||
{
|
{
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,13 @@
|
|||||||
Logical
|
Logical
|
||||||
CheckForJam();
|
CheckForJam();
|
||||||
|
|
||||||
|
//
|
||||||
|
// The "sim live" novice lockout (the owning BTPlayer's simLive
|
||||||
|
// experience flag): novice ballistics never jam.
|
||||||
|
//
|
||||||
|
Logical
|
||||||
|
LiveFireEnabled();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
typedef ProjectileWeapon__SubsystemResource SubsystemResource;
|
typedef ProjectileWeapon__SubsystemResource SubsystemResource;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,301 @@
|
|||||||
|
//===========================================================================//
|
||||||
|
// File: reservr.cpp //
|
||||||
|
// Project: BattleTech Brick: Mech subsystems //
|
||||||
|
// Contents: Reservoir -- the coolant store //
|
||||||
|
//---------------------------------------------------------------------------//
|
||||||
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
||||||
|
// All Rights reserved worldwide //
|
||||||
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
||||||
|
//===========================================================================//
|
||||||
|
|
||||||
#include <bt.hpp>
|
#include <bt.hpp>
|
||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#if !defined(RESERVR_HPP)
|
#if !defined(RESERVR_HPP)
|
||||||
# include <reservr.hpp>
|
# include <reservr.hpp>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(MECH_HPP)
|
#if !defined(MECH_HPP)
|
||||||
# include <mech.hpp>
|
# include <mech.hpp>
|
||||||
#endif
|
#endif
|
||||||
Derivation Reservoir::ClassDerivations(HeatSink::ClassDerivations, "Reservoir");
|
|
||||||
Reservoir::SharedData Reservoir::DefaultData(Reservoir::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount);
|
#if !defined(MECHWEAP_HPP)
|
||||||
Reservoir::Reservoir(Mech *owner, int subsystem_ID, SubsystemResource *r, SharedData &shared_data)
|
# include <mechweap.hpp>
|
||||||
: HeatSink(owner, subsystem_ID, r, shared_data)
|
#endif
|
||||||
{ Check(owner); Check_Pointer(r); Check_Fpu(); }
|
|
||||||
Reservoir::~Reservoir() {}
|
#include <math.h>
|
||||||
Logical Reservoir::TestClass(Mech &) { return True; }
|
|
||||||
Logical Reservoir::TestInstance() const { return IsDerivedFrom(ClassDerivations); }
|
Derivation
|
||||||
|
Reservoir::ClassDerivations(
|
||||||
|
HeatSink::ClassDerivations,
|
||||||
|
"Reservoir"
|
||||||
|
);
|
||||||
|
|
||||||
|
Reservoir::SharedData
|
||||||
|
Reservoir::DefaultData(
|
||||||
|
Reservoir::ClassDerivations,
|
||||||
|
Subsystem::MessageHandlers,
|
||||||
|
Subsystem::AttributeIndex,
|
||||||
|
Subsystem::StateCount
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// The coolant store (binary ctor @4af408): CoolantCapacity overlays the
|
||||||
|
// inherited HeatSink thermalCapacity slot; the charge starts full; the flush
|
||||||
|
// flow scale is zero (a reservoir never conducts like an ordinary sink). A
|
||||||
|
// master reservoir registers the CoolantSimulation performance.
|
||||||
|
//
|
||||||
|
// Deferred with the AggregateHeatSink wave: the authentic master path also
|
||||||
|
// attaches the reservoir into the central bank's radiator loop and rescales
|
||||||
|
// the capacity by 0.05 x the bank's heat-sink count (the central sink here is
|
||||||
|
// still a plain HeatSink, which has no count) -- until then the streamed
|
||||||
|
// capacity is used as-is (a larger tank than authentic; noted).
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
Reservoir::Reservoir(
|
||||||
|
Mech *owner,
|
||||||
|
int subsystem_ID,
|
||||||
|
SubsystemResource *r,
|
||||||
|
SharedData &shared_data
|
||||||
|
):
|
||||||
|
HeatSink(owner, subsystem_ID, r, shared_data)
|
||||||
|
{
|
||||||
|
Check(owner);
|
||||||
|
Check_Pointer(r);
|
||||||
|
|
||||||
|
reservoirAlarm.Initialize(2);
|
||||||
|
|
||||||
|
squirtEfficiency = 0.5f;
|
||||||
|
thermalCapacity = r->coolantCapacity;
|
||||||
|
coolantSquirtMass = r->coolantSquirtMass;
|
||||||
|
coolantLevel = thermalCapacity;
|
||||||
|
coolantFlowScale = 0.0f;
|
||||||
|
injectAccumulator = 0.0f;
|
||||||
|
reservoirAlarm.SetLevel(0);
|
||||||
|
|
||||||
|
if (getenv("BT_POWER_LOG"))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[resv] '" << GetName()
|
||||||
|
<< "' capacity=" << thermalCapacity
|
||||||
|
<< " squirtMass=" << coolantSquirtMass << endl << flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owner->GetInstance() != Entity::ReplicantInstance)
|
||||||
|
{
|
||||||
|
SetPerformance(&Reservoir::CoolantSimulation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
|
Reservoir::~Reservoir()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical
|
||||||
|
Reservoir::TestClass(Mech &)
|
||||||
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical
|
||||||
|
Reservoir::TestInstance() const
|
||||||
|
{
|
||||||
|
return IsDerivedFrom(ClassDerivations);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// DrawCoolant -- the SOURCE (binary @4af3b0): hand out up to coolantLevel of
|
||||||
|
// the requested amount and deduct it from the charge.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
Scalar
|
||||||
|
Reservoir::DrawCoolant(Scalar requested)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
Scalar supplied = 0.0f;
|
||||||
|
if (requested >= 0.0f)
|
||||||
|
{
|
||||||
|
supplied = requested;
|
||||||
|
if (coolantLevel < requested)
|
||||||
|
{
|
||||||
|
supplied = coolantLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coolantLevel -= supplied;
|
||||||
|
return supplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// CoolantSimulation -- the registered Performance (binary @4aef78): while
|
||||||
|
// injection is active, accumulate elapsed time and run the coolant
|
||||||
|
// distribution. The InjectCoolant COCKPIT BUTTON (the id-4 message handler
|
||||||
|
// that raises the alarm) joins with the cockpit-button message wave; the DEV
|
||||||
|
// hook BT_FORCE_FLUSH=1 raises it here so the flush machinery is exercisable
|
||||||
|
// headlessly (one press ~4 s after the sim starts ticking).
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
Reservoir::CoolantSimulation(Scalar time_slice)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
{
|
||||||
|
static int forceFlush = -1;
|
||||||
|
if (forceFlush < 0)
|
||||||
|
{
|
||||||
|
forceFlush = (getenv("BT_FORCE_FLUSH") != NULL) ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (forceFlush == 1)
|
||||||
|
{
|
||||||
|
static Scalar armAccum = 0.0f;
|
||||||
|
armAccum += time_slice;
|
||||||
|
if (armAccum >= 4.0f && reservoirAlarm.GetLevel() != 1
|
||||||
|
&& coolantLevel > 0.0f)
|
||||||
|
{
|
||||||
|
forceFlush = 2;
|
||||||
|
reservoirAlarm.SetLevel(1);
|
||||||
|
injectAccumulator = 0.0f;
|
||||||
|
if (getenv("BT_MECH_LOG"))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[resv] FLUSH ON (charge="
|
||||||
|
<< coolantLevel << ")" << endl << flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reservoirAlarm.GetLevel() == 1)
|
||||||
|
{
|
||||||
|
injectAccumulator += time_slice;
|
||||||
|
InjectCoolant(time_slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// InjectCoolant -- the coolant-flush distribution (binary @4aefa4). Walk the
|
||||||
|
// roster gathering the flush targets in the authentic pass order -- the
|
||||||
|
// condensers, then the weapons, then every heat sink, then the linked master
|
||||||
|
// (the duplicate visits are intentional weighting) -- and squirt
|
||||||
|
// (coolantSquirtMass x the target's coolantFlowScale x dt) into each,
|
||||||
|
// crediting a negative pending-heat chill for the moved mass. Squirts only
|
||||||
|
// ever leave the tank; the chill only ever cools.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
Reservoir::InjectCoolant(Scalar time_slice)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
if (fabs(coolantLevel) <= 1.0e-4f) // the tank is empty
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum { kMaxWork = 96 };
|
||||||
|
HeatSink *work[kMaxWork];
|
||||||
|
int workCount = 0;
|
||||||
|
Entity *own = (Entity *)owner;
|
||||||
|
int count = own->GetSubsystemCount();
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 2; i < count && workCount < kMaxWork; ++i)
|
||||||
|
{
|
||||||
|
Subsystem *s = own->GetSubsystem(i);
|
||||||
|
if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations))
|
||||||
|
{
|
||||||
|
work[workCount++] = (HeatSink *)s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 2; i < count && workCount < kMaxWork; ++i)
|
||||||
|
{
|
||||||
|
Subsystem *s = own->GetSubsystem(i);
|
||||||
|
if (s != NULL && s->IsDerivedFrom(MechWeapon::ClassDerivations))
|
||||||
|
{
|
||||||
|
work[workCount++] = (HeatSink *)s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 2; i < count && workCount < kMaxWork; ++i)
|
||||||
|
{
|
||||||
|
Subsystem *s = own->GetSubsystem(i);
|
||||||
|
if (s != NULL && s->IsDerivedFrom(HeatSink::ClassDerivations))
|
||||||
|
{
|
||||||
|
work[workCount++] = (HeatSink *)s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
HeatSink *link = (HeatSink *)linkedSinks.Resolve();
|
||||||
|
if (link != NULL && workCount < kMaxWork)
|
||||||
|
{
|
||||||
|
work[workCount++] = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int w = 0; w < workCount; ++w)
|
||||||
|
{
|
||||||
|
if (fabs(coolantLevel) <= 1.0e-4f) // ran dry mid-pass
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HeatSink *sink = work[w];
|
||||||
|
if (sink->coolantFlowScale == 0.0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0].
|
||||||
|
//
|
||||||
|
Scalar move = -coolantSquirtMass
|
||||||
|
* sink->coolantFlowScale
|
||||||
|
* time_slice;
|
||||||
|
Scalar lo = -coolantLevel;
|
||||||
|
if (move < lo) move = lo;
|
||||||
|
if (move > 0.0f) move = 0.0f;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The heat delta riding the moved mass (computed BEFORE the level
|
||||||
|
// updates, as in the binary).
|
||||||
|
//
|
||||||
|
Scalar den = (fabs(sink->coolantLevel) > 1.0e-4f)
|
||||||
|
? sink->coolantLevel
|
||||||
|
: sink->thermalCapacity;
|
||||||
|
Scalar moved = (move < 0.0f) ? -move : move;
|
||||||
|
Scalar fracSink = moved / den;
|
||||||
|
Scalar fracRes = moved / coolantLevel;
|
||||||
|
Scalar heatDelta =
|
||||||
|
sink->heatEnergy * fracSink
|
||||||
|
- heatEnergy * fracRes;
|
||||||
|
|
||||||
|
coolantLevel += move; // the reservoir drains (move <= 0)
|
||||||
|
sink->coolantLevel -= move; // the sink gains
|
||||||
|
if (sink->coolantLevel >= 0.0f)
|
||||||
|
{
|
||||||
|
if (sink->coolantLevel > sink->thermalCapacity)
|
||||||
|
{
|
||||||
|
sink->coolantLevel = sink->thermalCapacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sink->coolantLevel = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar cap = sink->thermalMass * startingTemperature;
|
||||||
|
if (heatDelta > cap)
|
||||||
|
{
|
||||||
|
heatDelta = cap;
|
||||||
|
}
|
||||||
|
Scalar chill = -heatDelta;
|
||||||
|
if (chill > 0.0f)
|
||||||
|
{
|
||||||
|
chill = 0.0f; // the flush only ever COOLS
|
||||||
|
}
|
||||||
|
sink->pendingHeat += chill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,104 @@
|
|||||||
|
//===========================================================================//
|
||||||
|
// File: reservr.hpp //
|
||||||
|
// Project: BattleTech Brick: Mech subsystems //
|
||||||
|
// Contents: Reservoir -- the coolant store //
|
||||||
|
//---------------------------------------------------------------------------//
|
||||||
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
||||||
|
// All Rights reserved worldwide //
|
||||||
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
||||||
|
//===========================================================================//
|
||||||
|
|
||||||
#if !defined(RESERVR_HPP)
|
#if !defined(RESERVR_HPP)
|
||||||
# define RESERVR_HPP
|
# define RESERVR_HPP
|
||||||
|
|
||||||
# if !defined(HEAT_HPP)
|
# if !defined(HEAT_HPP)
|
||||||
# include <heat.hpp>
|
# include <heat.hpp>
|
||||||
# endif
|
# endif
|
||||||
|
|
||||||
|
//##################### Forward Class Declarations #######################
|
||||||
class Mech;
|
class Mech;
|
||||||
struct Reservoir__SubsystemResource : public HeatSink::SubsystemResource {};
|
|
||||||
class Reservoir : public HeatSink
|
//###########################################################################
|
||||||
|
//#################### Reservoir Model Resource #########################
|
||||||
|
//###########################################################################
|
||||||
|
//
|
||||||
|
// CoolantCapacity overlays the inherited HeatSink thermalCapacity slot at
|
||||||
|
// construction; CoolantSquirtMass is the per-second flush rate base.
|
||||||
|
//
|
||||||
|
struct Reservoir__SubsystemResource:
|
||||||
|
public HeatSink::SubsystemResource
|
||||||
|
{
|
||||||
|
Scalar coolantCapacity;
|
||||||
|
Scalar coolantSquirtMass;
|
||||||
|
};
|
||||||
|
|
||||||
|
//###########################################################################
|
||||||
|
//############################# Reservoir ###############################
|
||||||
|
//###########################################################################
|
||||||
|
//
|
||||||
|
// The coolant store: a HeatSink that holds the mech's coolant charge and
|
||||||
|
// "squirts" it into the other sinks on demand -- it is the DrawCoolant SOURCE
|
||||||
|
// (every sink's UpdateCoolant top-up resolves here through the virtual), and
|
||||||
|
// the manual coolant FLUSH (the InjectCoolant cockpit button) distributes
|
||||||
|
// charge into the condensers / weapons / heatables, crediting each a negative
|
||||||
|
// pending-heat chill.
|
||||||
|
//
|
||||||
|
class Reservoir:
|
||||||
|
public HeatSink
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static Derivation ClassDerivations;
|
static Derivation ClassDerivations;
|
||||||
static SharedData DefaultData;
|
static SharedData DefaultData;
|
||||||
static Logical TestClass(Mech &);
|
|
||||||
Logical TestInstance() const;
|
static Logical
|
||||||
|
TestClass(Mech &);
|
||||||
|
Logical
|
||||||
|
TestInstance() const;
|
||||||
|
|
||||||
|
public:
|
||||||
typedef Reservoir__SubsystemResource SubsystemResource;
|
typedef Reservoir__SubsystemResource SubsystemResource;
|
||||||
Reservoir(Mech *owner, int subsystem_ID, SubsystemResource *r, SharedData &shared_data = DefaultData);
|
|
||||||
|
Reservoir(
|
||||||
|
Mech *owner,
|
||||||
|
int subsystem_ID,
|
||||||
|
SubsystemResource *r,
|
||||||
|
SharedData &shared_data = DefaultData
|
||||||
|
);
|
||||||
~Reservoir();
|
~Reservoir();
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// The coolant-store interface.
|
||||||
|
//
|
||||||
|
public:
|
||||||
|
//
|
||||||
|
// The DrawCoolant SOURCE: hand out up to coolantLevel of the requested
|
||||||
|
// amount and deduct it from the charge.
|
||||||
|
//
|
||||||
|
virtual Scalar
|
||||||
|
DrawCoolant(Scalar requested);
|
||||||
|
|
||||||
|
typedef void
|
||||||
|
(Reservoir::*Performance)(Scalar time_slice);
|
||||||
|
void
|
||||||
|
SetPerformance(Performance performance)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
activePerformance = (Simulation::Performance)performance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
CoolantSimulation(Scalar time_slice);
|
||||||
|
void
|
||||||
|
InjectCoolant(Scalar time_slice);
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Local Data. The inject flag is the reservoir alarm's level (1 = flushing).
|
||||||
|
//
|
||||||
|
protected:
|
||||||
|
AlarmIndicator reservoirAlarm;
|
||||||
|
Scalar coolantSquirtMass;
|
||||||
|
Scalar injectAccumulator;
|
||||||
|
Scalar squirtEfficiency;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user