From d7b900d108a715f772fd04c5d97a31d059ac7532 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 23:16:48 -0500 Subject: [PATCH] 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 --- context/open-questions.md | 25 ++++- context/reconstruction-gotchas.md | 12 +++ game/reconstructed/btplayer.cpp | 25 +++++ game/reconstructed/btplayer.hpp | 1 + game/reconstructed/mech4.cpp | 72 +++++++++++++ game/reconstructed/mechweap.cpp | 20 ++-- game/reconstructed/mechweap.hpp | 5 +- game/reconstructed/powersub.cpp | 171 ++++++++++++++++++++++++++++-- game/reconstructed/powersub.hpp | 53 ++++++++- 9 files changed, 355 insertions(+), 29 deletions(-) diff --git a/context/open-questions.md b/context/open-questions.md index eef5aea..c86fff0 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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 diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 6908198..57a910b 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -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); diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 56dafed..a817add 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -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 +} diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 75ad6b4..8add2a6 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 5478484..69c2109 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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 + // 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 msg( + pressedID /*4..8 (powersub.hpp ids)*/, + sizeof(ReceiverDataMessageOf), + (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 payload diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 3f945dc..01ca3b2 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -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[]= -{ - MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550 - MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8 -}; - +// @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 + }; static Receiver::MessageHandlerSet messageHandlers( - ELEMENTS(MechWeapon::MessageHandlerEntries), - MechWeapon::MessageHandlerEntries, + ELEMENTS(entries), entries, PoweredSubsystem::GetMessageHandlers() ); return messageHandlers; diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 0813bc9..a0fe84c 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -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; diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 228aa87..c45f3bd 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -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 *message) +{ + if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0) // FUN_004ac9c8 == 0 && press + SelectGenerator(1); +} +void + PoweredSubsystem::SelectGeneratorBMessageHandler(ReceiverDataMessageOf *message) +{ + if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0) + SelectGenerator(2); +} +void + PoweredSubsystem::SelectGeneratorCMessageHandler(ReceiverDataMessageOf *message) +{ + if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0) + SelectGenerator(3); +} +void + PoweredSubsystem::SelectGeneratorDMessageHandler(ReceiverDataMessageOf *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 *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 // diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index 30d2bfd..8d36603 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -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 *message), + SelectGeneratorBMessageHandler(ReceiverDataMessageOf *message), + SelectGeneratorCMessageHandler(ReceiverDataMessageOf *message), + SelectGeneratorDMessageHandler(ReceiverDataMessageOf *message), + ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf *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 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~