From 9353d59eb94a24e8a637dffbf49acd6135224c2a Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 15:02:51 -0500 Subject: [PATCH] Fix drive-to-range stall: BT_GOTO self-drives + steers correctly cross-net The BT_GOTO beeline never actually closed on a target -- three separate faults: 1. It was gated behind `if(gBTDrive.forced)`, and `forced` was set ONLY by BT_AUTODRIVE -- a chicken-and-egg where the block that would drive was gated on the drive already being on. So BT_GOTO alone just turned in place (the "stall"). Fix: BT_GOTO enables forced mode itself AND supplies its own forward throttle (a beeline must drive, not only steer), cutting the throttle inside a stop radius (weapon range for "enemy", so it holds + shoots). 2. Steering used the gDriveHeading SCALAR MIRROR (seeded to 0), but MP pilots spawn facing a non-zero heading -> it steered the WRONG way. Fix: 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 mode (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. Verified 2-node one-box: A with BT_GOTO="enemy" ALONE (no autodrive) drives dist 1673->231m, holds at weapon 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 still drives; goto="enemy" resolves the enemy, mechPicks=59, no crash). BT_GOTO_LOG traces the beeline. Co-Authored-By: Claude Fable 5 --- context/multiplayer.md | 25 +++++++++----- game/reconstructed/mech4.cpp | 67 +++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/context/multiplayer.md b/context/multiplayer.md index f538dd1..9f88ccf 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -122,16 +122,25 @@ groundPicks=0` → beam → `*** DESTROYED`. Also proved `PickRayHit` resolves a (`hit=1` at 757 m in a 2-node run); the replicant `localToWorld` is correct (DeadReckon + `localToWorld= localOrigin`, mech4.cpp:1191). [T2] -**STILL OPEN — drive-to-range.** `BT_GOTO`/`BT_AUTODRIVE` steer + move A, but A stalls (`gait adv=0`) short -of the enemy and often fails to line the boresight onto a peer at range — so an AUTOMATED 2-node kill via the -real beam path isn't demonstrated (the `BT_MP_FORCE_DMG` hook still is). A HUMAN can now aim (the ray is -correct in the default view); the gap is the auto-drive facing the target. Locomotion/goto issue, not MP. -Test aid: `BT_GOTO="enemy"` beelines toward the nearest live peer (solo dummy OR replicant); `scratchpad/ -mp_kill_test.sh`. +**FIXED — drive-to-range (the automated cross-pod kill now runs end-to-end).** The `BT_GOTO` beeline had +three separate faults: (1) it was gated behind `if(gBTDrive.forced)` (mech4.cpp), and `forced` was set ONLY +by `BT_AUTODRIVE` — so `BT_GOTO` alone never drove (chicken-and-egg: the block that would drive was gated on +the drive being on). Fix: `BT_GOTO` now enables forced mode itself + supplies its own forward throttle (a +"beeline" must drive, not just steer), cutting throttle inside a stop radius (weapon range for `enemy`, so 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. +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`; +`BT_GOTO_LOG` traces the beeline. ## Remaining (P6 phase 4 / Phase 7) -Drive-to-range / auto-face reliability (above) for a fully AUTOMATED cross-pod kill demo; replicant GAIT -animation (derive from replicated velocity); the pod-LAN config (real IPs, bare-IP pilot entries). See +Replicant GAIT animation (derive from replicated velocity); the pod-LAN config (real IPs, bare-IP pilot +entries). See [[open-questions]]. [T3] ## MP-front scout (task #45, 2026-07-09) — the P6 chain SURVIVES the combat/HUD rework [T2] diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 430499c..e86fdd0 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1356,12 +1356,15 @@ void { static int sAutoFire = -1; static float sAutoDrive = -1.0f; + static int sGotoDrive = -1; if (sAutoFire < 0) { const char *af = getenv("BT_AUTOFIRE"); sAutoFire = (af && *af == '1') ? 1 : 0; const char *ad = getenv("BT_AUTODRIVE"); sAutoDrive = ad ? (float)atof(ad) : 0.0f; + const char *gv = getenv("BT_GOTO"); + sGotoDrive = (gv && *gv) ? 1 : 0; } gBTDrive.fireForced = sAutoFire; if (sAutoDrive > 0.0f) @@ -1369,6 +1372,15 @@ void gBTDrive.forced = 1; gBTDrive.forcedThrottle = sAutoDrive; } + else if (sGotoDrive) + { + // BT_GOTO is self-driving: enable forced mode so the goto beeline + // block (gated on gBTDrive.forced, below) runs on its own without + // requiring BT_AUTODRIVE. That block sets the actual throttle + // (drive toward the target, 0 inside the stop radius). + gBTDrive.forced = 1; + gBTDrive.forcedThrottle = 0.8f; + } } if (gBTDrive.allStop) { sLever = 0.0f; sDetent = 0; gBTDrive.allStop = 0; } @@ -1528,19 +1540,56 @@ void { float ddx = s_gx - (float)localOrigin.linearPosition.x; float ddz = s_gz - (float)localOrigin.linearPosition.z; - float want = (float)atan2(-ddx, -ddz); // heading with fwd=-Z - float err = want - gDriveHeading; - while (err > 3.14159265f) err -= 6.2831853f; - while (err < -3.14159265f) err += 6.2831853f; + float dist2 = ddx * ddx + ddz * ddz; + // Heading error from the mech's ACTUAL forward (localToWorld's -Z + // axis), NOT the gDriveHeading scalar mirror: that mirror is seeded + // to 0, but MP pilots (and any non-zero spawn pose) start facing a + // different way, so `want - gDriveHeading` steered the beeline the + // WRONG way. err = signed angle (about +Y) from forward to target. + UnitVector zAxisG; + localToWorld.GetFromAxis(Z_Axis, &zAxisG); + float fwdX = -(float)zAxisG.x, fwdZ = -(float)zAxisG.z; // faces -Z + float err = 0.0f; + float tlen = (float)sqrt((double)dist2); + if (tlen > 1e-3f) + { + float tX = ddx / tlen, tZ = ddz / tlen; + float dot = fwdX * tX + fwdZ * tZ; + float crs = fwdZ * tX - fwdX * tZ; // signed; flip if it steers away + err = (float)atan2((double)crs, (double)dot); + } + // STOP radius: "enemy" holds at weapon range so it can shoot + // instead of ramming; a fixed point drives right up to the spot. + const float stopDist = (s_goto == 2) ? 300.0f : 5.0f; + const float stopDist2 = stopDist * stopDist; + const int arrived = (dist2 < stopDist2); // publish for the mapper bridge (the real-controls path reads // gBTDrive in mechmppr.cpp, NOT this local `turn`) gBTGotoTurn = (err > 0.3f) ? 1.0f : (err < -0.3f ? -1.0f : (err > 0.02f ? 0.3f : (err < -0.02f ? -0.3f : 0.0f))); - if (ddx * ddx + ddz * ddz < 25.0f) gBTGotoTurn = 0.0f; + if (arrived) gBTGotoTurn = 0.0f; // throttle down while far off-heading (the authentic // speed-vs-turn clamp makes run-speed turn circles huge) gBTGotoThrottle = (err > 0.5f || err < -0.5f) ? 0.2f : 1.0f; gBTGotoActive = 1; + if (getenv("BT_GOTO_LOG")) + { + static float s_gl = 0.0f; s_gl += dt; + if (s_gl >= 1.0f) { s_gl = 0.0f; + DEBUG_STREAM << "[goto] dist=" << (float)sqrt((double)dist2) + << " err=" << err << " turn=" << gBTGotoTurn + << " thr=" << gBTGotoThrottle << " arr=" << arrived + << " fwd=(" << fwdX << "," << fwdZ << ")" + << " tgt=(" << s_gx << "," << s_gz << ")\n" << std::flush; } + } + // SELF-DRIVE (task #48): a "beeline" harness must supply its OWN + // forward throttle, not only steering -- otherwise it just turns + // in place and never closes the distance (the "drive-to-range + // stall": BT_GOTO alone advanced nothing because the throttle came + // only from BT_AUTODRIVE). Drive forward toward the target and cut + // the throttle inside the stop radius so it holds at firing range. + gBTDrive.forced = 1; + gBTDrive.forcedThrottle = arrived ? 0.0f : 0.8f; turn = gBTGotoTurn; // non-real-controls fallback path } } @@ -1572,6 +1621,14 @@ void // turnDemand is the mode-shaped steering; speedDemand (world u/s, sign = // reverse) feeds the gait target below. turn = controlsMapper->turnDemand; + // BT_GOTO steering must reach the orientation integration DIRECTLY: the + // mapper round-trip (stickPosition -> turnDemand) zeroes out in -net mode + // (the key-bridge only shapes the local viewpoint mech there), which froze + // the beeline's heading (it drove straight past the target). The goto turn + // is a plain yaw demand -- apply it here, after the mapper read, so it works + // identically solo and cross-net. + extern int gBTGotoActive; extern float gBTGotoTurn; + if (gBTGotoActive) turn = gBTGotoTurn; static float s_mpprLog = 0.0f; s_mpprLog += dt; if (s_mpprLog >= 1.0f) {