From 827c5b295bf95aeb9ebfada2d177750e87d2c0e2 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sat, 8 Aug 2026 20:24:40 -0500 Subject: [PATCH] The controls answer only while the game is the window in front Testers taking notes in another window were flying the pod while they typed. RP412INPUTFOCUS=1 is the new default; 0 restores the old behaviour. The pod was the only thing running on its cabinet, so the virtual RIO reads the key state directly rather than waiting on the message pump. That is the right call for latency and it is why the pedals feel like pedals - but a direct read is a read of the WHOLE keyboard, whatever has focus. On a cabinet that distinction did not exist. On a desktop it is the difference between writing a bug report and steering into a wall while you write it. One choke point does the whole job: PadRIO::PollInputs is where the keyboard, the XInput pad and the DirectInput stick are all read, so a single flag covers the three of them. The joystick needs no change of its own - unfocused the resolve block is skipped, every device slot stays at -1, and the button, hat and axis loops find no device and read released on their own. It is opened DISCL_BACKGROUND on purpose, or it would stop answering the moment a cockpit pane took focus, so declining to poll it is what makes it go quiet. Each source reads as RELEASED rather than the poll returning early, and that is the part worth keeping: bail out instead and whatever was held at the moment you switched away stays held until you come back, which is the stuck throttle this is meant to prevent rather than cause. Reading released lets the diffs already in there turn it into proper release events. The throttle accumulator is the deliberate exception. It is the pod's one sticky axis and it integrates what the controls ask for, so controls asking for nothing simply stop moving it - you come back to the speed you left rather than to a dead stop. Focus is tested per PROCESS, not against one window handle. The cockpit is a shell full of child panes, the exploded view is six windows of its own and the plasma glass another; matching a single HWND would drop the controls the moment somebody clicked an MFD. Real RIO cockpit hardware is untouched - this is the keyboard, pad and joystick path only. The volume and bass keys in L4CTRL were already gated this way, unconditionally, which is where the idiom comes from. On by default because the alternative is every tester editing a file before the fix reaches them: an environ.ini written by an older build does not carry the line, so the built-in default is what they get. The log says which way it is set, and the option-mention check names it as one they have not heard of. Verified against the built exe both ways: a fresh run writes the documented default and applies 14 settings where it applied 13, and a file with the line removed reports exactly one unknown option and falls back to focus-gated. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA_L4/L4PADRIO.cpp | 85 ++++++++++++++++++++++++++++++++++++++++--- RP_L4/RPL4ENVIRON.cpp | 14 +++++++ 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/MUNGA_L4/L4PADRIO.cpp b/MUNGA_L4/L4PADRIO.cpp index d853b74..53568b3 100644 --- a/MUNGA_L4/L4PADRIO.cpp +++ b/MUNGA_L4/L4PADRIO.cpp @@ -42,6 +42,48 @@ namespace return (GetAsyncKeyState(virtual_key) & 0x8000) != 0; } + // + // RP412INPUTFOCUS - do the controls answer only while the game is the + // window in front? On unless the file says 0. + // + // The pod was the only thing running on its cabinet, so the virtual + // RIO reads the key state directly rather than waiting on the message + // pump. That is the right call for latency and it is why the pedals + // feel like pedals - but a direct read is a read of the WHOLE + // keyboard, whatever has focus, so a player alt-tabbed into a text + // editor was flying the pod with every note they typed. + // + Logical InputNeedsFocus() + { + static int setting = -1; + if (setting < 0) + { + const char *value = getenv("RP412INPUTFOCUS"); + setting = (value != NULL && *value == '0') ? 0 : 1; + } + return setting ? True : False; + } + + // + // Whether the foreground window is one of OURS - the process, not one + // particular handle. The cockpit is a shell full of child panes, the + // exploded view is six windows of its own and the plasma glass + // another, so any of them being in front is the game being in front. + // Matching a single HWND would drop the controls the moment somebody + // clicked an MFD. + // + Logical ProcessHasFocus() + { + HWND foreground = GetForegroundWindow(); + if (foreground == NULL) + { + return False; + } + DWORD foreground_process = 0; + GetWindowThreadProcessId(foreground, &foreground_process); + return (foreground_process == GetCurrentProcessId()) ? True : False; + } + // // A generic stick axis is already normalized -1..1, so the deadzone // is a plain cut about centre with the remainder rescaled - press @@ -231,6 +273,11 @@ PadRIO::PadRIO() activeInstance = this; DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush; + DEBUG_STREAM << "PadRIO: controls " + << (InputNeedsFocus() + ? "answer only while the game window has focus" + : "answer whether the game has focus or not (RP412INPUTFOCUS=0)") + << "\n" << std::flush; // // Only open DirectInput when the profile actually asks for it. A @@ -390,6 +437,25 @@ void } lastPollTick = now; + //--------------------------------------------------------------- + // Is the game the window in front? Every source below is gated on + // this - keyboard, pad and stick alike. + // + // Gated rather than skipped, and that is the whole trick: each + // source reads as RELEASED instead of the poll returning early, so + // the diffs further down turn whatever was held at the moment you + // switched away into proper release events. Bail out instead and a + // key held on alt-tab stays down until you come back, which is the + // stuck throttle this is meant to prevent rather than cause. + // + // The throttle accumulator is the deliberate exception. It is the + // pod's one sticky axis and it integrates what the controls ask + // for, so controls asking for nothing simply stop moving it - you + // come back to the speed you left, not to a dead stop. + //--------------------------------------------------------------- + Logical input_live = + (!InputNeedsFocus() || ProcessHasFocus()) ? True : False; + //--------------------------------------------------------------- // Find / keep the XInput pad. Probing empty slots is slow, so an // absent pad is only re-probed every 3 seconds. @@ -398,7 +464,7 @@ void memset(&pad, 0, sizeof(pad)); Logical pad_live = False; - if (padIndex >= 0) + if (input_live && padIndex >= 0) { pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS); if (!pad_live) @@ -407,7 +473,7 @@ void padIndex = -1; } } - if (padIndex < 0 && (now - lastPadCheckTick) >= 3000) + if (input_live && padIndex < 0 && (now - lastPadCheckTick) >= 3000) { lastPadCheckTick = now; for (DWORD i = 0; i < 4; ++i) @@ -442,7 +508,7 @@ void for (int i = 0; i < profile.keyButtonCount; ++i) { PadKeyButtonBinding *binding = &profile.keyButtons[i]; - Logical down = KeyDown(binding->virtualKey); + Logical down = input_live && KeyDown(binding->virtualKey); if (binding->toggle && down && !binding->wasDown) { binding->latched = !binding->latched; @@ -495,8 +561,15 @@ void { joyDevice[slot] = -1; } - if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 || - profile.joyHatCount > 0) + // + // Unfocused this whole block is skipped, which leaves every joyDevice + // slot at -1 - so the button, hat and axis loops below find no device + // and read released and centred on their own. The stick is opened + // DISCL_BACKGROUND (it has to be, or it stops answering the moment a + // pane takes focus), so not polling it is what makes it go quiet. + // + if (input_live && (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 || + profile.joyHatCount > 0)) { RPJoyPoll(); for (int slot = 0; slot < BindJoyDeviceSlots; ++slot) @@ -614,7 +687,7 @@ void for (int i = 0; i < profile.keyAxisCount; ++i) { const PadKeyAxisBinding *binding = &profile.keyAxes[i]; - if (KeyDown(binding->virtualKey)) + if (input_live && KeyDown(binding->virtualKey)) { if (binding->mode == BindKeyRate) { diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index aedb7ef..54fa930 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -50,6 +50,20 @@ namespace "# Unset falls back to KEYBOARD alone.\n" "L4CONTROLS=PAD;KEYBOARD\n" "\n" +"# Read the keyboard, pad and stick only while the game is the window in\n" +"# front. The pod was the only thing running on its cabinet, so the\n" +"# virtual RIO reads the key state directly rather than waiting on the\n" +"# message pump - which means it reads it whatever is in front, and\n" +"# switching to another window to type flies the pod around while you\n" +"# type in it.\n" +"# 1 controls go neutral when you switch away (default)\n" +"# 0 read them regardless, as earlier builds did\n" +"# Any window of the game counts as the game, so clicking an MFD pane or\n" +"# the plasma glass does not drop your controls. Real RIO cockpit\n" +"# hardware is unaffected either way - this is the keyboard, pad and\n" +"# joystick path only.\n" +"RP412INPUTFOCUS=1\n" +"\n" "# Renderer bring-up argument. Only its presence is checked (the DPL\n" "# resolution parsing it once fed is gone) and the game refuses to start\n" "# without it - any non-empty value works. Leave as shipped.\n"