KB: Gitea #12 incident findings -- @004bbd04 firing machine recovered, gauge-freeze root cause, dirty-bit gotcha

- decomp-reference: ProjectileWeaponSimulation @004bbd04 FULLY RECOVERED (capstone):
  fault gate reads simulationState@0x40==1 (not simulationFlags); ammo-bin gates;
  Loaded-case view/target denial = SetLevel(4);SetLevel(2) blip BEFORE FeedAmmo;
  updateModel@0x18|=1 is the replication dirty (== ForceUpdate); slot 17 @004b9c9c
  = rechargeLevel writer; FireWeapon @004bcc60 carries no gating.
- gauges-hud: CORRECTION -- launcher recharge dial is NOT authentically static
  (slot 17 @004b9c9c animates it); MP gauge-window freeze root-caused = lobby-built
  gauge tree dangling over the launch mech re-stream + permanent SEH disable.
- reconstruction-gotchas #20: updateModel@0x18 vs simulationFlags@0x28 bit 0
  (DelayWatchersFlag) mis-mapping class (Emitter::SetDirty, mislanch salvo mark).
- open-questions: #12 status ledger -- fixes pending a live-verify window; J/K/L
  preset hypothesis refuted for solo (scripted A/B); symptom-4 pip set unresolved.
- GAUGE_COMPOSITE row 13 verdict corrected (WRONG-STATIC, fix pending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 22:46:10 -05:00
co-authored by Claude Fable 5
parent 63b168cb92
commit 2215932016
5 changed files with 117 additions and 17 deletions
+28 -2
View File
@@ -199,8 +199,34 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
GENERATOR's `pendingHeat@0x1c8` (~3.5e8/full PPC charge). `ComputeOutputVoltage` @004ba738
snaps rechargeLevel@0x320 to 1.0 within 0.01. **FailureHeat consumers:** `this+0x184`
(own heatAlarm level) == 2 -> emitter @004baa88 resets firing + holds charge 0; ballistic
@004bbd36 pins `recoil=rechargeRate` + alarm 7. @004bbd04 (ProjectileWeaponSimulation)
opens with `call 0x4b0bd0` (the powered/heat step), like @004baa88. Cockpit power-routing
@004bbd36 pins `recoil=rechargeRate` + alarm 7.
- **@004bbd04 ProjectileWeaponSimulation FULLY RECOVERED (capstone disasm, Gitea #12,
2026-07-19) [T1]** — the port's RivetGun-modeled body diverges; the authentic machine:
PoweredSubsystemSimulation@4b0bd0; trigger=CheckFireEdge@4b9608; UpdateEject@4bbb50;
**gate 1 @4bbd36: `simulationState@0x40==1` (destroyed — NOT simulationFlags@0x28!) ||
heatAlarm@0x184==2 || FUN_49fb54(owner disabled) → recoil=rechargeRate + alarm 7**;
gate 2 @4bbd71: bin=Resolve(this+0x43c)@417ab4; bin alarm LEVEL@+0x1A8 ∈{2,3} || bin
simulationState@0x40==1 → alarm 7 (re-pinned per frame while dry). switch(alarm@0x364):
**Loaded(2) @4bbec2**: on trigger — `viewFireEnable@0x3E0==0 || ownerTarget@[owner+0x388]==0
→ SetLevel(4);SetLevel(2)` (denial BLIP, stays Loaded, NO ammo pull); else
`bin->FeedAmmo@4bd4f4` (fail → stays Loaded silently) → SetLevel(0 Firing) →
**`updateModel@0x18 |= 1`** → vtbl+0x48 FireWeapon → (bin level==2 → SetLevel(7)) else
(CheckForJam@4bbfcc → SetLevel(5)) else SetLevel(3) → `updateModel|=1`
recoil=rechargeRate. **Loading(3) @4bbdd2**: trigger → blip 4→3; if
electricalState@0x278==4 { DrawFiringCharge@4bbc78; recoil<0 → clamp 0 + (bin level==1 →
SetLevel(2)) }; always vtbl+0x44. **Jammed(5) @4bbe8e**: trigger → 6→5;
recoil=rechargeRate. **7 (unavailable/NoAmmo) @4bbe4d**: trigger → blip 1;
re-SetLevel(7) unconditionally (roach motel — permanent once entered);
recoil=rechargeRate; vtbl+0x44. States 0/1/4/6 = transient audio blips (no case body).
**vtbl+0x44 (slot 17) @004b9c9c = `rechargeLevel@0x320 = (rechargeRate@0x3DC
recoil@0x3E8)/rechargeRate`** — the projectile recharge-dial writer (Emitter overrides this
slot with ComputeOutputVoltage@4ba738). **MissileLauncher::FireWeapon @004bcc60 carries NO
view/target/ammo gating and NO recoil set** (part_013.c:8741+: heat add + spawn descriptor
only) — ALL gating lives in the caller's Loaded case. ⚠ The DIRTY-for-replication bit is
`Simulation::updateModel@0x18 |= 1` (== engine `ForceUpdate()`), NOT
`simulationFlags@0x28 |= 1` — bit 0 of simulationFlags is the engine **DelayWatchersFlag**
(SIMULATE.h): setting it permanently skips the subsystem's ExecuteWatchers (audio watchers).
Cockpit power-routing
handlers @004b099c/@004b09e4/@004b0a2c/@004b0a74 (bind generator by authored id 1-4 via
@004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30
(generator client capacity @0x1e4/count @0x1e8).
+35 -14
View File
@@ -77,6 +77,24 @@ over one shared `SVGA16` pixelBuffer; `SVGA16::DrawDevSurface`). On the POD they
intact). The `overlay` port (SectorDisplay lives there) shares the `sec` physical surface via a
different bit-plane (0x00C0). [T2]
## MP gauge-window FREEZE = dangling bindings + permanent SEH disable (Gitea #12, 2026-07-19) [T2 log-convicted]
The live-MP "dev-gauges window froze entirely mid-session" incident (issue #12) is NOT a
swap-chain/present bug: `scratchpad/incident_2157/operator_1.log` shows a burst of
`[gauge-fault] '<Widget>' Execute FAULTED -> gauge DISABLED` (PilotList, HeadingPointer,
MapDisplay, SectorDisplay, TwoPartBars, NumericDisplayScalar …) exactly while ALL THREE mechs
were being RE-CREATED at mission launch (second `[zonebuild]`/`[cyl]` set at new addresses —
the session had a pre-launch host drop/rejoin; a normal 2-player LAN launch on the same build
did not re-stream and did not freeze). The gauge tree had been built (lazily) during the
LOBBY phase against the FIRST-stream mechs; the re-stream freed them; every gauge Execute then
AV'd on its dangling [[attribute-pointer]]s and the `BT_DEV_GAUGES` SEH guard
(`Gauge::GuardedExecute`, GAUGE.cpp:618) DISABLED each one — `Disable(True)` sets `rate=0`
PERMANENTLY, nothing re-enables or rebinds → the whole window static for the rest of the
session (= symptoms 1+3). The AUTHENTIC engine flow handles mission transitions by
`Application::Shutdown → gaugeRenderer->Shutdown() → ShutdownImplementation → Remove(0)` (all
gauges deleted, tree lazily rebuilt next mission, APP.cpp:787/GAUGREND.cpp:3264) — the port's
in-session re-stream path bypasses it. OPEN: rebuild/rebind the gauge tree on a viewpoint-mech
re-stream (or route the rejoin through the engine Shutdown flow); see [[open-questions]].
## The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1/T2]
The `sec` port stacks three mode-gated mech-schematic layers at offset (50,0) over the
always-on radar/heading/speed/messageBoard (`Secondary1`): **Damage** (`ModeSecondaryDamage`,
@@ -176,16 +194,18 @@ locked**. **RESOLVED 2026-07-12 [T2]:** deaths now tally per node from LOCALLY O
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the 2/1 pre-acquire seed
to 0 for display. Own row counts correctly as before.
## Launcher-panel recharge dial — authentically STATIC [T1, writer census]
The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320). A FULL
writer census of the binary (all decomp spellings: `0x320) =`, `+ 800) =`, `[200] =`) finds
exactly four writers in the weapon family: the ctor (@004b99a8, 1.0), the stream init
(@004b8fec), ResetToInitialState (@004b96ec, 1.0), and the ONE live writer —
`Emitter::ComputeOutputVoltage` @004ba738, reached only through the Emitter vtable. Projectile
weapons (MissileLauncher/autocannon) never write it: their dial draws full ONCE and never moves
on the pod. The launcher panel's live indicators are the AMMO DIGITS (AmmoBin::ammoCount via
the complete-type bridges, 2026-07-12 — the old raw bin+0x180/0x18c reads were layout garbage),
the jam/fire lamps and the eject wipe. Do NOT "fix" the static ring — it is the authentic look.
## Launcher-panel recharge dial — CORRECTION 2026-07-19: it IS live (slot 17 @004b9c9c) [T1]
The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320).
**The old "authentically STATIC on projectile weapons" claim was WRONG** — the writer census
missed the anonymous vtable slot 17 (vtbl+0x44) body **@004b9c9c**:
`rechargeLevel = (rechargeRate@0x3DC recoil@0x3E8) / rechargeRate` (capstone disasm, issue
#12; Ghidra never emitted it). The recovered `ProjectileWeaponSimulation` @004bbd04 calls it
every frame in the Loading(3) and unavailable(7) alarm cases — so the launcher dial
authentically ANIMATES 0→1 through each reload. (The census's four other writers stand: ctor
@004b99a8, stream init @004b8fec, ResetToInitialState @004b96ec, and
`Emitter::ComputeOutputVoltage` @004ba738 — slot 17 is the Emitter override of the SAME slot;
@004b9c9c is the MechWeapon/projectile base body.) The launcher panel's other live indicators:
AMMO DIGITS (AmmoBin::ammoCount via the complete-type bridges), jam/fire lamps, eject wipe.
## ConfigMapGauge (the weapon panel's regroup lamp column) — authentically DORMANT
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
@@ -329,14 +349,15 @@ and every instrument is now live [T2]:**
(the weapon-GROUP bits Front/Rear/Left/Right). **Range plays NO part** — Execute never reads
the stored TargetWithinRange slots. **FIX 2026-07-18 [T2]:** the pip's "loaded" was a port
approximation reading `rechargeLevel ≥ 1` — correct for emitters (charge-driven) but
**statically 1.0 on projectile weapons** (MissileLauncher/autocannon never write it — see
§Launcher-panel above), so missile/AC pips were **permanently lit and never blinked on fire**.
**statically 1.0 on projectile weapons in the PORT** (the port sim never called the slot-17
updater @004b9c9c — see the §Launcher-panel CORRECTION), so missile/AC pips were
**permanently lit and never blinked on fire**.
Now reads the AUTHENTIC attr 0x1C = the weaponAlarm StateIndicator level (`WeaponStatePtr`
`GaugeAlarm54::LevelPtr`), compared `== stateConst2` (the const the binary itself stores in
AddWeapon). The state cycles Loaded(2)↔Firing(0)/Loading(3)/Jammed(5) for BOTH families, so a
missile pip now momentarily drops when fired, exactly like the emitter's. Verified live
(Blackhawk, `BT_AUTOFIRE=1 BT_AF_MISSILE=1`): both SRM6 pips toggle 2↔0/5 on each salvo. The
authentically-static `rechargeLevel` recharge DIAL (§Launcher-panel) is untouched.
(Blackhawk, `BT_AUTOFIRE=1 BT_AF_MISSILE=1`): both SRM6 pips toggle 2↔0/5 on each salvo.
(The recharge DIAL is driven by slot 17 @004b9c9c — §Launcher-panel CORRECTION.)
- **Lock ring** = subB9 (ring+cross) at frame centre, **SPINNING 4°/frame** while the Lock attr
(0xA) is up. The Lock PRODUCER is the authentic HudSimulation rule (part_013.c:5619-5634 [T1],
wired task #38): lock requires a target AND your own HUD's host zone damage < **0.75** (a
+33
View File
@@ -629,6 +629,39 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
numbers no longer point at that code).
## Multiplayer (Phase 7 / P6)
- **Gitea #12 MP incident (2026-07-19) — root causes found, FIXES NOT YET LANDED (blocked on a
live-verify window; the user was playing).** Findings [T1/T2 — see the #12 comments +
`scratchpad/incident_2157/`]:
(1) *Gauge-window freeze* = lobby-built gauge tree dangling over a mission-launch mech
RE-STREAM after a pre-launch host drop/rejoin; SEH guard disables each faulted gauge
permanently — see [[gauges-hud]] §MP gauge-window FREEZE. FIX TODO: rebuild/rebind the gauge
tree on a viewpoint re-stream (engine analog: `Application::Shutdown`
`gaugeRenderer->Shutdown()`); also consider re-enabling (not just disabling) a faulted gauge
after a rebind.
(2) *Missiles cycle-but-never-launch*: the port's ProjectileWeaponSimulation is a RivetGun
approximation — the AUTHENTIC @004bbd04 machine is now fully recovered ([[decomp-reference]]
§5). Port defects to fix faithfully: fault gate reads `simulationFlags==1` (binary:
`simulationState@0x40==1`); `simulationFlags|=0x1` on every salvo/beam = engine
DelayWatchersFlag poisoning ([[reconstruction-gotchas]] §20; binary = `updateModel|=1` ==
ForceUpdate); view/target gating + ConsumeRound live in the port's FireWeapon (binary: in the
caller, with a 4-blip stay-Loaded denial — the port instead fakes a full Firing→Loading pip
cycle on a denied shot, which is exactly the reported symptom); recoil never clamped at 0
(runs to 114); slot-17 @004b9c9c (rechargeLevel) never called. Solo repro on 4.11.387
(pre-stop-order): SRM6s latch alarm 7 PERMANENTLY mid-session after a heat-failure episode
(recoil pinned → released → state 7 forever) — missiles stop while lasers keep working, with
or WITHOUT J/K/L presses. The binary's state 7 is also permanent once entered [T1] — open
fidelity question: is launcher FailureHeat reachable at authentic heat economy, or does our
launcher self-heat run hot? (Emitters never trip it — task #11 duty-cycle measurement.)
(3) *J/K/L preset hypothesis REFUTED for solo* [T2]: BT_PRESET_TEST cycling on 387 — missiles
kept launching after presets (36 PUSH before / 24 after), pip count unchanged, gauges live,
[mode] masks touch ONLY ModeMFD bits 0-14 (`reticleElementMask` is a different object).
Corroborating: a 2-player LAN session on the same build (no J/K/L) had working missiles +
normal pips. UNTESTED: J/K/L during a live RELAY MP session.
(4) *"WAY MORE pips" (BLH, symptom 4)* — UNRESOLVED: the incident registered 10 pips
(8×3016 @750/500×5/225×2 + 2×3024 @800) which matches NO stock arsenal (solo dumps: bhk1=7,
madcat=6, thor=7); the reticle built identically TWICE (lobby + launch re-stream). Needs the
incident egg / a live 3-seat repro to identify the mech that owned those 10 weapons
(custom operator-egg loadout? viewpoint mis-assignment at build time?).
- **✅ Console-death peer-replication freeze — NOT REPRODUCIBLE + the one real bug FIXED
(2026-07-15) [T2].** The 2026-07-14 report (btconsole.py died mid-match → peer↔peer replication
froze both ways) did NOT reproduce in the current build under EITHER failure mode, tested directly
+20
View File
@@ -405,6 +405,26 @@ The name-keyed samplers (`PowerSourceConnection`/`GeneratorVoltageConnection`, w
---
## 20. Dirty-bit mis-mapping: `updateModel@0x18` (replication) vs `simulationFlags@0x28` bit 0 (DelayWatchersFlag)
**Symptom class (Gitea #12):** subsystem watchers (the AUDIO watchers — `ExecuteWatchers`,
SIMULATE.cpp:461) silently stop for a weapon after its FIRST shot; and any "flags == 1" state
test misfires. **Cause:** the binary's "needs replication" mark is
**`Simulation::updateModel@0x18 |= 1`** — in the disasm it reads `or word ptr [this+0x18], 1`
(i.e. "this+6" in dword-speak) and is EXACTLY what the engine `ForceUpdate()` does. Several
port reconstructions transcribed it onto **`simulationFlags@0x28 |= 0x1`** (`Emitter::SetDirty`
emitter.cpp:215, the MissileLauncher salvo mark mislanch.cpp:337) — but simulationFlags bit 0
is the engine **DelayWatchersFlag** [T0 SIMULATE.h:165]: once set (nothing clears it),
`PerformAndWatch` skips `ExecuteWatchers()` for that subsystem FOREVER. Bit 1 is
DontExecuteFlag; bits 2-3 the replicant-copy mask; bit 8 the master flag. A related misread:
the @004bbd04 fault gate tests **`simulationState@0x40 == 1`** (subsystem DESTROYED), which the
port transcribed as `simulationFlags == 1` — with the spurious `|=1` above, that arms a
permanent alarm-7 kill-switch for any launcher whose other flag bits are 0. **Rule:** a binary
`[this+0x18] |= 1` = `ForceUpdate()`; `[this+0x28]` = simulationFlags (engine-defined bits —
never invent meanings); `[this+0x40]` = MechSubsystem::simulationState (1 = destroyed). [T1]
---
## Diagnostic recipe (the standard loop)
1. Read the RAW decomp `reference/decomp/all/part_*.c` for the `FUN_xxxx`.
2. Map `FUN_`/`DAT_`/`this+0xNN` to engine symbols via BT headers + WinTesla MUNGA source +
+1 -1
View File
@@ -674,7 +674,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b
| 10 | Comm: SELECT-TARGET highlight | BTPilotIsSelected accessors (ex raw-offset AV) | commit 34aaa7d root-cause note | T2 | CORRECT |
| 11 | Plasma: rankAndScore | engine primitive → external serial annunciator | not a D3D surface (L4PLASMA=com2) | T0 | CORRECT (n/a on dev composite) |
| 12 | Quad mini-panels: temp bars / cooling-loop / power-bus lamps / evolt bar / cmode lamp | SubsystemCluster children (@004c8140 map) reading InputVoltage/HeatSink/temps | VEHICLE_SUBSYSTEMS.md phase 2; LIVE: panels render per-subsystem, [vss] roster matches authored aux screens 1-10 | T2 | CORRECT |
| 13 | Quad: recharge dial (SegmentArc270) | PercentDone → rechargeLevel@0x320 | writer census: Emitter::ComputeOutputVoltage @004ba738 only — projectile dial full-once | T1/T2 | CORRECT (emitters) + AUTH-STATIC (projectile) |
| 13 | Quad: recharge dial (SegmentArc270) | PercentDone → rechargeLevel@0x320 | ~~writer census: Emitter::ComputeOutputVoltage @004ba738 only — projectile dial full-once~~ **CORRECTED (Gitea #12, 2026-07-19): the census missed vtbl slot 17 @004b9c9c**`rechargeLevel=(rechargeRaterecoil)/rechargeRate`, called per frame by the recovered @004bbd04 Loading/state-7 cases: the projectile dial authentically ANIMATES through reload (port fix pending — the port sim never calls slot 17) | T1 | CORRECT (emitters) + **WRONG-STATIC (projectile; fix pending)** |
| 14 | Quad: ammo count (ammoCountA) | AmmoBin::ammoCount via BTAmmoBinCountPtr bridge | AFC100 fix 2026-07-12; LIVE: 24 fresh (audit4) vs 19 after missile autofire (audit3) — counter moves | T2 | CORRECT |
| 15 | Quad: ConfigMap regroup lamps | LBE4ControlsManager buttonGroup map state | task #6: NO SetColor caller in the binary → color=0, Execute early-outs | T1 | AUTH-STATIC (dormant; BT_CONFIGMAP dev enable) |
| 16 | Eng pages: prepEngr SYSTEM NN + subsystem label + class cells | roster walk, sub+0x1dc auxScreen == screen (BTGetSubsystemAuxScreen) | FUN_004c7e48 matched line-by-line; [vss] dump: scr1/2/4=PPC/Strk6/ERMed, 5-8=Sens/Myo/ERMed/ERMed, 9/10=Strk6/PPC — page headers+labels CORRECT | T1/T2 | CORRECT |