From a110041a703f1154f12858630b971c3436ef933f Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 18:16:46 -0500 Subject: [PATCH] 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 --- context/multiplayer.md | 9 ++++++--- context/reconstruction-gotchas.md | 9 +++++++++ game/reconstructed/mech4.cpp | 4 +++- game/reconstructed/mechmppr.cpp | 14 +++++++++++++- game/reconstructed/mechmppr.hpp | 13 +++++++++++-- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/context/multiplayer.md b/context/multiplayer.md index 5de9eef..690e681 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -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`; diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index d0b10a8..b744d92 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -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] diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 04675f4..04c0ed0 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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; } } diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 411b67f..887ca7b 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -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) diff --git a/game/reconstructed/mechmppr.hpp b/game/reconstructed/mechmppr.hpp index 3673f88..d2b9079 100644 --- a/game/reconstructed/mechmppr.hpp +++ b/game/reconstructed/mechmppr.hpp @@ -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,