diff --git a/README.md b/README.md index 1566525..5a98c73 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,62 @@ target, with the same four choices. `Desktop` uses the monitor on Windows and on Linux. `Rendering.TextureLodBias` shifts texture mip selection; negative values keep textures sharper at a distance. +## Mouse and keyboard + +Edit `[Controls]` in `lcs/config/LCSNative.ini`, then restart. A +binding is one or more names separated by commas. `None` clears it. +`Shift`, `Ctrl`, and `Alt` set both sides. The accepted names are +commented in that file. F10 opens host settings. F11 toggles +fullscreen. Moving the mouse looks around. A controller button +takes the same action names. + +| Binding | Default | Action | +| --- | --- | --- | +| `MoveForward` | W | On foot, move forward. In a vehicle, accelerate. | +| `MoveBack` | S | On foot, move back. In a vehicle, brake. | +| `MoveLeft` | A | Move or steer left. | +| `MoveRight` | D | Move or steer right. | +| `Walk` | Left Alt | Shorter steps. | +| `Sprint` | Space | Run on foot. Not sent in a vehicle. | +| `Jump` | Left Shift, Right Shift | Jump. In a vehicle the game reads this as the brake. | +| `Attack` | Left mouse button | Attack or fire. | +| `Aim` | Right mouse button | Aim on foot. Not sent in a vehicle. | +| `CenterCamera` | H, middle mouse button | Put the camera behind you. Also picks up a weapon. | +| `EnterVehicle` | F, Enter | Enter or exit a vehicle. | +| `WeaponPrevious` | Q, Left, wheel up | Previous weapon. In a vehicle, previous radio station. | +| `WeaponNext` | E, Right, wheel down | Next weapon. In a vehicle, next radio station. | +| `Up` | Up | Toggle a special mission. In a vehicle, also pitches up. | +| `Down` | Down | D-pad down. In a vehicle, horn and pitch down. | +| `Pause` | Escape | Pause. | +| `Camera` | V | Change camera. | +| `Handbrake` | Space | Handbrake in a vehicle. | + +Controller names follow the Xbox layout. On a PlayStation pad, A is +Cross, B is Circle, X is Square, and Y is Triangle. `LeftStick` and +`RightStick` are `Move` or `Camera`. A trigger does not send its +buttons in a vehicle; accelerate and brake still apply. + +| Button | Default | Action | +| --- | --- | --- | +| A / Cross | `Sprint` | Run. Also sent in a vehicle. | +| B / Circle | `Attack` | Attack or fire. | +| X / Square | `Jump` | In a vehicle the game reads this as the brake. | +| Y / Triangle | `EnterVehicle` | Enter or exit a vehicle. | +| LB / L1 | `CenterCamera` | Puts the camera behind you. Also picks up a weapon. | +| RB / R1 | `Aim` | Aim. In a vehicle, the handbrake. | +| LT / L2 | `MoveBack`, `CenterCamera` | Brake. On foot, also centers the camera. | +| RT / R2 | `MoveForward`, `Aim` | Accelerate. On foot, also aims. | +| D-pad left | `WeaponPrevious` | Previous weapon. In a vehicle, the previous radio station. | +| D-pad right | `WeaponNext` | Next weapon. In a vehicle, the next radio station. | +| D-pad up | `Up` | Special mission. | +| D-pad down | `Down` | Horn in a vehicle. | +| Start | `Pause` | Pause. | +| Back / Share | `Camera` | Change camera. | +| Left stick | `Move` | Movement. `LeftStick` swaps this with the camera. | +| Right stick | `Camera` | Camera. | +| Left stick click | `Down` | Horn in a vehicle. | +| Right stick click | `None` | `PadRightStickClick`. | + ## Linux The native build uses Vulkan for the GE and SDL2 for the window and diff --git a/lcs/config/LCSNative.ini b/lcs/config/LCSNative.ini index 214aa51..ed24f94 100644 --- a/lcs/config/LCSNative.ini +++ b/lcs/config/LCSNative.ini @@ -29,6 +29,62 @@ AspectRatio=auto MouseSensitivity=12 CameraSmoothing=50 +; Mouse and keyboard. Restart after editing. Names ignore case. +; Several keys: separate with commas. None clears a binding. +; Shift, Ctrl, and Alt set both sides. +; F10 opens host settings. F11 toggles fullscreen. +; Letters, digits, F1-F9, F12, Space, Enter, Escape, Tab, Backspace, +; LeftShift, RightShift, LeftCtrl, RightCtrl, LeftAlt, RightAlt, +; Up, Down, Left, Right, Numpad0-Numpad9, NumpadPlus, NumpadMinus, +; Delete, Insert, Home, End, PageUp, PageDown, Comma, Period, Minus, Equals, +; MouseLeft, MouseRight, MouseMiddle, Mouse4, Mouse5, WheelUp, WheelDown. +; On foot, Move* is the stick. In a vehicle, MoveForward accelerates, +; MoveBack brakes, MoveLeft and MoveRight steer, and Up and Down pitch. +; Sprint and Aim are not sent in a vehicle. Handbrake is. +MoveForward=W +MoveBack=S +MoveLeft=A +MoveRight=D +Walk=LeftAlt +Sprint=Space +Jump=LeftShift,RightShift +EnterVehicle=F,Enter +Attack=MouseLeft +Aim=MouseRight +CenterCamera=H,MouseMiddle +WeaponPrevious=Q,Left,WheelUp +WeaponNext=E,Right,WheelDown +Up=Up +Down=Down +Pause=Escape +Camera=V +Handbrake=Space + +; Controller. Xbox names. On a PlayStation pad, A is Cross, B is Circle, +; X is Square, and Y is Triangle. The value is an action from the list above. +; A trigger does not send its buttons in a vehicle. MoveForward still +; accelerates and MoveBack still brakes. +; LeftStick and RightStick are Move or Camera. +; Left stick click is the horn in a vehicle. +PadA=Sprint +PadB=Attack +PadX=Jump +PadY=EnterVehicle +PadLeftShoulder=CenterCamera +PadRightShoulder=Aim +PadLeftTrigger=MoveBack,CenterCamera +PadRightTrigger=MoveForward,Aim +PadStart=Pause +PadBack=Camera +PadDpadUp=Up +PadDpadDown=Down +PadDpadLeft=WeaponPrevious +PadDpadRight=WeaponNext +PadLeftStickClick=Down +PadRightStickClick=None +LeftStick=Move +RightStick=Camera + [Timing] ; 30 or 60. Tested at 60. FrameRate=60 diff --git a/lcs/host/display_window.cpp b/lcs/host/display_window.cpp index 2296429..3bcd071 100644 --- a/lcs/host/display_window.cpp +++ b/lcs/host/display_window.cpp @@ -6,6 +6,7 @@ #include "lcs_controls.hpp" #include "lcs_display_menu.hpp" #include "lcs_menu.hpp" +#include "lcs_key_bindings.hpp" #include "lcs_render_config.hpp" #if defined(_WIN32) @@ -44,51 +45,52 @@ void discard_pending_pointer() noexcept { g_mouse_dy.store(0, std::memory_order_relaxed); } -constexpr std::uint32_t kPspSelect = 0x000001u; -constexpr std::uint32_t kPspStart = 0x000008u; -constexpr std::uint32_t kPspUp = 0x000010u; -constexpr std::uint32_t kPspRight = 0x000020u; -constexpr std::uint32_t kPspDown = 0x000040u; -constexpr std::uint32_t kPspLeft = 0x000080u; -constexpr std::uint32_t kPspLTrigger = 0x000100u; -constexpr std::uint32_t kPspRTrigger = 0x000200u; -constexpr std::uint32_t kPspTriangle = 0x001000u; -constexpr std::uint32_t kPspCircle = 0x002000u; -constexpr std::uint32_t kPspCross = 0x004000u; -constexpr std::uint32_t kPspSquare = 0x008000u; - -struct KeyBinding { - int virtual_key; - std::uint32_t psp_button; -}; - -constexpr KeyBinding kKeyBindings[] = { - {VK_SPACE, kPspCross}, - {VK_LSHIFT, kPspSquare}, - {VK_RSHIFT, kPspSquare}, - {'F', kPspTriangle}, - {VK_RETURN, kPspTriangle}, - {'Q', kPspLeft}, - {'E', kPspRight}, - {'H', kPspLTrigger}, - {VK_UP, kPspUp}, - {VK_DOWN, kPspDown}, - {VK_LEFT, kPspLeft}, - {VK_RIGHT, kPspRight}, - {VK_ESCAPE, kPspStart}, - {'V', kPspSelect}, -}; - -constexpr KeyBinding kMouseBindings[] = { - {VK_LBUTTON, kPspCircle}, - {VK_RBUTTON, kPspRTrigger}, - {VK_MBUTTON, kPspLTrigger}, -}; - -constexpr int kMoveForward = 'W'; -constexpr int kMoveBack = 'S'; -constexpr int kMoveLeft = 'A'; -constexpr int kMoveRight = 'D'; +int win32_virtual_key(HostKey key) noexcept { + const int id = static_cast(key); + if (host_key_in(key, HostKey::A, HostKey::Z)) + return 'A' + (id - static_cast(HostKey::A)); + if (host_key_in(key, HostKey::Digit0, HostKey::Digit9)) + return '0' + (id - static_cast(HostKey::Digit0)); + if (host_key_in(key, HostKey::F1, HostKey::F12)) + return VK_F1 + (id - static_cast(HostKey::F1)); + if (host_key_in(key, HostKey::Numpad0, HostKey::Numpad9)) + return VK_NUMPAD0 + (id - static_cast(HostKey::Numpad0)); + switch (key) { + case HostKey::Space: return VK_SPACE; + case HostKey::Enter: return VK_RETURN; + case HostKey::Escape: return VK_ESCAPE; + case HostKey::Tab: return VK_TAB; + case HostKey::Backspace: return VK_BACK; + case HostKey::LeftShift: return VK_LSHIFT; + case HostKey::RightShift: return VK_RSHIFT; + case HostKey::LeftCtrl: return VK_LCONTROL; + case HostKey::RightCtrl: return VK_RCONTROL; + case HostKey::LeftAlt: return VK_LMENU; + case HostKey::RightAlt: return VK_RMENU; + case HostKey::Up: return VK_UP; + case HostKey::Down: return VK_DOWN; + case HostKey::Left: return VK_LEFT; + case HostKey::Right: return VK_RIGHT; + case HostKey::NumpadPlus: return VK_ADD; + case HostKey::NumpadMinus: return VK_SUBTRACT; + case HostKey::Delete: return VK_DELETE; + case HostKey::Insert: return VK_INSERT; + case HostKey::Home: return VK_HOME; + case HostKey::End: return VK_END; + case HostKey::PageUp: return VK_PRIOR; + case HostKey::PageDown: return VK_NEXT; + case HostKey::Comma: return VK_OEM_COMMA; + case HostKey::Period: return VK_OEM_PERIOD; + case HostKey::Minus: return VK_OEM_MINUS; + case HostKey::Equals: return VK_OEM_PLUS; + case HostKey::MouseLeft: return VK_LBUTTON; + case HostKey::MouseRight: return VK_RBUTTON; + case HostKey::MouseMiddle: return VK_MBUTTON; + case HostKey::Mouse4: return VK_XBUTTON1; + case HostKey::Mouse5: return VK_XBUTTON2; + default: return 0; + } +} bool key_down(int virtual_key) noexcept { return (GetAsyncKeyState(virtual_key) & 0x8000) != 0; @@ -120,6 +122,8 @@ constexpr std::uint16_t kPadDpadLeft = 0x0004u; constexpr std::uint16_t kPadDpadRight = 0x0008u; constexpr std::uint16_t kPadStart = 0x0010u; constexpr std::uint16_t kPadBack = 0x0020u; +constexpr std::uint16_t kPadLeftThumb = 0x0040u; +constexpr std::uint16_t kPadRightThumb = 0x0080u; constexpr std::uint16_t kPadLeftShoulder = 0x0100u; constexpr std::uint16_t kPadRightShoulder = 0x0200u; constexpr std::uint16_t kPadA = 0x1000u; @@ -141,16 +145,6 @@ PfnXInputGetState xinput_get_state() noexcept { return resolved; } -std::uint8_t stick_to_psp(std::int16_t value, bool invert) noexcept { - constexpr int kDeadZone = 7849; - int magnitude = std::abs(static_cast(value)); - if (magnitude <= kDeadZone) return 128u; - magnitude = (magnitude - kDeadZone) * 32767 / (32767 - kDeadZone); - int signed_value = value < 0 ? -magnitude : magnitude; - if (invert) signed_value = -signed_value; - return static_cast(std::clamp(128 + signed_value * 127 / 32767, 0, 255)); -} - void update_cursor_clip(HWND hwnd, bool capture) noexcept { if (!capture || hwnd == nullptr || !lcs_camera_hook_enabled() || !lcs_camera_in_use()) { ClipCursor(nullptr); @@ -629,32 +623,20 @@ HostInputState display_window_input() { const ControlsConfiguration &controls = lcs_render_configuration().controls; if (focused) { - for (const KeyBinding &binding : kKeyBindings) - if (key_down(binding.virtual_key)) input.buttons |= binding.psp_button; - if (cursor_in_client(g_window)) - for (const KeyBinding &binding : kMouseBindings) - if (key_down(binding.virtual_key)) input.buttons |= binding.psp_button; - if (driving) { - input.buttons &= ~(kPspCross | kPspRTrigger); - if (key_down(VK_SPACE)) input.buttons |= kPspRTrigger; - } - - int move_x = 0; - int move_y = 0; - if (key_down(kMoveLeft)) move_x -= 1; - if (key_down(kMoveRight)) move_x += 1; - if (!driving) { - if (key_down(kMoveForward)) move_y -= 1; - if (key_down(kMoveBack)) move_y += 1; - } else { - if (key_down(VK_UP)) move_y -= 1; - if (key_down(VK_DOWN)) move_y += 1; - } - input.accelerate = key_down(kMoveForward); - input.brake = key_down(kMoveBack); - const int reach = key_down(VK_LMENU) ? 60 : 127; - input.analog_x = static_cast(std::clamp(128 + move_x * reach, 0, 255)); - input.analog_y = static_cast(std::clamp(128 + move_y * reach, 0, 255)); + const bool mouse_in_window = cursor_in_client(g_window); + const auto is_down = [&](HostKey key) { + if (host_key_is_mouse(key) && !mouse_in_window) return false; + const int virtual_key = win32_virtual_key(key); + return virtual_key != 0 && key_down(virtual_key); + }; + const KeyboardSample keys = sample_keyboard(controls.bindings, driving, is_down); + input.buttons |= keys.buttons; + input.accelerate = keys.accelerate; + input.brake = keys.brake; + input.analog_x = static_cast( + std::clamp(128 + keys.move_x * keys.reach, 0, 255)); + input.analog_y = static_cast( + std::clamp(128 + keys.move_y * keys.reach, 0, 255)); const int sensitivity = static_cast(controls.mouse_sensitivity); const auto camera_response = [sensitivity](std::int32_t delta) { @@ -675,7 +657,7 @@ HostInputState display_window_input() { wheel_button = wheel > 0 ? kPspSquare : kPspCross; wheel_hold = 8; } else { - wheel_button = wheel > 0 ? kPspLeft : kPspRight; + wheel_button = binding_wheel_buttons(controls.bindings, wheel > 0, driving); wheel_hold = 4; } } @@ -689,38 +671,46 @@ HostInputState display_window_input() { XInputStatePacket pad{}; if (get_state(0u, &pad) == ERROR_SUCCESS) { const std::uint16_t b = pad.gamepad.buttons; - if (b & kPadA) input.buttons |= kPspCross; - if (b & kPadX) input.buttons |= kPspSquare; - if (b & kPadY) input.buttons |= kPspTriangle; - if (b & kPadB) input.buttons |= kPspCircle; - if (b & kPadLeftShoulder) input.buttons |= kPspLTrigger; - if (b & kPadRightShoulder) input.buttons |= kPspRTrigger; - if (b & kPadStart) input.buttons |= kPspStart; - if (b & kPadBack) input.buttons |= kPspSelect; - if (b & kPadDpadUp) input.buttons |= kPspUp; - if (b & kPadDpadDown) input.buttons |= kPspDown; - if (b & kPadDpadLeft) input.buttons |= kPspLeft; - if (b & kPadDpadRight) input.buttons |= kPspRight; - if (!driving) { - if (pad.gamepad.left_trigger > 64u) input.buttons |= kPspLTrigger; - if (pad.gamepad.right_trigger > 64u) input.buttons |= kPspRTrigger; - } - if (pad.gamepad.right_trigger > 64u) input.accelerate = true; - if (pad.gamepad.left_trigger > 64u) input.brake = true; - - const std::uint8_t pad_x = stick_to_psp(pad.gamepad.lx, false); - const std::uint8_t pad_y = stick_to_psp(pad.gamepad.ly, true); - if (pad_x != 128u || pad_y != 128u) { - input.analog_x = pad_x; - input.analog_y = pad_y; - } - const int camera_x = stick_to_psp(pad.gamepad.rx, false) - 128; - int camera_y = stick_to_psp(pad.gamepad.ry, false) - 128; - if (controls.invert_camera_y) camera_y = -camera_y; - if (camera_x != 0 || camera_y != 0) { - input.camera_x = std::clamp(camera_x, -127, 127); - input.camera_y = std::clamp(camera_y, -127, 127); - } + const auto pad_down = [&](PadButton button) { + switch (button) { + case PadButton::A: return (b & kPadA) != 0; + case PadButton::B: return (b & kPadB) != 0; + case PadButton::X: return (b & kPadX) != 0; + case PadButton::Y: return (b & kPadY) != 0; + case PadButton::LeftShoulder: return (b & kPadLeftShoulder) != 0; + case PadButton::RightShoulder: return (b & kPadRightShoulder) != 0; + case PadButton::Start: return (b & kPadStart) != 0; + case PadButton::Back: return (b & kPadBack) != 0; + case PadButton::DpadUp: return (b & kPadDpadUp) != 0; + case PadButton::DpadDown: return (b & kPadDpadDown) != 0; + case PadButton::DpadLeft: return (b & kPadDpadLeft) != 0; + case PadButton::DpadRight: return (b & kPadDpadRight) != 0; + case PadButton::LeftClick: return (b & kPadLeftThumb) != 0; + case PadButton::RightClick: return (b & kPadRightThumb) != 0; + case PadButton::LeftTrigger: return pad.gamepad.left_trigger > 64u; + case PadButton::RightTrigger: return pad.gamepad.right_trigger > 64u; + case PadButton::Count: return false; + } + return false; + }; + const PadSample pad_sample = sample_pad(controls.bindings, driving, pad_down); + input.buttons |= pad_sample.buttons; + if (pad_sample.accelerate) input.accelerate = true; + if (pad_sample.brake) input.brake = true; + const auto apply_stick = [&](std::int16_t raw_x, std::int16_t raw_y, StickRole role) { + const StickReading reading = + interpret_stick(role, raw_x, raw_y, true, controls.invert_camera_y); + if (reading.move) { + input.analog_x = reading.move_x; + input.analog_y = reading.move_y; + } + if (reading.camera) { + input.camera_x = reading.camera_x; + input.camera_y = reading.camera_y; + } + }; + apply_stick(pad.gamepad.lx, pad.gamepad.ly, controls.bindings.left_stick); + apply_stick(pad.gamepad.rx, pad.gamepad.ry, controls.bindings.right_stick); } } return publish(); diff --git a/lcs/host/display_window_sdl.cpp b/lcs/host/display_window_sdl.cpp index cfe5ebf..059db61 100644 --- a/lcs/host/display_window_sdl.cpp +++ b/lcs/host/display_window_sdl.cpp @@ -7,6 +7,7 @@ #include "lcs_menu.hpp" #include "lcs_mouse.hpp" #include "lcs_display_menu.hpp" +#include "lcs_key_bindings.hpp" #include "lcs_render_config.hpp" #include "host_font_5x7.hpp" @@ -49,56 +50,61 @@ void discard_pending_pointer() noexcept { g_mouse_dy.store(0, std::memory_order_relaxed); } -constexpr std::uint32_t kPspSelect = 0x000001u; -constexpr std::uint32_t kPspStart = 0x000008u; -constexpr std::uint32_t kPspUp = 0x000010u; -constexpr std::uint32_t kPspRight = 0x000020u; -constexpr std::uint32_t kPspDown = 0x000040u; -constexpr std::uint32_t kPspLeft = 0x000080u; -constexpr std::uint32_t kPspLTrigger = 0x000100u; -constexpr std::uint32_t kPspRTrigger = 0x000200u; -constexpr std::uint32_t kPspTriangle = 0x001000u; -constexpr std::uint32_t kPspCircle = 0x002000u; -constexpr std::uint32_t kPspCross = 0x004000u; -constexpr std::uint32_t kPspSquare = 0x008000u; +constexpr int sdl_scancode(HostKey key) noexcept { + const int id = static_cast(key); + if (host_key_in(key, HostKey::A, HostKey::Z)) + return SDL_SCANCODE_A + (id - static_cast(HostKey::A)); + if (key == HostKey::Digit0) + return SDL_SCANCODE_0; + if (host_key_in(key, HostKey::Digit1, HostKey::Digit9)) + return SDL_SCANCODE_1 + (id - static_cast(HostKey::Digit1)); + if (host_key_in(key, HostKey::F1, HostKey::F12)) + return SDL_SCANCODE_F1 + (id - static_cast(HostKey::F1)); + if (host_key_in(key, HostKey::Numpad0, HostKey::Numpad9)) { + const int number = id - static_cast(HostKey::Numpad0); + return number == 0 ? SDL_SCANCODE_KP_0 : SDL_SCANCODE_KP_1 + (number - 1); + } + switch (key) { + case HostKey::Space: return SDL_SCANCODE_SPACE; + case HostKey::Enter: return SDL_SCANCODE_RETURN; + case HostKey::Escape: return SDL_SCANCODE_ESCAPE; + case HostKey::Tab: return SDL_SCANCODE_TAB; + case HostKey::Backspace: return SDL_SCANCODE_BACKSPACE; + case HostKey::LeftShift: return SDL_SCANCODE_LSHIFT; + case HostKey::RightShift: return SDL_SCANCODE_RSHIFT; + case HostKey::LeftCtrl: return SDL_SCANCODE_LCTRL; + case HostKey::RightCtrl: return SDL_SCANCODE_RCTRL; + case HostKey::LeftAlt: return SDL_SCANCODE_LALT; + case HostKey::RightAlt: return SDL_SCANCODE_RALT; + case HostKey::Up: return SDL_SCANCODE_UP; + case HostKey::Down: return SDL_SCANCODE_DOWN; + case HostKey::Left: return SDL_SCANCODE_LEFT; + case HostKey::Right: return SDL_SCANCODE_RIGHT; + case HostKey::NumpadPlus: return SDL_SCANCODE_KP_PLUS; + case HostKey::NumpadMinus: return SDL_SCANCODE_KP_MINUS; + case HostKey::Delete: return SDL_SCANCODE_DELETE; + case HostKey::Insert: return SDL_SCANCODE_INSERT; + case HostKey::Home: return SDL_SCANCODE_HOME; + case HostKey::End: return SDL_SCANCODE_END; + case HostKey::PageUp: return SDL_SCANCODE_PAGEUP; + case HostKey::PageDown: return SDL_SCANCODE_PAGEDOWN; + case HostKey::Comma: return SDL_SCANCODE_COMMA; + case HostKey::Period: return SDL_SCANCODE_PERIOD; + case HostKey::Minus: return SDL_SCANCODE_MINUS; + case HostKey::Equals: return SDL_SCANCODE_EQUALS; + default: return -1; + } +} -struct KeyBinding { - SDL_Scancode scancode; - std::uint32_t psp_button; -}; - -constexpr KeyBinding kKeyBindings[] = { - {SDL_SCANCODE_SPACE, kPspCross}, - {SDL_SCANCODE_LSHIFT, kPspSquare}, - {SDL_SCANCODE_RSHIFT, kPspSquare}, - {SDL_SCANCODE_F, kPspTriangle}, - {SDL_SCANCODE_RETURN, kPspTriangle}, - {SDL_SCANCODE_Q, kPspLeft}, - {SDL_SCANCODE_E, kPspRight}, - {SDL_SCANCODE_H, kPspLTrigger}, - {SDL_SCANCODE_UP, kPspUp}, - {SDL_SCANCODE_DOWN, kPspDown}, - {SDL_SCANCODE_LEFT, kPspLeft}, - {SDL_SCANCODE_RIGHT, kPspRight}, - {SDL_SCANCODE_ESCAPE, kPspStart}, - {SDL_SCANCODE_V, kPspSelect}, -}; +static_assert(sdl_scancode(HostKey::Digit0) == SDL_SCANCODE_0); +static_assert(sdl_scancode(HostKey::Digit1) == SDL_SCANCODE_1); +static_assert(sdl_scancode(HostKey::Digit9) == SDL_SCANCODE_9); bool key_down(SDL_Scancode scancode) noexcept { const std::uint8_t *keys = SDL_GetKeyboardState(nullptr); return keys != nullptr && keys[scancode] != 0; } -std::uint8_t stick_to_psp(std::int16_t value, bool invert) noexcept { - constexpr int kDeadZone = 7849; - int magnitude = std::abs(static_cast(value)); - if (magnitude <= kDeadZone) return 128u; - magnitude = (magnitude - kDeadZone) * 32767 / (32767 - kDeadZone); - int signed_value = value < 0 ? -magnitude : magnitude; - if (invert) signed_value = -signed_value; - return static_cast(std::clamp(128 + signed_value * 127 / 32767, 0, 255)); -} - void close_pad() noexcept { if (g_pad == nullptr) return; SDL_GameControllerClose(g_pad); @@ -536,32 +542,30 @@ HostInputState display_window_input() { const bool driving = lcs_player_in_vehicle(); const ControlsConfiguration &controls = lcs_render_configuration().controls; if (focused) { - for (const KeyBinding &binding : kKeyBindings) - if (key_down(binding.scancode)) input.buttons |= binding.psp_button; const std::uint32_t mouse = SDL_GetMouseState(nullptr, nullptr); - if ((mouse & SDL_BUTTON_LMASK) != 0) input.buttons |= kPspCircle; - if ((mouse & SDL_BUTTON_RMASK) != 0) input.buttons |= kPspRTrigger; - if ((mouse & SDL_BUTTON_MMASK) != 0) input.buttons |= kPspLTrigger; - if (driving) { - input.buttons &= ~(kPspCross | kPspRTrigger); - if (key_down(SDL_SCANCODE_SPACE)) input.buttons |= kPspRTrigger; - } - int move_x = 0; - int move_y = 0; - if (key_down(SDL_SCANCODE_A)) move_x -= 1; - if (key_down(SDL_SCANCODE_D)) move_x += 1; - if (!driving) { - if (key_down(SDL_SCANCODE_W)) move_y -= 1; - if (key_down(SDL_SCANCODE_S)) move_y += 1; - } else { - if (key_down(SDL_SCANCODE_UP)) move_y -= 1; - if (key_down(SDL_SCANCODE_DOWN)) move_y += 1; - } - input.accelerate = key_down(SDL_SCANCODE_W); - input.brake = key_down(SDL_SCANCODE_S); - const int reach = key_down(SDL_SCANCODE_LALT) ? 60 : 127; - input.analog_x = static_cast(std::clamp(128 + move_x * reach, 0, 255)); - input.analog_y = static_cast(std::clamp(128 + move_y * reach, 0, 255)); + const auto mouse_held = [mouse](std::uint32_t mask) { + return (mouse & mask) != 0; + }; + const auto is_down = [&](HostKey key) { + const int code = sdl_scancode(key); + if (code >= 0) return key_down(static_cast(code)); + switch (key) { + case HostKey::MouseLeft: return mouse_held(SDL_BUTTON_LMASK); + case HostKey::MouseRight: return mouse_held(SDL_BUTTON_RMASK); + case HostKey::MouseMiddle: return mouse_held(SDL_BUTTON_MMASK); + case HostKey::Mouse4: return mouse_held(SDL_BUTTON_X1MASK); + case HostKey::Mouse5: return mouse_held(SDL_BUTTON_X2MASK); + default: return false; + } + }; + const KeyboardSample keys = sample_keyboard(controls.bindings, driving, is_down); + input.buttons |= keys.buttons; + input.accelerate = keys.accelerate; + input.brake = keys.brake; + input.analog_x = static_cast( + std::clamp(128 + keys.move_x * keys.reach, 0, 255)); + input.analog_y = static_cast( + std::clamp(128 + keys.move_y * keys.reach, 0, 255)); const int sensitivity = static_cast(controls.mouse_sensitivity); const auto camera_response = [sensitivity](std::int32_t delta) { const double scaled = std::abs(delta) * (sensitivity / 12.0); @@ -581,7 +585,7 @@ HostInputState display_window_input() { wheel_button = wheel > 0 ? kPspSquare : kPspCross; wheel_hold = 8; } else { - wheel_button = wheel > 0 ? kPspLeft : kPspRight; + wheel_button = binding_wheel_buttons(controls.bindings, wheel > 0, driving); wheel_hold = 4; } } @@ -591,45 +595,54 @@ HostInputState display_window_input() { } if (!focused || g_pad == nullptr) return publish(); - auto held = [&](SDL_GameControllerButton button) { - return SDL_GameControllerGetButton(g_pad, button) != 0; + const auto pad_down = [&](PadButton button) { + const auto held = [&](SDL_GameControllerButton id) { + return SDL_GameControllerGetButton(g_pad, id) != 0; + }; + const auto axis = [&](SDL_GameControllerAxis id) { + return SDL_GameControllerGetAxis(g_pad, id); + }; + switch (button) { + case PadButton::A: return held(SDL_CONTROLLER_BUTTON_A); + case PadButton::B: return held(SDL_CONTROLLER_BUTTON_B); + case PadButton::X: return held(SDL_CONTROLLER_BUTTON_X); + case PadButton::Y: return held(SDL_CONTROLLER_BUTTON_Y); + case PadButton::LeftShoulder: return held(SDL_CONTROLLER_BUTTON_LEFTSHOULDER); + case PadButton::RightShoulder: return held(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER); + case PadButton::Start: return held(SDL_CONTROLLER_BUTTON_START); + case PadButton::Back: return held(SDL_CONTROLLER_BUTTON_BACK); + case PadButton::DpadUp: return held(SDL_CONTROLLER_BUTTON_DPAD_UP); + case PadButton::DpadDown: return held(SDL_CONTROLLER_BUTTON_DPAD_DOWN); + case PadButton::DpadLeft: return held(SDL_CONTROLLER_BUTTON_DPAD_LEFT); + case PadButton::DpadRight: return held(SDL_CONTROLLER_BUTTON_DPAD_RIGHT); + case PadButton::LeftClick: return held(SDL_CONTROLLER_BUTTON_LEFTSTICK); + case PadButton::RightClick: return held(SDL_CONTROLLER_BUTTON_RIGHTSTICK); + case PadButton::LeftTrigger: return axis(SDL_CONTROLLER_AXIS_TRIGGERLEFT) > 8192; + case PadButton::RightTrigger: return axis(SDL_CONTROLLER_AXIS_TRIGGERRIGHT) > 8192; + case PadButton::Count: return false; + } + return false; }; - if (held(SDL_CONTROLLER_BUTTON_A)) input.buttons |= kPspCross; - if (held(SDL_CONTROLLER_BUTTON_X)) input.buttons |= kPspSquare; - if (held(SDL_CONTROLLER_BUTTON_Y)) input.buttons |= kPspTriangle; - if (held(SDL_CONTROLLER_BUTTON_B)) input.buttons |= kPspCircle; - if (held(SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) input.buttons |= kPspLTrigger; - if (held(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) input.buttons |= kPspRTrigger; - if (held(SDL_CONTROLLER_BUTTON_START)) input.buttons |= kPspStart; - if (held(SDL_CONTROLLER_BUTTON_BACK)) input.buttons |= kPspSelect; - if (held(SDL_CONTROLLER_BUTTON_DPAD_UP)) input.buttons |= kPspUp; - if (held(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) input.buttons |= kPspDown; - if (held(SDL_CONTROLLER_BUTTON_DPAD_LEFT)) input.buttons |= kPspLeft; - if (held(SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) input.buttons |= kPspRight; - const auto axis = [&](SDL_GameControllerAxis id) { - return SDL_GameControllerGetAxis(g_pad, id); + const PadSample pad = sample_pad(controls.bindings, driving, pad_down); + input.buttons |= pad.buttons; + if (pad.accelerate) input.accelerate = true; + if (pad.brake) input.brake = true; + const auto apply_stick = [&](SDL_GameControllerAxis x_axis, SDL_GameControllerAxis y_axis, + StickRole role) { + const StickReading reading = interpret_stick( + role, SDL_GameControllerGetAxis(g_pad, x_axis), SDL_GameControllerGetAxis(g_pad, y_axis), + false, controls.invert_camera_y); + if (reading.move) { + input.analog_x = reading.move_x; + input.analog_y = reading.move_y; + } + if (reading.camera) { + input.camera_x = reading.camera_x; + input.camera_y = reading.camera_y; + } }; - const std::int16_t left_trigger = axis(SDL_CONTROLLER_AXIS_TRIGGERLEFT); - const std::int16_t right_trigger = axis(SDL_CONTROLLER_AXIS_TRIGGERRIGHT); - if (!driving) { - if (left_trigger > 8192) input.buttons |= kPspLTrigger; - if (right_trigger > 8192) input.buttons |= kPspRTrigger; - } - if (right_trigger > 8192) input.accelerate = true; - if (left_trigger > 8192) input.brake = true; - const std::uint8_t pad_x = stick_to_psp(axis(SDL_CONTROLLER_AXIS_LEFTX), false); - const std::uint8_t pad_y = stick_to_psp(axis(SDL_CONTROLLER_AXIS_LEFTY), false); - if (pad_x != 128u || pad_y != 128u) { - input.analog_x = pad_x; - input.analog_y = pad_y; - } - const int camera_x = stick_to_psp(axis(SDL_CONTROLLER_AXIS_RIGHTX), false) - 128; - int camera_y = stick_to_psp(axis(SDL_CONTROLLER_AXIS_RIGHTY), true) - 128; - if (controls.invert_camera_y) camera_y = -camera_y; - if (camera_x != 0 || camera_y != 0) { - input.camera_x = std::clamp(camera_x, -127, 127); - input.camera_y = std::clamp(camera_y, -127, 127); - } + apply_stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, controls.bindings.left_stick); + apply_stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, controls.bindings.right_stick); return publish(); } diff --git a/lcs/host/lcs_key_bindings.hpp b/lcs/host/lcs_key_bindings.hpp new file mode 100644 index 0000000..29f4736 --- /dev/null +++ b/lcs/host/lcs_key_bindings.hpp @@ -0,0 +1,783 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lcs { + +inline constexpr std::uint32_t kPspSelect = 0x000001u; +inline constexpr std::uint32_t kPspStart = 0x000008u; +inline constexpr std::uint32_t kPspUp = 0x000010u; +inline constexpr std::uint32_t kPspRight = 0x000020u; +inline constexpr std::uint32_t kPspDown = 0x000040u; +inline constexpr std::uint32_t kPspLeft = 0x000080u; +inline constexpr std::uint32_t kPspLTrigger = 0x000100u; +inline constexpr std::uint32_t kPspRTrigger = 0x000200u; +inline constexpr std::uint32_t kPspTriangle = 0x001000u; +inline constexpr std::uint32_t kPspCircle = 0x002000u; +inline constexpr std::uint32_t kPspCross = 0x004000u; +inline constexpr std::uint32_t kPspSquare = 0x008000u; + +inline std::uint8_t stick_to_psp(std::int16_t value, bool invert) noexcept { + constexpr int kDeadZone = 7849; + int magnitude = std::abs(static_cast(value)); + if (magnitude <= kDeadZone) return 128u; + magnitude = (magnitude - kDeadZone) * 32767 / (32767 - kDeadZone); + int signed_value = value < 0 ? -magnitude : magnitude; + if (invert) signed_value = -signed_value; + return static_cast(std::clamp(128 + signed_value * 127 / 32767, 0, 255)); +} + +enum class HostKey : std::uint8_t { + None = 0, + A, B, C, D, E, F, G, H, I, J, K, L, M, + N, O, P, Q, R, S, T, U, V, W, X, Y, Z, + Digit0, Digit1, Digit2, Digit3, Digit4, + Digit5, Digit6, Digit7, Digit8, Digit9, + F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, + Space, Enter, Escape, Tab, Backspace, + LeftShift, RightShift, LeftCtrl, RightCtrl, LeftAlt, RightAlt, + Up, Down, Left, Right, + Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, + Numpad5, Numpad6, Numpad7, Numpad8, Numpad9, + NumpadPlus, NumpadMinus, + Delete, Insert, Home, End, PageUp, PageDown, + Comma, Period, Minus, Equals, + MouseLeft, MouseRight, MouseMiddle, Mouse4, Mouse5, + WheelUp, WheelDown, +}; + +static_assert(static_cast(HostKey::Z) - static_cast(HostKey::A) == 25); +static_assert(static_cast(HostKey::Digit9) - static_cast(HostKey::Digit0) == 9); +static_assert(static_cast(HostKey::F12) - static_cast(HostKey::F1) == 11); +static_assert(static_cast(HostKey::Numpad9) - static_cast(HostKey::Numpad0) == 9); +static_assert(static_cast(HostKey::Mouse5) - static_cast(HostKey::MouseLeft) == 4); + +enum class BindAction : std::uint8_t { + MoveForward, + MoveBack, + MoveLeft, + MoveRight, + Walk, + Sprint, + Jump, + EnterVehicle, + Attack, + Aim, + CenterCamera, + WeaponPrevious, + WeaponNext, + Up, + Down, + Pause, + Camera, + Handbrake, + Count, +}; + +enum class PadButton : std::uint8_t { + A, B, X, Y, + LeftShoulder, RightShoulder, + LeftTrigger, RightTrigger, + Start, Back, + DpadUp, DpadDown, DpadLeft, DpadRight, + LeftClick, RightClick, + Count, +}; + +enum class StickRole : std::uint8_t { Move, Camera, None }; + +inline constexpr int kMaxKeysPerAction = 4; + +struct ControlBindings { + std::array, + static_cast(BindAction::Count)> keys{}; + std::array(BindAction::Count)> count{}; + std::array, + static_cast(PadButton::Count)> pad{}; + std::array(PadButton::Count)> pad_count{}; + StickRole left_stick{StickRole::Move}; + StickRole right_stick{StickRole::Camera}; +}; + +struct KeyboardSample { + std::uint32_t buttons{}; + int move_x{}; + int move_y{}; + bool accelerate{}; + bool brake{}; + int reach{127}; +}; + +constexpr bool host_key_in(HostKey key, HostKey first, HostKey last) noexcept { + const int id = static_cast(key); + return id >= static_cast(first) && id <= static_cast(last); +} + +inline bool host_key_is_mouse(HostKey key) noexcept { + return host_key_in(key, HostKey::MouseLeft, HostKey::Mouse5); +} + +inline bool host_key_is_wheel(HostKey key) noexcept { + return key == HostKey::WheelUp || key == HostKey::WheelDown; +} + +inline std::size_t bind_index(BindAction action) noexcept { + return static_cast(action); +} + +inline bool binding_has(const ControlBindings &bindings, BindAction action, HostKey key) noexcept { + const std::size_t index = bind_index(action); + for (std::uint8_t slot = 0; slot < bindings.count[index]; ++slot) + if (bindings.keys[index][slot] == key) return true; + return false; +} + +inline const char *bind_action_name(BindAction action) noexcept { + switch (action) { + case BindAction::MoveForward: return "MoveForward"; + case BindAction::MoveBack: return "MoveBack"; + case BindAction::MoveLeft: return "MoveLeft"; + case BindAction::MoveRight: return "MoveRight"; + case BindAction::Walk: return "Walk"; + case BindAction::Sprint: return "Sprint"; + case BindAction::Jump: return "Jump"; + case BindAction::EnterVehicle: return "EnterVehicle"; + case BindAction::Attack: return "Attack"; + case BindAction::Aim: return "Aim"; + case BindAction::CenterCamera: return "CenterCamera"; + case BindAction::WeaponPrevious: return "WeaponPrevious"; + case BindAction::WeaponNext: return "WeaponNext"; + case BindAction::Up: return "Up"; + case BindAction::Down: return "Down"; + case BindAction::Pause: return "Pause"; + case BindAction::Camera: return "Camera"; + case BindAction::Handbrake: return "Handbrake"; + case BindAction::Count: return ""; + } + return ""; +} + +inline std::uint32_t action_buttons(BindAction action, bool driving) noexcept { + switch (action) { + case BindAction::Sprint: return driving ? 0u : kPspCross; + case BindAction::Jump: return kPspSquare; + case BindAction::EnterVehicle: return kPspTriangle; + case BindAction::Attack: return kPspCircle; + case BindAction::Aim: return driving ? 0u : kPspRTrigger; + case BindAction::CenterCamera: return kPspLTrigger; + case BindAction::WeaponPrevious: return kPspLeft; + case BindAction::WeaponNext: return kPspRight; + case BindAction::Up: return kPspUp; + case BindAction::Down: return kPspDown; + case BindAction::Pause: return kPspStart; + case BindAction::Camera: return kPspSelect; + case BindAction::Handbrake: return driving ? kPspRTrigger : 0u; + default: return 0u; + } +} + +inline ControlBindings default_control_bindings() { + ControlBindings bindings; + const auto add = [&](BindAction action, std::initializer_list keys) { + auto &count = bindings.count[bind_index(action)]; + for (const HostKey key : keys) { + if (count >= kMaxKeysPerAction) break; + bindings.keys[bind_index(action)][count++] = key; + } + }; + add(BindAction::MoveForward, {HostKey::W}); + add(BindAction::MoveBack, {HostKey::S}); + add(BindAction::MoveLeft, {HostKey::A}); + add(BindAction::MoveRight, {HostKey::D}); + add(BindAction::Walk, {HostKey::LeftAlt}); + add(BindAction::Sprint, {HostKey::Space}); + add(BindAction::Jump, {HostKey::LeftShift, HostKey::RightShift}); + add(BindAction::EnterVehicle, {HostKey::F, HostKey::Enter}); + add(BindAction::Attack, {HostKey::MouseLeft}); + add(BindAction::Aim, {HostKey::MouseRight}); + add(BindAction::CenterCamera, {HostKey::H, HostKey::MouseMiddle}); + add(BindAction::WeaponPrevious, {HostKey::Q, HostKey::Left, HostKey::WheelUp}); + add(BindAction::WeaponNext, {HostKey::E, HostKey::Right, HostKey::WheelDown}); + add(BindAction::Up, {HostKey::Up}); + add(BindAction::Down, {HostKey::Down}); + add(BindAction::Pause, {HostKey::Escape}); + add(BindAction::Camera, {HostKey::V}); + add(BindAction::Handbrake, {HostKey::Space}); + const auto add_pad = [&](PadButton button, std::initializer_list actions) { + auto &count = bindings.pad_count[static_cast(button)]; + for (const BindAction action : actions) { + if (count >= kMaxKeysPerAction) break; + bindings.pad[static_cast(button)][count++] = action; + } + }; + add_pad(PadButton::A, {BindAction::Sprint}); + add_pad(PadButton::B, {BindAction::Attack}); + add_pad(PadButton::X, {BindAction::Jump}); + add_pad(PadButton::Y, {BindAction::EnterVehicle}); + add_pad(PadButton::LeftShoulder, {BindAction::CenterCamera}); + add_pad(PadButton::RightShoulder, {BindAction::Aim}); + add_pad(PadButton::LeftTrigger, {BindAction::MoveBack, BindAction::CenterCamera}); + add_pad(PadButton::RightTrigger, {BindAction::MoveForward, BindAction::Aim}); + add_pad(PadButton::Start, {BindAction::Pause}); + add_pad(PadButton::Back, {BindAction::Camera}); + add_pad(PadButton::DpadUp, {BindAction::Up}); + add_pad(PadButton::DpadDown, {BindAction::Down}); + add_pad(PadButton::DpadLeft, {BindAction::WeaponPrevious}); + add_pad(PadButton::DpadRight, {BindAction::WeaponNext}); + add_pad(PadButton::LeftClick, {BindAction::Down}); + return bindings; +} + +template +bool action_held(const ControlBindings &bindings, BindAction action, IsDown &&is_down) { + const std::size_t index = bind_index(action); + for (std::uint8_t slot = 0; slot < bindings.count[index]; ++slot) { + const HostKey key = bindings.keys[index][slot]; + if (host_key_is_wheel(key)) continue; + if (is_down(key)) return true; + } + return false; +} + +template +KeyboardSample sample_keyboard(const ControlBindings &bindings, bool driving, IsDown &&is_down) { + KeyboardSample sample; + if (action_held(bindings, BindAction::MoveLeft, is_down)) sample.move_x -= 1; + if (action_held(bindings, BindAction::MoveRight, is_down)) sample.move_x += 1; + if (!driving) { + if (action_held(bindings, BindAction::MoveForward, is_down)) sample.move_y -= 1; + if (action_held(bindings, BindAction::MoveBack, is_down)) sample.move_y += 1; + } else { + if (action_held(bindings, BindAction::Up, is_down)) sample.move_y -= 1; + if (action_held(bindings, BindAction::Down, is_down)) sample.move_y += 1; + } + sample.accelerate = action_held(bindings, BindAction::MoveForward, is_down); + sample.brake = action_held(bindings, BindAction::MoveBack, is_down); + sample.reach = action_held(bindings, BindAction::Walk, is_down) ? 60 : 127; + for (std::uint8_t index = 0; index < static_cast(BindAction::Count); ++index) { + const auto action = static_cast(index); + if (action_held(bindings, action, is_down)) + sample.buttons |= action_buttons(action, driving); + } + return sample; +} + +inline std::uint32_t binding_wheel_buttons(const ControlBindings &bindings, bool up, + bool driving) noexcept { + const HostKey token = up ? HostKey::WheelUp : HostKey::WheelDown; + std::uint32_t buttons = 0u; + for (std::uint8_t index = 0; index < static_cast(BindAction::Count); ++index) { + const auto action = static_cast(index); + if (binding_has(bindings, action, token)) buttons |= action_buttons(action, driving); + } + return buttons; +} + +inline std::string normalize_binding_token(std::string_view token) { + std::string out; + out.reserve(token.size()); + for (const unsigned char ch : token) { + if (ch == ' ' || ch == '\t' || ch == '_' || ch == '-') continue; + out.push_back(static_cast(std::tolower(ch))); + } + return out; +} + +inline bool lookup_bind_action(std::string_view name, BindAction &action) noexcept { + struct Alias { + const char *name; + BindAction action; + }; + static constexpr Alias kAliases[] = { + {"moveforward", BindAction::MoveForward}, + {"forward", BindAction::MoveForward}, + {"moveback", BindAction::MoveBack}, + {"backward", BindAction::MoveBack}, + {"back", BindAction::MoveBack}, + {"moveleft", BindAction::MoveLeft}, + {"moveright", BindAction::MoveRight}, + {"walk", BindAction::Walk}, + {"sprint", BindAction::Sprint}, + {"run", BindAction::Sprint}, + {"jump", BindAction::Jump}, + {"entervehicle", BindAction::EnterVehicle}, + {"enterexit", BindAction::EnterVehicle}, + {"enter", BindAction::EnterVehicle}, + {"attack", BindAction::Attack}, + {"fire", BindAction::Attack}, + {"aim", BindAction::Aim}, + {"target", BindAction::Aim}, + {"centercamera", BindAction::CenterCamera}, + {"center", BindAction::CenterCamera}, + {"weaponprevious", BindAction::WeaponPrevious}, + {"weaponprev", BindAction::WeaponPrevious}, + {"previousweapon", BindAction::WeaponPrevious}, + {"prevweapon", BindAction::WeaponPrevious}, + {"weaponnext", BindAction::WeaponNext}, + {"nextweapon", BindAction::WeaponNext}, + {"up", BindAction::Up}, + {"dpadup", BindAction::Up}, + {"down", BindAction::Down}, + {"dpaddown", BindAction::Down}, + {"pause", BindAction::Pause}, + {"start", BindAction::Pause}, + {"camera", BindAction::Camera}, + {"select", BindAction::Camera}, + {"changecamera", BindAction::Camera}, + {"handbrake", BindAction::Handbrake}, + }; + for (const Alias &alias : kAliases) { + if (name == alias.name) { + action = alias.action; + return true; + } + } + return false; +} + +inline bool lookup_host_key(std::string_view token, HostKey &key, HostKey &extra) noexcept { + extra = HostKey::None; + if (token == "shift") { + key = HostKey::LeftShift; + extra = HostKey::RightShift; + return true; + } + if (token == "ctrl" || token == "control") { + key = HostKey::LeftCtrl; + extra = HostKey::RightCtrl; + return true; + } + if (token == "alt") { + key = HostKey::LeftAlt; + extra = HostKey::RightAlt; + return true; + } + if (token.size() == 1u) { + const char ch = token[0]; + if (ch >= 'a' && ch <= 'z') { + key = static_cast(static_cast(HostKey::A) + (ch - 'a')); + return true; + } + if (ch >= '0' && ch <= '9') { + key = static_cast(static_cast(HostKey::Digit0) + (ch - '0')); + return true; + } + } + if (token.size() >= 2u && token[0] == 'f') { + int number = 0; + bool digits = true; + for (std::size_t index = 1u; index < token.size(); ++index) { + const char ch = token[index]; + if (ch < '0' || ch > '9') digits = false; + else number = number * 10 + (ch - '0'); + } + if (digits && number >= 1 && number <= 12) { + key = static_cast(static_cast(HostKey::F1) + (number - 1)); + return true; + } + } + const auto numpad = [&](std::string_view prefix) -> bool { + if (!token.starts_with(prefix) || token.size() != prefix.size() + 1u) return false; + const char digit = token.back(); + if (digit < '0' || digit > '9') return false; + key = static_cast(static_cast(HostKey::Numpad0) + (digit - '0')); + return true; + }; + if (numpad("numpad") || numpad("kp") || numpad("num")) return true; + + struct Named { const char *name; HostKey key; }; + static constexpr Named kNamed[] = { + {"space", HostKey::Space}, + {"enter", HostKey::Enter}, + {"return", HostKey::Enter}, + {"escape", HostKey::Escape}, + {"esc", HostKey::Escape}, + {"tab", HostKey::Tab}, + {"backspace", HostKey::Backspace}, + {"leftshift", HostKey::LeftShift}, + {"lshift", HostKey::LeftShift}, + {"rightshift", HostKey::RightShift}, + {"rshift", HostKey::RightShift}, + {"leftctrl", HostKey::LeftCtrl}, + {"lctrl", HostKey::LeftCtrl}, + {"leftcontrol", HostKey::LeftCtrl}, + {"rightctrl", HostKey::RightCtrl}, + {"rctrl", HostKey::RightCtrl}, + {"rightcontrol", HostKey::RightCtrl}, + {"leftalt", HostKey::LeftAlt}, + {"lalt", HostKey::LeftAlt}, + {"rightalt", HostKey::RightAlt}, + {"ralt", HostKey::RightAlt}, + {"up", HostKey::Up}, + {"arrowup", HostKey::Up}, + {"down", HostKey::Down}, + {"arrowdown", HostKey::Down}, + {"left", HostKey::Left}, + {"arrowleft", HostKey::Left}, + {"right", HostKey::Right}, + {"arrowright", HostKey::Right}, + {"numpadplus", HostKey::NumpadPlus}, + {"kpplus", HostKey::NumpadPlus}, + {"numpadminus", HostKey::NumpadMinus}, + {"kpminus", HostKey::NumpadMinus}, + {"delete", HostKey::Delete}, + {"del", HostKey::Delete}, + {"insert", HostKey::Insert}, + {"ins", HostKey::Insert}, + {"home", HostKey::Home}, + {"end", HostKey::End}, + {"pageup", HostKey::PageUp}, + {"pgup", HostKey::PageUp}, + {"pagedown", HostKey::PageDown}, + {"pgdn", HostKey::PageDown}, + {"comma", HostKey::Comma}, + {"period", HostKey::Period}, + {"minus", HostKey::Minus}, + {"equals", HostKey::Equals}, + {"equal", HostKey::Equals}, + {"mouseleft", HostKey::MouseLeft}, + {"leftmouse", HostKey::MouseLeft}, + {"mouse1", HostKey::MouseLeft}, + {"lmb", HostKey::MouseLeft}, + {"mouseright", HostKey::MouseRight}, + {"rightmouse", HostKey::MouseRight}, + {"mouse2", HostKey::MouseRight}, + {"rmb", HostKey::MouseRight}, + {"mousemiddle", HostKey::MouseMiddle}, + {"middlemouse", HostKey::MouseMiddle}, + {"mouse3", HostKey::MouseMiddle}, + {"mmb", HostKey::MouseMiddle}, + {"mouse4", HostKey::Mouse4}, + {"mouse5", HostKey::Mouse5}, + {"wheelup", HostKey::WheelUp}, + {"wheeldown", HostKey::WheelDown}, + }; + for (const Named &named : kNamed) { + if (token == named.name) { + key = named.key; + return true; + } + } + return false; +} + +inline bool binding_token_clears(std::string_view token) noexcept { + return token == "none" || token == "clear" || token == "unbound"; +} + +inline bool apply_control_binding(ControlBindings &bindings, std::string_view action_key, + std::string_view value, std::string &warning) { + warning.clear(); + BindAction action{}; + if (!lookup_bind_action(normalize_binding_token(action_key), action)) return false; + + HostKey parsed[kMaxKeysPerAction]{}; + std::uint8_t parsed_count = 0u; + bool clear = false; + bool overflow = false; + bool reserved = false; + bool unknown = false; + std::string unknown_token; + std::size_t token_count = 0u; + + std::size_t start = 0u; + const std::string raw(value); + while (start <= raw.size()) { + const std::size_t comma = raw.find(',', start); + const std::string_view piece = std::string_view(raw).substr( + start, comma == std::string::npos ? std::string_view::npos : comma - start); + start = comma == std::string::npos ? raw.size() + 1u : comma + 1u; + const std::string token = normalize_binding_token(piece); + if (token.empty()) continue; + ++token_count; + if (binding_token_clears(token)) { + clear = true; + continue; + } + HostKey key = HostKey::None; + HostKey extra = HostKey::None; + if (!lookup_host_key(token, key, extra)) { + unknown = true; + if (unknown_token.empty()) unknown_token = token; + continue; + } + const HostKey keys[] = {key, extra}; + for (const HostKey item : keys) { + if (item == HostKey::None) continue; + if (item == HostKey::F10 || item == HostKey::F11) { + reserved = true; + continue; + } + bool duplicate = false; + for (std::uint8_t slot = 0; slot < parsed_count; ++slot) + if (parsed[slot] == item) duplicate = true; + if (duplicate) continue; + if (parsed_count >= kMaxKeysPerAction) { + overflow = true; + continue; + } + parsed[parsed_count++] = item; + } + } + + const char *name = bind_action_name(action); + if (token_count == 0u || (clear && parsed_count == 0u && !unknown && !reserved)) { + bindings.count[bind_index(action)] = 0u; + return true; + } + if (parsed_count == 0u) { + warning = std::string(name) + " was left unchanged"; + if (reserved) warning += "; F10 opens host settings and F11 toggles fullscreen"; + if (unknown) warning += "; ignores '" + unknown_token + "'"; + return true; + } + bindings.count[bind_index(action)] = parsed_count; + for (std::uint8_t slot = 0; slot < kMaxKeysPerAction; ++slot) + bindings.keys[bind_index(action)][slot] = + slot < parsed_count ? parsed[slot] : HostKey::None; + if (reserved || unknown || overflow || clear) { + warning = name; + if (reserved) warning += " ignores F10 and F11"; + if (unknown) warning += " ignores '" + unknown_token + "'"; + if (overflow) warning += " keeps the first 4 keys"; + if (clear) warning += " ignores None beside other keys"; + } + return true; +} + +struct PadSample { + std::uint32_t buttons{}; + bool accelerate{}; + bool brake{}; +}; + +struct StickReading { + bool move{}; + std::uint8_t move_x{128}; + std::uint8_t move_y{128}; + bool camera{}; + int camera_x{}; + int camera_y{}; +}; + +inline bool pad_button_is_trigger(PadButton button) noexcept { + return button == PadButton::LeftTrigger || button == PadButton::RightTrigger; +} + +inline std::uint32_t pad_action_buttons(BindAction action) noexcept { + if (action == BindAction::Handbrake) return kPspRTrigger; + return action_buttons(action, false); +} + +inline bool pad_has(const ControlBindings &bindings, PadButton button, BindAction action) noexcept { + const std::size_t index = static_cast(button); + for (std::uint8_t slot = 0; slot < bindings.pad_count[index]; ++slot) + if (bindings.pad[index][slot] == action) return true; + return false; +} + +template +PadSample sample_pad(const ControlBindings &bindings, bool driving, IsDown &&is_down) { + PadSample sample; + for (std::uint8_t index = 0; index < static_cast(PadButton::Count); ++index) { + const auto button = static_cast(index); + if (!is_down(button)) continue; + const bool trigger = pad_button_is_trigger(button); + for (std::uint8_t slot = 0; slot < bindings.pad_count[index]; ++slot) { + const BindAction action = bindings.pad[index][slot]; + if (action == BindAction::MoveForward) sample.accelerate = true; + else if (action == BindAction::MoveBack) sample.brake = true; + else if (!trigger || !driving) sample.buttons |= pad_action_buttons(action); + } + } + return sample; +} + +inline StickReading interpret_stick(StickRole role, std::int16_t raw_x, std::int16_t raw_y, + bool y_positive_up, bool invert_camera_y) noexcept { + StickReading reading; + if (role == StickRole::Move) { + reading.move_x = stick_to_psp(raw_x, false); + reading.move_y = stick_to_psp(raw_y, y_positive_up); + reading.move = reading.move_x != 128u || reading.move_y != 128u; + } else if (role == StickRole::Camera) { + reading.camera_x = std::clamp(static_cast(stick_to_psp(raw_x, false)) - 128, -127, 127); + int camera_y = static_cast(stick_to_psp(raw_y, !y_positive_up)) - 128; + if (invert_camera_y) camera_y = -camera_y; + reading.camera_y = std::clamp(camera_y, -127, 127); + reading.camera = reading.camera_x != 0 || reading.camera_y != 0; + } + return reading; +} + +inline const char *pad_button_name(PadButton button) noexcept { + switch (button) { + case PadButton::A: return "PadA"; + case PadButton::B: return "PadB"; + case PadButton::X: return "PadX"; + case PadButton::Y: return "PadY"; + case PadButton::LeftShoulder: return "PadLeftShoulder"; + case PadButton::RightShoulder: return "PadRightShoulder"; + case PadButton::LeftTrigger: return "PadLeftTrigger"; + case PadButton::RightTrigger: return "PadRightTrigger"; + case PadButton::Start: return "PadStart"; + case PadButton::Back: return "PadBack"; + case PadButton::DpadUp: return "PadDpadUp"; + case PadButton::DpadDown: return "PadDpadDown"; + case PadButton::DpadLeft: return "PadDpadLeft"; + case PadButton::DpadRight: return "PadDpadRight"; + case PadButton::LeftClick: return "PadLeftStickClick"; + case PadButton::RightClick: return "PadRightStickClick"; + case PadButton::Count: return ""; + } + return ""; +} + +inline bool lookup_pad_button(std::string_view name, PadButton &button) noexcept { + struct Alias { + const char *name; + PadButton button; + }; + static constexpr Alias kAliases[] = { + {"pada", PadButton::A}, + {"cross", PadButton::A}, + {"south", PadButton::A}, + {"padb", PadButton::B}, + {"circle", PadButton::B}, + {"east", PadButton::B}, + {"padx", PadButton::X}, + {"square", PadButton::X}, + {"west", PadButton::X}, + {"pady", PadButton::Y}, + {"triangle", PadButton::Y}, + {"north", PadButton::Y}, + {"padleftshoulder", PadButton::LeftShoulder}, + {"lb", PadButton::LeftShoulder}, + {"l1", PadButton::LeftShoulder}, + {"padrightshoulder", PadButton::RightShoulder}, + {"rb", PadButton::RightShoulder}, + {"r1", PadButton::RightShoulder}, + {"padlefttrigger", PadButton::LeftTrigger}, + {"lt", PadButton::LeftTrigger}, + {"l2", PadButton::LeftTrigger}, + {"padrighttrigger", PadButton::RightTrigger}, + {"rt", PadButton::RightTrigger}, + {"r2", PadButton::RightTrigger}, + {"padstart", PadButton::Start}, + {"options", PadButton::Start}, + {"padback", PadButton::Back}, + {"share", PadButton::Back}, + {"create", PadButton::Back}, + {"paddpadup", PadButton::DpadUp}, + {"paddpaddown", PadButton::DpadDown}, + {"paddpadleft", PadButton::DpadLeft}, + {"paddpadright", PadButton::DpadRight}, + {"padleftstickclick", PadButton::LeftClick}, + {"l3", PadButton::LeftClick}, + {"padrightstickclick", PadButton::RightClick}, + {"r3", PadButton::RightClick}, + }; + for (const Alias &alias : kAliases) { + if (name == alias.name) { + button = alias.button; + return true; + } + } + return false; +} + +inline bool apply_pad_binding(ControlBindings &bindings, std::string_view button_key, + std::string_view value, std::string &warning) { + warning.clear(); + PadButton button{}; + if (!lookup_pad_button(normalize_binding_token(button_key), button)) return false; + + BindAction parsed[kMaxKeysPerAction]{}; + std::uint8_t parsed_count = 0u; + bool clear = false; + bool overflow = false; + bool unknown = false; + std::string unknown_token; + std::size_t token_count = 0u; + const std::string raw(value); + std::size_t start = 0u; + while (start <= raw.size()) { + const std::size_t comma = raw.find(',', start); + const std::string_view piece = std::string_view(raw).substr( + start, comma == std::string::npos ? std::string_view::npos : comma - start); + start = comma == std::string::npos ? raw.size() + 1u : comma + 1u; + const std::string token = normalize_binding_token(piece); + if (token.empty()) continue; + ++token_count; + if (binding_token_clears(token)) { + clear = true; + continue; + } + BindAction action{}; + if (!lookup_bind_action(token, action)) { + unknown = true; + if (unknown_token.empty()) unknown_token = token; + continue; + } + bool duplicate = false; + for (std::uint8_t slot = 0; slot < parsed_count; ++slot) + if (parsed[slot] == action) duplicate = true; + if (duplicate) continue; + if (parsed_count >= kMaxKeysPerAction) { + overflow = true; + continue; + } + parsed[parsed_count++] = action; + } + + const char *name = pad_button_name(button); + const std::size_t index = static_cast(button); + if (token_count == 0u || (clear && parsed_count == 0u && !unknown)) { + bindings.pad_count[index] = 0u; + return true; + } + if (parsed_count == 0u) { + warning = std::string(name) + " was left unchanged"; + if (unknown) warning += "; ignores '" + unknown_token + "'"; + return true; + } + bindings.pad_count[index] = parsed_count; + for (std::uint8_t slot = 0; slot < kMaxKeysPerAction; ++slot) + bindings.pad[index][slot] = slot < parsed_count ? parsed[slot] : BindAction::MoveForward; + if (unknown || overflow || clear) { + warning = name; + if (unknown) warning += " ignores '" + unknown_token + "'"; + if (overflow) warning += " keeps the first 4 actions"; + if (clear) warning += " ignores None beside other actions"; + } + return true; +} + +inline bool apply_stick_binding(ControlBindings &bindings, std::string_view stick_key, + std::string_view value, std::string &warning) { + warning.clear(); + const std::string name = normalize_binding_token(stick_key); + const bool left = name == "leftstick"; + if (!left && name != "rightstick") return false; + const std::string role_name = normalize_binding_token(value); + StickRole role = StickRole::None; + if (role_name.empty() || binding_token_clears(role_name)) role = StickRole::None; + else if (role_name == "move" || role_name == "movement") role = StickRole::Move; + else if (role_name == "camera" || role_name == "look") role = StickRole::Camera; + else { + warning = std::string(left ? "LeftStick" : "RightStick") + " expects Move, Camera, or None"; + return true; + } + if (left) bindings.left_stick = role; + else bindings.right_stick = role; + return true; +} + +} // namespace lcs diff --git a/lcs/host/lcs_render_config.cpp b/lcs/host/lcs_render_config.cpp index b1ccb9a..a1d2d17 100644 --- a/lcs/host/lcs_render_config.cpp +++ b/lcs/host/lcs_render_config.cpp @@ -410,6 +410,13 @@ void apply_controls_key(LcsConfiguration &config, const std::string &key, warning(config, line, "Controls.ModernControlScheme expects true/false"); return; } + std::string binding_warning; + if (apply_control_binding(config.controls.bindings, key, value, binding_warning) || + apply_pad_binding(config.controls.bindings, key, value, binding_warning) || + apply_stick_binding(config.controls.bindings, key, value, binding_warning)) { + if (!binding_warning.empty()) warning(config, line, binding_warning); + return; + } warning(config, line, "unknown [Controls] key '" + key + "'"); } diff --git a/lcs/host/lcs_render_config.hpp b/lcs/host/lcs_render_config.hpp index 55afcd3..d444141 100644 --- a/lcs/host/lcs_render_config.hpp +++ b/lcs/host/lcs_render_config.hpp @@ -1,5 +1,7 @@ #pragma once +#include "lcs_key_bindings.hpp" + #include #include #include @@ -151,6 +153,7 @@ struct ControlsConfiguration { bool invert_camera_y{false}; std::uint32_t ped_camera_up_limit_degrees{45u}; bool modern_control_scheme{false}; + ControlBindings bindings{default_control_bindings()}; }; struct LcsConfiguration { diff --git a/tests/lcs_host_menu_test.cpp b/tests/lcs_host_menu_test.cpp index 5d5a16d..2caf72c 100644 --- a/tests/lcs_host_menu_test.cpp +++ b/tests/lcs_host_menu_test.cpp @@ -1,7 +1,10 @@ #include "lcs_display_menu.hpp" +#include "lcs_key_bindings.hpp" #include "lcs_render_config.hpp" #include +#include +#include #include #include #include @@ -111,6 +114,133 @@ int fps_counter() { return 0; } +int bindings() { + using namespace lcs; + const ControlBindings defaults = default_control_bindings(); + if (!binding_has(defaults, BindAction::Sprint, HostKey::Space)) return fail("default sprint"); + if (!binding_has(defaults, BindAction::Aim, HostKey::MouseRight)) return fail("default aim"); + if (!binding_has(defaults, BindAction::Handbrake, HostKey::Space)) return fail("default handbrake"); + if (binding_wheel_buttons(defaults, true, false) != kPspLeft) return fail("wheel up"); + if (binding_wheel_buttons(defaults, false, true) != kPspRight) return fail("wheel down"); + + const auto held = [](HostKey key) { + return key == HostKey::W || key == HostKey::Space || key == HostKey::MouseRight || + key == HostKey::LeftAlt || key == HostKey::Up || key == HostKey::A; + }; + const KeyboardSample foot = sample_keyboard(defaults, false, held); + if (foot.move_x != -1 || foot.move_y != -1 || !foot.accelerate || foot.brake || foot.reach != 60) + return fail("foot axes"); + if ((foot.buttons & kPspCross) == 0 || (foot.buttons & kPspRTrigger) == 0 || + (foot.buttons & kPspUp) == 0) + return fail("foot buttons"); + + const KeyboardSample car = sample_keyboard(defaults, true, held); + if (car.move_x != -1 || car.move_y != -1 || !car.accelerate || car.brake) + return fail("car axes"); + if ((car.buttons & kPspCross) != 0 || (car.buttons & kPspRTrigger) == 0) + return fail("car buttons"); + + const auto aim_only = [](HostKey key) { return key == HostKey::MouseRight; }; + if ((sample_keyboard(defaults, true, aim_only).buttons & kPspRTrigger) != 0) + return fail("aim suppressed in a vehicle"); + + ControlBindings edited = defaults; + std::string warning; + if (!apply_control_binding(edited, "Jump", "Shift", warning) || !warning.empty() || + !binding_has(edited, BindAction::Jump, HostKey::LeftShift) || + !binding_has(edited, BindAction::Jump, HostKey::RightShift)) + return fail("shift alias"); + if (!apply_control_binding(edited, "Aim", "F10", warning) || + warning.find("unchanged") == std::string::npos || + !binding_has(edited, BindAction::Aim, HostKey::MouseRight)) + return fail("reserved key"); + if (!apply_control_binding(edited, "Sprint", "None", warning) || !warning.empty() || + edited.count[bind_index(BindAction::Sprint)] != 0u) + return fail("clear"); + + const auto path = std::filesystem::temp_directory_path() / "lcs-key-bindings-test.ini"; + { + std::ofstream out(path); + out << "[Controls]\n" + "MouseSensitivity=40\n" + "Sprint=E\n" + "WeaponPrevious=Q\n" + "Attack=MouseMiddle, Nope\n" + "Handbrake=MouseRight\n"; + } + const LcsConfiguration loaded = load_lcs_render_configuration(path); + std::filesystem::remove(path); + if (loaded.controls.mouse_sensitivity != 40u) return fail("sensitivity"); + if (!binding_has(loaded.controls.bindings, BindAction::Sprint, HostKey::E) || + loaded.controls.bindings.count[bind_index(BindAction::Sprint)] != 1u) + return fail("loaded sprint"); + if (binding_has(loaded.controls.bindings, BindAction::WeaponPrevious, HostKey::WheelUp)) + return fail("wheel left bound"); + if (!binding_has(loaded.controls.bindings, BindAction::Attack, HostKey::MouseMiddle) || + !binding_has(loaded.controls.bindings, BindAction::MoveForward, HostKey::W)) + return fail("partial rebind"); + if ((sample_keyboard(loaded.controls.bindings, true, aim_only).buttons & kPspRTrigger) == 0) + return fail("handbrake rebound"); + bool saw_unknown = false; + for (const std::string &message : loaded.warnings) + if (message.find("nope") != std::string::npos) saw_unknown = true; + if (!saw_unknown) return fail("missing warning"); + + const auto shipped_path = std::filesystem::path(__FILE__).parent_path() / ".." / "lcs" / + "config" / "LCSNative.ini"; + const LcsConfiguration shipped = load_lcs_render_configuration(shipped_path); + if (!shipped.warnings.empty()) { + for (const std::string &message : shipped.warnings) + std::cerr << message << '\n'; + return fail("shipped ini"); + } + if (!binding_has(shipped.controls.bindings, BindAction::CenterCamera, HostKey::H) || + !binding_has(shipped.controls.bindings, BindAction::CenterCamera, HostKey::MouseMiddle)) + return fail("shipped center"); + + const auto pad_a = [](PadButton button) { return button == PadButton::A; }; + const auto pad_rt = [](PadButton button) { return button == PadButton::RightTrigger; }; + const auto pad_rb = [](PadButton button) { return button == PadButton::RightShoulder; }; + const auto pad_lt = [](PadButton button) { return button == PadButton::LeftTrigger; }; + const auto pad_l3 = [](PadButton button) { return button == PadButton::LeftClick; }; + const PadSample pad_foot = sample_pad(defaults, false, pad_rt); + if (!pad_foot.accelerate || (pad_foot.buttons & kPspRTrigger) == 0) + return fail("trigger on foot"); + const PadSample pad_car = sample_pad(defaults, true, pad_rt); + if (!pad_car.accelerate || (pad_car.buttons & kPspRTrigger) != 0) + return fail("trigger in a vehicle"); + if ((sample_pad(defaults, true, pad_a).buttons & kPspCross) == 0) + return fail("pad run in a vehicle"); + if ((sample_pad(defaults, true, pad_rb).buttons & kPspRTrigger) == 0) + return fail("shoulder handbrake"); + if (!sample_pad(defaults, true, pad_lt).brake || + (sample_pad(defaults, true, pad_lt).buttons & kPspLTrigger) != 0) + return fail("left trigger"); + if (!apply_pad_binding(edited, "Cross", "Attack", warning) || !warning.empty() || + (sample_pad(edited, false, pad_a).buttons & kPspCircle) == 0) + return fail("pad rebind"); + if (!apply_stick_binding(edited, "LeftStick", "Camera", warning) || !warning.empty() || + edited.left_stick != StickRole::Camera) + return fail("stick swap"); + const StickReading sdl_move = interpret_stick(StickRole::Move, 0, -20000, false, false); + if (!sdl_move.move || sdl_move.move_y != stick_to_psp(-20000, false)) + return fail("sdl stick"); + const StickReading xinput_look = interpret_stick(StickRole::Camera, 0, 20000, true, false); + if (!xinput_look.camera || + xinput_look.camera_y != static_cast(stick_to_psp(20000, false)) - 128) + return fail("xinput stick"); + if ((sample_pad(defaults, true, pad_l3).buttons & kPspDown) == 0) + return fail("left stick horn"); + if (!pad_has(shipped.controls.bindings, PadButton::A, BindAction::Sprint) || + !pad_has(shipped.controls.bindings, PadButton::LeftClick, BindAction::Down) || + shipped.controls.bindings.left_stick != StickRole::Move || + shipped.controls.bindings.right_stick != StickRole::Camera || + shipped.controls.bindings.pad_count[static_cast(PadButton::RightClick)] != 0u) + return fail("shipped pad"); + std::cout << "defaults=1 rebound=1 shipped=1 pad=1\n"; + return 0; +} + int f10_key() { using namespace lcs; const Win32HostKeyDecision sys_down = @@ -141,6 +271,7 @@ int main(int argc, char **argv) { if (mode == "menu") return menu_keys(); if (mode == "present") return present_policy(); if (mode == "f10") return f10_key(); + if (mode == "bindings") return bindings(); if (mode == "fps") return fps_counter(); return 2; }