diff --git a/context/pod-hardware.md b/context/pod-hardware.md index e08555e..fcbb951 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -118,6 +118,20 @@ The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity q binary's 3) — every streamed record landed one member late (a real-pod landmine: the RIO throttle scalar drove `pedalsPosition`). Ids are now PINNED to the binary numbering with an id-2 pad + `static_assert` locks (`mechmppr.hpp/.cpp`); audit tool `BT_CTRLMAP_LOG=1`. + **⚠ The streamed BUTTON mappings (like record [2]) are NOT CONSUMED yet** — the SCALAR records + drive their attributes, but `MechControlsMapper::AddOrErase` (both overloads, @004b02b0/ + @004b02d4) is still the unreconstructed `Fail("Unhandled mapping!")` stub, so a streamed + button never reaches its attribute. Consequence found the hard way (user report 2026-07-24, + "i cant seem to reverse"): **REVERSE THRUST was dead on the desktop.** LALT→0x3F was bound and + PadRIO pushed the RIO ButtonPressed event correctly, but nothing applied it to attr 6, and the + only writer of `reverseThrust` was the keyboard bridge's `key_throttle < 0` test — which is + (a) bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the + desktop) and (b) unreachable anyway, since `L4PADRIO` clamps the Throttle channel to **[0,1]** + (`low = 0.0f`). Fixed by publishing the 0x3F hold state from `PadRIO::EmitButton` (the single + chokepoint keyboard/pad/joystick/glass clicks all share) and applying it in + `InterpretControls`, **gated on `BTPadRIOActive()`** so pod hardware is untouched — marked + [T3], delete when streamed button mappings land. Verified: hold LALT ⇒ `rev=1`, + `speedDemand −61.5`; release ⇒ forward. Reverse is a **HOLD**, per the manual. [T2] Details: `docs/GLASS_COCKPIT.md` §2a; see [[glass-cockpit]]. 4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software detent — snap to 1.0 when |t−1.0| ≤ 0.05 — then `MechControlsMapper::InterpretControls` @004afd10 diff --git a/engine/MUNGA_L4/L4PADRIO.cpp b/engine/MUNGA_L4/L4PADRIO.cpp index 128a2e1..c7cca9a 100644 --- a/engine/MUNGA_L4/L4PADRIO.cpp +++ b/engine/MUNGA_L4/L4PADRIO.cpp @@ -28,6 +28,14 @@ PadRIO *PadRIO::activeInstance = NULL; // int gBTPadViewToggleEdges = 0; +// +// REVERSE THRUST hold state (the pod's throttle-handle button, unit 0x3F -- +// bound to LALT on the keyboard). Set in EmitButton, the single chokepoint all +// input sources share; consumed by MechControlsMapper::InterpretControls's +// desktop bridge, which owns `reverseThrust` (mapper attr 6 @0x124) every frame. +// +int gBTReverseHeld = 0; + // // The desktop per-MFD preset-page cycle edges (J/K/L -> Mfd1/2/3), consumed // by L4MechControlsMapper::InterpretControls (btl4mppr.cpp step 3b -- the @@ -332,6 +340,24 @@ void void PadRIO::EmitButton(int address, int pressed) { + // + // REVERSE THRUST (the red button on the pod's throttle HANDLE, unit 0x3F). + // BTL4.RES's "L4" streamed record [2] maps `Button Throttle1 (0x3F)` to the + // mapper's attr 6 `ReverseThrust@0x124`, and the 1995 manual is explicit that + // you HOLD it (release = forward) -- see context/pod-hardware.md. Publish the + // hold state here, at the ONE chokepoint every source funnels through + // (keyboard bindings, gamepad, DirectInput joystick, and glass-panel clicks + // via SetScreenButton), so the desktop bridge can honour the button exactly + // like the pod's RIO board did. + // + if (address == 0x3F) + { + gBTReverseHeld = pressed ? 1 : 0; + if (getenv("BT_PAD_LOG")) + DEBUG_STREAM << "[padwrite] reverse button 0x3F " + << (pressed ? "HELD" : "released") << "\n" << std::flush; + } + RIOEvent event; event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent; event.Data.Unit = address; @@ -1053,6 +1079,16 @@ void activeInstance->EmitButton(unit, pressed); } +// +// Is the desktop PAD RIO the live input device? (On real pod hardware the RIO +// is the serial Ranger board and this is 0, so pod behaviour is never altered by +// the desktop-only seams that consult it.) +// +int BTPadRIOActive(void) +{ + return PadRIO::HasActiveInstance(); +} + int PadRIO::GetLampState(int unit) { diff --git a/engine/MUNGA_L4/L4PADRIO.h b/engine/MUNGA_L4/L4PADRIO.h index 70425a7..db441e8 100644 --- a/engine/MUNGA_L4/L4PADRIO.h +++ b/engine/MUNGA_L4/L4PADRIO.h @@ -158,4 +158,12 @@ protected: static PadRIO *activeInstance; + +public: + // + // Is the desktop pad RIO the live input device? (0 on real pod hardware, + // whose RIO is the serial Ranger board -- see BTPadRIOActive.) + // + static int + HasActiveInstance() { return activeInstance != 0; } }; diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 5695c8b..2a65d2a 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -763,7 +763,29 @@ void } } throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle; - reverseThrust = (key_throttle < 0.0f) ? 1 : 0; + // + // REVERSE THRUST -- the pod's red throttle-HANDLE button (RIO unit + // 0x3F, LALT on the keyboard), NOT a lever sweep through zero. The + // authentic wiring is BTL4.RES's "L4" streamed record [2]: + // `Button Throttle1 (0x3F) -> attr 6 ReverseThrust@0x124`, and the + // 1995 manual says HOLD it (release = forward) -- see + // context/pod-hardware.md. + // + // This line used to read ONLY the negative-throttle case, which is + // unreachable on the desktop: L4PADRIO clamps the Throttle channel to + // [0,1] (see its "low = 0.0f" clamp), so key_throttle never goes + // negative -- and because the bridge owns reverseThrust EVERY frame, + // it also zeroed the flag right back out even when the streamed + // button mapping had set it. Net effect: Alt did nothing. + // (User-reported 2026-07-24, "i cant seem to reverse".) + // + // The negative-throttle term stays for the headless harnesses, which + // drive forcedThrottle directly and CAN go negative. + // + { + extern int gBTReverseHeld; // L4PADRIO::EmitButton (any 0x3F source) + reverseThrust = (gBTReverseHeld || key_throttle < 0.0f) ? 1 : 0; + } // (task #68) look-behind: hold 'V' = the pod's rear-view button. // (The other look buttons stay unbound on the keyboard rig.) { @@ -867,6 +889,35 @@ void << mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush; mech->forwardThrottleScale = 1.0f; } + // + // REVERSE THRUST on the DESKTOP. The authentic wiring is BTL4.RES's "L4" + // streamed record [2]: `Button Throttle1 (0x3F, the red button on the throttle + // handle) -> attr 6 ReverseThrust@0x124`, held for reverse / released for + // forward (context/pod-hardware.md, manual-verified). PadRIO publishes that + // button's hold state from EmitButton -- the one chokepoint every desktop + // source funnels through (keyboard LALT, gamepad, DirectInput joystick, glass + // panel click) -- but our streamed BUTTON mappings are not consumed yet + // (MechControlsMapper::AddOrErase is still the unreconstructed Fail stub), so + // the event never reaches attr 6 on its own. Apply it here, gated on the pad + // RIO being the live device so REAL POD hardware is untouched (there + // BTPadRIOActive() is 0 and the streamed mapping owns the flag). + // + // This is why LALT did nothing: the only writer was the keyboard bridge's + // `key_throttle < 0` test, which (a) is bypassed entirely whenever a RIO is + // operational -- which the pad RIO always is -- and (b) can never fire anyway, + // because L4PADRIO clamps the Throttle channel to [0,1]. + // [T3 accommodation, marked: delete this block once streamed button mappings + // land, at which point record [2] drives attr 6 by itself.] + // + { + extern int BTPadRIOActive(void); + extern int gBTReverseHeld; + if (BTPadRIOActive()) + { + reverseThrust = gBTReverseHeld ? 1 : 0; + } + } + if (reverseThrust < 1) { speedDemand = diff --git a/scratchpad/revtest.py b/scratchpad/revtest.py new file mode 100644 index 0000000..c2b6b64 --- /dev/null +++ b/scratchpad/revtest.py @@ -0,0 +1,88 @@ +"""Reverse-thrust verification: hold W to build throttle, then hold LALT and +confirm the mapper flips to reverse (speedDemand goes negative). + +Injects keys with keybd_event into a btl4 launched with BT_KEY_NOFOCUS=1, so it +does not need window focus. Kills ONLY the PID it spawned. +""" +import ctypes +import os +import re +import subprocess +import sys +import time + +VK_W = 0x57 +VK_LMENU = 0xA4 # left Alt +KEYEVENTF_KEYUP = 0x0002 + +user32 = ctypes.windll.user32 + +def hold(vk, seconds): + user32.keybd_event(vk, 0, 0, 0) + time.sleep(seconds) + +def release(vk): + user32.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0) + +CONTENT = r"C:\git\bt411\content" +EXE = r"C:\git\bt411\build\Release\btl4.exe" +LOG = r"C:\git\bt411\scratchpad_rev.log" + +if os.path.exists(LOG): + os.remove(LOG) + +env = dict(os.environ) +env.update({ + "BT_START_INSIDE": "1", + "BT_KEY_NOFOCUS": "1", + "BT_MPPR_TRACE": "1", # [mppr-c] thr= rev= -> dmd= + "BT_PAD_LOG": "1", # [padwrite] reverse button + "BT_LOG": LOG, +}) +proc = subprocess.Popen([EXE, "-egg", "LAST.EGG"], cwd=CONTENT, env=env) +print("spawned pid", proc.pid) + +try: + # wait for the mission to be running (the trace only prints in-mission) + deadline = time.time() + 180 + while time.time() < deadline: + if os.path.exists(LOG): + txt = open(LOG, errors="replace").read() + if "[mppr-c]" in txt: + break + time.sleep(2) + else: + print("FAIL: never reached the mapper trace") + sys.exit(1) + + # 1) throttle up with W for 3s, no Alt -> expect rev=0, dmd > 0 + hold(VK_W, 6.0) + release(VK_W) + time.sleep(1.5) + mark = len(open(LOG, errors="replace").read()) + + # 2) now hold LALT for 4s -> expect rev=1 and dmd < 0 + hold(VK_LMENU, 5.0) + release(VK_LMENU) + time.sleep(1.5) + + txt = open(LOG, errors="replace").read() + before = txt[:mark] + during = txt[mark:] + + def rows(chunk): + return re.findall(r"\[mppr-c\] thr=([-\d.]+) rev=(\d+).*?dmd=([-\d.]+)", chunk) + + b = rows(before) + d = rows(during) + print("\n--- forward phase (last 3 samples):", b[-3:]) + print("--- alt-held phase (samples): ", d[:6]) + print("--- [padwrite] lines:", re.findall(r"\[padwrite\] reverse button.*", txt)[:4]) + + fwd_ok = any(int(r[1]) == 0 and float(r[2]) > 0.5 for r in b) + rev_ok = any(int(r[1]) == 1 and float(r[2]) < -0.5 for r in d) + print("\nforward (rev=0, dmd>0): %s" % ("PASS" if fwd_ok else "FAIL")) + print("reverse (rev=1, dmd<0): %s" % ("PASS" if rev_ok else "FAIL")) +finally: + proc.terminate() + print("killed", proc.pid)