Files
BT411/game/reconstructed/powersub.hpp
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

658 lines
29 KiB
C++

//===========================================================================//
// File: powersub.hpp //
// 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 (Ghidra pseudo-C, module cluster
// @0x4b02f0 .. @0x4b2678). The single tagged function is the PoweredSubsystem
// constructor @004b0f74 (file=bt/powersub.cpp); the surrounding cluster was
// recovered by following the call graph, the class vtables and the
// "d:\tesla_bt\bt\POWERSUB.CPP" assertion strings (@0050f72d / @0050f780).
// See powersub.cpp for per-method @ADDR evidence.
//
// Method names and member names follow the embedded string pool
// (PoweredSubsystem, Generator, Myomers, PowerWatcher, the *ClassID enum,
// "VoltageSource", "ThermalResistivityCoefficient", "RatedVoltage", ...) and
// the sibling heat-subsystem source (heat.hpp / heat.cpp); the remainder is
// inferred from usage and flagged where uncertain. Field offsets noted in
// comments are the byte offsets observed in the decompiled object
// (e.g. "@0x1dc" == this[0x77]).
//
// This file derives from the heat subsystems: a PoweredSubsystem IS-A
// HeatSink (it dissipates heat while it consumes power), so the thermal
// machinery in heat.hpp/heat.cpp is the immediate base for everything here.
//
#if !defined(POWERSUB_HPP)
# define POWERSUB_HPP
#if !defined(HEAT_HPP)
# include <heat.hpp>
#endif
#if !defined(HEATFAMILY_RESLICE_HPP)
# include <heatfamily_reslice.hpp> // HeatWatcher -- PowerWatcher's real base (RESOURCE_AUDIT.md B)
#endif
//##################### Forward Class Declarations #######################
class Mech;
class Generator;
//###########################################################################
//######################### PoweredSubsystem ############################
//###########################################################################
//
// Base class for any subsystem that is driven from a voltage source (a
// Generator segment elsewhere in the mech). It tracks a startup timer, an
// electrical state machine (Starting -> Ready, with NoVoltage / Shorted /
// GeneratorOff fault states) and which generator currently powers it.
//
// vtable @0050f9d8, ctor @004b0f74, dtor @004b115c.
// RegisteredClass::PoweredSubsystemClassID == 0x0bc2 (resource +0x20),
// streamed model size 0x190 (resource +0x24).
//
class PoweredSubsystem:
public HeatSink
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation *GetClassDerivations(); // @0050f4bc
// task #12: the binary's PoweredSubsystem handler table @0x50F4EC --
// {4,"SelectGeneratorA"->@004b099c} .. {8,"ToggleGeneratorMode"->@004b0abc}.
// (The previous qualified calls resolved to the inherited engine
// Receiver::GetMessageHandlers() root set, so the generator-select
// messages had no handlers anywhere.) MechWeapon's set chains this
// one, so every weapon inherits the five handlers. The entry table is
// a function-local static INSIDE the accessor (static-init order).
static Receiver::MessageHandlerSet& GetMessageHandlers();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Messaging Support -- the cockpit POWER-ROUTING messages (task #12).
// Ids PINNED to the binary table (MechWeapon extends the shared id space
// with 9/10, the config-mode session). The aux-panel type-6 EventMappings
// deliver these to a weapon: assign it to Generator 1-4, or toggle its
// Manual/Auto reconnect mode.
//
public:
enum {
SelectGeneratorAMessageID = 4, // @004b099c
SelectGeneratorBMessageID = 5, // @004b09e4
SelectGeneratorCMessageID = 6, // @004b0a2c
SelectGeneratorDMessageID = 7, // @004b0a74
ToggleGeneratorModeMessageID = 8 // @004b0abc
};
// (ControlsButton == int, engine CONTROLS.h; spelled int here so TUs
// that include this header without the controls layer still compile.)
void
SelectGeneratorAMessageHandler(ReceiverDataMessageOf<int> *message),
SelectGeneratorBMessageHandler(ReceiverDataMessageOf<int> *message),
SelectGeneratorCMessageHandler(ReceiverDataMessageOf<int> *message),
SelectGeneratorDMessageHandler(ReceiverDataMessageOf<int> *message),
ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf<int> *message);
protected:
void
SelectGenerator(int generator_number); // the shared @004b099c.. body
Subsystem *
FindGeneratorByNumber(int generator_number); // @004b0b18
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Electrical state machine (electricalStateAlarm @0x264, level @0x278,
// reported by PrintState @004b1224).
//
public:
enum ElectricalState {
Starting = 0, // powering up; startTimer counting toward startTime
NoVoltage = 1, // voltage source missing / not resolvable
Shorted = 2, // source state == 3 (shorted)
GeneratorOff = 3, // source state == 1 or 4 (off / not ready)
Ready = 4 // powered and operating
};
// Three-level connection-mode indicator (modeAlarm @0x2b8, level @0x2cc).
// Drives the auto-reconnect search in the simulation.
enum ConnectMode {
ManualConnect = 0,
Connected = 1, // ctor default
AutoConnect = 2 // scan all GeneratorClassID segments and attach
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support (gauge data-binding wave)
//
// The engineering subsystem clusters bind PoweredSubsystem/InputVoltage --
// the power-source gate that decides whether the power-lamp / generator-
// voltage-bar / state-lamp sub-branch is even constructed (SubsystemCluster
// ctor skips it when InputVoltage resolves NULL). Published on top of
// HeatSink's dense index; Sensor/Myomers/MechWeapon chain to GetAttributeIndex()
// below and inherit it (so InputVoltage + the HeatSink temps reach every
// powered/weapon subsystem's cluster).
//
public:
enum {
InputVoltageAttributeID = HeatSink::NextAttributeID,
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
public:
static AttributeIndexSet& GetAttributeIndex();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Streamed model resource.
//
// Extends the HeatableSubsystem/HeatSink resource record. The
// power-specific fields begin at +0xFC (see CreateStreamedSubsystem
// @004b13ac).
//
struct PoweredSubsystem__SubsystemResource:
public HeatSink::SubsystemResource
{
int voltageSourceIndex; // +0xFC "VoltageSource" (segment index, +2 bias; -1 == unset)
Scalar thermalResistivityCoefficient; // +0x100 "ThermalResistivityCoefficient"
int auxScreenNumber; // +0x104 "AuxScreenNumber"
int auxScreenPlacement; // +0x108 "AuxScreenPlacement" (-1 == unset)
char auxScreenLabel[64]; // +0x10C "AuxScreenLabel"
char engScreenLabel[64]; // +0x14C "EngScreenLabel"
Scalar startTime; // +0x18C "StartTime"
};
typedef PoweredSubsystem__SubsystemResource SubsystemResource;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const; // @004b1208
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
PoweredSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @004b0f74
~PoweredSubsystem(); // @004b115c
static int
CreateStreamedSubsystem( // @004b13ac
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
typedef void
(PoweredSubsystem::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
PoweredSubsystemSimulation(Scalar time_slice); // @004b0bd0 (Performance, PTR @0050f5b0)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @0050f9d8)
//
public:
void
ResetToInitialState(Logical powered = True); // slot 10, @004b0e6c
// RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with
// the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo
// classes overrode it -- the whole heat/coolant/power family fell through
// to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the
// respawn reset was a silent no-op for them and coolant/heat/generator
// state persisted from the previous life. `ResetToInitialState` is NOT
// virtual here, so each class that has its own body needs its own
// forwarder (a derived class with no body inherits the nearest one --
// e.g. Reservoir, the coolant tank, correctly uses HeatSink's).
void
DeathReset(int reset_command);
Logical
HandleMessage(int message); // slot 9, @004b0efc
LWord
GetStatusFlags(); // slot 12, @004b0f48
void
PrintState(); // slot 13, @004b1224
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Voltage-source linkage helpers
//
public:
int
AttachToVoltageSource(Subsystem *source); // @004b0dd8 (returns tap index, -1 on fail)
void
DetachFromVoltageSource(); // @004b0e30
Logical
IsSourceShorted(Subsystem *source = 0); // @004b0b5c
// @004b11bc -- on a short event drive the live source's alarm to shorted.
void
ForceShortRecovery();
// virtual "has usable voltage" query (vtable+0x40).
virtual Logical
HasVoltage();
// resolve our powering source (used by PowerWatcher).
Subsystem*
ResolveVoltageSource() { return voltageSource.Resolve(); }
// @004b0d50 -- the charge time-scale for the powered charge integrators
// (Emitter::TrackSeekVoltage divides the source-voltage gap by this):
// voltageScale stretched by (1 + thermalResistivityCoefficient x the
// SOURCE generator's temperature rise) -- a hot generator recharges its
// weapons slower. Reads voltageScale@0x310 back every tick (task #10:
// the old "the binary never reads it back" claim was wrong).
Scalar
ChargeTimeScale();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data for the powered-subsystem class.
// Offsets are byte offsets into the shipped object.
//
public:
SubsystemConnection voltageSource; // @0x1D0 0xC reference to the powering Generator segment
int auxScreenNumber; // @0x1DC resource +0x104
int auxScreenPlacement; // @0x1E0 resource +0x108
char auxScreenLabel[64]; // @0x1E4 resource +0x10C
char engScreenLabel[64]; // @0x224 resource +0x14C
// The binary alarms are 0x54-byte AlarmIndicators (ctor @004b0f74:
// FUN_0041b9ec(param_1+0x99,5) @0x264 and (param_1+0xae,3) @0x2B8; level @+0x14
// == 0x278/0x2CC). Now byte-exact via GaugeAlarm54 -- part of the subsystem-tree
// alarm unification (the whole PoweredSubsystem subtree byte-exacted together).
GaugeAlarm54 electricalStateAlarm; // @0x264 5-level (ElectricalState); level @0x278 -> ends 0x2B8
GaugeAlarm54 modeAlarm; // @0x2B8 3-level (ConnectMode); level @0x2CC -> ends 0x30C
Scalar thermalResistivityCoefficient; // @0x30C resource +0x100
Scalar voltageScale; // @0x310 init 1.0f
Scalar startTime; // @0x314 resource +0x18C
Scalar startTimer; // @0x318 init = startTime, counts up while Starting -> ends 0x31C
friend struct PoweredSubsystemLayoutCheck;
};
struct PoweredSubsystemLayoutCheck {
static_assert(offsetof(PoweredSubsystem, voltageSource) == 0x1D0, "PoweredSubsystem::voltageSource @0x1D0");
static_assert(offsetof(PoweredSubsystem, auxScreenNumber) == 0x1DC, "PoweredSubsystem::auxScreenNumber @0x1DC (res +0x104)");
static_assert(offsetof(PoweredSubsystem, auxScreenLabel) == 0x1E4, "PoweredSubsystem::auxScreenLabel @0x1E4");
static_assert(offsetof(PoweredSubsystem, engScreenLabel) == 0x224, "PoweredSubsystem::engScreenLabel @0x224");
static_assert(offsetof(PoweredSubsystem, electricalStateAlarm) == 0x264, "PoweredSubsystem::electricalStateAlarm @0x264");
static_assert(offsetof(PoweredSubsystem, modeAlarm) == 0x2B8, "PoweredSubsystem::modeAlarm @0x2B8");
static_assert(offsetof(PoweredSubsystem, thermalResistivityCoefficient)== 0x30C, "PoweredSubsystem::thermalResistivityCoefficient @0x30C");
static_assert(sizeof(PoweredSubsystem) == 0x31C, "sizeof(PoweredSubsystem) 0x31C");
};
// (task #14: the "Myomers" class that lived here -- vtable 0050fb0c / ctor
// @4b1d18 / classID 0xBC3, members outputVoltage/powered/voltageAvailable --
// was a MISLABEL of **Sensor** (the real reconstruction is sensor.cpp/hpp;
// the real Myomers, classID 0xBC6, is myomers.cpp/hpp). Its definitions
// ODR-DUPLICATED ?DefaultData@Myomers@@ etc. against the real class, and
// /FORCE silently picked the winner by link order -- the real Myomers could
// receive this block's SharedData or vice versa. RETIRED whole.)
//###########################################################################
//############################# Generator ##############################
//###########################################################################
//
// A voltage SOURCE. Sibling of PoweredSubsystem (also a HeatSink, not a
// PoweredSubsystem): a generator spins up over StartTime, can be toggled
// on/off, and recovers from a short over ShortRecoveryTime. PoweredSubsystem
// segments resolve their "VoltageSource" name to one of these.
//
// vtable @0050fd74, ctor @004b225c, dtor @004b237c, shared data @0050fb50.
// RegisteredClass::GeneratorClassID == 0x0bc1 (matched by the auto-connect
// scan in PoweredSubsystemSimulation).
//
class Generator:
public HeatSink
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation *GetClassDerivations(); // @0050fb50 region
static Derivation ClassDerivations; // @0050f4bc datum (IsDerivedFrom operand)
static SharedData DefaultData;
void ForceShortRecovery(); // @004b11bc (short-event recovery)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support (gauge data-binding wave)
//
// The GeneratorCluster (engineering-screen buttons 9-12) binds Generator/
// OutputVoltage for its 0..12000 voltage bar. The temps (CurrentTemperature/
// DegradationTemperature/FailureTemperature/CoolantMassLeakRate) already reach
// it via the inherited HeatSink table; OutputVoltage is a real Generator member
// (@0x1DC, NOT the Emitter alias). Dense from HeatSink::NextAttributeID.
//
public:
enum {
OutputVoltageAttributeID = HeatSink::NextAttributeID,
GeneratorStateAttributeID, // audio binds an AudioStateWatcher -> stateAlarm (0x54 StateIndicator)
GeneratorOnAttributeID, // audio binds an AudioLogicalTrigger -> generatorOn (@0x1D4)
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
public:
static AttributeIndexSet& GetAttributeIndex();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Generator state machine (stateAlarm @0x1FC, level @0x210, 5 levels).
// Names best-effort from the embedded "GeneratorState" / message strings.
//
public:
enum GeneratorState {
GeneratorStarting = 0, // spinning up; startTimer -> startTime
GeneratorIdle = 1, // off / no output
GeneratorReady = 2, // producing RatedVoltage
GeneratorShorted = 3, // shorted; shortTimer -> shortRecoveryTime
GeneratorRecovered = 4 // TODO: confirm 4th/5th level names
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Streamed model resource (power-source fields begin at +0xFC).
//
struct Generator__SubsystemResource:
public HeatSink::SubsystemResource
{
Scalar ratedVoltage; // +0xFC "RatedVoltage"
int maxTapCount; // +0x100 "MaxTapCount" (max simultaneous loads, -1 == unset)
Scalar startTime; // +0x104 "StartTime"
Scalar shortRecoveryTime; // +0x108 "ShortRecoveryTime"
};
typedef Generator__SubsystemResource SubsystemResource;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
Generator(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @004b225c
~Generator(); // @004b237c
static int
CreateStreamedSubsystem( // @004b23bc
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation / overrides
//
public:
typedef void
(Generator::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
GeneratorSimulation(Scalar time_slice); // @004b1f7c (Performance, PTR @0050fc14)
Logical
HandleMessage(int message); // slot 9, @004b21d0
void
ResetToInitialState(Logical powered = True); // slot 10 -- recovered verbatim from GNRATOR.TCP
// RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with
// the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo
// classes overrode it -- the whole heat/coolant/power family fell through
// to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the
// respawn reset was a silent no-op for them and coolant/heat/generator
// state persisted from the previous life. `ResetToInitialState` is NOT
// virtual here, so each class that has its own body needs its own
// forwarder (a derived class with no body inherits the nearest one --
// e.g. Reservoir, the coolant tank, correctly uses HeatSink's).
void
DeathReset(int reset_command);
// Source-state queries used by PoweredSubsystem / PowerWatcher.
int
GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210
Scalar
MeasuredVoltage() { return outputVoltage; } // source+0x1dc
Scalar
RatedVoltageOf() { return ratedVoltage; } // source+0x1d8
// The generator's "short" state is its stateAlarm level (GeneratorShorted==3) +
// outputVoltage (IsSourceShorted @004b0b5c reads 0x210/0x1dc). There is NO
// separate shortFlag field on the Generator (it ends at 0x250 == stateAlarm end).
//
// task #12 CORRECTION (THE e17 HEAT EXPLOSION): the raw read
// *(this[0x38]+0x158) is the engine-base DamageZone pointer @0xE0 and
// the ZONE's damageLevel [0..1] -- the SAME named-member pattern as
// the bank radiator's zone read (heatfamily_reslice.cpp:138), NOT
// linkedSinks->heatEnergy. The old misread made a restarting
// generator emit (1.0 - heatEnergy~4e8) x 10000 volts; squared
// through the customers' I^2R feed, one generator breaker-restart
// blew the whole thermal network to e17 (observed live, task #12
// duty run). Authentic: a DAMAGED generator yields proportionally
// less voltage -- outputVoltage = (1 - zoneDamage) x ratedVoltage.
Scalar
SourceLevel()
{
::DamageZone *ownZone = this->Subsystem::damageZone; // @0xE0 (word 0x38)
return (ownZone != 0) ? (Scalar)ownZone->damageLevel : (Scalar)0.0f; // +0x158
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Message handling (Gitea #53 -- the Generator A-D ON/OFF buttons).
//
// Generator had NO handler set of its own, so every message aimed at it hit a
// default-constructed blackhole and was dropped in silence. Proved live with
// the new unhandled-message trace by pressing button 0x1A:
// [msg] UNHANDLED: Generator has no handler for message id 4
//
// The set MUST chain HeatSink, not PoweredSubsystem: on PoweredSubsystem id 4
// is `SelectGeneratorA` (a completely different function -- the weapon-side
// generator SELECTION that already works), so chaining there would make these
// four buttons do the WRONG thing rather than nothing. HeatSink owns id 3
// (ToggleCooling), leaving id 4 free.
//
public:
enum { ToggleGeneratorOnOffMessageID = 4 }; // table @0050fb90, fn @004b1ed0
static Receiver::MessageHandlerSet& GetMessageHandlers();
void
ToggleGeneratorOnOffMessageHandler(ReceiverDataMessageOf<int> *message);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data.
//
public:
Scalar percentVoltageAvailable; // @0x1D0 init 1.0f -- outputVoltage / ratedVoltage (clamped 0..1)
int generatorOn; // @0x1D4 init 1
Scalar ratedVoltage; // @0x1D8 resource +0xFC
Scalar outputVoltage; // @0x1DC init = ratedVoltage
int generatorNumber; // @0x1E0 derived from segment-name suffix ('A' -> 1 ...)
int maxTapCount; // @0x1E4 resource +0x100
int currentTapCount; // @0x1E8 init 0 -- attached loads
Scalar startTime; // @0x1EC resource +0x104
Scalar startTimer; // @0x1F0 init = startTime
Scalar shortRecoveryTime; // @0x1F4 resource +0x108
Scalar shortTimer; // @0x1F8 init = shortRecoveryTime
GaugeAlarm54 stateAlarm; // @0x1FC 0x54 alarm, 5-level (GeneratorState); level @0x210 -> ends 0x250
// stateAlarm is the LAST field (ctor @004b225c: FUN_0041b9ec(param_1+0x7f,5) at 0x1FC).
// The old shortFlag@0x25C was a phantom: HandleMessage @004b0efc reads *(owner+0x190)+0x25c
// (the message-manager's short-event flag), NOT a Generator field -- Generator ends at 0x250.
friend struct GeneratorLayoutCheck;
};
struct GeneratorLayoutCheck {
static_assert(offsetof(Generator, percentVoltageAvailable) == 0x1D0, "Generator::percentVoltageAvailable @0x1D0 (word 0x74)");
static_assert(offsetof(Generator, outputVoltage) == 0x1DC, "Generator::outputVoltage @0x1DC (word 0x77)");
static_assert(offsetof(Generator, stateAlarm) == 0x1FC, "Generator::stateAlarm @0x1FC (word 0x7f)");
static_assert(sizeof(Generator) == 0x250, "sizeof(Generator) 0x250 (factory alloc, stateAlarm is last)");
};
//###########################################################################
//############################ PowerWatcher #############################
//###########################################################################
//
// A lightweight monitor subsystem that watches its voltage source and raises
// a "BadVoltage" / self-test alarm when the available voltage drops below
// MinVoltagePercent of rated. Used to drive a cockpit warning gauge.
// BEST-EFFORT: its base lies just below the captured window (base ctor
// @004aeb40, in the heat/sensor cluster); only the power-specific override
// bodies were recovered.
//
// vtable @0050f99c, ctor @004b18a4, dtor @004b1930.
// RegisteredClass classID 0x0bd5 (resource +0x20), model size 0xF4.
//
class PowerWatcher:
public HeatWatcher // RESOURCE-LAYOUT FIX (RESOURCE_AUDIT.md root cause B): real
// base is HeatWatcher (ctor chains @004aeb40, parse @004aec54);
// was approximated as HeatableSubsystem, which sized the resource
// at 0xFC and slid minVoltagePercent to 0xFC instead of 0xF0.
{
public:
static Derivation *GetClassDerivations(); // @0050f5cc
static SharedData DefaultData;
struct PowerWatcher__SubsystemResource:
public HeatWatcher::SubsystemResource // HeatWatcher record ends 0xF0
{
Scalar minVoltagePercent; // +0xF0 "MinVoltagePercent"
}; // ends 0xF4
typedef PowerWatcher__SubsystemResource SubsystemResource;
static_assert(offsetof(PowerWatcher__SubsystemResource, minVoltagePercent) == 0xF0, "PowerWatcher minVoltagePercent must be at 0xF0");
static_assert(sizeof(PowerWatcher__SubsystemResource) == 0xF4, "PowerWatcher record must end at 0xF4");
typedef void
(PowerWatcher::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
static Logical TestClass(Mech&);
Logical TestInstance() const; // @004b1970
PowerWatcher(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @004b18a4
~PowerWatcher(); // @004b1930
static int
CreateStreamedSubsystem( // @004b198c
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
Logical
HandleMessage(int message); // @004b179c (chains to base @004aea84)
void
ResetToInitialState(Logical powered); // @004b1804 (slot 10; chains to base @004aea9c)
void
UpdateWatch(); // @004b181c -- watchdog mirror of the watched
// subsystem's electrical state (+ brownout);
// the Torso sims call this directly
void
Simulation(Scalar time_slice); // the registered Performance (PTR @0050f5fc
// -> @004b181c): forwards to UpdateWatch()
public:
// BASE-CHAIN RE-BASE: the shadow watchedLink was DELETED -- the binary has a
// SINGLE watchedLink, inherited from HeatWatcher@0x114. PowerWatcher own
// fields now follow HeatWatcher (which ends 0x180): minVoltage@0x180, then a
// 0x54-byte WatcherGaugeAlarm@0x184 that ends exactly at 0x1D8 = Torso start.
Scalar minVoltage; // @0x180 init = MinVoltageScale * resource->minVoltagePercent
WatcherGaugeAlarm watchdogAlarm; // @0x184 (0x54) 5-level self-test -> ends 0x1D8
};
// BASE-CHAIN RE-BASE locks: PowerWatcher own = minVoltage@0x180 + watchdogAlarm@0x184;
// sizeof==0x1D8 proves the shared Watcher base ends exactly where Torso own-fields
// (currentTwist@0x1D8, the gyro cross-link target) begin.
static_assert(offsetof(PowerWatcher, minVoltage) == 0x180, "PowerWatcher minVoltage must be at 0x180");
static_assert(offsetof(PowerWatcher, watchdogAlarm)== 0x184, "PowerWatcher watchdogAlarm must be at 0x184");
static_assert(sizeof(PowerWatcher) == 0x1D8, "PowerWatcher must end at 0x1D8 (== Torso currentTwist)");
//###########################################################################
//####################### VoltageSourceConnection #######################
//###########################################################################
//
// The small ref-counted handle that links a PoweredSubsystem to its powering
// Generator segment. Functionally a specialised SharedData (cf. HeatSink's
// linkedSinks). vtable @0050f994, ctor @004b1bb1, dtor @004b1bd0.
//
class VoltageSourceConnection:
public SubsystemConnection
{
public:
VoltageSourceConnection(int initial = 0); // @004b1bb1
~VoltageSourceConnection(); // @004b1bd0
};
//###########################################################################
//####### Cross-family helpers provided by the Mech game layer ##########
//###########################################################################
//
// These reach into the owning Mech (subsystem message manager / power bus /
// myomer back-pointer). They are NOT part of the heat/power family -- they
// are declared here (definitions live in the mech family) so the recovered
// power bodies link against them without this family editing mech.hpp.
// See "CROSS-FAMILY NEEDS" in the porting notes.
//
class SubsystemMessageManager;
SubsystemMessageManager* BT_GetMessageManager(Mech *owner); // *(owner+0x190)
Logical BT_IsBusLive(Mech *owner); // *(*(owner+0x190)+0x274)
void BT_ClearMyomers(Mech *owner); // owner+0x374 = 0
#endif