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,