From a0f14cdf1468d5663ddf5d8387891efefec01662 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:53:42 +0200 Subject: [PATCH] feat(libScePad): tilt keyboard sticks partially while a modifier key is held --- .../prx/libScePad/include/InputMapping.hpp | 1 + .../prx/libScePad/include/PadInputTypes.hpp | 1 + .../prx/libSceVideoOut/src/InputMapping.cpp | 8 +++- .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 5 ++- .../prx/libSceVideoOut/tests/InputMapping.cpp | 23 +++++++++++ .../libSceVideoOut/tests/KeyboardMouse.cpp | 38 +++++++++++++++++++ docs/user/INPUT_MAPPING.md | 12 +++++- 7 files changed, 84 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index a8914303d5..e2105ace07 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -26,6 +26,7 @@ struct InputSettings { double mouseMaxSpeed = 1.0; double mouseSmoothingMs = 10.0; double mouseDecayMs = 30.0; + double stickModifierScale = 0.5; }; struct InputConfiguration { diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 81842bae72..b6a49220a1 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -19,6 +19,7 @@ enum class InputControl { RightStickDown, TouchLeft, TouchRight, + StickModifier, ToggleMouse, ToggleFullscreen }; diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index 334984d1f7..91bea15ffc 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -51,6 +51,7 @@ constexpr auto actions = std::array{ InputAction{"RightStickDown", Pad::InputControl::RightStickDown, Pad::PadButton::None}, InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None}, InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None}, + InputAction{"StickModifier", Pad::InputControl::StickModifier, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None} }; @@ -132,7 +133,8 @@ enum class InputSetting { MouseDeadzoneOffset, MouseMaxSpeed, MouseSmoothingMs, - MouseDecayMs + MouseDecayMs, + StickModifierScale }; struct InputSettingName { @@ -149,7 +151,8 @@ constexpr auto settingNames = std::array{ InputSettingName{"MouseDeadzoneOffset", InputSetting::MouseDeadzoneOffset}, InputSettingName{"MouseMaxSpeed", InputSetting::MouseMaxSpeed}, InputSettingName{"MouseSmoothingMs", InputSetting::MouseSmoothingMs}, - InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs} + InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs}, + InputSettingName{"StickModifierScale", InputSetting::StickModifierScale} }; const InputSettingName* findSetting(std::string_view name) { @@ -207,6 +210,7 @@ void applySetting(Pad::InputSettings& settings, InputSetting setting, std::strin case InputSetting::MouseMaxSpeed: settings.mouseMaxSpeed = parseNumber(value, "mouse max speed", 0.1, 10.0); break; case InputSetting::MouseSmoothingMs: settings.mouseSmoothingMs = parseNumber(value, "mouse smoothing", 0.0, 500.0); break; case InputSetting::MouseDecayMs: settings.mouseDecayMs = parseNumber(value, "mouse decay", 1.0, 1000.0); break; + case InputSetting::StickModifierScale: settings.stickModifierScale = parseNumber(value, "stick modifier scale", 0.05, 1.0); break; } } diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index 9b58c374de..3d00959173 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -162,6 +162,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const PadInputState state = controller; std::array negative{}; std::array positive{}; + bool modifier = false; for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { if (!pressed[index]) continue; const auto& binding = configuration.bindings[index]; @@ -181,12 +182,14 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const case InputControl::RightStickDown: positive[3] = true; break; case InputControl::TouchLeft: state.touchLeft = true; break; case InputControl::TouchRight: state.touchRight = true; break; + case InputControl::StickModifier: modifier = true; break; case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; } } + const double scale = modifier ? configuration.settings.stickModifierScale : 1.0; for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { - if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; + if (negative[axis] || positive[axis]) state.sticks[axis] = StickByte((static_cast(positive[axis]) - static_cast(negative[axis])) * scale); } if (mouseMode) { const auto deflection = mouse.Deflection(); diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 1fe0f4b30b..33b86fdfde 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -147,6 +147,28 @@ void nonAsciiPaths() { require(message.find(utf8Name) != std::string::npos, "a missing file names the path in UTF-8"); } +std::size_t stickModifierBindings(const Pad::InputConfiguration& configuration) { + std::size_t count = 0; + for (const auto& binding : configuration.bindings) count += binding.control == Pad::InputControl::StickModifier ? 1 : 0; + return count; +} + +void stickModifier() { + useConfig("Cross = KEY:F\n"); + auto configuration = Pad::LoadInputMapping(); + require(stickModifierBindings(configuration) == 0, "no key is a stick modifier by default"); + require(configuration.settings.stickModifierScale == 0.5, "the stick modifier scale defaults to 0.5"); + useConfig("StickModifier = KEY:Z\nstickmodifier = MOUSE:X2\nStickModifierScale = 0.35\n"); + configuration = Pad::LoadInputMapping(); + require(stickModifierBindings(configuration) == 2, "StickModifier takes a key and a mouse button"); + require(configuration.settings.stickModifierScale == 0.35, "the stick modifier scale is read"); + require(rejects("StickModifier = WHEEL:Up\n"), "a wheel direction cannot be a stick modifier"); + require(rejects("StickModifierScale = 0.04\n"), "a stick modifier scale below 0.05 is rejected"); + require(rejects("StickModifierScale = 1.5\n"), "a stick modifier scale above 1 is rejected"); + require(rejects("StickModifierScale = half\n"), "a non-numeric stick modifier scale is rejected"); + require(rejects("StickModifierScale = 0.5\nStickModifierScale = 0.6\n"), "a stick modifier scale given twice is rejected"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -183,6 +205,7 @@ int main() try { mouseLookDefault(); mouseLookParse(); mouseLookReject(); + stickModifier(); nonAsciiPaths(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index fa9b6f7e3a..2e7a85e3e6 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -254,6 +254,43 @@ void testKeyboardSticksUnchanged() { check(samples[44].state.sticks[0] == 128, "released keys center the stick"); } +Pad::InputConfiguration withModifier(double scale) { + auto configuration = defaults(); + configuration.bindings.push_back({SDL_SCANCODE_Z, Pad::MouseButton::None, Pad::InputControl::StickModifier}); + configuration.bindings.push_back({SDL_SCANCODE_UNKNOWN, Pad::MouseButton::X2, Pad::InputControl::StickModifier}); + configuration.settings.stickModifierScale = scale; + return configuration; +} + +void testStickModifier() { + const std::vector events{ + At(0.0, KeyEvent(SDL_SCANCODE_W, true)), + At(10.0, KeyEvent(SDL_SCANCODE_Z, true)), + At(20.0, KeyEvent(SDL_SCANCODE_D, true)), + At(30.0, KeyEvent(SDL_SCANCODE_A, true)), + At(40.0, KeyEvent(SDL_SCANCODE_Z, false)), + At(50.0, KeyEvent(SDL_SCANCODE_A, false)), + At(60.0, KeyEvent(SDL_SCANCODE_D, false)), + At(70.0, PadSimulation::ButtonEvent(SDL_BUTTON_X2, true)), + At(80.0, KeyEvent(SDL_SCANCODE_S, true)), + At(90.0, PadSimulation::ButtonEvent(SDL_BUTTON_X2, false))}; + const auto samples = Simulate(withModifier(0.5), events, 1.0, 100.0); + check(samples[4].state.sticks[1] == 0, "keys tilt fully before the modifier is held"); + check(samples[14].state.sticks[1] == 64 && samples[14].state.sticks[0] == 128, "the modifier halves the tilt of a held key"); + check(samples[24].state.sticks[0] == 192 && samples[24].state.sticks[1] == 64, "the modifier halves a second axis"); + check(samples[34].state.sticks[0] == 128, "opposite keys still cancel under the modifier"); + check(samples[44].state.sticks[1] == 0 && samples[44].state.sticks[0] == 128, "letting go of the modifier restores the full tilt"); + check(samples[74].state.sticks[1] == 64 && samples[74].state.sticks[0] == 128, "a modifier on a mouse button scales the held keys"); + check(samples[84].state.sticks[1] == 128, "opposite keys cancel under a mouse button modifier"); + check(samples[94].state.sticks[1] == 128, "opposite keys still cancel once the mouse button modifier is released"); + const auto alone = Simulate(withModifier(0.5), {At(0.0, KeyEvent(SDL_SCANCODE_Z, true))}, 1.0, 10.0); + check(alone.back().state.sticks[0] == 128 && alone.back().state.sticks[1] == 128, "the modifier alone moves no stick"); + const auto quarter = Simulate(withModifier(0.25), {At(0.0, KeyEvent(SDL_SCANCODE_Z, true)), At(0.0, KeyEvent(SDL_SCANCODE_D, true))}, 1.0, 10.0); + check(quarter.back().state.sticks[0] == 160, "the scale setting sets the tilt"); + const auto full = Simulate(withModifier(1.0), {At(0.0, KeyEvent(SDL_SCANCODE_Z, true)), At(0.0, KeyEvent(SDL_SCANCODE_D, true))}, 1.0, 10.0); + check(full.back().state.sticks[0] == 255, "a scale of 1 tilts fully"); +} + void testWheelPress() { const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1))}, 1.0, 200.0); const auto up = static_cast(Pad::PadButton::Up); @@ -345,6 +382,7 @@ int main() { {"vertical scale", testVerticalScale}, {"mouse mode off", testMouseModeOff}, {"keyboard sticks unchanged", testKeyboardSticksUnchanged}, + {"stick modifier", testStickModifier}, {"wheel press", testWheelPress}, {"wheel and keys on focus loss", testWheelFocusLoss}, {"focus loss releases the mouse", testFocusLossReleasesMouse}, diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 882ce0a000..740ed4e104 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -16,7 +16,7 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `StickModifier`, `ToggleMouse`, and `ToggleFullscreen`. ### Default bindings @@ -69,6 +69,7 @@ The same file also accepts these settings, each at most once, in the form `Setti | `MouseMaxSpeed` | Number from 0.1 to 10, the highest tracked speed as a multiple of the full-tilt speed. Above 1, a fast flick keeps the stick at full tilt a little longer after the mouse stops. | `1` | | `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. 0 follows every mouse report directly. | `10` | | `MouseDecayMs` | Number from 1 to 1000, the milliseconds without movement after which the stick is centered. | `30` | +| `StickModifierScale` | Number from 0.05 to 1, the tilt of keyboard stick bindings while a `StickModifier` binding is held. | `0.5` | ```ini LeftStickDeadzone = 12 @@ -76,6 +77,15 @@ RightStickDeadzone = 8 MouseSensitivity = 2.5 ``` +## Partial stick tilt + +`StickModifier` has no built-in binding. While a key or mouse button bound to it is held, the keyboard stick bindings tilt the stick by `StickModifierScale` of its range instead of fully, for walking or fine aim. Controller sticks and the mouse stick are not affected, and opposite keys still cancel each other. + +```ini +StickModifier = KEY:Z +StickModifierScale = 0.4 +``` + ## Mouse as the right stick `ToggleMouse` (the middle mouse button by default) captures the mouse and drives the right stick from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus.