feat(libScePad): tilt keyboard sticks partially while a modifier key is held

This commit is contained in:
AlxcBTW committed 2026-10-18 11:00:01 +02:00
1 parent cdb7ed0355
commit a0f14cdf14
7 files changed
+84 -4

No files matched your search

@@ -26,6 +26,7 @@ struct InputSettings {
double mouseMaxSpeed = 1.0;
double mouseSmoothingMs = 10.0;
double mouseDecayMs = 30.0;
double stickModifierScale = 0.5;
};
struct InputConfiguration {
@@ -19,6 +19,7 @@ enum class InputControl {
RightStickDown,
TouchLeft,
TouchRight,
StickModifier,
ToggleMouse,
ToggleFullscreen
};
@@ -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;
}
}
@@ -162,6 +162,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const
PadInputState state = controller;
std::array<bool, 4> negative{};
std::array<bool, 4> 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<double>(positive[axis]) - static_cast<double>(negative[axis])) * scale);
}
if (mouseMode) {
const auto deflection = mouse.Deflection();
@@ -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();
@@ -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<TimedEvent> 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<std::uint32_t>(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},
+11 -1
View File
@@ -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.