Cockpit: GENERATOR POWER ROUTING -- assign weapons to generators (task #12)

The pilot's second heat-management tool: the PoweredSubsystem message table
@0x50F4EC (ids 4-8) registered and implemented -- SelectGeneratorA-D
(@004b099c..@004b0a74: FindGeneratorByNumber roster walk on generatorNumber
@0x1E0 -> AttachToVoltageSource re-tap with tap accounting -> modeAlarm
Connected) and ToggleGeneratorMode (@004b0abc: Manual -> Auto -> detach+Manual
cycle).  Weapons inherit via the MechWeapon handler chain.  Desktop: F5-F8
assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
reconnect mode; BT_GENSEL_TEST scripts a headless verify.

Verified live end-to-end: dispatch -> handler -> re-tap ("PPC_1 ->
GeneratorD (tapped)") -> the charging I^2R physically moved (GenD cold ~90K
baseline -> ~1570K carrying the PPC; GenA relieved), stable over a sustained
autofire soak with thermal-breaker trips.

FOUR defects found and fixed on the way [all T1/T2]:
- THE e17 HEAT EXPLOSION: Generator::SourceLevel misread *(this[0x38]+0x158)
  as linkedSinks->heatEnergy; it is the engine-base DamageZone @0xE0 ->
  damageLevel [0..1] (the same named-member pattern as the bank radiator's
  zone read).  A breaker-restarting generator emitted (1 - 4e8) x 10000
  volts; squared through the customers' I^2R feed, one restart blew the
  whole thermal network to e17.  Authentic: a damaged generator yields
  proportionally less voltage.
- FUN_004ac9c8 is NOT "IsDamaged": raw body = owner -> mech+0x190 player ->
  roleClassIndex(+0x274) == 0 -- the ROOKIE-role lockout for advanced
  cockpit systems.  New bridge BTPlayerRoleLocksAdvanced (NULL player =
  unlocked [T3]; bring-up role 2 = unlocked).  The old stand-in gated the
  handlers off permanently (healthy subsystems have simulationState==1).
- MESSAGE_ENTRY tables must be FUNCTION-LOCAL statics inside the accessor:
  as namespace-scope arrays they are read by other TUs' static-init chains
  before their own initializers run -- Build copies zeros and every id in
  the table is silently dropped (ids 9/10 only worked by TU-order luck;
  both tables relocated; gotcha recorded, reconstruction-gotchas #9).
- The AutoConnect hunt scanned GetSegment() -- the raw @004b0bd0 walks the
  subsystem ROSTER (+0x124/+0x128); EntitySegments were being cast to
  Subsystems (the classic +0x128 gotcha).

Also: the dense handler table's GAP slots (skipped ids) are uninitialized
heap -- the name-based Find strcmp-walks them and AVs; diagnostic probes use
the id-based Find only (the 1995 binary's own tables carry the same holes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 23:16:48 -05:00
co-authored by Claude Fable 5
parent 092408041c
commit d7b900d108
9 changed files with 355 additions and 29 deletions
+23 -2
View File
@@ -175,8 +175,29 @@ authentic path scoped.
the only MechWeapon attribute" claim was wrong (fixed in mechweap.hpp).
* Tails: weapon+0x110 (write 0/-1 on session open/close; port name vitalSubsystemIndex)
has NO confirmed reader yet; @004d1acc (+0x48 L4 Notify override) is unexported —
re-export from Ghidra; the PoweredSubsystem generator-select handlers (ids 4-8,
@004b099c..@004b0abc) are decoded but not yet registered in the port.
re-export from Ghidra.
* **Task #12 (2026-07-11) [T2]: the PoweredSubsystem POWER-ROUTING handlers landed** --
ids 4-8 registered (table @0x50F4EC): SelectGeneratorA-D = FindGeneratorByNumber
(@004b0b18 ROSTER walk matching generatorNumber@0x1E0) -> AttachToVoltageSource ->
modeAlarm=Connected; ToggleGeneratorMode (@004b0abc) = <2 -> AutoConnect, ==2 ->
Detach + ManualConnect. Weapons inherit via the MechWeapon handler chain (the old
qualified GetMessageHandlers() calls silently resolved to the engine Receiver ROOT
set -- same trap as the task #6 weapon handlers). The AutoConnect hunt's
GetSegment() scan corrected to the subsystem ROSTER (the +0x128 gotcha). Desktop:
F5-F8 assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
Manual/Auto; BT_GENSEL_TEST scripts a headless re-tap. Note: Myomers/Sensor still
chain the root set (their binary tables unverified) -- extend if aux maps prove
they receive these ids. TWO MORE TRAPS found landing this: (a) MESSAGE_ENTRY
tables must be function-local statics INSIDE the accessor (cross-TU static-init
order emptied the table -- see [[reconstruction-gotchas]] #9 last bullet); (b)
**FUN_004ac9c8 is NOT IsDamaged** -- raw body: `owner(+0xD0) -> mech+0x190 player
-> roleClassIndex(+0x274) == 0` = the ROOKIE-role lockout for advanced cockpit
systems (port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp; NULL player =
unlocked [T3]; bring-up role defaults to 2 = unlocked). AUDIT TAIL: the other
powersub.cpp sites annotated FUN_004ac9c8 (the coolant-draw gate ~:427,
ForceShortRecovery ~:444, the Generator/PowerWatcher site ~:1085) still call the
heat-family IsDamaged (simulationState != 0) stand-in -- each needs its raw fn
re-checked and swapped to the role bridge where the binary calls 4ac9c8.
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
Real coupling needs the advanced-damage gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning
**BTPlayer** `mech+0x190`+0x260, NOT 0xBD3 — see the mech+0x190 item above) + `MoverAttach` routing
+12
View File
@@ -160,6 +160,18 @@ real PoweredSubsystem). Keep the alloc SIZE + special-cache when swapping a fact
Receiver::NextMessageID` early branch does NOT save you — it lacks a `return`, and the console IDs
aren't in that range anyway.) [T2]
- **MESSAGE_ENTRY tables must be FUNCTION-LOCAL statics inside the GetMessageHandlers()
accessor** (task #12). A namespace-scope `HandlerEntry MessageHandlerEntries[]` can be read
by ANOTHER TU's static-init chain (DefaultData -> accessor -> Build) before its own TU's
dynamic initializers run -- Build copies ZEROS, and every id in that table is silently
dropped at dispatch (the set LOOKS built; ids added later in the chain still work, which
hides it). Symptom: message transmitted, handler never runs, no error. The engine's own
APP.cpp idiom (table + set both function-local in the accessor) is init-order-proof --
always use it. Related trap: the dense handler table (Build indexes slots by id-1) leaves
GAP slots (skipped ids) as uninitialized heap -- the NAME-based `Find(const char*)`
strcmp-walks every slot and AVs on a gap's garbage entryName (the id-based Find is safe).
The 1995 binary's own tables carry the same holes. [T2]
## 10. Container-Execute must override (gauges)
The 2007 engine `Gauge::Execute` base is `Fail("not overridden")``abort()` (GAUGE.cpp:598);
+25
View File
@@ -1467,3 +1467,28 @@ void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP deat
victim_player->Dispatch(&dead);
}
}
//#############################################################################
// BTPlayerRoleLocksAdvanced -- complete-type bridge (task #12)
//
// FUN_004ac9c8 [T1]: `return *(*(*(sub+0xD0) + 0x190) + 0x274) == 0` -- the
// subsystem's owner Mech -> the owning BTPlayer (mech+0x190, GetPlayerLink)
// -> roleClassIndex (+0x274, role resource +0xE4). TRUE = the ROOKIE role
// (class 0) LOCKS the advanced cockpit systems (generator routing, coolant
// valves); nonzero role classes unlock them. The old reconstruction
// mislabeled this fn "Subsystem::IsDamaged" -- with healthy subsystems at
// simulationState==1, that stand-in gated the power-routing handlers OFF
// permanently. A NULL player (the target dummy / unbound solo mech) would AV
// in the binary (every pod mech has a player); the port reads NULL as
// UNLOCKED so dev rigs work [T3].
//
int BTPlayerRoleLocksAdvanced(void *owner_mech)
{
if (owner_mech == 0)
return 0;
BTPlayer *player = MECH_OWNING_PLAYER(owner_mech);
if (player == 0)
return 0; // dev-permissive [T3]
return (player->roleClassIndex == 0) ? 1 : 0; // player+0x274 == 0
}
+1
View File
@@ -373,6 +373,7 @@ class DropZone__ReplyMessage;
roleReturnDelay2; // @0x26c
int
roleClassIndex; // @0x274 (role resource +0xe4)
friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12)
int
killCount; // @0x27c kills credited to this player
+72
View File
@@ -645,6 +645,8 @@ static int gBTLaserKey = 0; // raw key states (set by the keyboar
static int gBTPPCKey = 0;
static int gBTMissileKey = 0;
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
// (0 idle; else the message id 4..8)
// Damage: each shot dispatches a REAL Entity::TakeDamageMessage to the target. Now
// that the damage zones are constructed (mech.cpp Pass-3 zone build), the engine base
@@ -1529,6 +1531,15 @@ void
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
// held, the fire keys TOGGLE its group membership.
gBTConfigKey = focused && (pAsync('G') & dn) ? 1 : 0;
// task #12: F5..F8 assign the selected weapon to Generator
// A..D; F9 toggles Manual/Auto reconnect.
gBTGenSelKey =
(focused && (pAsync(0x74 /*F5*/) & dn)) ? 4
: (focused && (pAsync(0x75 /*F6*/) & dn)) ? 5
: (focused && (pAsync(0x76 /*F7*/) & dn)) ? 6
: (focused && (pAsync(0x77 /*F8*/) & dn)) ? 7
: (focused && (pAsync(0x78 /*F9*/) & dn)) ? 8
: 0;
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// damage dispatcher + the beam-visual keepalive)
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0;
@@ -3101,6 +3112,67 @@ void
}
// task #12 scripted verify (BT_GENSEL_TEST=1): one SelectGeneratorD
// press at frame ~2500 -- the PPC re-taps from its authored GeneratorA
// onto GeneratorD; the charging I^2R then lands on GenD ([heat-t]).
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_GENSEL_TEST"))
{
static int s_gsFrame = 0;
++s_gsFrame;
gBTGenSelKey = (s_gsFrame >= 600 && s_gsFrame < 610) ? 7 : 0;
}
// task #12 dev harness: the POWER-ROUTING buttons. F5..F9 edge ->
// dispatch SelectGeneratorA..D / ToggleGeneratorMode (ids 4..8) to the
// selected weapon -- the same ReceiverDataMessageOf<ControlsButton>
// press payload the pod's aux-panel EventMappings deliver. The
// handler chain is PoweredSubsystem's (weapons inherit via MechWeapon).
if ((Entity *)this == application->GetViewpointEntity())
{
static int s_prevGenSel = 0;
if (gBTGenSelKey != s_prevGenSel)
{
int pressedID = gBTGenSelKey; // 0 on release
s_prevGenSel = gBTGenSelKey;
if (pressedID != 0)
{
Subsystem *weapon = 0;
const char *slotEnv = getenv("BT_CONFIG_SLOT");
int wantSlot = (slotEnv != 0) ? atoi(slotEnv) : -1;
for (int s = 1; s < GetSubsystemCount(); ++s)
{
Subsystem *sub = GetSubsystem(s);
if (sub == 0)
continue;
int cid = (int)sub->GetClassID();
if (wantSlot >= 0 ? (s == wantSlot)
: (cid == 0xBC8 || cid == 0xBCA || cid == 0xBCE
|| cid == 0xBD0 || cid == 0xBD4))
{
weapon = sub;
break;
}
}
if (weapon != 0)
{
if (getenv("BT_FIRE_LOG"))
{
extern void BTGenSelProbe();
BTGenSelProbe();
DEBUG_STREAM << "[gensel-tx] id=" << pressedID
<< " -> " << weapon->GetName() << std::endl;
}
ReceiverDataMessageOf<ControlsButton> msg(
pressedID /*4..8 (powersub.hpp ids)*/,
sizeof(ReceiverDataMessageOf<ControlsButton>),
(ControlsButton)1 /*press*/);
weapon->Dispatch(&msg);
}
}
}
}
// task #6 dev harness: the config-session BRACKET. 'G' edge ->
// dispatch ConfigureMappables (id 9) press/release to the selected
// weapon -- the exact ReceiverDataMessageOf<ControlsButton> payload
+9 -9
View File
@@ -84,20 +84,20 @@ Derivation
// task #6 -- the binary's MechWeapon handler table @0x511860 (section_dump:
// {9, "ConfigureMappables"@0x511994, @004b9550}, {10, "ChooseButton"@0x5119A7,
// @004b95b8}); chained onto the PoweredSubsystem set.
const Receiver::HandlerEntry
MechWeapon::MessageHandlerEntries[]=
// @004b95b8}); chained onto the PoweredSubsystem set. Table INSIDE the
// accessor (function-local static) -- the cross-TU static-init order trap
// emptied the powersub table this same way (task #12); this one only worked
// by TU-order luck.
Receiver::MessageHandlerSet&
MechWeapon::GetMessageHandlers()
{
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550
MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8
};
Receiver::MessageHandlerSet&
MechWeapon::GetMessageHandlers()
{
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(MechWeapon::MessageHandlerEntries),
MechWeapon::MessageHandlerEntries,
ELEMENTS(entries), entries,
PoweredSubsystem::GetMessageHandlers()
);
return messageHandlers;
+2 -3
View File
@@ -107,9 +107,8 @@ class CockpitHud;
static Derivation ClassDerivations;
// task #6: the binary registers TWO handlers ({9,"ConfigureMappables"},
// {10,"ChooseButton"} -- table @0x511860); the old empty static set
// meant no pilot input could ever reach the config session. The
// accessor idiom (function-local static) dodges cross-TU init order.
static const Receiver::HandlerEntry MessageHandlerEntries[];
// meant no pilot input could ever reach the config session. Table +
// set both function-local statics in the accessor (init-order-proof).
static Receiver::MessageHandlerSet& GetMessageHandlers();
static AttributeIndexSet AttributeIndex;
static SharedData DefaultData;
+160 -11
View File
@@ -33,7 +33,9 @@
// FUN_004add30 HeatSink::GetStatusFlags
// FUN_004ac868 HeatableSubsystem::~HeatableSubsystem (chained from PowerController dtor)
// FUN_004ac530 Subsystem ctor (PowerController base)
// FUN_004ac9c8 Subsystem::IsDamaged() (returns True when the part is dead)
// 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
@@ -110,6 +112,35 @@ PoweredSubsystem::AttributeIndexSet&
//#############################################################################
// 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
};
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(entries), entries,
Receiver::GetMessageHandlers()
);
return messageHandlers;
}
PoweredSubsystem::SharedData
PoweredSubsystem::DefaultData(
PoweredSubsystem::GetClassDerivations(),
@@ -317,19 +348,19 @@ void
&& 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;
for (int i = 0; ; ++i) // owner+0x124 segment scan
int count = mech->GetSubsystemCount(); // owner+0x124
for (int i = 0; i < count; ++i)
{
EntitySegment *segment_node = mech->GetSegment(i); // owner+0x128[i]
if (segment_node == 0)
{
break;
}
Subsystem *segment = (Subsystem *)segment_node;
if (
segment->GetClassID() == RegisteredClass::GeneratorClassID // segment+4 == 0xbc1
Subsystem *sub = mech->GetSubsystem(i); // owner+0x128[i]
if (sub != 0
&& sub->GetClassID() == RegisteredClass::GeneratorClassID // +4 == 0xbc1
&& GetStatusFlags() != 0
&& AttachToVoltageSource(segment) != -1 // FUN_004b0dd8
&& AttachToVoltageSource(sub) != -1 // FUN_004b0dd8
)
{
break;
@@ -497,6 +528,124 @@ void
}
//#############################################################################
// 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;
}
//#############################################################################
// Voltage-source linkage helpers
//
+50 -3
View File
@@ -67,8 +67,45 @@ class Generator;
//
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).
@@ -469,12 +506,22 @@ class Generator;
// 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).
// SourceLevel == linked master sink's stored heat energy (this[0x38]+0x158).
//
// 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()
{
HeatSink *master = (HeatSink *)linkedSinks.Resolve();
return (master != 0) ? master->heatEnergy : (Scalar)0.0f;
::DamageZone *ownZone = this->Subsystem::damageZone; // @0xE0 (word 0x38)
return (ownZone != 0) ? (Scalar)ownZone->damageLevel : (Scalar)0.0f; // +0x158
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~