Fix "can't turn in MP": pilotArray[1] overrun stomped controlMode

User report from the first live 2-window session: throttle worked, A/D turn
did nothing.  [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).

Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe).  Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.

ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it.  In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0.  MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode.  This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).

Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits).  Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).

VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 18:16:46 -05:00
co-authored by Claude Fable 5
parent d09cda6c36
commit a110041a70
5 changed files with 42 additions and 7 deletions
+6 -3
View File
@@ -130,9 +130,12 @@ the drive being on). Fix: `BT_GOTO` now enables forced mode itself + supplies it
holds and shoots instead of ramming). (2) The steering used the `gDriveHeading` SCALAR MIRROR (seeded to 0),
but MP pilots spawn facing a non-zero heading, so the beeline steered the WRONG way. Fix: compute the heading
error from the mech's ACTUAL forward (`localToWorld`'s -Z axis), signed angle to the target. (3) The goto turn
was routed through `controlsMapper->turnDemand`, which zeroes out in -net (the mapper key-bridge only shapes
the local viewpoint mech there) — the heading froze and A drove straight past B. Fix: apply the goto turn to
the orientation integration DIRECTLY (`if(gBTGotoActive) turn=gBTGotoTurn`), after the mapper read.
was routed through `controlsMapper->turnDemand`, which read 0 in -net the heading froze and A drove straight
past B. Fix: apply the goto turn to the orientation integration DIRECTLY (`if(gBTGotoActive) turn=gBTGotoTurn`),
after the mapper read. **CAUSE FOUND LATER (task #51): turnDemand zeroed because `controlMode` was pointer
garbage — the `pilotArray[1]` shrunk-span overrun (see [[reconstruction-gotchas]] §5), MP-only because solo's
overrun wrote 0==BasicMode. Fixed at the root (10-slot array); the direct goto-turn stays (harmless, and it
skips the 1-frame mapper latency). KEYBOARD turning in MP was restored by the root fix.**
VERIFIED 2-node: A `BT_GOTO="enemy"` alone (no autodrive) drives `dist 1673→231 m`, holds at range, `mechPicks=59`,
autofire → **B hdlr=192, DESTROYED** — a fully automated human-style cross-pod kill via the real beam path
(not the FORCE_DMG hook). Solo un-regressed (plain autodrive + goto both work). `scratchpad/mp_kill_test.sh`;
+9
View File
@@ -90,6 +90,15 @@ The 33-agent RESOURCE_AUDIT fixed 8 such bugs (docs/RESOURCE_AUDIT.md). [T2]
member (HeatSink `heatState@0x184` == `heatAlarm.GetLevel()`). → route to the accessor.
- **Phantom field:** a member at an offset PAST the object (Generator `shortFlag@0x25C` is
really `*(owner+0x190)+0x25c` the msg-manager). → remove; read the real source.
- **Shrunk-span array:** a binary FIXED-SPAN block declared as `member[1]` ("variable length"
comment) — every write past slot 0 stomps the members declared after it. Archetype:
`MechControlsMapper::pilotArray` — the binary reserves 0x15C..0x183 (10 slots); the `[1]`
declaration let MP's `FillPilotArray` write the PEER's `Player*` over `controlMode`
("can't turn in MP": turn shaping dispatched on pointer garbage → `turnDemand=0`), MASKED
in solo because the overrun wrote 0 == BasicMode. → size the array to the binary's
inter-member span ((next-member offset array offset) / stride) + clamp the fill loops.
Caught live with cdb `ba w4` on the compiled member address (log `&member` from the ctor,
offset delta gives the compiled position). [T2]
`GaugeAlarm54` = 0x54 (the real `AlarmIndicator`; STATUS level at +0x14, so
`subsystem+0x184 == heatAlarm+0x14 == GetLevel()`); `SubsystemConnection` = 0xC. [T1]
+3 -1
View File
@@ -1688,7 +1688,9 @@ void
<< " stickX=" << controlsMapper->stickPosition.x
<< " -> speedDemand=" << controlsMapper->speedDemand
<< " turnDemand=" << controlsMapper->turnDemand
<< " mode=" << controlsMapper->controlMode << "\n" << std::flush;
<< " mode=" << controlsMapper->controlMode
<< " mapper=" << (void*)controlsMapper
<< " &mode=" << (void*)&controlsMapper->controlMode << "\n" << std::flush;
}
}
+13 -1
View File
@@ -342,6 +342,10 @@ MechControlsMapper::MechControlsMapper(
controlMode = BasicMode;
displayMode = 0;
pilotArrayPage = 0;
if (getenv("BT_MODE_LOG"))
DEBUG_STREAM << "[mode-ctor] mapper=" << (void*)this
<< " controlMode=" << (int)controlMode
<< " &controlMode=" << (void*)&controlMode << "\n" << std::flush;
//
// Recenter the torso articulation: current yaw/pitch <- neutral yaw/pitch.
@@ -364,7 +368,8 @@ MechControlsMapper::MechControlsMapper(
pilotArrayBuilt = False;
pilotIDs = 0;
pilotArray[0] = 0;
for (int pi = 0; pi < PilotArraySlots; ++pi)
pilotArray[pi] = 0;
Check_Fpu();
}
@@ -913,6 +918,13 @@ void
}
}
// Capacity clamp: the binary reserves 10 slots (local + up to 9 remotes;
// an 8-pod game fits). pilotCount beyond that would overrun the block in
// the binary too -- clamp so the `<= pilotCount` zero-loop (which zeroes
// the local slot 0 PLUS pilotCount remotes, faithful) stays in bounds.
if (pilotCount > PilotArraySlots - 1)
pilotCount = PilotArraySlots - 1;
pilotIDs = new int[pilotCount]; // FUN_004022b0
for (int i = 0; i <= pilotCount; ++i)
+11 -2
View File
@@ -176,8 +176,17 @@ class Pilot;
ControlsButton torsoCenter; // @0x154
int pilotArrayPage; // @0x158 (which "page" of pilots)
Pilot *pilotArray[1]; // @0x15C [0]=local; [1..] remote
// (variable length; pilotCount entries)
// FIXED 10-SLOT block, the binary's own reservation: 0x15C..0x183 =
// (pilotArrayBuilt@0x184 - 0x15C)/4 = 10 pointers ([0]=local, [1..9]
// remote; an 8-pod game + the local fits). The old `[1]` "variable
// length" declaration made EVERY write past slot 0 stomp the members
// declared after it: in MP, FillPilotArray wrote the PEER's Player
// pointer over controlMode (the turn shaping dispatched on pointer
// garbage -> turnDemand 0 -> "I can't turn"); in solo the zero-loop
// wrote 0 there, masked only because 0 == BasicMode. Caught live via
// cdb `ba w4` on &controlMode (task #51).
enum { PilotArraySlots = 10 };
Pilot *pilotArray[PilotArraySlots]; // @0x15C..0x183
enum ControlMode {
BasicMode = 0,