Files
BT411/game/reconstructed/powersub.cpp
T
arcattackandClaude Opus 5 62513b2f8a Gitea #53: generators can be switched off -- ToggleGeneratorOnOff reconstructed from @004b1ed0
TWO missing pieces, both now in place.

1. Generator had NO handler set of its own, so every message aimed at it hit a
   default-constructed blackhole.  Proved empirically with the new
   unhandled-message trace by pressing button 0x1A:
     [msg] UNHANDLED: Generator has no handler for message id 4 -- silently dropped
   Added Generator::GetMessageHandlers() chained to **HeatSink**, deliberately NOT
   PoweredSubsystem: id 4 there is SelectGeneratorA (the weapon-side generator
   SELECTION that already works), so chaining to it would have made these four
   buttons invoke the WRONG function instead of doing nothing.  HeatSink owns id 3
   (ToggleCooling), leaving id 4 free.

2. ToggleGeneratorOnOff had no body at all (0 references in game/).  Transcribed
   instruction-for-instruction from @004b1ed0:
     - call @004ac9c8 -> BTPlayerRoleLocksAdvanced: a NOVICE pilot cannot use it
     - [msg+0xc] <= 0 -> press only, ignore the release
     - ON  : startTimer = 0; if heatAlarm(@0x184) < 1 also stateAlarm -> 0
             (Starting, so it spins up -- a heat-faulted generator keeps its fault
             level); generatorOn = 1, coolantAvailable = 1, coolantFlowScale = 1.0
     - OFF : stateAlarm -> 1 (GeneratorIdle); generatorOn = 0,
             coolantAvailable = 0, coolantFlowScale = 0 (its coolant share is
             released back to the loop)
   So taking a generator off line idles it, drops its coolant draw and stops it
   feeding its taps -- the authentic heat-management trade from the manual.

VERIFIED LIVE (BT_BTNTEST=0x1A,400,900 through the real click seam):
  [btntest] PRESS 0x1a at poll 400
  [gensel] GeneratorA generator OFF LINE (stateAlarm 1, coolantFlowScale 0)
and the UNHANDLED warning for Generator is gone.

Answers Cyd's report from two playtests ('still cannot turn of gennies').  Note
this is the ON/OFF control -- generator SELECTION (which generator a subsystem
draws from) is a different message and already worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-25 10:15:25 -05:00

1716 lines
60 KiB
C++

//===========================================================================//
// File: powersub.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Powered (electrically driven) subsystems -- voltage routing, //
// generators, myomer drive and the low-voltage watchdog //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C for the module cluster @0x4b02f0..@0x4b2678. The only function the
// decomp tagged is the PoweredSubsystem ctor @004b0f74 (file=bt/powersub.cpp);
// the cluster was recovered via the call graph, the class vtables and the
// "d:\tesla_bt\bt\POWERSUB.CPP" assertion strings. Each non-trivial method
// cites the originating @ADDR.
//
// Hex/byte float constants converted to decimal:
// 0x3f800000 = 1.0f 0x3f000000 = 0.5f 0xbf800000 = -1.0f
// _DAT_004b0bcc = 1.0e-4f _DAT_004b297c = 0.0f
//
// Helper-function name mapping (engine internals referenced by the decomp;
// the heat.* names are carried over from the heat-subsystem reconstruction):
// FUN_004adda0 HeatSink::HeatSink (PoweredSubsystem / Generator base ctor)
// FUN_004adfd4 HeatSink::~HeatSink
// FUN_004ad924 HeatSink::HeatSinkSimulation (base Performance)
// FUN_004ad760 HeatSink::ResetToInitialState
// FUN_004ad748 HeatSink::HandleMessage (base message dispatch)
// FUN_004add30 HeatSink::GetStatusFlags
// FUN_004ac868 HeatableSubsystem::~HeatableSubsystem (chained from PowerController dtor)
// FUN_004ac530 Subsystem ctor (PowerController base)
// FUN_004ac9c8 BTPlayerRoleLocksAdvanced() (owner mech+0x190 player ->
// roleClassIndex+0x274 == 0: the ROOKIE role locks the
// advanced cockpit systems -- NOT "IsDamaged"; task #12)
// FUN_0049fb54 Subsystem::IsDamaged()/IsDead variant used by PowerController
// FUN_00417ab4 SharedData::Resolve() (connection -> Subsystem*)
// FUN_004179d4 SharedData ctor FUN_004179f8 SharedData dtor FUN_00417a5c SharedData::Clear
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041baa4 ~AlarmIndicator
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_0040385c Verify()/assert(msg,file,line)
// FUN_004dcd00 fabsf()
// FUN_004dbb24 DebugStream::operator<<(char*) FUN_004d9c38 DebugStream::endl
// FUN_004d4a78 strlen()
// FUN_00404088 NotationFile::Read(name,key,char**)
// FUN_00404118 NotationFile::Read(name,key,float*)
// FUN_004040d8 NotationFile::Read(name,key,int*)
// FUN_004215b0 Get_Segment_Index(file,name)
// FUN_004ae150 HeatSink::CreateStreamedSubsystem
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(POWERSUB_HPP)
# include <powersub.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
//
// Tuning constants observed as read-only globals in the decomp.
//
static const Scalar VoltageEpsilon = 1.0e-4f; // _DAT_004b0bcc (short-detect threshold)
static const Scalar MinOutputVoltage = 0.0f; // _DAT_004b2158 / _DAT_004b297c
static const Scalar MaxOutputVoltage = 1.0f; // _DAT_004b2154 / _DAT_004b2258
static const Scalar RatedVoltageRef = 1.0f; // _DAT_004b1d10 (myomer rated reference)
static const Scalar DegradedDriveScale = 0.5f; // _DAT_004b1d14 (heat-degradation throttle)
static const Scalar ResourceUnset = -1.0f; // _DAT_004b177c / _DAT_004b1a3c / _DAT_004b2580
static const Scalar MinVoltageScale = 0.01f; // _DAT_004b1924 -- a 10-byte x87 literal
// (0a d7 a3 70 3d 0a d7 a3 f8 3f = 0.01,
// byte-read from the exe, task #57): converts
// the authored MinVoltagePercent to a fraction.
// Was 1.0f, which made minVoltage 100x too big
// and latched the watchdog brownout (level 1).
//###########################################################################
//###########################################################################
// PoweredSubsystem
//###########################################################################
//###########################################################################
//#############################################################################
// Attribute Support (gauge data-binding wave)
//
// InputVoltage -> voltageSource (the 0xC connection to the powering Generator;
// the cluster's power-branch gate + the source of GeneratorVoltageConnection).
// Chained to HeatSink's dense index; a function-local static so the parent
// index is safely constructed on first use regardless of static-init order.
//
const PoweredSubsystem::IndexEntry
PoweredSubsystem::AttributePointers[]=
{
ATTRIBUTE_ENTRY(PoweredSubsystem, InputVoltage, voltageSource) // @0x1D0
};
PoweredSubsystem::AttributeIndexSet&
PoweredSubsystem::GetAttributeIndex()
{
static PoweredSubsystem::AttributeIndexSet attributeIndex(
ELEMENTS(PoweredSubsystem::AttributePointers),
PoweredSubsystem::AttributePointers,
HeatSink::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Shared Data Support
//
// task #12 -- the binary handler table @0x50F4EC (section_dump: ids 4-8,
// names "SelectGeneratorA".."ToggleGeneratorMode"). Chained onto the engine
// Receiver root set.
//
// STATIC-INIT ORDER (the silent-drop bug): the table lives INSIDE the accessor
// as a function-local static. As a namespace-scope array it was read by
// Emitter::DefaultData's cross-TU static-init chain BEFORE this TU's dynamic
// initializers ran -- the Build saw zeros, so weapons carried ids 9/10 but the
// 4-8 slots stayed empty (observed: [gensel-tx] logged, handler never ran).
// Function-local statics initialize on first call -- order-proof; the same
// idiom the T0 engine uses (APP.cpp GetMessageHandlers).
Receiver::MessageHandlerSet&
PoweredSubsystem::GetMessageHandlers()
{
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorA), // id 4 @004b099c
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorB), // id 5 @004b09e4
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorC), // id 6 @004b0a2c
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorD), // id 7 @004b0a74
MESSAGE_ENTRY(PoweredSubsystem, ToggleGeneratorMode) // id 8 @004b0abc
};
// Chain through HeatSink (id 3 ToggleCooling) so a powered subsystem's
// dispatch -- weapons/avionics -- reaches the Eng-page "Coolant" button. The
// old chain went straight to the Receiver root, SKIPPING HeatSink/
// HeatableSubsystem, which is why msg 3 was unreachable (decomp-reference.md
// "ToggleCooling ... UNWIRED cause"). HeatSink then chains to the root.
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(entries), entries,
HeatSink::GetMessageHandlers()
);
return messageHandlers;
}
PoweredSubsystem::SharedData
PoweredSubsystem::DefaultData(
PoweredSubsystem::GetClassDerivations(),
PoweredSubsystem::GetMessageHandlers(),
PoweredSubsystem::GetAttributeIndex(),
PoweredSubsystem::StateCount
);
Derivation*
PoweredSubsystem::GetClassDerivations()
{
static Derivation classDerivations(HeatSink::GetClassDerivations(), "PoweredSubsystem");
return &classDerivations;
}
//#############################################################################
// Construction / Destruction
//
// @004b0f74 -- THE tagged powersub.cpp function (size 486). Builds on the
// HeatSink base, then resolves and attaches the voltage source named in the
// resource and primes the electrical state machine.
//
PoweredSubsystem::PoweredSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004adda0
voltageSource(0), // FUN_004b1bb1(this+0x74, 0)
electricalStateAlarm(5), // FUN_0041b9ec(this+0x99, 5)
modeAlarm(3) // FUN_0041b9ec(this+0xae, 3)
{
Check(owner);
Check_Pointer(subsystem_resource);
//
// Resolve the "VoltageSource" index to the generator subsystem.
//
// ⚠ ROOT-CAUSE FIX (sibling of the heat.cpp heap corruption): voltageSourceIndex
// indexes the owner's SUBSYSTEM ROSTER, not the skeleton segment table. Raw
// PoweredSubsystem ctor (part_013.c:1198):
// if (res->voltageSourceIndex /*+0xFC*/ < owner->subsystemCount /*+0x124*/)
// source = owner->subsystemArray[voltageSourceIndex] /*+0x128*/;
// The earlier GetSegment draft cast an EntitySegment (288B) to Generator*, so
// AttachToVoltageSource's `currentTapCount += 1` wrote 136 bytes past that heap
// block at every mech spawn. Roster slots are pre-zeroed -> NULL = warn path.
Subsystem *source = 0;
if (subsystem_resource->voltageSourceIndex >= 0
&& subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount())
{
source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex);
}
else
{
// @004b0f74: "Bad voltageSourceIndex to connect power to!" POWERSUB.CPP:0x264
Verify(False, "Bad voltageSourceIndex to connect power to!", __FILE__, 0x264);
}
//
// A "master" instance (segment flagged 0x100, not a sub-/damaged copy)
// runs the per-frame electrical simulation and owns the live link.
//
// INTEGRATION (gate reconcile): master-gate reads OWNER simulationFlags
// (param_2+0x28), the oracle-verified authoritative source — not the per-
// segment resource flags (which stream 0 here). See heat.cpp note.
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // (owner flags & 0xC) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0 // owner flags & 0x100
)
{
SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation); // this[7..9] = PTR @0050f5b0
if (source == 0)
{
// @004b0f74: POWERSUB.CPP:0x272
Verify(False, "Master PoweredSubsystem is missing required generator", __FILE__, 0x272);
}
else
{
AttachToVoltageSource(source); // FUN_004b0dd8
}
}
else if (
// gate on OWNER flags, matching raw part_013.c:1216 (param_2+0x28 & 0xc)==4
// (the earlier draft read the resource flags here -- always 0 -> dead branch)
(owner->simulationFlags & SegmentCopyMask) == 4
&& source != 0
)
{
AttachToVoltageSource(source);
}
thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient; // +0x100
startTime = subsystem_resource->startTime; // +0x18C
startTimer = startTime;
electricalStateAlarm.SetLevel(Ready); // FUN_0041bbd8(this+0x99, 4)
modeAlarm.SetLevel(Connected); // FUN_0041bbd8(this+0xae, 1)
voltageScale = 1.0f;
auxScreenNumber = subsystem_resource->auxScreenNumber; // +0x104
auxScreenPlacement = subsystem_resource->auxScreenPlacement; // +0x108
strcpy(auxScreenLabel, subsystem_resource->auxScreenLabel); // inline copy from +0x10C
strcpy(engScreenLabel, subsystem_resource->engScreenLabel); // inline copy from +0x14C
Check_Fpu();
}
//
// @004b115c -- tears down the alarms and the voltage-source connection, then
// chains to ~HeatSink.
//
PoweredSubsystem::~PoweredSubsystem()
{
Check(this);
// modeAlarm (@0xae) / electricalStateAlarm (@0x99) / voltageSource (@0x74)
// are released here; FUN_004adfd4 finishes the HeatSink chain.
Check_Fpu();
}
//###########################################################################
// TestClass / TestInstance -- PoweredSubsystem
//
Logical
PoweredSubsystem::TestClass(Mech &)
{
return True;
}
Logical
PoweredSubsystem::TestInstance() const // @004b1208
{
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x50f4bc)
}
//#############################################################################
// Per-frame simulation
//
//
// @004b0bd0 -- registered Performance for a master PoweredSubsystem. Runs the
// HeatSink simulation, then advances the electrical state machine from the
// state of the powering generator and (in AutoConnect mode) hunts for a
// replacement generator when the link is lost.
//
void
PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice)
{
HeatSink::HeatSinkSimulation(time_slice); // FUN_004ad924
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x74)
if (source == 0)
{
electricalStateAlarm.SetLevel(NoVoltage);
}
else
{
if (source->GeneratorStateOf() == 3) // source+0x210 == 3 (shorted)
{
electricalStateAlarm.SetLevel(Shorted);
}
if (source->GeneratorStateOf() == 1 || source->GeneratorStateOf() == 4)
{
electricalStateAlarm.SetLevel(GeneratorOff);
}
}
switch (electricalStateAlarm.GetLevel()) // this[0x9e] @0x278
{
case Starting:
startTimer += time_slice;
if (startTime <= startTimer)
{
electricalStateAlarm.SetLevel(Ready);
}
break;
case NoVoltage:
if (source != 0)
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
case Shorted:
case GeneratorOff:
if (source != 0 && source->GeneratorStateOf() == 2) // generator back to Ready
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
}
//
// AutoConnect: if the mode alarm is in the auto state and we are not
// damaged and currently unpowered, scan every GeneratorClassID segment in
// the mech and attach to the first one that will accept a tap.
//
if (
modeAlarm.GetLevel() == AutoConnect // this[0xb3] @0x2cc == 2
&& GetStatusFlags() == 0 // (*this[0x40])(this,0) == 0
)
{
// task #12 ROSTER CORRECTION: the raw @004b0bd0 loop walks owner+0x124/
// +0x128 -- the SUBSYSTEM ROSTER, not the skeleton segment table (the
// same +0x128 gotcha as the heat/voltage index resolves). The old
// GetSegment() read cast EntitySegments to Subsystems.
Mech *mech = (Mech *)owner;
int count = mech->GetSubsystemCount(); // owner+0x124
for (int i = 0; i < count; ++i)
{
Subsystem *sub = mech->GetSubsystem(i); // owner+0x128[i]
if (sub != 0
&& sub->GetClassID() == RegisteredClass::GeneratorClassID // +4 == 0xbc1
&& GetStatusFlags() != 0
&& AttachToVoltageSource(sub) != -1 // FUN_004b0dd8
)
{
break;
}
}
}
Check_Fpu();
}
//
// @004b0d50 -- the charge time-scale (task #10, the heat/firepower feedback):
// rise = max(0, srcTemperature - srcStartingTemperature) (src+0x114 - src+0x13C)
// scale = max(voltageScale,
// (thermalResistivityCoefficient * rise + 1.0) * voltageScale)
// (_DAT_004b0dd0 = 0.0f, _DAT_004b0dd4 = 1.0f, both byte-verified.) The binary
// dereferences the resolved source UNGUARDED (the master ctor Verify guarantees
// the link); the port falls back to the unstretched voltageScale instead.
//
Scalar
PoweredSubsystem::ChargeTimeScale()
{
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1D0)
Scalar rise = 0.0f;
if (source != 0)
{
rise = source->currentTemperature // src+0x114
- source->startingTemperature; // src+0x13C
}
if (rise < 0.0f) // _DAT_004b0dd0
{
rise = 0.0f;
}
Scalar scale = (thermalResistivityCoefficient * rise + 1.0f) // this+0x30C, _DAT_004b0dd4
* voltageScale; // this+0x310
if (scale < voltageScale)
{
scale = voltageScale;
}
return scale;
}
//#############################################################################
// Subsystem virtual overrides
//
//
// @004b0e6c -- powered/unpowered transition. When power is cut the electrical
// alarm goes idle; when restored it returns to "GeneratorOff" with the start
// timer reset, but only while the source is actually Ready (state 2).
//
void
PoweredSubsystem::DeathReset(int /*reset_command*/)
{
ResetToInitialState(True); // @004b0e6c
}
void
PoweredSubsystem::ResetToInitialState(Logical powered)
{
HeatSink::ResetToInitialState(True); // FUN_004ad760
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x74)
if (!powered)
{
if (source != 0)
{
electricalStateAlarm.SetLevel(NoVoltage); // SetLevel 0 in decomp -> Starting/idle
startTimer = 0.0f; // this[0xc6] = 0
}
}
else
{
if (source != 0 && source->GeneratorStateOf() == 2)
{
electricalStateAlarm.SetLevel(Ready); // SetLevel 4
startTimer = startTime; // this[0xc6] = this[0xc5]
}
}
}
//
// @004b0efc -- pass the message to the HeatSink base; if the part is alive,
// the source short flag is set and message 4 arrives, force a short-recovery.
//
extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c; issue #2)
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8: player+0x274 == 0)
Logical
PoweredSubsystem::HandleMessage(int message)
{
HeatSink::HandleMessage(message); // FUN_004ad748
// CORRECTED (issue #2 [T1]): @004b0efc reads *(int*)(*(owner+0x190)+0x25c)
// -- owner mech+0x190 is the PLAYER LINK, and +0x25c is the BTPlayer's
// "sim live" EXPERIENCE flag (0 only for novice), NOT a message-manager
// field (the old misattribution left this path dead behind the null
// BT_GetMessageManager stub). The leading FUN_004ac9c8==0 term is the
// not-novice predicate (BTPlayerRoleLocksAdvanced), not a damage query.
if (!BTPlayerRoleLocksAdvanced(owner) // FUN_004ac9c8 == 0 (not novice)
&& BTPlayerExperienceSimLive(owner) != 0 // *(player+0x25c) != 0
&& message == 4)
{
ForceShortRecovery(); // FUN_004b11bc(this)
}
return True;
}
//
// @004b11bc -- on a short event, if neither this part nor its live source is
// damaged, drive the source's alarm to "shorted" (3) and clear its output.
//
void
PoweredSubsystem::ForceShortRecovery() // internal helper for @004b0efc
{
// CORRECTED (issue #2 [T1]): both FUN_004ac9c8 calls here are the
// not-novice EXPERIENCE predicate on the owning player (each subsystem
// hops sub->owner mech->playerLink; this part and its source share the
// same mech, hence the same player) -- NOT damage-state queries (the old
// HeatSink::IsDamaged stand-in read simulationState).
if (BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8(this)
return;
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
if (source != 0 && !BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8(source): same mech -> same player
{
source->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3
source->outputVoltage = 0.0f; // source+0x1f8 = 0
}
}
//
// @004b0f48 -- base HeatSink flags plus 0x40 when this part has no usable
// voltage (the slot-0x40 virtual returns 0).
//
LWord
PoweredSubsystem::GetStatusFlags()
{
LWord flags = HeatSink::GetStatusFlags(); // FUN_004add30
if (!HasVoltage()) // virtual "has voltage" query, slot @vtable+0x40
{
flags |= 0x40;
}
return flags;
}
//
// Virtual "has usable voltage" query (vtable+0x40). Base powered subsystem
// reports voltage present whenever the electrical state machine is Ready.
//
Logical
PoweredSubsystem::HasVoltage()
{
return (electricalStateAlarm.GetLevel() == Ready) ? True : False;
}
//
// @004b1224 -- prints "<name> = <electrical state>".
//
void
PoweredSubsystem::PrintState()
{
HeatSink::PrintState(); // FUN_004ae050
switch (electricalStateAlarm.GetLevel()) // this+0x278
{
case Starting: DebugStream << GetName() << " = Starting" << endl; break;
case NoVoltage: DebugStream << GetName() << " = NoVoltage" << endl; break;
case Shorted: DebugStream << GetName() << " = Shorted" << endl; break;
case GeneratorOff: DebugStream << GetName() << " = GeneratorOff" << endl; break;
case Ready: DebugStream << GetName() << " = Ready" << endl; break;
default: DebugStream << GetName() << " Unknown Electrical State!" << endl; break;
}
}
//#############################################################################
// The cockpit POWER-ROUTING message handlers (task #12)
//
// @004b099c/@004b09e4/@004b0a2c/@004b0a74 -- one per generator; identical
// bodies (press only, undamaged only): find Generator N on the owner's
// SUBSYSTEM ROSTER, re-tap onto it, and drop the connect mode back to
// Connected (manual selection ends any auto-hunt).
//
void
PoweredSubsystem::SelectGenerator(int generator_number)
{
Subsystem *gen = FindGeneratorByNumber(generator_number); // FUN_004b0b18
if (gen == 0)
{
// the binary dereferences the result unguarded (authored mechs always
// carry generators A-D); fail loud instead of crashing.
Verify(False, "SelectGenerator: no such generator", __FILE__, __LINE__);
return;
}
int tap = AttachToVoltageSource(gen); // FUN_004b0dd8
modeAlarm.SetLevel(Connected); // FUN_0041bbd8(this+0x2b8, 1)
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
DEBUG_STREAM << "[gensel] " << GetName() << " -> " << gen->GetName()
<< (tap >= 0 ? " (tapped)" : " (REFUSED: no spare tap)") << std::endl;
}
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
void
PoweredSubsystem::SelectGeneratorAMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0) // FUN_004ac9c8 == 0 && press
SelectGenerator(1);
}
void
PoweredSubsystem::SelectGeneratorBMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0)
SelectGenerator(2);
}
void
PoweredSubsystem::SelectGeneratorCMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0)
SelectGenerator(3);
}
void
PoweredSubsystem::SelectGeneratorDMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (getenv("BT_FIRE_LOG"))
DEBUG_STREAM << "[gensel-rx] D on " << GetName()
<< " locked=" << BTPlayerRoleLocksAdvanced(owner)
<< " v=" << message->dataContents << std::endl;
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0)
SelectGenerator(4);
}
// diag (task #12): registration probe -- is the handler live in the set?
// (id-based Find only: the NAME Find strcmp-walks every slot including the
// uninitialized id-3 GAP slot between the root set and ours -- garbage
// entryName pointer -> AV. The dense-table gap is engine T0 behavior; the
// 1995 binary's own table has the same hole.)
void BTGenSelProbe()
{
Receiver::MessageHandlerSet &set = PoweredSubsystem::GetMessageHandlers();
Receiver::Handler byId = set.Find((Receiver::MessageID)7);
DEBUG_STREAM << "[gensel-reg] byId=" << (int)(byId != Receiver::NullHandler)
<< std::endl;
}
//
// @004b0abc -- Manual/Auto connect-mode toggle (press only, undamaged only):
// below AutoConnect -> arm the auto-hunt; at AutoConnect -> drop the current
// tap and go fully Manual (the pilot takes over routing).
//
void
PoweredSubsystem::ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (BTPlayerRoleLocksAdvanced(owner) || message->dataContents <= 0)
return;
if ((unsigned)modeAlarm.GetLevel() < (unsigned)AutoConnect) // this+0x2cc < 2
{
modeAlarm.SetLevel(AutoConnect); // 2
}
else if (modeAlarm.GetLevel() == AutoConnect)
{
DetachFromVoltageSource(); // FUN_004b0e30
modeAlarm.SetLevel(ManualConnect); // 0
}
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
DEBUG_STREAM << "[gensel] " << GetName() << " mode -> "
<< modeAlarm.GetLevel() << std::endl;
}
//
// @004b0b18 -- walk the owner's SUBSYSTEM ROSTER (mech+0x128, count +0x124)
// for the Generator segment whose generatorNumber (+0x1E0, from the name
// suffix 'A'..'D') matches. NULL when absent.
//
Subsystem *
PoweredSubsystem::FindGeneratorByNumber(int generator_number)
{
Mech *mech = (Mech *)owner; // this+0xD0
int count = mech->GetSubsystemCount(); // +0x124
for (int i = 0; i < count; ++i)
{
Subsystem *sub = mech->GetSubsystem(i); // +0x128[i]
if (sub != 0
&& sub->GetClassID() == RegisteredClass::GeneratorClassID // +4 == 0xbc1
&& ((Generator *)sub)->generatorNumber == generator_number) // +0x1E0
{
return sub;
}
}
return 0;
}
//
// BTPowerSourceFrame -- complete-type bridge for the AnimatedSourceLamp
// (btpbus/btebus generator-letter lamp). The binary's PowerSourceConnection
// sampler (@004c3258) resolves the subsystem's InputVoltage link and reads
// resolved+0x1E0 == Generator::generatorNumber (1..4 -> btpbus frames A..D; 0
// == OFF).
//
// The port cannot reproduce the binary's path -- it walked
// ResolveLink( AttributePointerOf(subsystem,"InputVoltage") )
// but AttributePointerOf now returns the WRONG slot: the BT_DEV_GAUGES audio
// attribute rows shifted the chained attribute ids, so the name->pointer lookup
// no longer lands on voltageSource@0x1D0 (the same regression that broke the
// CoolingLoop lamp -- see btl4gau2.cpp). The link therefore always resolved to
// 0 and every generator lamp drew frame 0 ("OFF") even though the master
// PoweredSubsystem ctor DID bind voltageSource to its Generator.
//
// This bridge takes the subsystem directly and reads the NAMED member via
// PoweredSubsystem::ResolveVoltageSource() (voltageSource.Resolve()), bypassing
// the attribute table entirely -- exactly the BTCoolingLoopFrame pattern.
//
int BTPowerSourceFrame(void *subsystem_v)
{
static int s_log = -1;
if (s_log < 0) s_log = getenv("BT_LOOP_LOG") ? 1 : 0;
static int s_n = 0;
int trace = s_log && (s_n++ < 40);
Subsystem *sub = (Subsystem *)subsystem_v;
if (sub == 0 || !sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations()))
{
if (trace) DEBUG_STREAM << "[busfeed] " << (sub ? sub->GetName() : "(null)")
<< " -> 0 (not a PoweredSubsystem)\n" << std::flush;
return 0;
}
Generator *gen = (Generator *)((PoweredSubsystem *)sub)->ResolveVoltageSource();
if (gen == 0)
{
if (trace) DEBUG_STREAM << "[busfeed] " << sub->GetName()
<< " -> 0 (voltageSource unbound)\n" << std::flush;
return 0;
}
if (gen->GetClassID() != RegisteredClass::GeneratorClassID)
{
if (trace) DEBUG_STREAM << "[busfeed] " << sub->GetName()
<< " -> 0 (source not a Generator)\n" << std::flush;
return 0;
}
int frame = gen->generatorNumber; // @0x1E0 (A..D)
if (trace) DEBUG_STREAM << "[busfeed] " << sub->GetName() << " -> frame " << frame
<< " (generator '" << gen->GetName() << "')\n" << std::flush;
return frame;
}
//
// BTGeneratorVoltage -- complete-type bridge for the GeneratorVoltageConnection
// (@004c3288), which drives the eng-page generator-voltage bar (evolt.pcc,
// ScalarBarGauge @004c721c) and the MyomerCluster seek-voltage graph. The
// binary resolved the subsystem's InputVoltage link and read resolved+0x1DC ==
// Generator::outputVoltage (0..12000, == MeasuredVoltage()). Same databinding
// trap as BTPowerSourceFrame: the port's ResolveLink(AttributePointerOf(...,
// "InputVoltage")) path is dead after the BT_DEV_GAUGES audio attribute rows
// shifted the chained ids, so the bar read 0. This reads the NAMED member via
// PoweredSubsystem::ResolveVoltageSource(), bypassing the attribute table.
//
Scalar BTGeneratorVoltage(void *subsystem_v)
{
static int s_log = -1;
if (s_log < 0) s_log = getenv("BT_LOOP_LOG") ? 1 : 0;
static int s_n = 0;
int trace = s_log && (s_n++ < 40);
Subsystem *sub = (Subsystem *)subsystem_v;
if (sub == 0 || !sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations()))
{
if (trace) DEBUG_STREAM << "[voltfeed] " << (sub ? sub->GetName() : "(null)")
<< " -> 0 (not a PoweredSubsystem)\n" << std::flush;
return 0.0f;
}
Generator *gen = (Generator *)((PoweredSubsystem *)sub)->ResolveVoltageSource();
if (gen == 0 || gen->GetClassID() != RegisteredClass::GeneratorClassID)
{
if (trace) DEBUG_STREAM << "[voltfeed] " << sub->GetName()
<< " -> 0 (no Generator)\n" << std::flush;
return 0.0f;
}
Scalar volts = gen->MeasuredVoltage(); // outputVoltage @0x1DC
if (trace) DEBUG_STREAM << "[voltfeed] " << sub->GetName() << " -> " << volts
<< "V (generator '" << gen->GetName() << "')\n" << std::flush;
return volts;
}
//#############################################################################
// Voltage-source linkage helpers
//
//
// @004b0dd8 -- claim a tap on 'source' if it has a spare one, drop any current
// link, then bind the connection object. Returns the new tap index, -1 on
// failure (source already fully loaded).
//
int
PoweredSubsystem::AttachToVoltageSource(Subsystem *source)
{
int tap;
Generator *gen = (Generator *)source; // the voltage source is a Generator
if (gen->currentTapCount < gen->maxTapCount) // source+0x1e8 < source+0x1e4
{
gen->currentTapCount += 1;
tap = gen->currentTapCount;
}
else
{
tap = -1;
}
if (tap >= 0)
{
DetachFromVoltageSource(); // FUN_004b0e30
voltageSource.Add(source); // (**(this[0x74]+4))(this+0x74, source)
}
return tap;
}
//
// @004b0e30 -- release our tap on the current source and raise the
// "disconnected" mode alarm.
//
void
PoweredSubsystem::DetachFromVoltageSource()
{
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
if (source != 0)
{
source->currentTapCount -= 1; // source+0x1e8 -= 1
voltageSource.Clear(); // FUN_00417a5c(this+0x1d0)
}
electricalStateAlarm.SetLevel(NoVoltage); // FUN_0041bbd8(this+0x264, 1) -- 0x264 IS electricalStateAlarm (was wrongly modeAlarm@0x2B8)
}
//
// @004b0b5c -- True when the (resolved or supplied) source is in the shorted
// state (state 2 in this query) and its measured voltage exceeds the short
// threshold.
//
Logical
PoweredSubsystem::IsSourceShorted(Subsystem *source)
{
if (source == 0)
{
source = voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
}
Generator *gen = (Generator *)source;
if (
gen != 0
&& gen->GeneratorStateOf() == 2 // source+0x210 == 2
&& fabsf(gen->MeasuredVoltage() - VoltageEpsilon) > VoltageEpsilon // source+0x1dc
)
{
return True;
}
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- PoweredSubsystem (@004b13ac)
//
// Parses the power-specific resource fields on top of the HeatSink record.
//
int
PoweredSubsystem::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->subsystemModelSize = 0x190; // resource+0x24
subsystem_resource->classID = RegisteredClass::PoweredSubsystemClassID; // 0x0bc2, resource+0x20
if (passes == 1)
{
// first pass: prime fields to "unset"
subsystem_resource->voltageSourceIndex = -1;
subsystem_resource->thermalResistivityCoefficient = ResourceUnset; // -1.0f
subsystem_resource->auxScreenPlacement = -1;
memset(subsystem_resource->auxScreenLabel, 0, 64); // resource+0x10C
memset(subsystem_resource->engScreenLabel, 0, 64); // resource+0x14C
subsystem_resource->startTime = ResourceUnset;
}
//
// "VoltageSource" -- segment name; resolve to a segment index (biased +2).
//
const char *voltageSourceName = "Unspecified";
int found = model_file->GetEntry(subsystem_name, "VoltageSource", &voltageSourceName);
if (!found && subsystem_resource->voltageSourceIndex == -1)
{
DebugStream << subsystem_name << " missing VoltageSource!";
return False;
}
if (strcmp(voltageSourceName, "Unspecified") != 0)
{
subsystem_resource->voltageSourceIndex =
Get_Segment_Index(model_file, model_name, directories, voltageSourceName); // FUN_004215b0
}
if (subsystem_resource->voltageSourceIndex < 0)
{
DebugStream << subsystem_name << " has an invalid voltage source!";
return False;
}
if (strcmp(voltageSourceName, "Unspecified") != 0)
{
subsystem_resource->voltageSourceIndex += 2;
}
if (
!model_file->GetEntry(subsystem_name, "ThermalResistivityCoefficient",
&subsystem_resource->thermalResistivityCoefficient)
&& subsystem_resource->thermalResistivityCoefficient == ResourceUnset
)
{
DebugStream << subsystem_name << " missing ThermalResistivityCoefficient!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "AuxScreenPlacement",
&subsystem_resource->auxScreenPlacement)
&& subsystem_resource->auxScreenPlacement == -1
)
{
subsystem_resource->auxScreenPlacement = -1; // left unset (optional)
}
if (
!model_file->GetEntry(subsystem_name, "AuxScreenNumber",
&subsystem_resource->auxScreenNumber)
&& subsystem_resource->auxScreenNumber == -1
)
{
DebugStream << subsystem_name << " missing AuxScreenNumber!";
return False;
}
const char *label;
if (model_file->GetEntry(subsystem_name, "AuxScreenLabel", &label))
{
strcpy(subsystem_resource->auxScreenLabel, label);
}
else if (subsystem_resource->auxScreenLabel[0] == '\0')
{
DebugStream << subsystem_name << " missing AuxScreenLabel!";
return False;
}
if (model_file->GetEntry(subsystem_name, "EngScreenLabel", &label))
{
strcpy(subsystem_resource->engScreenLabel, label);
}
else if (subsystem_resource->engScreenLabel[0] == '\0')
{
DebugStream << subsystem_name << " missing EngScreenLabel!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "StartTime", &subsystem_resource->startTime)
&& subsystem_resource->startTime == ResourceUnset
)
{
DebugStream << subsystem_name << " missing StartTime!";
return False;
}
Check_Fpu();
return True;
}
//###########################################################################
//###########################################################################
// (Myomers mislabel -- RETIRED, task #14)
//###########################################################################
//###########################################################################
//
// The "Myomers" block that lived here (ctor @4b1d18, classID 0xBC3,
// outputVoltage/powered/voltageAvailable) was a MISLABEL of Sensor (real
// reconstruction: sensor.cpp). Its Myomers::DefaultData / ctor / dtor /
// TestInstance / MyomersSimulation / ResetToInitialState / HandleMessage /
// CreateStreamedSubsystem definitions ODR-DUPLICATED the REAL Myomers
// (myomers.cpp, classID 0xBC6) -- /FORCE picked the winner by link order,
// so which SharedData live Myomers instances got was link-order-dependent
// (the same archetype as the fixed Condenser stub dedup). Retired whole.
//
//###########################################################################
//###########################################################################
// Generator
//###########################################################################
//###########################################################################
//#############################################################################
// Attribute Support (gauge data-binding wave)
//
// OutputVoltage -> outputVoltage@0x1DC (== MeasuredVoltage(); GeneratorSimulation
// drives it). Chained to HeatSink's dense index so the temps/coolant the
// GeneratorCluster also reads stay reachable. DefaultData below already calls
// GetAttributeIndex() (was resolving to the inherited HeatSink one).
//
const Generator::IndexEntry
Generator::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage), // @0x1DC
ATTRIBUTE_ENTRY(Generator, GeneratorState, stateAlarm), // @0x1FC (0x54 StateIndicator-compatible alarm; SetLevel fires audio)
ATTRIBUTE_ENTRY(Generator, GeneratorOn, generatorOn) // @0x1D4 (AudioLogicalTrigger; init 1 -- static until a shutdown writer drives it to 0)
};
Generator::AttributeIndexSet&
Generator::GetAttributeIndex()
{
static Generator::AttributeIndexSet attributeIndex(
ELEMENTS(Generator::AttributePointers),
Generator::AttributePointers,
HeatSink::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Shared Data Support
//
Generator::SharedData
Generator::DefaultData(
Generator::GetClassDerivations(),
Generator::GetMessageHandlers(),
Generator::GetAttributeIndex(),
Generator::StateCount
);
Derivation*
Generator::GetClassDerivations()
{
static Derivation classDerivations(HeatSink::GetClassDerivations(), "Generator");
return &classDerivations;
}
//
// @004b225c -- a generator is itself a HeatSink (it gets hot producing power).
// It starts on-line at rated voltage; the trailing letter of the segment name
// ("GeneratorA".."GeneratorD") becomes the generator number.
//
Generator::Generator(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, /*shared_data=*/DefaultData), // FUN_004adda0(..,&DAT_0050fb50,0,0)
stateAlarm(5) // FUN_0041b9ec(this+0x7f, 5)
{
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // owner+0x28 (gate reconcile)
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0
)
{
SetPerformance(&Generator::GeneratorSimulation); // PTR @0050fc14
}
ratedVoltage = subsystem_resource->ratedVoltage; // resource+0xFC
outputVoltage = ratedVoltage;
maxTapCount = subsystem_resource->maxTapCount; // resource+0x100
currentTapCount = 0;
percentVoltageAvailable = 1.0f;
startTime = subsystem_resource->startTime; // resource+0x104
startTimer = startTime;
stateAlarm.SetLevel(GeneratorReady); // SetLevel 2
generatorOn = 1;
shortRecoveryTime = subsystem_resource->shortRecoveryTime; // resource+0x108
shortTimer = shortRecoveryTime;
simulationFlags |= 0x8; // this[0xa] |= 8
// last character of the segment name -> generator number ('A'==0x41 -> 1)
const char *name = GetName(); // this[0x35]
generatorNumber = name[strlen(name) - 1] - 0x40; // FUN_004d4a78
Check_Fpu();
}
//
// @004b237c
//
Generator::~Generator()
{
Check(this);
// stateAlarm (@0x7f) released here; FUN_004adfd4 finishes the HeatSink chain.
Check_Fpu();
}
Logical Generator::TestClass(Mech &) { return True; }
Logical Generator::TestInstance() const { return IsDerivedFrom(*GetClassDerivations()); }
//
Receiver::MessageHandlerSet&
Generator::GetMessageHandlers()
{
//
// Gitea #53. Chains **HeatSink**, deliberately NOT PoweredSubsystem: id 4 on
// PoweredSubsystem is `SelectGeneratorA` (the weapon-side generator SELECTION,
// which already works), so chaining there would make the four ON/OFF buttons
// invoke the wrong function instead of doing nothing. HeatSink owns id 3
// (ToggleCooling) and chains onward to the root, leaving id 4 free for us.
//
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(Generator, ToggleGeneratorOnOff) // id 4 @004b1ed0
};
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(entries), entries,
HeatSink::GetMessageHandlers()
);
return messageHandlers;
}
//
// @004b1ed0 -- ToggleGeneratorOnOff: the pod's Generator A-D ON/OFF buttons (RIO
// addresses 0x1A-0x1D on the radar rail, one per generator). Transcribed
// instruction-for-instruction from the binary; the body did not exist before
// (Gitea #53 -- 0 references in game/).
//
// call 0x4ac9c8(this); if (ret) return; -> BTPlayerRoleLocksAdvanced: a
// NOVICE pilot cannot touch it
// if ([msg+0xc] <= 0) return; -> press only, ignore the release
// if ([this+0x1d4] != 0) goto OFF -> generatorOn -> we are turning it off
//
// ON: eax = [this+0x184] -> heatAlarm level
// if (eax >= 1) { [this+0x1f0] = 0; } -> heat degraded/failed: clear the
// start timer but LEAVE the state
// alarm in its fault level
// else { [this+0x1f0] = 0;
// Set_Alarm_Level(this+0x1fc, 0); }
// -> healthy: also drop the state alarm
// to Starting so it spins up
// [this+0x1d4] = 1 -> generatorOn = 1
// [this+0x134] = 1 -> coolantAvailable = 1
// [this+0x15c] = 1.0f -> coolantFlowScale = 1.0
//
// OFF: Set_Alarm_Level(this+0x1fc, 1) -> stateAlarm = GeneratorIdle
// [this+0x1d4] = 0 -> generatorOn = 0
// [this+0x134] = 0 -> coolantAvailable = 0
// [this+0x15c] = 0.0f -> coolantFlowScale = 0 (its coolant
// share is released to the loop)
//
// So taking a generator off line idles it, drops its coolant draw, and -- via the
// state machine reading generatorOn -- stops it supplying voltage to its taps.
// That is the authentic heat-management trade the manual describes.
//
void
Generator::ToggleGeneratorOnOffMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (BTPlayerRoleLocksAdvanced(owner)) // @004ac9c8 -- NOVICE lock
{
return;
}
if (message->dataContents <= 0) // press only
{
return;
}
if (generatorOn == 0)
{
// ---- turning it ON ----
startTimer = 0.0f; // @0x1F0
if ((unsigned)heatAlarm.GetLevel() < 1) // @0x184 -- healthy
{
stateAlarm.SetLevel(0); // @0x1FC -> Starting (spin up)
}
generatorOn = 1; // @0x1D4
coolantAvailable = 1; // @0x134
coolantFlowScale = 1.0f; // @0x15C
}
else
{
// ---- taking it OFF LINE ----
stateAlarm.SetLevel(1); // @0x1FC -> GeneratorIdle
generatorOn = 0; // @0x1D4
coolantAvailable = 0; // @0x134
coolantFlowScale = 0.0f; // @0x15C
}
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
DEBUG_STREAM << "[gensel] " << GetName() << " generator "
<< (generatorOn ? "ON LINE" : "OFF LINE")
<< " (stateAlarm " << stateAlarm.GetLevel()
<< ", coolantFlowScale " << coolantFlowScale << ")" << std::endl;
}
//
void
Generator::ResetToInitialState(Logical powered)
{
//
// RE-TRANSCRIBED FROM THE BINARY (Gitea #55). The old body chained
// `HeatableSubsystem::ResetToInitialState` and set `outputVoltage = 0` -- which
// is what GNRATOR.TCP says (see gnrator.hpp), but **BT's binary diverges from
// the TCP source here**, and the binary is what shipped in the pod.
//
// Disassembled @004b215c, instruction for instruction:
// call 0x4ad760 (this, powered) -> HeatSink::ResetToInitialState (NOT
// HeatableSubsystem -- so the coolant
// refill DOES run for a generator)
// [this+0x1d4] = 1 -> generatorOn = 1
// [this+0x1f0] = 0 -> startTimer = 0
// Set_Alarm_Level(this+0x1fc, 0) -> stateAlarm 0
// Set_Alarm_Level(this+0x1fc, 2) -> stateAlarm 2 (on line)
// eax = [this+0x1d8]; [this+0x1dc] = eax
// -> outputVoltage = ratedVoltage
// [this+0x134] = 1 -> coolantAvailable = 1
// [this+0x15c] = 0x3f800000 -> coolantFlowScale = 1.0f
// ecx = [this+0x1ec]; [this+0x1f0] = ecx
// -> startTimer = startTime
//
// Why this matters: the old version brought a generator back from a respawn at
// **ZERO output voltage** with its state alarm untouched. Energy weapons charge
// from the generator's voltage, so a respawned pilot's lasers could never reach
// ready -- no recharge ring, no fire-ready dot, nothing damaged (a strong
// candidate for Gitea #54, David's three dead energy weapons).
//
HeatSink::ResetToInitialState(powered); // @004ad760 (was HeatableSubsystem)
generatorOn = 1; // @0x1D4
startTimer = 0.0f; // @0x1F0 (cleared, then seeded below)
stateAlarm.SetLevel(0); // @0x1FC
stateAlarm.SetLevel(2); // ... to GeneratorOnLine
outputVoltage = ratedVoltage; // @0x1DC = @0x1D8 -- back ON LINE
coolantAvailable = 1; // @0x134
coolantFlowScale = 1.0f; // @0x15C
startTimer = startTime; // @0x1F0 = @0x1EC
}
//
// DeathReset (Gitea #55): the respawn sweep's entry for a Generator. Without it
// the sweep hit the empty `Subsystem::DeathReset` base and a respawned mech kept
// its previous life's generator state.
//
void
Generator::DeathReset(int /*reset_command*/)
{
ResetToInitialState(True); // @004b215c
}
//
// @004b1f7c -- generator state machine: spin-up, short recovery, and the
// resulting percent-of-rated voltage made available to attached loads.
//
void
Generator::GeneratorSimulation(Scalar time_slice)
{
HeatSink::HeatSinkSimulation(time_slice); // FUN_004ad924
if (simulationState == 1) // this[0x10] @0x40
{
stateAlarm.SetLevel(GeneratorIdle); // SetLevel 1
}
if (heatAlarm.GetLevel() == HeatSink::FailureHeat) // this+0x184 == 2
{
// The THERMAL BREAKER: output collapses to 0 until the generator cools
// past NormalHeat and re-spins (task #11 diag below shows the duty cycle).
if (stateAlarm.GetLevel() != GeneratorRecovered && getenv("BT_HEAT_LOG"))
DEBUG_STREAM << "[gen] " << GetName() << " TRIPPED (T="
<< currentTemperature << ")" << std::endl;
stateAlarm.SetLevel(GeneratorRecovered); // SetLevel 4
}
switch (stateAlarm.GetLevel()) // this[0x84] @0x210
{
case GeneratorStarting: // 0
outputVoltage = 0.0f;
startTimer += time_slice;
if (startTime <= startTimer)
{
stateAlarm.SetLevel(GeneratorReady); // 2
outputVoltage = (MaxOutputVoltage - SourceLevel()) * ratedVoltage; // (1.0 - *(this[0x38]+0x158)) * this[0x76]
}
break;
case GeneratorIdle: // 1
outputVoltage = 0.0f;
break;
case GeneratorShorted: // 3
outputVoltage = 0.0f;
shortTimer += time_slice;
if (shortRecoveryTime <= shortTimer)
{
stateAlarm.SetLevel(GeneratorReady); // 2
outputVoltage = (MaxOutputVoltage - SourceLevel()) * ratedVoltage;
}
break;
case GeneratorRecovered: // 4
outputVoltage = 0.0f;
if (heatAlarm.GetLevel() == HeatSink::NormalHeat) // this+0x184 == 0
{
if (generatorOn == 0) // this[0x75]
{
stateAlarm.SetLevel(GeneratorIdle); // 1
}
else
{
if (getenv("BT_HEAT_LOG"))
DEBUG_STREAM << "[gen] " << GetName() << " RESTARTING (T="
<< currentTemperature << ")" << std::endl;
startTimer = 0.0f;
stateAlarm.SetLevel(GeneratorStarting); // 0
}
}
break;
}
//
// percent of rated voltage actually available, clamped to [0, 1].
//
percentVoltageAvailable = outputVoltage / ratedVoltage; // this[0x74]
if (percentVoltageAvailable < MinOutputVoltage)
{
percentVoltageAvailable = MinOutputVoltage; // 0.0
}
else if (percentVoltageAvailable > MaxOutputVoltage)
{
percentVoltageAvailable = MaxOutputVoltage; // 1.0
}
}
//
// @004b21d0 -- when the mech's electrical bus is live: message 4 with the
// generator damaged/off drives it into the shorted-recovery state; any other
// message recomputes the output from the source level.
//
Logical
Generator::HandleMessage(int message)
{
HeatSink::HandleMessage(message); // FUN_004ad748
Mech *mech = (Mech *)owner;
if (BT_IsBusLive(mech)) // *(*(this[0x34]+0x190)+0x274) != 0
{
if (message == 4)
{
if ((simulationState == 2 || simulationState == 0) // this[0x10]
&& !BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8 == 0 (not novice; issue #2 -- was the state-based IsDamaged stand-in, which wrongly blocked state 2)
{
stateAlarm.SetLevel(GeneratorShorted); // 3
shortTimer = 0.0f; // this[0x7e]
}
}
else
{
outputVoltage = (MaxOutputVoltage - SourceLevel()) * ratedVoltage;
}
}
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- Generator (@004b23bc)
//
int
Generator::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->subsystemModelSize = 0x10c;
subsystem_resource->classID = RegisteredClass::GeneratorClassID; // 0x0bc1, resource+0x20
if (passes == 1)
{
subsystem_resource->ratedVoltage = ResourceUnset; // -1.0f
subsystem_resource->startTime = ResourceUnset;
subsystem_resource->shortRecoveryTime = ResourceUnset;
}
if (
!model_file->GetEntry(subsystem_name, "RatedVoltage", &subsystem_resource->ratedVoltage)
&& subsystem_resource->ratedVoltage == ResourceUnset
)
{
DebugStream << subsystem_name << " missing RatedVoltage!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "MaxTapCount", &subsystem_resource->maxTapCount)
&& subsystem_resource->maxTapCount == -1
)
{
DebugStream << subsystem_name << " missing MaxTapCount!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "StartTime", &subsystem_resource->startTime)
&& subsystem_resource->startTime == ResourceUnset
)
{
DebugStream << subsystem_name << " missing StartTime!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "ShortRecoveryTime", &subsystem_resource->shortRecoveryTime)
&& subsystem_resource->shortRecoveryTime == ResourceUnset
)
{
DebugStream << subsystem_name << " missing ShortRecoveryTime!";
return False;
}
Check_Fpu();
return True;
}
//###########################################################################
//###########################################################################
// PowerWatcher (BEST-EFFORT)
//###########################################################################
//###########################################################################
//
// NOTE: PowerWatcher's base class lies just below the captured decomp window
// (base ctor @004aeb40, base streamer @004aec54, base message/sim @004aea84 /
// @004aea9c). Only the power-specific override bodies were recovered; the
// base relationship is reconstructed and flagged best-effort.
//
PowerWatcher::SharedData
PowerWatcher::DefaultData(
PowerWatcher::GetClassDerivations(),
PowerWatcher::GetMessageHandlers(),
PowerWatcher::GetAttributeIndex(),
PowerWatcher::StateCount
);
Derivation*
PowerWatcher::GetClassDerivations()
{
// Chains to the REAL base HeatWatcher (@004aeb40) -- task #57: the old
// HeatableSubsystem stand-in predated the base-chain re-base and broke
// IsDerivedFrom(HeatWatcher) for every PowerWatcher descendant, which
// silently skipped the Torso/Searchlight/ThermalSight in the factory
// watcher-CONNECT pass (their watchdogs stayed at 0 = never Ready).
static Derivation classDerivations(HeatWatcher::GetClassDerivations(), "PowerWatcher");
return &classDerivations;
}
//
// @004b18a4 -- threshold = scale * resource->minVoltagePercent.
//
PowerWatcher::PowerWatcher(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatWatcher(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004aeb40 -- HeatWatcher base
watchdogAlarm(5) // FUN_0041b9ec(this+0x61, 5)
{
minVoltage = MinVoltageScale * subsystem_resource->minVoltagePercent; // _DAT_004b1924 * (resource+0xf0)
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // owner+0x28 (gate reconcile)
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0
)
{
SetPerformance(&PowerWatcher::Simulation); // PTR @0050f5fc
}
}
//
// @004b1930
//
PowerWatcher::~PowerWatcher()
{
Check(this);
Check_Fpu();
}
Logical PowerWatcher::TestClass(Mech &) { return True; }
Logical PowerWatcher::TestInstance() const { return IsDerivedFrom(*GetClassDerivations()); } // @004b1970
//
// @004b179c -- on message 4, if the watched source's source is shorted (and
// alive), force the short-recovery alarm; then chain to the base handler.
//
Logical
PowerWatcher::HandleMessage(int message)
{
if (message == 4)
{
Subsystem *watched = watchedLink.Resolve(); // FUN_00417ab4(this+0x45)
Generator *source = (watched != 0) // FUN_00417ab4(watched+0x1d0)
? (Generator *)((PoweredSubsystem *)watched)->ResolveVoltageSource()
: 0;
// @004b179c's FUN_004ac9c8(source) [T1] = the not-novice EXPERIENCE
// predicate on the source's owner (same mech as ours; issue #2 -- was
// the state-based IsDamaged stand-in).
if (source != 0 && !BTPlayerRoleLocksAdvanced(owner))
{
source->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3
source->outputVoltage = 0.0f; // source+0x1f8 = 0
}
}
return MechSubsystem::HandleMessage(message); // FUN_004aea84 -> FUN_004ac0bc = MechSubsystem::HandleMessage
}
//
// @004b1804 (slot 10) -- ResetToInitialState: chain to the HeatWatcher base.
// (This body was previously MISLABELED as the Simulation. The vtable
// @0050f99c slot 10 (offset 0x28) is 004b1804, exactly where every other
// subsystem carries ResetToInitialState; the registered Performance PTR
// @0050f5fc points at 004b181c -- UpdateWatch below.)
//
void
PowerWatcher::ResetToInitialState(Logical /*powered*/)
{
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c
}
//
// @004b181c -- the per-tick watch update: THE registered Performance
// (PTR @0050f5fc -> 004b181c), and called directly by the Torso master/copy
// simulations (@004b5cf0/@004b65f8 first line). Ghidra missed the function
// start; recovered from the raw pseudocode at part_013.c:1645:
//
// FUN_004aeac4(this); // heat mirror
// watched = watchedLink.Resolve(); // +0x114
// gen = watched->voltageSource.Resolve(); // +0x1D0
// watchdogAlarm.SetLevel(watched->elecLevel); // mirror +0x278
// if (elecLevel == 4 && gen && gen->outputVoltage(+0x1DC)
// <= minVoltage(+0x180) * gen->ratedVoltage(+0x1D8))
// watchdogAlarm.SetLevel(1); // brownout
//
// The watchdog MIRRORS the watched subsystem's electrical state -- this is
// what drives ElectricalStateLevel()==Ready on the Torso (the twist-rate
// power gate) and WatchedVoltageLevel on Searchlight/ThermalSight.
//
void
PowerWatcher::UpdateWatch()
{
WatchSimulation(0.0f); // FUN_004aeac4 -- heat-alarm mirror
PoweredSubsystem *watched = (PoweredSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
if (watched == 0)
{
// The binary derefs unconditionally (the link always binds on the
// master node). Null only on replicants / bring-up: hold Not-Ready.
watchdogAlarm.SetLevel(0);
return;
}
int level = watched->electricalStateAlarm.GetLevel(); // *(watched+0x278)
watchdogAlarm.SetLevel(level); // FUN_0041bbd8(this+0x184, level)
Generator *source = (Generator *)watched->ResolveVoltageSource(); // FUN_00417ab4(watched+0x1d0)
if (level == PoweredSubsystem::Ready && source != 0
&& source->MeasuredVoltage() <= minVoltage * source->RatedVoltageOf())
{
watchdogAlarm.SetLevel(1); // brownout: Ready but sagging source
}
}
//
// The Performance wrapper (PTR @0050f5fc -> @004b181c takes no dt).
//
void
PowerWatcher::Simulation(Scalar /*time_slice*/)
{
UpdateWatch();
}
//
// @004b198c -- reads "MinVoltagePercent".
//
int
PowerWatcher::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!HeatWatcher::CreateStreamedSubsystem( // FUN_004aec54 -- HeatWatcher parse
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = 0xf4;
subsystem_resource->classID = RegisteredClass::PowerWatcherClassID; // resource+0x20
if (passes == 1)
{
subsystem_resource->minVoltagePercent = ResourceUnset; // -1.0f
}
if (
!model_file->GetEntry(subsystem_name, "MinVoltagePercent",
&subsystem_resource->minVoltagePercent)
&& subsystem_resource->minVoltagePercent == ResourceUnset
)
{
DebugStream << subsystem_name << " missing MinVoltagePercent!";
return False;
}
return True;
}
//###########################################################################
//###########################################################################
// VoltageSourceConnection
//###########################################################################
//###########################################################################
//
// Small SharedData specialisation that links a PoweredSubsystem to its
// Generator segment (vtable @0050f994).
//
//
// @004b1bb1
//
VoltageSourceConnection::VoltageSourceConnection(int initial):
SubsystemConnection(initial) // FUN_004179d4
{
// *this = &PTR_FUN_0050f994
}
//
// @004b1bd0
//
VoltageSourceConnection::~VoltageSourceConnection()
{
// *this = &PTR_FUN_0050f994; FUN_004179f8(this, 0)
}
//===========================================================================//
// WAVE 3a factory bridges -- the power bus (Generator 0xBC1, PoweredSubsystem
// 0xBC2). Energy weapons resolve a real VoltageSource off these to charge.
//===========================================================================//
Subsystem *CreateGeneratorSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(Generator) <= 0x250);
return (Subsystem *) new (Memory::Allocate(0x250))
Generator(owner, id, (Generator::SubsystemResource *)seg, Generator::DefaultData);
}
Subsystem *CreatePoweredSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(PoweredSubsystem) <= 0x31c);
return (Subsystem *) new (Memory::Allocate(0x31c))
PoweredSubsystem(owner, id, (PoweredSubsystem::SubsystemResource *)seg, PoweredSubsystem::DefaultData);
}
//
// Bridge for the vehicleSubSystems gauge factory (btl4gau2.cpp). The engineering-
// screen assignment lives on PoweredSubsystem (auxScreenNumber / auxScreenPlacement
// / auxScreenLabel, resource +0x104/+0x108/+0x10C). Reading it from btl4gau2.cpp
// via a raw offset (sub+0x1dc) is wrong because raw offsets on our compiled
// objects are the databinding trap (2026-07-12: the "heat-leaf not byte-exact"
// wording is stale -- the leaf IS byte-exact since the sensor re-base,
// sensor.cpp:407-413 -- but the bridge stays: named members are the rule)
// -- so the gauge factory calls this bridge, which casts through
// the real PoweredSubsystem type and returns the NAMED fields. Returns False for
// non-PoweredSubsystem-derived subsystems (== the FUN_0041a1a4 / 0x50f4bc type
// filter the vehicleSubSystems Make applies before dispatch).
//
// FUN_0041a1a4(sub->classDerivations, 0x50f4bc) -- is the subsystem PoweredSubsystem-
// derived? (The vehicleSubSystems type filter + the generator stateLamp gate.)
bool BTIsPoweredSubsystem(Subsystem *sub)
{
return sub != NULL && sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations());
}
bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64)
{
if (!BTIsPoweredSubsystem(sub)) // FUN_0041a1a4(...,0x50f4bc)
return false;
PoweredSubsystem *ps = (PoweredSubsystem *)sub;
if (screen != NULL) *screen = ps->auxScreenNumber; // +0x104
if (placement != NULL) *placement = ps->auxScreenPlacement; // +0x108
if (label64 != NULL) strcpy(label64, ps->auxScreenLabel); // +0x10C
return true;
}
//
// issue #11 bridge -- the SeekVoltageGraph's destroyed test:
// *(subsystem+0x40) == 1 in the binary (@004c6934) == MechSubsystem::
// simulationState == 1 (Destroyed). Named-member read through the real type
// (databinding rule); every subsystem that owns a graph is MechSubsystem-
// derived (Emitter/PPC/Myomers), so the PoweredSubsystem gate is sufficient
// and a non-match reads "not destroyed".
//
int BTSubsystemDestroyed(Subsystem *sub)
{
if (!BTIsPoweredSubsystem(sub))
return 0;
return (((PoweredSubsystem *)sub)->GetSimulationState() == 1) ? 1 : 0; // +0x40 == 1
}