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
+6 -3
View File
@@ -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)
+1 -1
View File
@@ -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;
+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)
{
+12 -5
View File
@@ -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*