Files
BT412/game/reconstructed/heatfamily_reslice.cpp
T
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00

1008 lines
38 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
default: return; // @4ae497 unknown -> no change / no recompute
}
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,
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)
if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0 // (flags & 0xC) == 0
&& (subsystem_resource->subsystemFlags & 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
//
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)
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)
if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0
&& (subsystem_resource->subsystemFlags & 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)
// The binary reads *(float*)(link+0x1d0) -- the master heat sink's first OWN
// field (HeatSink now ends exactly at 0x1d0, so this is the master subclass's
// own scale word). field_1d0 was deleted from HeatSink; mirror the raw byte
// read (the master's dynamic subclass isn't known statically here).
Scalar masterScale = *(Scalar *)((char *)link + 0x1d0);
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 -- "InjectCoolant" message / per-state update. When the watched
// segment has positive count and the reservoir is active above 0 temperature,
// raise the inject alarm and zero the inject accumulator; otherwise clear it.
//
Logical
Reservoir::HandleMessage(int message) // FUN_004aee70
{
if (!HeatableSubsystem::HandleMessage(message)) // FUN_004ac9c8
{
if (message /*->count*/ < 1)
{
reservoirAlarm.SetLevel(0); // this+0x1d0
}
else
{
if (reservoirAlarm.GetLevel() != 1 && currentTemperature > 0.0f) // injectActive == alarm level @0x1e4
{
reservoirAlarm.SetLevel(1);
}
injectAccumulator = 0; // this+0x228
}
ForceUpdate(); // this+0x18
}
return True;
}
//
// @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
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) -- distribute the reservoir charge across the linked
// heat-sink network. Iterates three engine collections off the owner
// (+0x7ac/+0x7bc/+0x7cc) plus the local sink, builds a work list, and for each
// member moves a clamped, time-scaled "squirt" of coolant -- bounded by the
// per-sink remaining capacity and by the reservoir's own coolantLevel -- while
// tracking the per-sink coolant-flow delta (+0x1c8). Bails out as soon as the
// reservoir's currentTemperature drops to ~0 (|temp| <= 1e-4).
//
// (Uses the engine collection-iterator helpers @4af9cf..@4afb96 as locals --
// see notes at the foot of this file. Body reproduced structurally; the
// per-sink arithmetic mirrors HeatSink::ComputeHeatFlow.)
//
void
Reservoir::InjectCoolant(Scalar time_slice) // FUN_004aefa4
{
if (fabsf(currentTemperature) <= 1.0e-4f) // _DAT_004af3a0
{
return;
}
// ... iterate owner collections (+0x7ac/+0x7bc/+0x7cc) and self,
// squirt clamped coolant to each linked sink, update +0x1c8 ...
// (full per-sink math recovered in heatfamily_gap.c @4aefa4)
}
//
// @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;
}
//#############################################################################
// 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)
// 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();
// 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;
}
}