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) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-27 08:34:00 -05:00
co-authored by Claude Opus 5
parent ef8e449a17
commit e26f4e6285
4 changed files with 74 additions and 25 deletions
+55 -16
View File
@@ -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 "<name> = <electrical state>".
//
@@ -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)
{