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
+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
//