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:
co-authored by
Claude Fable 5
parent
092408041c
commit
d7b900d108
+160
-11
@@ -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
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user