From e26f4e6285218ca37485a19801c83fa691d65c47 Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 27 Jul 2026 08:34:00 -0500 Subject: [PATCH] Gitea #62 FIXED: AutoConnect can re-attach again -- slot 16 is HasVoltage(source), not GetStatusFlags() A weapon detached by two eng-page BUS MODE presses stayed dark for the REST OF THE MISSION: no voltage, blank recharge arc, dead ready dot, will not fire. The arcade recovers by pressing bus-mode back to Auto and letting the per-frame auto-hunt re-tap a generator. In the port that hunt's body was UNREACHABLE. THE DEFECT, visible in three lines of powersub.cpp: if (modeAlarm == AutoConnect && GetStatusFlags() == 0) // outer: NOT damaged for (...) if (... && GetStatusFlags() != 0 && Attach(sub)) // inner: DAMAGED?! Both call sites modelled the no-argument GetStatusFlags(), so the outer demanded "no status flags" and the inner "some status flag", with nothing in between able to change the value -- mutually exclusive, body never entered. The binary's @004b0bd0 calls vtable slot +0x40 (slot 16) with TWO SHAPES: slot16(this, 0) == 0 ("am I unpowered?") and slot16(this, candidate) != 0 ("would THIS generator supply me?"). WHAT WAS ACTUALLY WRONG was narrower than the issue assumed -- the BODY was already a faithful transcription of @004b0b5c (complete-type accessors, no raw offsets). Only its NAME and its WIRING were wrong: * it was called `IsSourceShorted`, asserting the INVERSE of what it computes: state 2 is the generator's ON-LINE state (Generator::GeneratorReady, what @004b215c's stateAlarm->2 sets) and the fabsf test requires meaningfully non-zero output voltage. True means "live and supplying". * it was declared NON-VIRTUAL, so it could not be the slot-16 body, and a weak stand-in (`electricalStateAlarm == Ready`, no source arg, no voltage test) occupied `HasVoltage()` instead. FIX: rename to `virtual Logical HasVoltage(Subsystem *source = 0)`, delete the stand-in, restore the two AutoConnect call shapes, and rename Myomers' slot-16 override (`HasAdequateVoltage` -> `HasVoltage`) so it actually overrides -- it tightens the test to "at least the SELECTED SEEK voltage", which is why it exists. BLAST RADIUS (the issue's warning): GetStatusFlags ORs the BadPower bit 0x40 on !HasVoltage(), and that bit feeds #47's annunciator -- so power, firing and annunciation semantics moved together. Checked: a healthy solo mission shows zero spurious BadPower, subsystems simulate normally, no faults. VERIFIED with a new diagnostic hook in the codebase's existing family (BT_POWER_DETACH_TEST=1, off by default): it reproduces the player's exact state -- drop the voltage link and force Auto, i.e. what the second BUS MODE press does -- so the fix can be tested without the eng-page UI. Live result: [power] TEST: detaching Avionics (forcing Auto) -- the auto-hunt must recover it [power] AutoConnect RE-ATTACHED Avionics -> generator GeneratorA Before the fix that second line was impossible. BT_POWER_LOG=1 prints every re-attach; both hooks stay for regression use. (The file-static one-shot is deliberate: PoweredSubsystem's layout is byte-locked by PoweredSubsystemLayoutCheck, so a test flag as an instance member would break sizeof and every offset assert downstream.) Co-Authored-By: Claude Opus 5 (1M context) --- game/reconstructed/myomers.cpp | 9 +++-- game/reconstructed/myomers.hpp | 2 +- game/reconstructed/powersub.cpp | 71 +++++++++++++++++++++++++-------- game/reconstructed/powersub.hpp | 17 +++++--- 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index ab0acdf..f60cba3 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -420,10 +420,13 @@ Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage) //*************************************************************************** -// Myomers::HasAdequateVoltage slot 16 @004b8f3c +// Myomers::HasVoltage slot 16 @004b8f3c //*************************************************************************** // -// Override of PoweredSubsystem::IsSourceShorted (slot 16, base @004b0b5c). +// Override of PoweredSubsystem::HasVoltage (slot 16, base @004b0b5c) -- Gitea +// #62 rename: the base was misnamed IsSourceShorted, asserting the inverse of +// what it computes. Myomers tightens it: adequate = at least the SELECTED SEEK +// voltage, not merely non-zero. // Returns True only when the (resolved) voltage source can supply at least // the currently-selected seek voltage AND is in the Ready state (state==2). // @@ -432,7 +435,7 @@ Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage) // return seekVoltage[currentSeekVoltageIndex] <= source->outputVoltage // && source->state == 2; // source+0x1DC, source+0x210 // -Logical Myomers::HasAdequateVoltage(Subsystem *source) +Logical Myomers::HasVoltage(Subsystem *source) { Generator *gen = source ? (Generator *)source : (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1D0) diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 3b68ced..c35acf5 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -264,7 +264,7 @@ class Mech; // requires the source to supply at least the selected seek voltage // AND be in the Ready (==2) state. Logical - HasAdequateVoltage(Subsystem *source = 0); // slot 16, @004b8f3c (overrides IsSourceShorted) + HasVoltage(Subsystem *source = 0); // slot 16, @004b8f3c (overrides PoweredSubsystem::HasVoltage; Gitea #62 rename) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Mover-connection message handlers (bound via the simulation message map; diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index f261fa6..ae8ca76 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -353,9 +353,40 @@ void // damaged and currently unpowered, scan every GeneratorClassID segment in // the mech and attach to the first one that will accept a tap. // + // + // GITEA #62 VERIFICATION HOOK (BT_POWER_DETACH_TEST=1, off by default). + // The player-visible bug needs a DETACHED subsystem to show itself, and a + // detach only happens via two eng-page BUS MODE presses. This reproduces + // that state directly: once per subsystem, drop the voltage link and force + // Auto mode -- exactly what the second BUS MODE press does -- so the + // auto-hunt below must re-attach it on a later frame. Before the fix the + // hunt's body was unreachable and the part stayed dark forever; with the + // fix a "[power] AutoConnect RE-ATTACHED" line follows within a frame or + // two. Same diagnostic-hook family as BT_VALVE / BT_MP_FORCE_DMG. + // + // (file-static one-shot, NOT an instance member: the PoweredSubsystem layout + // is byte-locked by PoweredSubsystemLayoutCheck -- adding a field would + // break sizeof and every offset assert downstream.) + static int s_detachTestDone = 0; + if (getenv("BT_POWER_DETACH_TEST") && !s_detachTestDone) + { + s_detachTestDone = 1; + DEBUG_STREAM << "[power] TEST: detaching " << GetName() + << " (forcing Auto) -- the auto-hunt must recover it\n" << std::flush; + DetachFromVoltageSource(); // @004b0e30 (the BUS MODE press) + modeAlarm.SetLevel(AutoConnect); // ...and back to Auto + } + + // GITEA #62: the two call shapes restored. Both sites modelled the + // no-argument `GetStatusFlags()`, so the outer demanded "no status flags at + // all" while the inner demanded "some status flag" -- mutually exclusive + // with nothing in between able to change the value, so the roster walk's + // body was UNREACHABLE and a subsystem detached by the BUS MODE button + // could never re-attach (dark recharge arc for the rest of the mission). + // Outer = "am I currently unpowered?" (slot16(this, 0) == 0) if ( modeAlarm.GetLevel() == AutoConnect // this[0xb3] @0x2cc == 2 - && GetStatusFlags() == 0 // (*this[0x40])(this,0) == 0 + && !HasVoltage() // (*this[0x40])(this,0) == 0 ) { // task #12 ROSTER CORRECTION: the raw @004b0bd0 loop walks owner+0x124/ @@ -367,12 +398,20 @@ void for (int i = 0; i < count; ++i) { Subsystem *sub = mech->GetSubsystem(i); // owner+0x128[i] + // Inner = "would THIS generator supply me?" (slot16(this, sub) != 0) if (sub != 0 && sub->GetClassID() == RegisteredClass::GeneratorClassID // +4 == 0xbc1 - && GetStatusFlags() != 0 + && HasVoltage(sub) // (*this[0x40])(this,sub) != 0 && AttachToVoltageSource(sub) != -1 // FUN_004b0dd8 ) { + // Gitea #62 reachability probe (BT_POWER_LOG): this body was + // UNREACHABLE before the two call shapes were restored, so a + // single line here is the proof the auto-hunt runs at all. + if (getenv("BT_POWER_LOG")) + DEBUG_STREAM << "[power] AutoConnect RE-ATTACHED " + << GetName() << " -> generator " + << sub->GetName() << "\n" << std::flush; break; } } @@ -524,16 +563,6 @@ LWord return flags; } -// -// Virtual "has usable voltage" query (vtable+0x40). Base powered subsystem -// reports voltage present whenever the electrical state machine is Ready. -// -Logical - PoweredSubsystem::HasVoltage() -{ - return (electricalStateAlarm.GetLevel() == Ready) ? True : False; -} - // // @004b1224 -- prints " = ". // @@ -813,12 +842,22 @@ void } // -// @004b0b5c -- True when the (resolved or supplied) source is in the shorted -// state (state 2 in this query) and its measured voltage exceeds the short -// threshold. +// @004b0b5c -- SLOT 16 (vtable+0x40): the one-argument USABLE-VOLTAGE query. +// +// Gitea #62 RENAME (the body was already a faithful transcription; only its +// name and its wiring were wrong). The old name `IsSourceShorted` asserted the +// opposite of what these instructions compute: state 2 is the generator's +// ON-LINE state (Generator::GeneratorReady, set by @004b215c's +// stateAlarm->2), and the fabsf test requires the output voltage to be +// meaningfully non-zero. So a True return means "this source is live and +// supplying", not "shorted". +// +// if (source == 0) source = resolve(this+0x1d0); // MY own link +// return source && source->state(+0x210) == 2 +// && fabsf(source->outputVoltage(+0x1dc) - eps) > eps; // Logical - PoweredSubsystem::IsSourceShorted(Subsystem *source) + PoweredSubsystem::HasVoltage(Subsystem *source) { if (source == 0) { diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index 42ccaae..432d476 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -249,16 +249,23 @@ class Generator; AttachToVoltageSource(Subsystem *source); // @004b0dd8 (returns tap index, -1 on fail) void DetachFromVoltageSource(); // @004b0e30 - Logical - IsSourceShorted(Subsystem *source = 0); // @004b0b5c + // SLOT 16 (vtable+0x40), @004b0b5c -- the one-argument voltage query. + // Gitea #62: this WAS declared non-virtual and named `IsSourceShorted`, + // which is the INVERSE of what @004b0b5c computes: it returns True when + // the source is ON LINE with usable voltage (state 2 + the output-voltage + // test), i.e. "HasVoltage". Two shapes, both used by AutoConnect: + // HasVoltage() -> resolve MY own source (this+0x1d0): am I powered? + // HasVoltage(source) -> would THIS candidate generator supply me? + virtual Logical + HasVoltage(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(); + // (the weak no-arg stand-in that used to sit here -- `electricalStateAlarm + // == Ready`, no source parameter, no output-voltage test -- is GONE: + // Gitea #62. The real slot-16 body above replaces it.) // resolve our powering source (used by PowerWatcher). Subsystem*