The playtest report (sound plays, no gauge drop, no cloud) traced to the
Reservoir's InjectCoolant chain being dead in three places:
- The handler was never REGISTERED: the binary's Reservoir handler table
@0x50e680 has one entry {4, "InjectCoolant", @4aee70}; added
Reservoir::GetMessageHandlers + the press/release handler (press starts
the flush gated on coolantLevel@0x12C > 0 -- the old body misread +0x12C
as currentTemperature; release stops it; novice lockout via FUN_004ac9c8).
- Reservoir::InjectCoolant (@4aefa4, 1019 bytes) was an empty stub -- the
drain the coolant gauge reads. Reconstructed in full: work list =
condenser/weapon/heatable chains (+0x7cc/+0x7bc/+0x7ac, roster-walk
emulation with the binary's duplicate-visit weighting; HeatSink-filtered
[T2 guarded]) + the linked master sink; per sink with flowScale != 0 move
squirtMass x flowScale x dt (clamped to the tank / sink capacity) and
credit pendingHeat with the negative carried-heat delta capped at
sinkMass x reservoir startingTemperature -- the set%-biased flush of the
manual (p24), riding the existing heat model.
- Two latent ctor decode bugs surfaced and fixed: the master gate @4af408
(and @4aeb40 HeatWatcher, swept) reads the OWNER MECH's simulationFlags
(*(param_2+0x28)), not the resource's subsystemFlags (the misread left
the CoolantSimulation Performance unregistered); and the capacity scale
FILDs the bank's INTEGER HeatSinkCount ((float10)*(int*)(link+0x1d0)) --
the float reinterpret gave ~1e-44 -> a permanently empty tank.
Capacity = 0.05 x heatSinkCount x streamed CoolantCapacity (BLH: 6.0).
Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built
for classID 0xBC0 on "ReservoirState", part_014.c:5439; tick @part_007.c:
8780) starts psfx 19 = FLUSH.PFX ("Coolant flush", BTDPL.INI) when the
state changes to 1 -- BTSpawnFlushCloud (mech4.cpp) spawns it on the same
alarm edge as an attached emitter at torso height.
Input: new CONTROLS.MAP action "Flush" on 'H' (HELD; press+release both
dispatch, the held-button payload). Diags: BT_FLUSH_LOG, BT_FLUSH_TEST.
Verified live (FOGDAY): [flush] Reservoir level 6 -> 0.13 -> 0 over ~0.6 s
held (= the manual's 3-4 punches to empty), the coolant vertBar source
Reservoir/CoolantMass drains, and the bluish condensation cloud rises from
the mech (scratchpad/flush_cloud.png vs flush_before.png).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1279 lines
49 KiB
C++
1279 lines
49 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)
|
|
|
|
// (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,
|
|
Receiver::GetMessageHandlers()
|
|
);
|
|
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;
|
|
}
|
|
}
|