Measured A/B/C (Blackhawk, sustained autofire, 150s soaks): SRM6 on
Condenser2 plateaus ~531 and COOLS at share 0.91 (boost 2), climbs to
the jam band at balanced (0.167), and runs at the 2000 failure line
starved (0.018). MoveValve reruns the redistribute every press;
veteran passes the novice guard. The field 'boosted loop 2 still
cooked' therefore means the valve was NOT at the 50 detent during the
fight -- the 1->5->50->0 cycle turns the loop OFF one press past max.
MoveValve presses now log always-on ('[valve] <name> -> detent N' +
a CLOSED warning) so the next field session records the detents.
Also: solo mission clock verified live (60s egg self-ends,
'[mission] solo game clock expired' -> RunMissions returns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1371 lines
53 KiB
C++
1371 lines
53 KiB
C++
//===========================================================================//
|
|
// File: heatfamily_reslice.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Re-slice -- Condenser completion + HeatWatcher / Reservoir / //
|
|
// aggregate-HeatSink bodies recovered from the heat-family gap. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from heatfamily_gap.c (Ghidra pseudo-C, 0x4ae4d8-0x4afbe0).
|
|
// Names follow heat.cpp + the surviving DATA-section strings. Each method
|
|
// cites its @ADDR. Helper-function mapping is identical to heat.cpp; the
|
|
// additions used here:
|
|
// FUN_004adda0 HeatSink base constructor (heat.cpp @004adda0)
|
|
// FUN_004adfd4 ~HeatSink (heat.cpp @004adfd4)
|
|
// FUN_004ad748 HeatSink simulation step (vtbl slot9)
|
|
// FUN_004ad7d4 HeatModelActive()
|
|
// FUN_004ad7f0 HeatSink::UpdateHeatLoad
|
|
// FUN_004ac644 HeatableSubsystem base constructor (heat.cpp)
|
|
// FUN_004ac0bc Subsystem slot-9 base impl
|
|
// FUN_004ac22c Subsystem::ResetToInitialState
|
|
// FUN_004ae150 HeatSink::CreateStreamedSubsystem (heat.cpp)
|
|
// FUN_004ac9ec HeatableSubsystem::CreateStreamedSubsystem
|
|
// FUN_004ae150/004040d8/00404118/00404088 NotationFile reads
|
|
// FUN_00417ab4 SharedData::Resolve()
|
|
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041bbd8 AlarmIndicator::SetLevel
|
|
// FUN_004dca38 expf() FUN_004dcd00 fabsf()
|
|
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(HEAT_HPP)
|
|
# include <heat.hpp>
|
|
#endif
|
|
#if !defined(HEATFAMILY_RESLICE_HPP)
|
|
# include <heatfamily_reslice.hpp>
|
|
# include <app.hpp> // application (BT_HEAT_LOG census)
|
|
#endif
|
|
|
|
#define JM_CLOSE_ENOUGH 0.0001f // _DAT_004ae8ac / _DAT_004af3a0
|
|
|
|
//
|
|
// Recovered .data constants (best-effort; exact double bit-patterns folded to
|
|
// decimal where known). TODO: confirm against section_dump.txt.
|
|
//
|
|
static const Scalar CoolantCapacityScale = 0.05f; // _DAT_004af518 (float80, byte-verified
|
|
// task #9; both prior readings -- 1.738
|
|
// and the audit's 1.675 -- were misreads)
|
|
|
|
// FUN_0049f788 -- defined near the foot of this file; forward-declared for MoveValve.
|
|
void BTRecomputeCondenserValves(Entity *owner);
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// Condenser (COMPLETION of heat.cpp best-effort)
|
|
//###########################################################################
|
|
//###########################################################################
|
|
//
|
|
// vtable @0050ed88 ctor @4ae568 dtor @4ae5fc classID 0x0BBD : HeatSink
|
|
//
|
|
|
|
//
|
|
// @4ae568 -- chains the HeatSink base ctor, installs the Condenser vtable,
|
|
// captures the refrigeration factor, opens the valve, and derives the
|
|
// condenser number from the trailing digit of the instance name.
|
|
//
|
|
Condenser::Condenser(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004adda0(...,&DAT_0050e4ec,0,0)
|
|
condenserAlarm(3) // FUN_0041b9ec(this+0x77, 3)
|
|
{
|
|
// *this = &PTR_FUN_0050ed88;
|
|
valveState = 1; // this[0x74] @0x1D0
|
|
coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57")
|
|
massScale = subsystem_resource->refrigerationFactor; // this[0x58] @0x160 (refrigeration output reuses massScale)
|
|
refrigerationFactor = massScale; // this[0x76] @0x1D8
|
|
simulationFlags |= 0x8; // this[10] |= 8 (participates in heat model)
|
|
|
|
// condenserNumber = atoi(lastChar(name)):
|
|
// pcVar1 = GetName(); this[0x35]
|
|
// iVar2 = strlen(pcVar1); FUN_004d4a78
|
|
// condenserNumber = atoi(pcVar1+iVar2-1); FUN_004dd1e0 (this[0x75] @0x1D4)
|
|
condenserNumber = NameTrailingNumber(GetName());
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @4ae5fc
|
|
//
|
|
Condenser::~Condenser() // FUN_004ae5fc(this, byte deleteFlag)
|
|
{
|
|
Check(this);
|
|
// condenserAlarm (@0x77) and the HeatSink base chain are torn down
|
|
// automatically; the decompiler's explicit member/base dtor calls are
|
|
// compiler-generated artifacts and are intentionally not reproduced.
|
|
Check_Fpu();
|
|
}
|
|
|
|
Logical
|
|
Condenser::TestInstance() const // @4ae63c
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x50e4fc)
|
|
}
|
|
|
|
Logical
|
|
Condenser::TestClass(Mech &)
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//
|
|
// @4ae4d8 -- vtable slot 9 (the per-frame step). Computes the refrigeration
|
|
// output as a function of how much heat the master heat-sink has stored, then
|
|
// runs the base HeatSink step.
|
|
//
|
|
// refrigerationOutput = (1.0f - master->heatEnergy) * refrigerationFactor;
|
|
// if (refrigerationOutput < 1.0f) refrigerationOutput = 1.0f; // clamp
|
|
// HeatSink::Step(time_slice); // FUN_004ad748
|
|
//
|
|
void
|
|
Condenser::RefrigerationSimulation(Scalar time_slice) // FUN_004ae4d8
|
|
{
|
|
// AUTHENTIC (heatmodel decode): massScale = clamp>=1( (1 - ownDamageZone.damageLevel)
|
|
// * refrigerationFactor ). The binary reads *(this[0x38]+0x158) = this condenser's
|
|
// OWN DamageZone.damageLevel (the engine base zone @0xE0, word 0x38): undamaged 0 ->
|
|
// massScale = refrigerationFactor (3.0), degrading toward 1.0 as the condenser is
|
|
// damaged. The earlier recon read linkedSinks->heatEnergy (~1.3e7) -> (1 - 1.3e7)
|
|
// clamps massScale permanently to 1.0 (minimum refrigeration) AND null-derefs a
|
|
// Condenser whose linkedSinks is skipped (the ctor guard).
|
|
::DamageZone *ownZone = this->Subsystem::damageZone; // @0xE0 (word 0x38)
|
|
Scalar zoneDamage = (ownZone != 0) ? ownZone->damageLevel : 0.0f; // +0x158
|
|
massScale = // refrigeration output reuses massScale @0x160 (word 0x58)
|
|
(1.0f - zoneDamage) // _DAT_004ae530 = 1.0f ; own DamageZone +0x158
|
|
* refrigerationFactor; // this[0x76]
|
|
if (massScale < 1.0f) // _DAT_004ae530
|
|
{
|
|
massScale = 1.0f;
|
|
}
|
|
HeatSink_Step(time_slice); // FUN_004ad748(this, time_slice)
|
|
}
|
|
|
|
//
|
|
// @4ae464 -- the "MoveValve" message handler (id 4, table @0x50E52C [T1]).
|
|
// Cycles the coolant valve through its FOUR settings 1 -> 5 -> 50 -> 0 -> 1,
|
|
// then redistributes coolant flow across all condensers
|
|
// (RecomputeCondenserValves) so every ValveSetting gauge updates.
|
|
//
|
|
// task #13 -- ROUTE + GUARD LANDED (the old deferral note claimed the guard
|
|
// was the 0xBD3 messmgr; task #12 proved FUN_004ac9c8 reads mech+0x190 ->
|
|
// the owning BTPlayer's roleClassIndex(+0x274) == 0 -- the ROOKIE-role
|
|
// lockout, implemented as the BTPlayerRoleLocksAdvanced bridge). Registered
|
|
// under id 4 via Condenser::GetMessageHandlers(); the pod route is the
|
|
// engineering-screen aux buttons (type-6 EventMappings), the desktop route
|
|
// is the 'C' key (mech4 harness).
|
|
//
|
|
void
|
|
Condenser::MoveValveMessageHandler(ReceiverDataMessageOf<int> *message) // FUN_004ae464
|
|
{
|
|
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
|
|
if (BTPlayerRoleLocksAdvanced(owner))
|
|
return;
|
|
if (message->dataContents <= 0) // press only (*(int*)(msg+0xc))
|
|
return;
|
|
|
|
switch (valveState) // this[0x74] @0x1D0
|
|
{
|
|
case 0: valveState = 1; break; // @4ae499
|
|
case 1: valveState = 5; break; // @4ae4a5
|
|
case 5: valveState = 50; break; // @4ae4b1 (0x32)
|
|
case 50: valveState = 0; break; // @4ae4bd -- one past max = CLOSED
|
|
default: return; // @4ae497 unknown -> no change / no recompute
|
|
}
|
|
// ALWAYS log a valve press (rare + forensic -- the 2026-07-23 "boosted loop
|
|
// still cooked" field case could not be reconstructed without the detents)
|
|
DEBUG_STREAM << "[valve] " << GetName() << " valve -> detent " << valveState
|
|
<< (valveState == 0 ? " (CLOSED -- this loop is now UNCOOLED)" : "")
|
|
<< "\n" << std::flush;
|
|
BTRecomputeCondenserValves((Entity *)owner); // FUN_0049f788(this[0xd0]=owner Mech)
|
|
}
|
|
|
|
//
|
|
// task #13 -- the Condenser handler registration (table @0x50E52C: exactly
|
|
// one entry). Chained onto the engine Receiver root set; function-local
|
|
// statics per the static-init-order rule (reconstruction-gotchas #9).
|
|
//
|
|
Receiver::MessageHandlerSet&
|
|
Condenser::GetMessageHandlers()
|
|
{
|
|
static const Receiver::HandlerEntry entries[]=
|
|
{
|
|
MESSAGE_ENTRY(Condenser, MoveValve) // id 4 @4ae464
|
|
};
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(entries), entries,
|
|
HeatSink::GetMessageHandlers() // chain via HeatSink so id 3 ToggleCooling reaches condensers too
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
//
|
|
// @004ad6f8 -- the "ToggleCooling" message handler (binary table @0x50E41C id 3;
|
|
// the Eng-page "Coolant" button on every weapon/avionics). DISASSEMBLED
|
|
// 2026-07-20 (tools/disas2.py 0x4ad6f8): NOVICE-locked (the SAME guard MoveValve
|
|
// uses -- owner->BTPlayer->roleClassIndex(+0x274)==0), press only, then TOGGLE
|
|
// this heat sink's coolant flow: coolantAvailable(+0x134) 0<->1 and
|
|
// coolantFlowScale(+0x15C) 0.0f<->1.0f. Cutting a system's cooling frees the
|
|
// shared loop for the rest -- the "coolant priority" Lynx described (the effect;
|
|
// the mechanism is a per-subsystem on/off toggle). Reconstructed at HeatSink
|
|
// (the abstract HeatableSubsystem never instantiates; every concrete heatable
|
|
// subsystem is a HeatSink, where coolantAvailable/coolantFlowScale live) --
|
|
// identical coverage to the binary's abstract-base registration, no downcast.
|
|
//
|
|
void
|
|
HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf<int> *message) // FUN_004ad6f8
|
|
{
|
|
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
|
|
int coolLog = getenv("BT_COOL_LOG") ? 1 : 0;
|
|
if (coolLog) // proves the dispatch chain REACHED id 3
|
|
DEBUG_STREAM << "[cool] " << GetName() << " ToggleCooling reached (data="
|
|
<< message->dataContents << ")\n" << std::flush;
|
|
if (BTPlayerRoleLocksAdvanced(owner)) // @4ad700 novice lockout
|
|
{
|
|
if (coolLog) DEBUG_STREAM << "[cool] -> NOVICE-locked, no toggle\n" << std::flush;
|
|
return;
|
|
}
|
|
if (message->dataContents <= 0) // @4ad70d press only (*(int*)(msg+0xc))
|
|
return;
|
|
if (coolantAvailable != 0) // @4ad714 cooling ON -> OFF
|
|
{
|
|
coolantAvailable = 0; // @4ad722 [this+0x134]
|
|
coolantFlowScale = 0.0f; // @4ad728 [this+0x15c]
|
|
}
|
|
else // @4ad731 OFF -> ON
|
|
{
|
|
coolantAvailable = 1; // [this+0x134] = 1
|
|
coolantFlowScale = 1.0f; // [this+0x15c] = 0x3f800000
|
|
}
|
|
if (coolLog)
|
|
DEBUG_STREAM << "[cool] -> " << GetName() << " coolant "
|
|
<< (coolantAvailable ? "ON (flow 1.0)" : "OFF (flow 0.0)") << "\n" << std::flush;
|
|
}
|
|
|
|
//
|
|
// HeatSink handler registration: {3, ToggleCooling}. The binary put id 3 on the
|
|
// abstract HeatableSubsystem table; we put it on HeatSink (same reach -- every
|
|
// concrete heatable subsystem is a HeatSink). Chained onto the Receiver root
|
|
// (HeatableSubsystem/MechSubsystem carry no own handlers). PoweredSubsystem
|
|
// (weapons/avionics) chains onto THIS so a weapon's dispatch reaches id 3 --
|
|
// restoring the base-table reach the old PoweredSubsystem->Receiver-root
|
|
// shortcut skipped. Function-local statics (static-init-order rule).
|
|
//
|
|
Receiver::MessageHandlerSet&
|
|
HeatSink::GetMessageHandlers()
|
|
{
|
|
static const Receiver::HandlerEntry entries[]=
|
|
{
|
|
MESSAGE_ENTRY(HeatSink, ToggleCooling) // id 3 @004ad6f8
|
|
};
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(entries), entries,
|
|
Receiver::GetMessageHandlers()
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
//
|
|
// @4ae658 -- parse the Condenser resource record. Stamps classID 0x0BBD and
|
|
// model size 0x100, defaults RefrigerationFactor to -1.0f on pass 1, then
|
|
// requires "RefrigerationFactor".
|
|
//
|
|
int
|
|
Condenser::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
if (!HeatSink::CreateStreamedSubsystem( // FUN_004ae150
|
|
model_file, model_name, subsystem_name,
|
|
subsystem_resource, subsystem_file, directories, passes))
|
|
{
|
|
return False;
|
|
}
|
|
|
|
subsystem_resource->classID = RegisteredClass::CondenserClassID; // res +0x20
|
|
subsystem_resource->subsystemModelSize = 0x100; // res +0x24
|
|
|
|
if (passes == 1)
|
|
{
|
|
subsystem_resource->refrigerationFactor = -1.0f; // res +0xFC, bytes bf 80 00 00
|
|
}
|
|
|
|
if (!model_file->GetEntry(subsystem_name, "RefrigerationFactor",
|
|
&subsystem_resource->refrigerationFactor)
|
|
&& subsystem_resource->refrigerationFactor == -1.0f) // _DAT_004ae708
|
|
{
|
|
DebugStream << subsystem_name << " missing RefrigerationFactor!";
|
|
return False;
|
|
}
|
|
|
|
Check_Fpu();
|
|
return True;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// HeatWatcher (base of powersub's PowerWatcher)
|
|
//###########################################################################
|
|
//###########################################################################
|
|
//
|
|
// vtable @0050ed10 ctor @4aeb40 dtor @4aebe8 classID HeatWatcherClassID
|
|
// : HeatableSubsystem (PowerWatcher @4b18a4 chains to this ctor)
|
|
//
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
HeatWatcher::SharedData
|
|
HeatWatcher::DefaultData(
|
|
HeatWatcher::GetClassDerivations(),
|
|
HeatWatcher::GetMessageHandlers(),
|
|
HeatWatcher::GetAttributeIndex(),
|
|
HeatWatcher::StateCount
|
|
);
|
|
|
|
Derivation*
|
|
HeatWatcher::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "HeatWatcher");
|
|
return &classDerivations;
|
|
}
|
|
|
|
//
|
|
// @4aeb40 -- "PowerWatcher base ctor". Builds on the HeatableSubsystem base,
|
|
// installs the HeatWatcher vtable, captures the degradation/failure setpoints
|
|
// and the watched-subsystem index, builds the 3-level alarm, and (for a master
|
|
// instance) registers the WatchSimulation performance.
|
|
//
|
|
HeatWatcher::HeatWatcher(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
MechSubsystem(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004ac644(...,0,0) -- MechSubsystem base
|
|
watchedLink() // FUN_004af9cf(this+0x45, 0) (vtbl 0050eccc)
|
|
{
|
|
// *this = &PTR_FUN_0050ed10;
|
|
degradationTemperature = subsystem_resource->degradationTemperature; // this[0x48] = res +0xE8
|
|
failureTemperature = subsystem_resource->failureTemperature; // this[0x49] = res +0xEC
|
|
watchedSubsystem = subsystem_resource->watchedSubsystem; // this[0x4a] = res +0xE4
|
|
heatAlarm.Initialize(3); // FUN_0041b9ec(this+0x4b, 3)
|
|
|
|
// (Gitea #7 sweep) the binary @4aeb40 tests *(param_2+0x28) = the OWNER
|
|
// MECH's simulationFlags (the same misread fixed in the Reservoir ctor;
|
|
// the watcher happened to work because its resource flags mirror the
|
|
// master bits on the player mech -- read the authentic source anyway).
|
|
if ((owner->simulationFlags & SegmentCopyMask) == 0 // (flags & 0xC) == 0
|
|
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0) // flags & 0x100
|
|
{
|
|
SetPerformance(&HeatWatcher::WatchSimulation); // this[7..9] = PTR 0050e634 -> @4aeac4
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @4aebe8
|
|
//
|
|
HeatWatcher::~HeatWatcher() // FUN_004aebe8
|
|
{
|
|
Check(this);
|
|
// heatAlarm (@0x4b) / watchedLink (@0x45) and the HeatableSubsystem base
|
|
// chain destruct automatically; the decompiler's explicit dtor calls are
|
|
// compiler-generated artifacts and are not reproduced.
|
|
Check_Fpu();
|
|
}
|
|
|
|
Logical HeatWatcher::TestInstance() const // @4aec38 -> IsDerivedFrom 0x50e604
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|
|
Logical HeatWatcher::TestClass(Mech &) { return True; }
|
|
|
|
//
|
|
// @4aea84 -- slot 9 (base passthrough). @4aea9c -- slot 10 ResetToInitialState.
|
|
//
|
|
void HeatWatcher::ResetToInitialState(Logical /*powered*/) // @4aea9c
|
|
{
|
|
MechSubsystem::ResetToInitialState(True); // FUN_004ac22c (base is MechSubsystem)
|
|
heatAlarm.SetLevel(0); // FUN_0041bbd8(this+0x4b, 0)
|
|
}
|
|
|
|
//
|
|
// @4aeac4 -- the registered Performance. Resolve the watched subsystem, read
|
|
// its currentTemperature (+0x114) and drive the 3-level alarm.
|
|
//
|
|
void
|
|
HeatWatcher::WatchSimulation(Scalar /*time_slice*/) // FUN_004aeac4
|
|
{
|
|
HeatableSubsystem *watched = (HeatableSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
|
|
|
|
// The watch link is now BOUND by the authentic factory connect pass (task
|
|
// #57: the mech factory's post-roster loop calls the binary's vtable slot
|
|
// +0x38 -- FUN_004aee2c / FUN_004b1a40 -> BTWatcherBindTarget below), so
|
|
// Resolve() succeeds on every master-node watcher whose resource named a
|
|
// WatchedSubsystem. The binary derefs `watched` unconditionally; the null
|
|
// guard stays for replicant nodes (the binary never binds there either --
|
|
// the +0x38 body is master-gated -- and their watcher Performances don't
|
|
// run) and for bring-up safety.
|
|
if (watched == 0)
|
|
{
|
|
heatAlarm.SetLevel(0); // NormalHeat (no watched source yet)
|
|
return;
|
|
}
|
|
|
|
Scalar temp = watched->currentTemperature; // *(watched + 0x114)
|
|
|
|
if (temp > failureTemperature) // > this+0x124
|
|
{
|
|
heatAlarm.SetLevel(2); // FailureHeat
|
|
}
|
|
else if (temp > degradationTemperature) // > this+0x120
|
|
{
|
|
heatAlarm.SetLevel(1); // DegradationHeat
|
|
}
|
|
else
|
|
{
|
|
heatAlarm.SetLevel(0); // NormalHeat
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004aee2c (HeatWatcher vtable slot 14, offset +0x38; the PowerWatcher/Torso
|
|
// override @004b1a40 is byte-identical) -- the factory post-roster CONNECT
|
|
// pass, disassembled from the raw binary (Ghidra missed both function starts):
|
|
//
|
|
// owner = this->owner (+0xD0)
|
|
// if ((owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100))
|
|
// watchedLink(+0x114).Add( owner->roster(+0x128)[ watchedSubsystem(+0x128) ] )
|
|
//
|
|
// The mech factory loop tests IsDerivedFrom(HeatWatcher @0x50e604) then calls
|
|
// the slot. Split across the TU boundary the same way as the other
|
|
// cross-family helpers: the OWNER-side master gate + roster lookup live in the
|
|
// mech family (complete Mech type); these two bridges are the family-side
|
|
// derivation test and the link Add. [T1: bytes @004aee2c/@004b1a40]
|
|
//
|
|
int
|
|
BTWatcherWatchedIndex(Subsystem *sub) // -1 = not a HeatWatcher
|
|
{
|
|
if (sub == 0 || !sub->IsDerivedFrom(*HeatWatcher::GetClassDerivations()))
|
|
{
|
|
return -1;
|
|
}
|
|
return ((HeatWatcher *)sub)->watchedSubsystem; // @0x128 (resource +0xE4, name index +2)
|
|
}
|
|
|
|
void
|
|
BTWatcherBindTarget(Subsystem *sub, Subsystem *target)
|
|
{
|
|
((HeatWatcher *)sub)->watchedLink.Add(target); // (**(link+4))(link, target) = FUN_00417a80
|
|
}
|
|
|
|
//
|
|
// Cross-family derivation probe: 0x50e604 is the HEATWATCHER derivation tag
|
|
// (TestInstance @4aec38 checks its own class against it) -- NOT HeatSink, as
|
|
// an old btl4gaug label claimed (HeatSink : HeatableSubsystem would make an
|
|
// OR-test after HeatableSubsystem redundant; the binary's alternate branch is
|
|
// the disjoint watcher family). Bridged so gauge TUs can test without
|
|
// including this family's headers.
|
|
//
|
|
int
|
|
BTIsHeatWatcher(Subsystem *sub)
|
|
{
|
|
return sub != 0 && sub->IsDerivedFrom(*HeatWatcher::GetClassDerivations());
|
|
}
|
|
|
|
//
|
|
// @4aec54 (468 bytes) -- parse the HeatWatcher resource. Stamps classID 0x0BBF
|
|
// / size 0xF0; defaults the watched index to -1; requires DegradationTemperature,
|
|
// FailureTemperature and WatchedSubsystem (resolved name -> segment index +2).
|
|
//
|
|
int
|
|
HeatWatcher::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
if (!MechSubsystem::CreateStreamedSubsystem( // FUN_004ac9ec -- MechSubsystem parse (0x30..0xE4)
|
|
model_file, model_name, subsystem_name,
|
|
subsystem_resource, subsystem_file, directories, passes))
|
|
{
|
|
return False;
|
|
}
|
|
|
|
subsystem_resource->classID = RegisteredClass::HeatWatcherClassID; // res +0x20
|
|
subsystem_resource->subsystemModelSize = 0xF0; // res +0x24 (bytes f0)
|
|
|
|
if (passes == 1)
|
|
{
|
|
subsystem_resource->watchedSubsystem = -1; // res +0xE4
|
|
}
|
|
|
|
if (!model_file->GetEntry(subsystem_name, "DegradationTemperature",
|
|
&subsystem_resource->degradationTemperature) // res +0xE8
|
|
&& subsystem_resource->degradationTemperature == -1.0f) // _DAT_004aee28
|
|
{
|
|
DebugStream << subsystem_name << " missing DegradationTemperature!";
|
|
return False;
|
|
}
|
|
|
|
if (!model_file->GetEntry(subsystem_name, "FailureTemperature",
|
|
&subsystem_resource->failureTemperature) // res +0xEC
|
|
&& subsystem_resource->failureTemperature == -1.0f)
|
|
{
|
|
DebugStream << subsystem_name << " missing FailureTemperature!";
|
|
return False;
|
|
}
|
|
|
|
const char *watched = "Unspecified";
|
|
int found = model_file->GetEntry(subsystem_name, "WatchedSubsystem", &watched);
|
|
if (!found && subsystem_resource->watchedSubsystem == -1)
|
|
{
|
|
DebugStream << subsystem_name << " missing WatchedSubsystem!";
|
|
return False;
|
|
}
|
|
if (strcmp(watched, "Unspecified") != 0)
|
|
{
|
|
subsystem_resource->watchedSubsystem =
|
|
Get_Segment_Index(model_file, model_name, directories, watched); // FUN_004215b0
|
|
}
|
|
if (subsystem_resource->watchedSubsystem < 0)
|
|
{
|
|
DebugStream << subsystem_name << " has an invalid watched subsystem!";
|
|
return False;
|
|
}
|
|
subsystem_resource->watchedSubsystem += 2; // +2 bias
|
|
Check_Fpu();
|
|
return True;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// Reservoir
|
|
//###########################################################################
|
|
//###########################################################################
|
|
//
|
|
// vtable @0050ecd4/ecd8 ctor @4af408 classID ReservoirClassID : HeatSink
|
|
//
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
// ReservoirState -> reservoirAlarm (@0x1D0). Chained to HeatSink so the inherited
|
|
// coolant/temp attrs stay reachable; dense from HeatSink::NextAttributeID.
|
|
const Reservoir::IndexEntry
|
|
Reservoir::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(Reservoir, ReservoirState, reservoirAlarm) // @0x1D0 (0x54 StateIndicator-compatible alarm)
|
|
};
|
|
|
|
Reservoir::AttributeIndexSet&
|
|
Reservoir::GetAttributeIndex()
|
|
{
|
|
static Reservoir::AttributeIndexSet attributeIndex(
|
|
ELEMENTS(Reservoir::AttributePointers),
|
|
Reservoir::AttributePointers,
|
|
HeatSink::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
Reservoir::SharedData
|
|
Reservoir::DefaultData(
|
|
Reservoir::GetClassDerivations(),
|
|
Reservoir::GetMessageHandlers(),
|
|
Reservoir::GetAttributeIndex(),
|
|
Reservoir::StateCount
|
|
);
|
|
|
|
Derivation*
|
|
Reservoir::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(HeatSink::GetClassDerivations(), "Reservoir");
|
|
return &classDerivations;
|
|
}
|
|
|
|
//
|
|
// @4af408 -- HeatSink base + reservoir charge. CoolantCapacity overlays the
|
|
// HeatSink thermalCapacity slot; coolantLevel starts full. A master reservoir
|
|
// scales its capacity by the linked sink's +0x1D0 field and registers the
|
|
// CoolantSimulation performance; a copy just flags itself inactive (|2).
|
|
//
|
|
Reservoir::Reservoir(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004adda0(...,param_5,0,0)
|
|
reservoirAlarm(2) // FUN_0041b9ec(this+0x74, 2)
|
|
{
|
|
// *this = &PTR_LAB_0050ecd4;
|
|
squirtEfficiency = 0.5f; // this[0x8b] @0x22C
|
|
thermalCapacity = subsystem_resource->coolantCapacity; // this[0x4a] @0x128 (coolantCapacity reuses thermalCapacity)
|
|
|
|
// SPEC AUDIT (BT_SPEC_LOG): streamed CoolantCapacity vs the manual's
|
|
// "Reservoir Size (Liters)".
|
|
if (getenv("BT_SPEC_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[spec] reservoir: coolantCapacity="
|
|
<< subsystem_resource->coolantCapacity << "\n" << std::flush;
|
|
}
|
|
coolantSquirtMass = subsystem_resource->coolantSquirtMass; // this[0x89] @0x224 = res +0x100
|
|
coolantLevel = thermalCapacity; // this[0x4b] @0x12C
|
|
coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57")
|
|
reservoirAlarm.SetLevel(0); // FUN_0041bbd8(this+0x74, 0) -- inject flag = 0 (was redundant injectActive=0)
|
|
|
|
// Gitea #7 gate correction: the binary @4af408 tests *(param_2+0x28) --
|
|
// param_2 is the OWNER MECH (the factory passes the mech as arg 2), so
|
|
// this is owner->simulationFlags, NOT the resource's subsystemFlags (the
|
|
// old resource read was false on the player mech -> no CoolantSimulation
|
|
// Performance -> the flush never drained). Same gate the AggregateHeatSink
|
|
// ctor (@4ae8d0, below) already reads correctly. [T1]
|
|
if ((owner->simulationFlags & SegmentCopyMask) == 0
|
|
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0)
|
|
{
|
|
SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78
|
|
HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59)
|
|
link->Attach(this); // (**(link+0x1d8+4))(link+0x1d8, this)
|
|
// Gitea #7 decode correction: the binary loads `*(int *)(link+0x1d0)`
|
|
// and FILDs it -- `(float10)*(int *)(iVar1 + 0x1d0)` @4af408 -- i.e.
|
|
// the master bank's INTEGER HeatSinkCount (AggregateHeatSink @0x1D0),
|
|
// not a float scale word. CoolantCapacity = 0.05 x heatSinkCount x
|
|
// streamed capacity (MadCat: 0.05 x 14 x 3800 = 2660). The old raw
|
|
// FLOAT reinterpret read the int bits as ~1e-44 -> capacity ~0 (latent
|
|
// while the master-gate bug kept this branch dead). The master's
|
|
// dynamic subclass isn't known statically here, so the read stays a
|
|
// guarded raw offset -- but of the documented int slot.
|
|
Scalar masterScale = (Scalar)*(int *)((char *)link + 0x1d0); // FILD heatSinkCount
|
|
thermalCapacity = CoolantCapacityScale * masterScale * thermalCapacity; // scale by master (coolantCapacity reuses thermalCapacity)
|
|
coolantLevel = thermalCapacity;
|
|
}
|
|
else
|
|
{
|
|
simulationFlags |= 2; // this[10] |= 2 (inactive copy)
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
Logical Reservoir::TestInstance() const // @4af564 -> IsDerivedFrom 0x50e650
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|
|
Logical Reservoir::TestClass(Mech &) { return True; }
|
|
|
|
//
|
|
// @4aee70 -- the "InjectCoolant" (COOLANT FLUSH) message handler, registered
|
|
// under id 4 in the Reservoir handler table @0x50e680 (one entry, name string
|
|
// @0x50eba5 "InjectCoolant") [T1]. Gitea #7.
|
|
//
|
|
// Decode (task-#7-flush correction of the old best-effort body): the binary
|
|
// bVar1 = FUN_004ac9c8(this); // the novice lockout gate (the
|
|
// // same gate MoveValve uses; reads
|
|
// // owning BTPlayer @mech+0x190,
|
|
// // +0x274 experience level == 0)
|
|
// if (!bVar1) {
|
|
// if (*(int*)(msg+0xc) < 1) // RELEASE -> stop the flush
|
|
// SetLevel(alarm@0x1d0, 0);
|
|
// else { // PRESS (or hold)
|
|
// if (level@0x1e4 != 1 && _DAT_004aeedc(=0.0f) < *(float*)(this+0x12C))
|
|
// SetLevel(alarm@0x1d0, 1); // ... gate is +0x12C = COOLANT-
|
|
// *(this+0x228) = 0; // LEVEL (the old recon misread it
|
|
// } // as currentTemperature@0x114:
|
|
// *(ushort*)(this+0x18) |= 1; // can't flush an EMPTY tank)
|
|
// }
|
|
// While the alarm is 1 the CoolantSimulation Performance runs InjectCoolant
|
|
// every frame (the drain the coolant gauge reads); the audio watcher bound to
|
|
// ReservoirState fires the CoolantDump layers on the 0/1 transitions, and the
|
|
// view side spawns the condensation cloud (flush.pfx, psfx 19 -- BTDPL.INI
|
|
// "Coolant flush") on the 0->1 edge, matching the binary's mode-1
|
|
// BTTracerEffectRenderable (built for classID 0xBC0 on the "ReservoirState"
|
|
// attr, part_014.c:5439; its tick @part_007.c:8780 starts the pfx exactly
|
|
// when the watched state CHANGES to 1).
|
|
//
|
|
void
|
|
Reservoir::InjectCoolantMessageHandler(ReceiverDataMessageOf<int> *message) // FUN_004aee70
|
|
{
|
|
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
|
|
if (BTPlayerRoleLocksAdvanced(owner))
|
|
return;
|
|
if (message->dataContents < 1) // *(int*)(msg+0xc) < 1 = release
|
|
{
|
|
reservoirAlarm.SetLevel(0); // this+0x1d0 -- flush OFF
|
|
}
|
|
else
|
|
{
|
|
if (reservoirAlarm.GetLevel() != 1 // injectActive == alarm level @0x1e4
|
|
&& coolantLevel > 0.0f) // _DAT_004aeedc = 0.0f; this+0x12C
|
|
{
|
|
reservoirAlarm.SetLevel(1); // flush ON
|
|
// The binary's flush-cloud renderable spawns on this 0->1 edge
|
|
// (see the header comment); the port's effect renderables are
|
|
// consolidated in the psfx layer, so the spawn hook rides the
|
|
// same transition here.
|
|
extern void BTSpawnFlushCloud(void *owner_mech); // mech4.cpp (psfx 19)
|
|
BTSpawnFlushCloud(owner);
|
|
}
|
|
injectAccumulator = 0; // this+0x228
|
|
}
|
|
ForceUpdate(); // this+0x18 |= 1
|
|
}
|
|
|
|
//
|
|
// Gitea #7 -- the Reservoir handler registration (table @0x50e680: exactly
|
|
// one entry {4, "InjectCoolant", @4aee70}). Chained onto the engine Receiver
|
|
// root set exactly like the Condenser's (task #13); function-local statics
|
|
// per the static-init-order rule (reconstruction-gotchas #9).
|
|
//
|
|
Receiver::MessageHandlerSet&
|
|
Reservoir::GetMessageHandlers()
|
|
{
|
|
static const Receiver::HandlerEntry entries[]=
|
|
{
|
|
MESSAGE_ENTRY(Reservoir, InjectCoolant) // id 4 @4aee70
|
|
};
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(entries), entries,
|
|
HeatSink::GetMessageHandlers() // chain via HeatSink so id 3 ToggleCooling reaches the reservoir too
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
//
|
|
// @4aef78 -- registered Performance. While injection is active, accumulate
|
|
// elapsed time and run the coolant distribution.
|
|
//
|
|
void
|
|
Reservoir::CoolantSimulation(Scalar time_slice) // FUN_004aef78
|
|
{
|
|
if (reservoirAlarm.GetLevel() == 1) // injectActive == alarm level @0x1e4
|
|
{
|
|
injectAccumulator += time_slice; // this+0x228
|
|
|
|
// DIAG (BT_FLUSH_LOG, Gitea #7): the reservoir drain the coolant
|
|
// gauge reads (vertBar C binds Reservoir/CoolantMass = coolantLevel).
|
|
if (getenv("BT_FLUSH_LOG"))
|
|
{
|
|
static Scalar s_flAcc = 1.0e9f; // log the first active frame too
|
|
s_flAcc += time_slice;
|
|
if (s_flAcc >= 0.5f)
|
|
{
|
|
s_flAcc = 0.0f;
|
|
DEBUG_STREAM << "[flush] " << GetName()
|
|
<< " level=" << coolantLevel << "/" << thermalCapacity
|
|
<< " squirtMass=" << coolantSquirtMass
|
|
<< " held=" << injectAccumulator << "s\n" << std::flush;
|
|
}
|
|
}
|
|
InjectCoolant(time_slice); // FUN_004aefa4
|
|
}
|
|
}
|
|
|
|
//
|
|
// @4af3b0 -- slot-14 DrawCoolant SOURCE. Hand out up to coolantLevel of the
|
|
// requested amount and deduct it from the charge.
|
|
//
|
|
Scalar
|
|
Reservoir::DrawCoolant(Scalar requested) // FUN_004af3b0
|
|
{
|
|
Scalar supplied = 0.0f; // _DAT_004af404
|
|
if (requested >= 0.0f)
|
|
{
|
|
supplied = requested;
|
|
if (coolantLevel < requested) // this+0x12c
|
|
{
|
|
supplied = coolantLevel;
|
|
}
|
|
}
|
|
coolantLevel -= supplied;
|
|
return supplied;
|
|
}
|
|
|
|
//
|
|
// @4aefa4 (1019 bytes) -- the COOLANT FLUSH proper (Gitea #7; was a stub that
|
|
// did NOTHING -- the reported "gauge doesn't show a drop"). Distribute the
|
|
// reservoir charge across the mech's heat-sink network: build a work list
|
|
// from the owner's three capability chains -- condensers @+0x7cc (GUID
|
|
// 0x50e4fc), weapons @+0x7bc (0x511830 = MechWeapon), heatables @+0x7ac
|
|
// (0x51155c) -- plus the linked master sink (linkedSinks.Resolve @+0x164);
|
|
// then, for each member with a nonzero coolant-flow share (+0x15C -- the
|
|
// valve set% bias, manual p24 "with a bias based on your set% levels"), move
|
|
// a squirt of coolantSquirtMass x flowScale x dt coolant out of the
|
|
// reservoir into the sink (clamped to what the reservoir still holds, and
|
|
// the sink to its capacity), and credit the sink's pendingHeat (+0x1C8) with
|
|
// the NEGATIVE heat delta carried by the fresh coolant -- capped at
|
|
// sink->thermalMass x reservoir->startingTemperature (+0x154 x +0x13C, the
|
|
// coolant enters at the reservoir's base temperature) and clamped to only
|
|
// ever COOL. Bails out as soon as the reservoir runs dry
|
|
// (|coolantLevel@0x12C| <= 1e-4 -- the old stub's "currentTemperature"
|
|
// reading of +0x12C was a misdecode; +300 decimal == 0x12C == coolantLevel).
|
|
// Constants: _DAT_004af3a0 = 1e-4f, _DAT_004af3a4 = _DAT_004af3a8 = 0.0f
|
|
// (byte-verified @4af3a0).
|
|
//
|
|
// PORT NOTE (chain emulation, same pattern as BTRecomputeCondenserValves):
|
|
// the factory's capability-chain fill loops ride the SubProxy stub in the
|
|
// port (loops 2/4 add nothing), so the work list is rebuilt here by walking
|
|
// the populated roster with the SAME membership tests, in the binary's
|
|
// order: condensers, weapons, HeatSink-derived heatables, the linked master
|
|
// sink. Condensers and weapons therefore appear TWICE (they are heatable-
|
|
// derived too), the bank twice (heatable + linked sink) -- exactly the
|
|
// binary's duplicate-visit weighting. ONE guarded deviation [T2]: the
|
|
// binary's heatable chain also contains the WATCHER branch (HeatWatcher
|
|
// chains the HeatableSubsystem derivation), whose bytes at +0x15C/+0x12C are
|
|
// interior alarm state in OUR layout (writing them = the databinding trap);
|
|
// real coolant carriers are all HeatSink-derived, so the walk filters to
|
|
// HeatSink -- a watcher's incidental +0x15C in the 1995 layout is alarm
|
|
// interior there too, so the faithful economics are identical.
|
|
//
|
|
void
|
|
Reservoir::InjectCoolant(Scalar time_slice) // FUN_004aefa4
|
|
{
|
|
if (fabsf(coolantLevel) <= 1.0e-4f) // _DAT_004af3a0; +0x12C (tank empty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
extern int BTWeaponIsRearFiring(::Subsystem *sub); // mechweap.cpp (-1 = not a weapon)
|
|
|
|
enum { kMaxWork = 96 }; // 20-subsystem roster x duplicate passes
|
|
HeatSink *work[kMaxWork];
|
|
int workCount = 0;
|
|
Derivation &condenserClass = *Condenser::GetClassDerivations(); // 0x50e4fc
|
|
Derivation &heatSinkClass = *HeatSink::GetClassDerivations();
|
|
Entity *own = (Entity *)owner; // roster access (Entity-complete TU)
|
|
int count = own->GetSubsystemCount();
|
|
|
|
for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7cc
|
|
{
|
|
::Subsystem *s = own->GetSubsystem(i);
|
|
if (s != 0 && s->IsDerivedFrom(condenserClass))
|
|
work[workCount++] = (HeatSink *)s;
|
|
}
|
|
for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7bc
|
|
{
|
|
::Subsystem *s = own->GetSubsystem(i);
|
|
if (s != 0 && BTWeaponIsRearFiring(s) >= 0) // MechWeapon-derived
|
|
work[workCount++] = (HeatSink *)s; // (weapons are HeatSinks)
|
|
}
|
|
for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7ac
|
|
{
|
|
::Subsystem *s = own->GetSubsystem(i);
|
|
if (s != 0 && s->IsDerivedFrom(heatSinkClass)) // see PORT NOTE
|
|
work[workCount++] = (HeatSink *)s;
|
|
}
|
|
{
|
|
HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164)
|
|
if (link != 0 && workCount < kMaxWork)
|
|
work[workCount++] = link;
|
|
}
|
|
|
|
for (int w = 0; w < workCount; ++w)
|
|
{
|
|
if (fabsf(coolantLevel) <= 1.0e-4f) // ran dry mid-pass -> stop
|
|
{
|
|
return;
|
|
}
|
|
HeatSink *sink = work[w];
|
|
if (sink->coolantFlowScale == 0.0f) // +0x15C == _DAT_004af3a4
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0]
|
|
Scalar move = -coolantSquirtMass // this+0x224
|
|
* sink->coolantFlowScale // sink+0x15C
|
|
* time_slice;
|
|
Scalar lo = -coolantLevel; // local_14
|
|
if (move < lo) move = lo; // can't move more than the tank holds
|
|
if (move > 0.0f) move = 0.0f; // squirts only ever LEAVE the tank
|
|
|
|
// the heat delta riding the moved mass (computed BEFORE the level
|
|
// updates, as in the binary):
|
|
Scalar den = (fabsf(sink->coolantLevel) > 1.0e-4f)
|
|
? sink->coolantLevel // sink+0x12C
|
|
: sink->thermalCapacity; // sink+0x128 (empty-sink fallback)
|
|
Scalar fracSink = fabsf(move) / den; // local_20
|
|
Scalar fracRes = fabsf(move) / coolantLevel; // local_1c
|
|
Scalar heatDelta =
|
|
sink->heatEnergy * fracSink // sink+0x158
|
|
- heatEnergy * fracRes; // this+0x158
|
|
|
|
coolantLevel += move; // the reservoir DRAINS (move <= 0)
|
|
sink->coolantLevel -= move; // the sink gains
|
|
if (sink->coolantLevel >= 0.0f) // clamp to [0, capacity]
|
|
{
|
|
if (sink->coolantLevel > sink->thermalCapacity)
|
|
sink->coolantLevel = sink->thermalCapacity;
|
|
}
|
|
else
|
|
{
|
|
sink->coolantLevel = 0.0f; // _DAT_004af3a8
|
|
}
|
|
|
|
Scalar cap = sink->thermalMass // sink+0x154
|
|
* startingTemperature; // this+0x13C (reservoir base temp)
|
|
if (heatDelta > cap)
|
|
heatDelta = cap;
|
|
Scalar chill = -heatDelta;
|
|
if (chill > 0.0f) // _DAT_004af3a4 < local_2c -> clamp
|
|
chill = 0.0f; // the flush only ever COOLS
|
|
sink->pendingHeat += chill; // sink+0x1C8
|
|
}
|
|
}
|
|
|
|
//
|
|
// @4aeef8 -- PrintState (ReservoirState). @4aef40 -- slot 7 state query
|
|
// (reports state code 0x14 plus the active flag).
|
|
//
|
|
void Reservoir::PrintState() // FUN_004aeef8 (best-effort label)
|
|
{
|
|
HeatableSubsystem::PrintState();
|
|
// << GetName() << " ReservoirState " << reservoirAlarm.Level() ...
|
|
}
|
|
|
|
//
|
|
// @4af580 -- parse the Reservoir resource. classID 0x0BC0 / size 0x104.
|
|
//
|
|
int
|
|
Reservoir::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
if (!HeatSink::CreateStreamedSubsystem( // FUN_004ae150
|
|
model_file, model_name, subsystem_name,
|
|
subsystem_resource, subsystem_file, directories, passes))
|
|
{
|
|
return False;
|
|
}
|
|
|
|
subsystem_resource->classID = RegisteredClass::ReservoirClassID; // res +0x20
|
|
subsystem_resource->subsystemModelSize = 0x104; // res +0x24
|
|
|
|
if (passes == 1)
|
|
{
|
|
subsystem_resource->coolantCapacity = -1.0f; // res +0xFC
|
|
subsystem_resource->coolantSquirtMass = -1.0f; // res +0x100
|
|
}
|
|
|
|
if (!model_file->GetEntry(subsystem_name, "CoolantCapacity",
|
|
&subsystem_resource->coolantCapacity)
|
|
&& subsystem_resource->coolantCapacity == -1.0f) // _DAT_004af694
|
|
{
|
|
DebugStream << subsystem_name << " missing CoolantCapacity!";
|
|
return False;
|
|
}
|
|
if (!model_file->GetEntry(subsystem_name, "CoolantSquirtMass",
|
|
&subsystem_resource->coolantSquirtMass)
|
|
&& subsystem_resource->coolantSquirtMass == -1.0f)
|
|
{
|
|
DebugStream << subsystem_name << " missing CoolantSquirtMass!";
|
|
return False;
|
|
}
|
|
Check_Fpu();
|
|
return True;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// AggregateHeatSink @0050ed4c/ed50 -- the mech heat-sink BANK (classID 0x0BBE)
|
|
//###########################################################################
|
|
//###########################################################################
|
|
//
|
|
// vtable @0050ed4c/ed50 ctor @4ae8d0 classID 0x0BBE own GUID 0x50e590 : HeatSink
|
|
//
|
|
// Parses "HeatSinkCount" and holds a frozen "AmbientTemperature" setpoint (300 K)
|
|
// bound by the numeric-R cockpit gauge (HeatSink/AmbientTemperature, the last
|
|
// config-binding NULL). See heatfamily_reslice.hpp for the deviation note: the
|
|
// base HeatSinkSimulation the HeatSink base ctor installs is kept (the verified,
|
|
// un-regressed 0xBBE behavior); the authentic Performance @4ae73c -- which derefs
|
|
// a raw self+0xE0 -> [+0x158] that does not map in our layout and runs for EVERY
|
|
// mech -- is DEFERRED (ambientTemperature is a frozen constant so the gauge reads
|
|
// 300 either way).
|
|
//
|
|
|
|
//#############################################################################
|
|
// Attribute Support -- the two aggregate-only bindings, dense-appended after
|
|
// HeatSink's table (chained via GetAttributeIndex) so AttributeIndexSet::Build
|
|
// has NO gap. CoolantMass/CoolantCapacity stay resolvable via the inherited
|
|
// HeatSink table (also bound to this same "HeatSink" subsystem).
|
|
//
|
|
const AggregateHeatSink::IndexEntry
|
|
AggregateHeatSink::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(AggregateHeatSink, HeatSinkCount, heatSinkCount), // @0x1D0
|
|
ATTRIBUTE_ENTRY(AggregateHeatSink, AmbientTemperature, ambientTemperature) // @0x1D4 FROZEN 300
|
|
};
|
|
|
|
AggregateHeatSink::AttributeIndexSet&
|
|
AggregateHeatSink::GetAttributeIndex()
|
|
{
|
|
static AggregateHeatSink::AttributeIndexSet attributeIndex(
|
|
ELEMENTS(AggregateHeatSink::AttributePointers),
|
|
AggregateHeatSink::AttributePointers,
|
|
HeatSink::GetAttributeIndex() // parent chain
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// BTSetBankAmbientTemperature -- bridge for Mech::PlayerLinkMessageHandler
|
|
// (@0049f624 writes bank+0x1d4 = mission temperature): mech.cpp cannot see
|
|
// AggregateHeatSink (local-stub collision), so the write lands here where the
|
|
// type is complete. This is the AUTHENTIC ambientTemperature writer -- the
|
|
// old "frozen 300" note stands corrected (the ctor default only holds until
|
|
// the mission's PlayerLink pass runs on the master).
|
|
//
|
|
void
|
|
BTSetBankAmbientTemperature(Subsystem *bank, Scalar temperature)
|
|
{
|
|
if (bank != NULL)
|
|
{
|
|
((AggregateHeatSink *)bank)->ambientTemperature = temperature;
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
AggregateHeatSink::SharedData
|
|
AggregateHeatSink::DefaultData(
|
|
AggregateHeatSink::GetClassDerivations(),
|
|
AggregateHeatSink::GetMessageHandlers(), // inherited (HeatSink's)
|
|
AggregateHeatSink::GetAttributeIndex(), // OWN -- chains HeatSink's + the 2 new ids
|
|
AggregateHeatSink::StateCount // inherited (MechSubsystem::StateCount)
|
|
);
|
|
|
|
Derivation*
|
|
AggregateHeatSink::GetClassDerivations()
|
|
{
|
|
// own GUID 0x50e590 (TestInstance @4ae9c0). The name only feeds IsDerivedFrom
|
|
// identity, never a resource/name lookup (the gauge binds by the subsystem name
|
|
// "HeatSink", not this) -- "HeatSinkBank" is a safe unique label.
|
|
static Derivation classDerivations(HeatSink::GetClassDerivations(), "HeatSinkBank");
|
|
return &classDerivations;
|
|
}
|
|
|
|
//
|
|
// @4ae8d0 -- HeatSink base + aggregate count/setpoint. See the deviation note:
|
|
// we do NOT scale thermalConductance nor install @4ae73c (both feed the deferred
|
|
// relaxation Performance); the base HeatSinkSimulation stands.
|
|
//
|
|
AggregateHeatSink::AggregateHeatSink(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004adda0(...,&DAT_0050e580,0,0)
|
|
helper() // this[0x76]: default-empty 0xC link node
|
|
{
|
|
Check(owner);
|
|
Check_Pointer(subsystem_resource);
|
|
|
|
heatSinkCount = subsystem_resource->heatSinkCount; // this[0x74] @0x1D0 = res +0xFC
|
|
ambientTemperature = 300.0f; // this[0x75] @0x1D4 (_DAT_004ae89c)
|
|
|
|
// SPEC AUDIT (BT_SPEC_LOG): the streamed HeatSinkCount vs the manual's
|
|
// "Number of Heat Sinks" per-mech stat.
|
|
if (getenv("BT_SPEC_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[spec] heatsinks: count=" << heatSinkCount
|
|
<< "\n" << std::flush;
|
|
}
|
|
|
|
// task #9 (the ambient radiator lands): the binary ctor @4ae8d0 scales the
|
|
// bank's conductance by 0.1 x HeatSinkCount (_DAT_004ae974 float80 = 0.1
|
|
// byte-verified; madcat count 14 -> x1.4) and installs the RADIATOR
|
|
// Performance @4ae73c on masters -- the system's ONLY heat exit.
|
|
thermalConductance = 0.1f * (Scalar)heatSinkCount * thermalConductance;
|
|
if ((owner->simulationFlags & 0xC) == 0
|
|
&& (owner->simulationFlags & 0x100) != 0)
|
|
{
|
|
SetPerformance((HeatSink::Performance)
|
|
&AggregateHeatSink::RadiatorSimulation); // PTR @0050e5e8 = @4ae73c
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @4ae73c -- the bank's authentic Performance (task #9; was DEFERRED): its own
|
|
// absorb/temperature/load step, then the AMBIENT RADIATOR -- relax toward the
|
|
// 300 K setpoint (target = 300 - (300 - ambientTemperature) x 3.0; = 300 with
|
|
// the frozen setpoint) with rate k = conductance x (1 - ownZoneDamage) x
|
|
// (coolant/capacity) x flowScale / mass -- the only place heat LEAVES the
|
|
// mech. Tail: top the bank's coolant up from the linked reservoir
|
|
// (DrawCoolant, vtable +0x38) whenever below capacity. The old deferral note
|
|
// ("raw self+0xE0 -> [+0x158] does not map") was wrong -- that read is the
|
|
// engine-base DamageZone's damageLevel, the same named-member pattern
|
|
// UpdateCoolant already uses. [T1 constants: 300 / 3.0 / 1.0 / 0.0 / 1e-4]
|
|
//
|
|
void
|
|
AggregateHeatSink::RadiatorSimulation(Scalar time_slice)
|
|
{
|
|
if (HeatModelActive()) // FUN_004ad7d4
|
|
{
|
|
heatEnergy += pendingHeat; // [0x56] += [0x72]
|
|
currentTemperature = heatEnergy / thermalMass; // [0x45] = [0x56]/[0x55]
|
|
UpdateHeatLoad(); // FUN_004ad7f0
|
|
pendingHeat = 0.0f;
|
|
|
|
Scalar target = 300.0f
|
|
- (300.0f - ambientTemperature) * 3.0f; // _DAT_004ae89c/_DAT_004ae8a0
|
|
::DamageZone *ownZone = this->Subsystem::damageZone; // @0xE0 (word 0x38)
|
|
Scalar zoneDamage = (ownZone != 0) ? (Scalar)ownZone->damageLevel : 0.0f;
|
|
double ex = (double)(-(time_slice * thermalConductance
|
|
* (1.0f - zoneDamage) // _DAT_004ae8a4 - dmg
|
|
* (coolantLevel / thermalCapacity) * coolantFlowScale)
|
|
/ thermalMass);
|
|
Scalar decay = 1.0f - (Scalar)exp(ex); // FUN_004dca38
|
|
Scalar shed = -((currentTemperature * massScale - target)
|
|
* thermalMass * decay);
|
|
pendingHeat += shed;
|
|
|
|
// DIAG census (BT_HEAT_LOG, viewpoint mech, 5 s) -- the bank runs THIS
|
|
// Performance instead of HeatSinkSimulation, so it needs its own line.
|
|
if (getenv("BT_HEAT_LOG") && application != 0
|
|
&& (Entity *)owner == application->GetViewpointEntity())
|
|
{
|
|
static Scalar s_bankAcc = 0.0f;
|
|
s_bankAcc += time_slice;
|
|
if (s_bankAcc >= 5.0f)
|
|
{
|
|
s_bankAcc = 0.0f;
|
|
DEBUG_STREAM << "[heat-t] " << GetName() << " (bank)"
|
|
<< " T=" << currentTemperature
|
|
<< " shed=" << shed
|
|
<< " cool=" << coolantLevel << "/" << thermalCapacity
|
|
<< " load=" << heatLoad << "\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
// coolant top-up from the reservoir (the binary tail: deficit > 0 and
|
|
// past the 1e-4 gate -> DrawCoolant via vtable +0x38).
|
|
Scalar deficit = thermalCapacity - coolantLevel; // [0x4a] - [0x4b]
|
|
if (deficit > 0.0f && fabsf(deficit) > 1.0e-4f) // _DAT_004ae8a8/_DAT_004ae8ac
|
|
{
|
|
coolantLevel += DrawCoolant(deficit); // vcall +0x38
|
|
}
|
|
}
|
|
|
|
AggregateHeatSink::~AggregateHeatSink()
|
|
{
|
|
Check(this);
|
|
// `helper` (trivial POD, always empty) and the HeatSink base chain destroy
|
|
// IMPLICITLY at the closing brace. Per the dtor-epilogue rule, do NOT write
|
|
// explicit ~HeatSink()/member-dtor calls (re-runs the base chain = P5 double-free).
|
|
Check_Fpu();
|
|
}
|
|
|
|
Logical AggregateHeatSink::TestInstance() const // @4ae9c0 -> IsDerivedFrom 0x50e590
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|
|
Logical AggregateHeatSink::TestClass(Mech &) { return True; }
|
|
|
|
//
|
|
// @4ae9dc -- OFFLINE content-build parser (runtime reads pre-built BTL4.RES;
|
|
// completeness-only, never exercised at runtime).
|
|
//
|
|
int
|
|
AggregateHeatSink::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
// FUN_004ae150 == HeatSink::CreateStreamedSubsystem (thermal fields + link name).
|
|
if (!HeatSink::CreateStreamedSubsystem(
|
|
model_file, model_name, subsystem_name,
|
|
(HeatSink::SubsystemResource *)subsystem_resource,
|
|
subsystem_file, directories, passes))
|
|
return False;
|
|
|
|
subsystem_resource->classID = (RegisteredClass::ClassID)0xBBE; // param_4+0x20
|
|
subsystem_resource->subsystemModelSize = 0x100; // param_4+0x24
|
|
|
|
if (passes == 1)
|
|
subsystem_resource->heatSinkCount = -1; // param_4+0xFC default
|
|
|
|
Logical found = subsystem_file->GetEntry(subsystem_name, "HeatSinkCount",
|
|
&subsystem_resource->heatSinkCount);
|
|
if (!found && subsystem_resource->heatSinkCount == -1)
|
|
{
|
|
DebugStream << subsystem_name << " missing HeatSinkCount!";
|
|
return False;
|
|
}
|
|
return True;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
// Engine collection-iterator helper thunks (NOT heat family)
|
|
//###########################################################################
|
|
//
|
|
// Tiny ctor/dtor wrappers for the engine's generic collection/iterator
|
|
// library, used only as locals inside Reservoir::InjectCoolant (@4aefa4).
|
|
// Listed for completeness; they belong to the collection module, not here.
|
|
//
|
|
// @4af9cf / @4af9ee vtable 0050eccc (on base 004179d4/004179f8)
|
|
// @4afa1a / @4afa39 vtable 0050ecc4 (on base 004179d4/004179f8)
|
|
// @4afa65 / @4afa84 vtable 0050ecbc (on base 00417be0/00417c0c)
|
|
// @4afab0 @4afacf @4afaee / @4afb0d vtable 0050ec6c (on 00417d00/d28/d54)
|
|
// @4afb39 @4afb58 @4afb77 / @4afb96 vtable 0050ec1c (on 00417d00/d28/d54)
|
|
//
|
|
|
|
//===========================================================================//
|
|
// WAVE 2 factory bridge -- Reservoir (factory case 0xBC0, "Condenser" label).
|
|
//===========================================================================//
|
|
Subsystem *CreateReservoirSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
Check(sizeof(Reservoir) <= 0x230);
|
|
return (Subsystem *) new (Memory::Allocate(0x230))
|
|
Reservoir(owner, id, (Reservoir::SubsystemResource *)seg, Reservoir::DefaultData);
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Factory bridge -- AggregateHeatSink (factory case 0xBBE, "Sensor" label).
|
|
// Binary @9993: alloc 0x1e4, ctor, store, param_1[0x1f7]=slot. The sizeof lock
|
|
// (AggregateHeatSinkLayoutCheck) proves ==0x1E4 at compile time.
|
|
//===========================================================================//
|
|
Subsystem *CreateHeatSinkBankSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
Check(sizeof(AggregateHeatSink) <= 0x1e4);
|
|
return (Subsystem *) new (Memory::Allocate(0x1e4))
|
|
AggregateHeatSink(owner, id,
|
|
(AggregateHeatSink::SubsystemResource *)seg, AggregateHeatSink::DefaultData);
|
|
}
|
|
|
|
//===========================================================================//
|
|
// @0049f788 -- RecomputeCondenserValves. Distribute coolant flow across the
|
|
// mech's condensers so each one's ValveSetting gauge (coolantFlowScale@0x15C)
|
|
// reads its share of the total valve opening: flow_i = valveState_i / sum(valveState).
|
|
//
|
|
// The binary iterates the condenser chain @mech+0x7cc (GUID 0x50e4fc == Condenser);
|
|
// we walk the populated subsystem roster and filter for Condensers via IsDerivedFrom
|
|
// -- behaviorally identical (the chain holds exactly the condensers) and independent
|
|
// of whether the @0x7cc chain is wired. Called at the end of the Mech ctor (binary
|
|
// @9457, the post-init pass) so the valve gauge shows the authentic 1/N per condenser
|
|
// instead of 0 (the ctor leaves coolantFlowScale=0 by design; this is its first writer).
|
|
//
|
|
// The per-condenser condenserAlarm@0x1DC toggle (2 if flow<=old else 1, then 0) is the
|
|
// binary's change-notification pulse; reproduced exactly (FUN_0041bbd8 == SetLevel).
|
|
//===========================================================================//
|
|
void BTRecomputeCondenserValves(Entity *owner)
|
|
{
|
|
Check(owner);
|
|
Derivation &condClass = *Condenser::GetClassDerivations();
|
|
int count = owner->GetSubsystemCount();
|
|
|
|
// DIAG (BT_VALVE=<n>): force every condenser's valve detent so the coolant/jam
|
|
// mechanic can be tested at a known coolant level (0=closed/uncooled .. 50=max).
|
|
// DIAG (BT_VALVE_BOOST=<condenserNumber>): the authentic "coolant priority" test --
|
|
// give ONE condenser the max detent (50) and starve the rest to the min (1), i.e.
|
|
// route the coolant SHARE to that condenser (share ~0.9 vs the ~1/N baseline).
|
|
{
|
|
const char *fv = getenv("BT_VALVE");
|
|
const char *fb = getenv("BT_VALVE_BOOST");
|
|
if (fb && fb[0])
|
|
{
|
|
int boostNum = atoi(fb);
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *s = owner->GetSubsystem(i);
|
|
if (s != 0 && s->IsDerivedFrom(condClass))
|
|
((Condenser *)s)->valveState =
|
|
(((Condenser *)s)->condenserNumber == boostNum) ? 50 : 1;
|
|
}
|
|
}
|
|
else if (fv && fv[0])
|
|
{
|
|
int v = atoi(fv);
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *s = owner->GetSubsystem(i);
|
|
if (s != 0 && s->IsDerivedFrom(condClass))
|
|
((Condenser *)s)->valveState = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
// pass 1: total valve opening across all condensers
|
|
int total = 0;
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *s = owner->GetSubsystem(i);
|
|
if (s != 0 && s->IsDerivedFrom(condClass))
|
|
total += ((Condenser *)s)->valveState; // iVar3 += *(int*)(iVar1+0x1d0)
|
|
}
|
|
|
|
// pass 2: each condenser's flow = its share of the total
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *s = owner->GetSubsystem(i);
|
|
if (s == 0 || !s->IsDerivedFrom(condClass))
|
|
continue;
|
|
Condenser *c = (Condenser *)s;
|
|
Scalar flow = 0.0f; // _DAT_0049f850 (no-condenser fallback)
|
|
if (total > 0)
|
|
flow = (Scalar)c->valveState / (Scalar)total;
|
|
c->condenserAlarm.SetLevel(flow <= c->coolantFlowScale ? 2 : 1); // @0x1dc change pulse
|
|
c->coolantFlowScale = flow; // *(float*)(iVar1+0x15c) = local_8
|
|
c->condenserAlarm.SetLevel(0);
|
|
if (getenv("BT_VALVE_LOG"))
|
|
DEBUG_STREAM << "[valve] condenser#" << c->condenserNumber
|
|
<< " valveState=" << c->valveState << " flow=" << c->coolantFlowScale
|
|
<< " (total=" << total << ")\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
// BTBalanceCondenserValves -- the body of Mech's BalanceCoolant handler
|
|
// (@0049f728, id 0x16 -- issue #20): set EVERY condenser's valveState@0x1D0
|
|
// to 1 (equal weights) then run the shared redistribute @0049f788. Lives
|
|
// here because Condenser is a complete type in this TU (the Mech TU cannot
|
|
// touch valveState).
|
|
//
|
|
void BTBalanceCondenserValves(Entity *owner)
|
|
{
|
|
Check(owner);
|
|
Derivation &condClass = *Condenser::GetClassDerivations();
|
|
int count = owner->GetSubsystemCount();
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *s = owner->GetSubsystem(i);
|
|
if (s != 0 && s->IsDerivedFrom(condClass))
|
|
((Condenser *)s)->valveState = 1; // *(iVar1+0x1d0) = 1
|
|
}
|
|
BTRecomputeCondenserValves(owner); // FUN_0049f788
|
|
}
|