mirror of
https://github.com/elmasas/lcs-recomp
synced 2026-10-20 23:19:46 -04:00
Merge pull request #16 from ragkousism/lcs-recomp:main
Add ability to change keybindings from ini
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 + "'");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user