Merge pull request #16 from ragkousism/lcs-recomp:main

Add ability to change keybindings from ini
This commit is contained in:
elmasas
2026-09-30 23:24:17 -03:00
committed by GitHub
8 changed files with 1259 additions and 220 deletions
+56
View File
@@ -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
+56
View File
@@ -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
+104 -114
View File
@@ -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<int>(key);
if (host_key_in(key, HostKey::A, HostKey::Z))
return 'A' + (id - static_cast<int>(HostKey::A));
if (host_key_in(key, HostKey::Digit0, HostKey::Digit9))
return '0' + (id - static_cast<int>(HostKey::Digit0));
if (host_key_in(key, HostKey::F1, HostKey::F12))
return VK_F1 + (id - static_cast<int>(HostKey::F1));
if (host_key_in(key, HostKey::Numpad0, HostKey::Numpad9))
return VK_NUMPAD0 + (id - static_cast<int>(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<int>(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::uint8_t>(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::uint8_t>(std::clamp(128 + move_x * reach, 0, 255));
input.analog_y = static_cast<std::uint8_t>(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::uint8_t>(
std::clamp(128 + keys.move_x * keys.reach, 0, 255));
input.analog_y = static_cast<std::uint8_t>(
std::clamp(128 + keys.move_y * keys.reach, 0, 255));
const int sensitivity = static_cast<int>(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();
+119 -106
View File
@@ -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<int>(key);
if (host_key_in(key, HostKey::A, HostKey::Z))
return SDL_SCANCODE_A + (id - static_cast<int>(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<int>(HostKey::Digit1));
if (host_key_in(key, HostKey::F1, HostKey::F12))
return SDL_SCANCODE_F1 + (id - static_cast<int>(HostKey::F1));
if (host_key_in(key, HostKey::Numpad0, HostKey::Numpad9)) {
const int number = id - static_cast<int>(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<int>(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::uint8_t>(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::uint8_t>(std::clamp(128 + move_x * reach, 0, 255));
input.analog_y = static_cast<std::uint8_t>(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<SDL_Scancode>(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::uint8_t>(
std::clamp(128 + keys.move_x * keys.reach, 0, 255));
input.analog_y = static_cast<std::uint8_t>(
std::clamp(128 + keys.move_y * keys.reach, 0, 255));
const int sensitivity = static_cast<int>(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();
}
+783
View File
@@ -0,0 +1,783 @@
#pragma once
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <initializer_list>
#include <string>
#include <string_view>
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<int>(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::uint8_t>(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<int>(HostKey::Z) - static_cast<int>(HostKey::A) == 25);
static_assert(static_cast<int>(HostKey::Digit9) - static_cast<int>(HostKey::Digit0) == 9);
static_assert(static_cast<int>(HostKey::F12) - static_cast<int>(HostKey::F1) == 11);
static_assert(static_cast<int>(HostKey::Numpad9) - static_cast<int>(HostKey::Numpad0) == 9);
static_assert(static_cast<int>(HostKey::Mouse5) - static_cast<int>(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<std::array<HostKey, kMaxKeysPerAction>,
static_cast<std::size_t>(BindAction::Count)> keys{};
std::array<std::uint8_t, static_cast<std::size_t>(BindAction::Count)> count{};
std::array<std::array<BindAction, kMaxKeysPerAction>,
static_cast<std::size_t>(PadButton::Count)> pad{};
std::array<std::uint8_t, static_cast<std::size_t>(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<int>(key);
return id >= static_cast<int>(first) && id <= static_cast<int>(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<std::size_t>(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<HostKey> 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<BindAction> actions) {
auto &count = bindings.pad_count[static_cast<std::size_t>(button)];
for (const BindAction action : actions) {
if (count >= kMaxKeysPerAction) break;
bindings.pad[static_cast<std::size_t>(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 <class IsDown>
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 <class IsDown>
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<std::uint8_t>(BindAction::Count); ++index) {
const auto action = static_cast<BindAction>(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<std::uint8_t>(BindAction::Count); ++index) {
const auto action = static_cast<BindAction>(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<char>(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<HostKey>(static_cast<int>(HostKey::A) + (ch - 'a'));
return true;
}
if (ch >= '0' && ch <= '9') {
key = static_cast<HostKey>(static_cast<int>(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<HostKey>(static_cast<int>(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<HostKey>(static_cast<int>(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<std::size_t>(button);
for (std::uint8_t slot = 0; slot < bindings.pad_count[index]; ++slot)
if (bindings.pad[index][slot] == action) return true;
return false;
}
template <class IsDown>
PadSample sample_pad(const ControlBindings &bindings, bool driving, IsDown &&is_down) {
PadSample sample;
for (std::uint8_t index = 0; index < static_cast<std::uint8_t>(PadButton::Count); ++index) {
const auto button = static_cast<PadButton>(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<int>(stick_to_psp(raw_x, false)) - 128, -127, 127);
int camera_y = static_cast<int>(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<std::size_t>(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
+7
View File
@@ -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 + "'");
}
+3
View File
@@ -1,5 +1,7 @@
#pragma once
#include "lcs_key_bindings.hpp"
#include <cstdint>
#include <filesystem>
#include <string>
@@ -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 {
+131
View File
@@ -1,7 +1,10 @@
#include "lcs_display_menu.hpp"
#include "lcs_key_bindings.hpp"
#include "lcs_render_config.hpp"
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
@@ -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<int>(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<std::size_t>(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;
}