//===========================================================================// // 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 #pragma hdrstop #if !defined(HEAT_HPP) # include #endif #if !defined(HEATFAMILY_RESLICE_HPP) # include # include // 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 *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; } }