Issue #21: overcharge-bricked weapons -- latent ARCADE bug, deliberate divergence

The tester's ER small laser died permanently (recharge arc + ready dot dark,
no fire) after idle seek-gear clicking.  Root cause, byte-verified + headless-
reproduced -- a latent bug in the SHIPPED ARCADE BINARY:

- The charge integrates toward the GENERATOR voltage (@004ba838), not the
  gear target; "full" is only detected inside the +-1% snap window around
  seekVoltage[idx] (@004ba738), and overcharge reads ZERO (the byte-verified
  _DAT_004ba830 = 0.0 clamp).
- Toggle seek while a charge is in flight and the level can land ABOVE the
  new gear's window: rechargeLevel pins to 0, the ==1.0 Loaded test can
  never fire, and charging continues to the generator ceiling where EVERY
  gear reads overcharged.  Permanent brick; generator reselect cannot help.
- The arcade dodged it by circumstance (locked 60 fps, rare seek use); the
  port's clickable seek button hits it in seconds of casual clicking.

Fix (marked divergence in the Loading tick): overcharge counts as fully
charged -> Loaded.  The arcade's own discharge algebra says full == the
gear's seek voltage, so an overcharged weapon IS full.

Verified: the same 2.5-min BT_SEEKTEST abuse that bricked the laser pre-fix
now fires 38 overcharge rescues and ends Loaded/pct=1.  [seek] log now also
prints the per-gear voltage table + gen voltages (diagnosis instrumentation).

Ruled out on the way (documented in #21): overheat fuse (no firing occurred;
a 4-min autofire+coolant-abuse run wouldn't fuse), generator detach (gen
cycling + healthy siblings), seek-table corruption (table {6000,7000,8000,
9900} healthy).

KB: decomp-reference.md records the arcade-latent-bug analysis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-21 15:39:56 -05:00
co-authored by Claude Opus 4.8
parent e9db161404
commit 21b2bfc1c9
2 changed files with 50 additions and 1 deletions
+14
View File
@@ -274,6 +274,20 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
ResetFiringState); Emitter's faithful body existed as the orphaned "AdvanceSeekVoltage" (zero
callers) -- both registered via MESSAGE_ENTRY (ids 9/0xb). Verify: BT_SEEKTEST=1 +
BT_SEEK_LOG=1 → `[seek] <name> -> gear N` cycling. EjectAmmo remains unwired.
**LATENT ARCADE BUG found + diverged (issue #21, 2026-07-21) [T1 byte-verified + T2 repro]:**
the Emitter charge integrates toward the GENERATOR voltage (@004ba838 `fld [src+0x1dc]`), full
is detected only inside the +-1% snap window around `seekVoltage[idx]` (ComputeOutputVoltage
@004ba738: `|rl-1|<=0.01 -> 1.0`), and OVERCHARGE reads ZERO (`rl>1.01 -> 0`,
`_DAT_004ba830 = 0.0` byte-verified). Toggle the seek gear while a charge is in flight and
the level lands ABOVE the new gear's window -> rechargeLevel pinned 0, the `==1.0` Loaded test
(`_DAT_004bac04`) never fires, charging continues to the generator ceiling where EVERY gear
reads overcharged -> the weapon is PERMANENTLY bricked (arc dark, ready-dot dark, no fire; gen
re-select cannot help). The arcade shipped this (locked 60 fps + rare seek use hid it); the
port's clickable seek button triggers it in seconds. **Deliberate divergence** (emitter.cpp
Loading tick): overcharge (`currentLevel > seekVoltage[idx]`) counts as fully charged ->
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
- **MechRIOMapper** @0x51DD30: RE-REGISTERS the base ids 3..0x13 (Aux1Quad..ZoomOut) with the
RIO override handlers @004d22fc..@004d24f8, + {0x19, Keypress→@004d2514} (RIO's OWN keypress,
unreconstructed — we use the shared L4 0x17 @004d1bf0) + {0x1a, Hotbox→@004d2574} @0x51DE98.
+36 -1
View File
@@ -510,6 +510,32 @@ void
weaponAlarm.SetLevel(2); // -> Loaded
break;
}
// OVERCHARGE RESCUE (issue #21, 2026-07-21) -- a DELIBERATE
// divergence from the binary, which DEADLOCKS here: change the
// seek gear while a charge is in flight and the level can land
// ABOVE the new gear's snap window (level/seekV > 1.01) -- the
// byte-verified ComputeOutputVoltage clamp then zeroes
// rechargeLevel (_DAT_004ba830 = 0.0) and the ==1.0 Loaded test
// can never fire again while TrackSeekVoltage keeps charging
// toward the generator voltage: the weapon is PERMANENTLY
// bricked (arc dark, dot dark, never fires -- reproduced
// headless, and trivial to trigger from the clickable seek
// button; the pod's locked 60 fps + rare seek use hid it).
// The arcade's own math says "full == the gear's seek voltage"
// (discharge energy computes from seekV[rec]), so an
// overcharged weapon IS fully charged -- treat it as Loaded.
if (currentLevel > seekVoltage[seekVoltageIndex]
&& seekVoltage[seekVoltageIndex] > 0.0f)
{
rechargeLevel = 1.0f; // full (the display/damage ratio)
weaponAlarm.SetLevel(2); // -> Loaded
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] " << GetName()
<< " overcharge rescue: level=" << currentLevel
<< " > seekV=" << seekVoltage[seekVoltageIndex]
<< " -> Loaded" << std::endl;
break;
}
}
}
break;
@@ -683,7 +709,16 @@ void
ResetFiringState(); // @004ba9a8
}
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] " << GetName() << " -> gear " << next << std::endl;
{
Generator *src = (Generator *)ResolveVoltageSource();
DEBUG_STREAM << "[seek] " << GetName() << " -> gear " << next
<< " seekV[" << next << "]=" << seekVoltage[next]
<< " table={" << seekVoltage[0] << "," << seekVoltage[1] << ","
<< seekVoltage[2] << "," << seekVoltage[3] << "," << seekVoltage[4]
<< "} max=" << maxSeekVoltageIndex << " level=" << currentLevel
<< " genV=" << (src ? src->MeasuredVoltage() : -1.0f)
<< " genRated=" << (src ? src->RatedVoltageOf() : -1.0f) << std::endl;
}
}