From c57ed5f69b6858ca256a3f5db85017c2746367d8 Mon Sep 17 00:00:00 2001 From: GalaxisBeast Date: Sat, 5 Sep 2026 13:42:29 -0400 Subject: [PATCH] add keyboard support, analog triggers to digital input, and rebinding overhaul --- README.md | 12 + aurora-main/include/dolphin/pad.h | 14 + aurora-main/lib/dolphin/pad/pad.cpp | 47 +++- runtime/include/controller_button_names.h | 21 +- runtime/src/input_bindings.cpp | 7 +- runtime/src/settings_overlay.cpp | 314 +++++++++++++++++++--- 6 files changed, 358 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d616cb4..426699c 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,22 @@ Press **F10** while the game window has focus: Everything you change is saved to `Config.toml` on the spot and restored next launch. **Real controller support.** +Keyboard and mouse are also available through **F10 > Controller settings > Keyboard and mouse** +for each port. Enabling this replaces that port's gamepad input. The default preset uses WASD +for the main stick, left mouse for A (accelerate), Space for B (brake), right mouse for R +(drift), middle mouse for Z (item), arrow keys for the D-pad (tricks), and Enter for Start. +Keys and mouse buttons can be remapped, including both sticks and triggers; mouse movement +is not used. These settings are saved in `keyboard_bindings.dat` and restored next launch. + Controllers are fed to the game as a GameCube controller. Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra inputs like paddles, touchpads and share buttons show up when the hardware reports them. +Both button-binding slots also accept SDL triggers and stick directions. Selecting an analog +input shows a threshold slider beneath it (1–100%, default 50%); reaching that amount of travel +holds the chosen digital button. Each binding's threshold is saved independently in `Config.toml` +(for example, `a = "right_trigger@35,south"`). + **Dolphin-compatible input expressions.** Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators diff --git a/aurora-main/include/dolphin/pad.h b/aurora-main/include/dolphin/pad.h index 6f821c3..df5f358 100644 --- a/aurora-main/include/dolphin/pad.h +++ b/aurora-main/include/dolphin/pad.h @@ -171,6 +171,20 @@ typedef struct PADButtonMapping { PADButton padButton; } PADButtonMapping; +// Explicitly disabled, unlike INVALID which permits default L/R trigger input. +#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu + +// Axis-to-button bindings share the persisted nativeButton field without +// changing the binary layout of existing controller mapping files. +constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) { + return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8); +} +constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; } +constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; } +constexpr u32 PADAxisButtonIdentity(u32 binding) { + return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding; +} + typedef struct PADAxisMapping { PADSignedNativeAxis nativeAxis; s32 nativeButton; diff --git a/aurora-main/lib/dolphin/pad/pad.cpp b/aurora-main/lib/dolphin/pad/pad.cpp index bb82c24..61deb3f 100644 --- a/aurora-main/lib/dolphin/pad/pad.cpp +++ b/aurora-main/lib/dolphin/pad/pad.cpp @@ -319,6 +319,18 @@ std::array g_suppressLeftTrigger{}; std::array g_suppressRightTrigger{}; bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; } +bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) { + if (PADIsAxisButton(binding)) { + const u32 axis = binding & 0x7fu; + const u32 threshold = PADAxisButtonThreshold(binding); + if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false; + int value = SDL_GetGamepadAxis(gamepad, static_cast(axis)); + if ((binding & 0x80u) != 0) value = -value; + return value > 0 && value * 100 >= static_cast(threshold) * 32767; + } + return binding < SDL_GAMEPAD_BUTTON_COUNT && + SDL_GetGamepadButton(gamepad, static_cast(binding)); +} bool is_mouse_button_pressed(const s32 scancode) { const int32_t buttonNum = -(scancode + 1); if (buttonNum < 1 || buttonNum > 5) { @@ -724,10 +736,10 @@ u32 PADRead(PADStatus* status) { } status[i].err = PAD_ERR_NONE; - if (g_keyboardBindings[i].m_mappingsSet) { + if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) { std::ranges::for_each( - g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) { - if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) { + g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) { + if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) { status[i].button |= mapping.padButton; } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { status[i].button |= mapping.padButton; @@ -788,7 +800,7 @@ u32 PADRead(PADStatus* status) { status[i].triggerRight = static_cast(std::min(static_cast(status[i].triggerRight) + tr, 255)); } - if (controller) { + if (controller && !g_keyboardBindings[i].m_mappingsSet) { EnsureMappingLoaded(controller); // Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts @@ -835,7 +847,7 @@ u32 PADRead(PADStatus* status) { bool rightTriggerSet = false; std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) { - if (SDL_GetGamepadButton(controller->m_controller, static_cast(mapping.nativeButton))) { + if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) { status[i].button |= mapping.padButton; } @@ -852,7 +864,7 @@ u32 PADRead(PADStatus* status) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { return; } - if (SDL_GetGamepadButton(controller->m_controller, static_cast(mapping.nativeButton))) { + if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) { status[i].button |= mapping.padButton; } @@ -946,6 +958,15 @@ u32 PADRead(PADStatus* status) { Sint16 tl = std::max(static_cast(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L)); Sint16 tr = std::max(static_cast(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R)); + // Games can read either the digital L/R bits or their analog pressure. + // An explicit button binding must drive both, otherwise the original + // L2/R2 axis still activates L/R even when it was rebound to L1/R1. + // Real GC pads retain independent analog travel and end-stop clicks. + if (!controller->m_isGameCube) { + if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0; + if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0; + } + if (controller->m_deadZones.emulateTriggers) { if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) { status[i].button |= PAD_TRIGGER_L; @@ -990,12 +1011,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) { } if (controller->m_isGameCube) { - if (cmd == PAD_MOTOR_STOP) { - aurora::input::controller_rumble(instance, 0, 1, 0); + if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) { + // Use an unambiguous motor-off request. The (0, 1) coast encoding + // requires SDL's GameCube brake mode; other backends or an overridden + // hint interpret it as rumble and can leave the controller vibrating. + aurora::input::controller_rumble(instance, 0, 0, 0); } else if (cmd == PAD_MOTOR_RUMBLE) { aurora::input::controller_rumble(instance, 1, 1, 0); - } else if (cmd == PAD_MOTOR_STOP_HARD) { - aurora::input::controller_rumble(instance, 0, 0, 0); } } else { if (cmd == PAD_MOTOR_STOP) { @@ -1278,6 +1300,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU } PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) { + PADInit(); + if (!g_keyboardBindingsLoaded) { + g_keyboardBindingsLoaded = true; + load_keyboard_bindings(); + } if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) { return nullptr; } diff --git a/runtime/include/controller_button_names.h b/runtime/include/controller_button_names.h index d0d2b81..8dcd5d8 100644 --- a/runtime/include/controller_button_names.h +++ b/runtime/include/controller_button_names.h @@ -48,8 +48,19 @@ struct NativeButtonItem { uint32_t nativeButton; }; -inline constexpr std::array kNativeButtons = {{ - {"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID}, +inline constexpr std::array kNativeButtons = {{ + {"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED}, + {"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)}, + {"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)}, + {"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)}, + {"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)}, + {"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)}, + {"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)}, + {"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)}, + {"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)}, + {"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)}, + {"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)}, + {"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID}, {"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH}, {"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST}, {"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST}, @@ -88,7 +99,7 @@ inline std::string TrimToken(std::string_view token) { } inline const NativeButtonItem* FindNativeButton(std::string_view configName) { - const std::string name = TrimToken(configName); + const std::string name = TrimToken(configName.substr(0, configName.find('@'))); const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) { return name == item.configName; }); return it == kNativeButtons.end() ? nullptr : &*it; @@ -97,8 +108,8 @@ inline const NativeButtonItem* FindNativeButton(std::string_view configName) { // Falls back to the "unmapped" entry so callers always have a label to draw. inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) { const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), - [&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; }); - return it == kNativeButtons.end() ? kNativeButtons.front() : *it; + [&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); }); + return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it; } inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) { diff --git a/runtime/src/input_bindings.cpp b/runtime/src/input_bindings.cpp index 6ffa984..f81a16b 100644 --- a/runtime/src/input_bindings.cpp +++ b/runtime/src/input_bindings.cpp @@ -94,7 +94,12 @@ double ReadInput(SDL_Gamepad* gamepad, const std::string& name) { // Fall back to this project's own positional names, so a binding written // here does not have to use Dolphin vocabulary. if (const auto* native = ControllerNames::FindNativeButton(name)) { - if (native->nativeButton != PAD_NATIVE_BUTTON_INVALID) { + if (PADIsAxisButton(native->nativeButton)) { + const auto axis = static_cast(native->nativeButton & 0x7fu); + const double sign = (native->nativeButton & 0x80u) != 0 ? -1.0 : 1.0; + return std::clamp(SDL_GetGamepadAxis(gamepad, axis) / 32767.0 * sign, 0.0, 1.0); + } + if (native->nativeButton < SDL_GAMEPAD_BUTTON_COUNT) { return SDL_GetGamepadButton(gamepad, static_cast(native->nativeButton)) ? 1.0 : 0.0; } diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 8856ba5..b0572b7 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -186,6 +187,18 @@ void LimitResolutionForFrameRate() { using ControllerNames::FindNativeButton; +uint32_t ConfiguredNativeButton(const NativeButtonItem& item, const std::string& token) { + if (!PADIsAxisButton(item.nativeButton)) return item.nativeButton; + const size_t separator = token.find('@'); + if (separator == std::string::npos) return item.nativeButton; + uint32_t threshold = 0; + const char* end = token.data() + token.size(); + const auto parsed = std::from_chars(token.data() + separator + 1, end, threshold); + if (parsed.ec != std::errc{} || parsed.ptr != end || threshold < 1 || threshold > 100) + return item.nativeButton; + return PADAxisButtonIdentity(item.nativeButton) | (threshold << 8); +} + struct ControllerBindingPair { std::string primary; std::string secondary; @@ -204,6 +217,13 @@ ControllerBindingPair SplitControllerBinding(const std::string& value) { using ControllerNames::NativeButtonForValue; +std::string NativeBindingConfig(uint32_t binding) { + std::string value = NativeButtonForValue(binding).configName; + if (PADIsAxisButton(binding)) value += '@' + std::to_string(PADAxisButtonThreshold(binding)); + return value; +} + + void SetTopBarVisible(bool visible) { if (g_topBarVisible == visible) { return; @@ -242,7 +262,7 @@ void ApplyConfiguredMappings() { } const ControllerBindingPair binding = SplitControllerBinding(*configured); if (const NativeButtonItem* native = FindNativeButton(binding.primary)) { - PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton}); + PADSetButtonMapping(port, PADButtonMapping{ConfiguredNativeButton(*native, binding.primary), kControllerButtons[i].padButton}); } else { RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey << " button '" << binding.primary << "'" << std::endl; @@ -250,7 +270,7 @@ void ApplyConfiguredMappings() { uint32_t altNative = PAD_NATIVE_BUTTON_INVALID; if (!binding.secondary.empty()) { if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) { - altNative = native->nativeButton; + altNative = ConfiguredNativeButton(*native, binding.secondary); } else { RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey << " secondary button '" << binding.secondary << "'" << std::endl; @@ -395,6 +415,210 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) { ImGui::EndMenu(); } +const char* KeyBindingName(int scancode) { + switch (scancode) { + case PAD_KEY_MOUSE_LEFT: return "Mouse left"; + case PAD_KEY_MOUSE_RIGHT: return "Mouse right"; + case PAD_KEY_MOUSE_MIDDLE: return "Mouse middle"; + case PAD_KEY_MOUSE_X1: return "Mouse side 1"; + case PAD_KEY_MOUSE_X2: return "Mouse side 2"; + case PAD_KEY_INVALID: return "Unmapped"; + default: + return scancode >= 0 && scancode < SDL_SCANCODE_COUNT + ? SDL_GetScancodeName(static_cast(scancode)) : "Unknown"; + } +} + +enum class RebindKind { KeyboardButton, KeyboardAxis, Controller }; +struct RebindState { + bool active = false; + bool openPopup = false; + RebindKind kind{}; + uint32_t port = 0; + uint16_t target = 0; + bool secondary = false; + SDL_JoystickID instance = 0; + Clock::time_point deadline{}; + std::string label; + std::array keys{}; + uint32_t mouse = 0; + std::array buttons{}; + std::array axesReady{}; +} g_rebind; + +void BeginRebind(RebindKind kind, uint16_t target, const char* label, bool secondary = false) { + g_rebind = {}; + g_rebind.active = true; + g_rebind.openPopup = true; + g_rebind.kind = kind; + g_rebind.port = static_cast(g_controllerPort); + g_rebind.target = target; + g_rebind.secondary = secondary; + g_rebind.label = label; + g_rebind.deadline = Clock::now() + std::chrono::seconds(10); + int count = 0; + const bool* keys = SDL_GetKeyboardState(&count); + std::copy_n(keys, std::min(count, static_cast(g_rebind.keys.size())), g_rebind.keys.begin()); + g_rebind.mouse = SDL_GetMouseState(nullptr, nullptr); + const int index = PADGetIndexForPort(g_rebind.port); + if (kind == RebindKind::Controller && index >= 0) { + if (auto* pad = PADGetSDLGamepadForIndex(index)) { + g_rebind.instance = SDL_GetGamepadID(pad); + for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i) + g_rebind.buttons[i] = SDL_GetGamepadButton(pad, static_cast(i)); + for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) + g_rebind.axesReady[i] = std::abs(static_cast(SDL_GetGamepadAxis(pad, static_cast(i)))) < 8000; + } + } +} + +void CompleteRebind(uint32_t value) { + const auto& capture = g_rebind; + if (capture.kind == RebindKind::Controller) { + const int index = PADGetIndexForPort(capture.port); + auto* pad = index >= 0 ? PADGetSDLGamepadForIndex(index) : nullptr; + if (pad == nullptr || SDL_GetGamepadID(pad) != capture.instance) { + g_rebind.active = false; + return; + } + if (capture.secondary) PADSetAltButtonMapping(capture.port, {value, capture.target}); + else PADSetButtonMapping(capture.port, {value, capture.target}); + uint32_t count = 0, altCount = 0; + auto* primary = PADGetButtonMappings(capture.port, &count); + auto* alternate = PADGetAltButtonMappings(capture.port, &altCount); + uint32_t primaryValue = PAD_NATIVE_BUTTON_INVALID, alternateValue = PAD_NATIVE_BUTTON_INVALID; + for (uint32_t i = 0; i < count; ++i) + if (primary[i].padButton == capture.target) primaryValue = primary[i].nativeButton; + for (uint32_t i = 0; i < altCount; ++i) + if (alternate[i].padButton == capture.target) alternateValue = alternate[i].nativeButton; + std::string config = NativeBindingConfig(primaryValue); + if (alternateValue != PAD_NATIVE_BUTTON_INVALID) config += ',' + NativeBindingConfig(alternateValue); + for (size_t i = 0; i < kControllerButtons.size(); ++i) + if (kControllerButtons[i].padButton == capture.target) RuntimeConfigFile::SetControllerButton(i, config); + } else if (capture.kind == RebindKind::KeyboardButton) { + PADSetKeyButtonBinding(capture.port, {static_cast(value), capture.target}); + } else { + PADSetKeyAxisBinding(capture.port, {static_cast(value), capture.target, 1}); + } + PADSerializeMappings(); + g_rebind.active = false; +} + +void DrawRebindPrompt() { + if (g_rebind.openPopup) { + ImGui::OpenPopup("Rebind input"); + g_rebind.openPopup = false; + } + if (!ImGui::BeginPopupModal("Rebind input", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; + if (g_rebind.active) { + ImGui::Text("Rebind: %s", g_rebind.label.c_str()); + ImGui::TextUnformatted(g_rebind.kind == RebindKind::Controller + ? "Press a controller button, pull a trigger, or move a stick." + : "Press a keyboard key or click a mouse button."); + ImGui::TextUnformatted("Release any held input first. F10 is reserved for settings."); + const float remaining = std::chrono::duration(g_rebind.deadline - Clock::now()).count(); + ImGui::Text("Unmapped in %d seconds", std::max(0, static_cast(std::ceil(remaining)))); + if (remaining <= 0.0f) { + CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED + : static_cast(PAD_KEY_INVALID)); + } else if (SDL_GetKeyboardFocus() != nullptr && g_rebind.kind != RebindKind::Controller) { + int count = 0; + const bool* keys = SDL_GetKeyboardState(&count); + for (int i = 1; i < std::min(count, static_cast(SDL_SCANCODE_COUNT)) && g_rebind.active; ++i) { + if (keys[i] && !g_rebind.keys[i] && i != SDL_SCANCODE_F10) CompleteRebind(i); + g_rebind.keys[i] = keys[i]; + } + const uint32_t mouse = SDL_GetMouseState(nullptr, nullptr); + for (int i = 1; i <= 5 && g_rebind.active; ++i) + if ((mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast(-i - 1)); + g_rebind.mouse = mouse; + } else if (SDL_GetKeyboardFocus() != nullptr && g_rebind.kind == RebindKind::Controller) { + auto* pad = SDL_GetGamepadFromID(g_rebind.instance); + if (pad != nullptr) { + for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT && g_rebind.active; ++i) { + const bool pressed = SDL_GetGamepadButton(pad, static_cast(i)); + if (pressed && !g_rebind.buttons[i]) CompleteRebind(i); + g_rebind.buttons[i] = pressed; + } + for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT && g_rebind.active; ++i) { + const int value = SDL_GetGamepadAxis(pad, static_cast(i)); + if (std::abs(value) < 8000) g_rebind.axesReady[i] = true; + if (g_rebind.axesReady[i] && std::abs(value) >= 16384) + CompleteRebind(PADEncodeAxisButton(i, value < 0)); + } + } + } + } + if (!g_rebind.active) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); +} + +void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t target) { + const std::string caption = std::string(KeyBindingName(scancode)) + "##binding"; + if (ImGui::Button(caption.c_str(), ImVec2(220.0f, 0.0f))) BeginRebind(kind, target, label); + ImGui::SameLine(); + ImGui::TextUnformatted(label); + +} + +bool DrawKeyboardSettings(uint32_t port) { + uint32_t count = 0; + auto* buttons = PADGetKeyButtonBindings(port, &count); + bool enabled = buttons != nullptr; + bool usePreset = false; + if (ImGui::Checkbox("Keyboard and mouse", &enabled)) { + PADSetKeyboardActive(port, enabled); + PADSerializeMappings(); + buttons = PADGetKeyButtonBindings(port, &count); + usePreset = enabled && std::all_of(buttons, buttons + count, [](const auto& binding) { + return binding.scancode == PAD_KEY_INVALID; + }); + } + if (!enabled) return false; + ImGui::TextDisabled("Replaces the gamepad on this port. F10 opens settings."); + if (ImGui::Button("Use WASD + mouse preset") || usePreset) { + const std::array keys = { + PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q, + SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT, + SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, + }; + for (size_t i = 0; i < keys.size(); ++i) + PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton}); + const std::array axes = { + SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S, + SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K, + SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT, + }; + uint32_t axisCount = 0; + auto* mappings = PADGetKeyAxisBindings(port, &axisCount); + for (uint32_t i = 0; i < axisCount; ++i) + PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1}); + PADSerializeMappings(); + } + ImGui::SeparatorText("Button mapping"); + for (uint32_t i = 0; i < count; ++i) { + int key = buttons[i].scancode; + ImGui::PushID(static_cast(i)); + ImGui::SetNextItemWidth(220.0f); + DrawKeyBinding(PADGetButtonName(buttons[i].padButton), key, RebindKind::KeyboardButton, buttons[i].padButton); + ImGui::PopID(); + } + ImGui::SeparatorText("Stick and trigger mapping"); + uint32_t axisCount = 0; + auto* axes = PADGetKeyAxisBindings(port, &axisCount); + for (uint32_t i = 0; i < axisCount; ++i) { + int key = axes[i].scancode; + ImGui::PushID(static_cast(count + i)); + const char* direction = PADGetAxisDirectionLabel(axes[i].padAxis); + const std::string label = std::string(PADGetAxisName(axes[i].padAxis)) + " " + + (direction != nullptr ? direction : ""); + ImGui::SetNextItemWidth(220.0f); + DrawKeyBinding(label.c_str(), key, RebindKind::KeyboardAxis, axes[i].padAxis); + ImGui::PopID(); + } + return true; +} + // Controller settings menu: port selection, controller assignment and button mapping. int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) { if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { @@ -501,6 +725,10 @@ void DrawControllerSettings() { ImGui::Separator(); const uint32_t selectedGamePort = static_cast(g_controllerPort); + if (DrawKeyboardSettings(selectedGamePort)) { + return; + } + ImGui::Separator(); const char* currentName = PADGetName(selectedGamePort); ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None"); if (ImGui::MenuItem("Unassign controller")) { @@ -542,10 +770,10 @@ void DrawControllerSettings() { PADGetAltButtonMappings(static_cast(g_controllerPort), &altMappingCount); const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) { - std::string value = NativeButtonForValue(primaryNative).configName; + std::string value = NativeBindingConfig(primaryNative); if (altNative != PAD_NATIVE_BUTTON_INVALID) { value += ','; - value += NativeButtonForValue(altNative).configName; + value += NativeBindingConfig(altNative); } RuntimeConfigFile::SetControllerButton(index, value); }; @@ -603,6 +831,11 @@ void DrawControllerSettings() { } ImGui::SeparatorText("Button mapping"); + ImGui::TextDisabled("LT / L2 = left trigger. RT / R2 = right trigger."); + ImGui::TextDisabled("LB / L1 = left shoulder. RB / R1 = right shoulder."); + ImGui::TextDisabled("Click a binding, then press an input. No input for 10 seconds clears it."); + const float bindingWidth = ImGui::CalcTextSize("Right shoulder (RB / R1)").x + + ImGui::GetFrameHeight() + ImGui::GetStyle().FramePadding.x * 2.0f; for (size_t i = 0; i < kControllerButtons.size(); ++i) { auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) { return mapping.padButton == kControllerButtons[i].padButton; @@ -622,30 +855,38 @@ void DrawControllerSettings() { const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton); ImGui::PushID(static_cast(i)); - ImGui::SetNextItemWidth(190.0f); - if (ImGui::BeginCombo("##primary", current.label)) { - for (const auto& candidate : kNativeButtons) { - const bool selected = candidate.nativeButton == mappingIt->nativeButton; - if (ImGui::Selectable(candidate.label, selected)) { - const uint32_t port = static_cast(g_controllerPort); - PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton}); - writeBinding(i, candidate.nativeButton, - altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID); - PADSerializeMappings(); - mappings = PADGetButtonMappings(port, &mappingCount); - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } + const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) { + if (!PADIsAxisButton(mapping->nativeButton)) return; + int threshold = static_cast(PADAxisButtonThreshold(mapping->nativeButton)); + ImGui::SetNextItemWidth(bindingWidth); + if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold, + 1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) { + const PADButtonMapping updated = { + PADAxisButtonIdentity(mapping->nativeButton) | (static_cast(threshold) << 8), + mapping->padButton, + }; + if (secondary) PADSetAltButtonMapping(selectedGamePort, updated); + else PADSetButtonMapping(selectedGamePort, updated); + writeBinding(i, mappingIt->nativeButton, + altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID); + PADSerializeMappings(); } - ImGui::EndCombo(); + }; + ImGui::BeginGroup(); + ImGui::SetNextItemWidth(bindingWidth); + const std::string primaryCaption = std::string(current.label) + "##primary"; + if (ImGui::Button(primaryCaption.c_str(), ImVec2(bindingWidth, 0.0f))) { + BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label); } + drawThreshold(mappingIt, false); + ImGui::EndGroup(); if (altIt != nullptr) { const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID; if (!altBound && !altRowExpanded[i]) { ImGui::SameLine(); if (ImGui::SmallButton("+")) { altRowExpanded[i] = true; + BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Add a second binding; pressing either one works"); @@ -654,27 +895,15 @@ void DrawControllerSettings() { ImGui::SameLine(); ImGui::TextUnformatted("or"); ImGui::SameLine(); + ImGui::BeginGroup(); const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None"; - ImGui::SetNextItemWidth(190.0f); - if (ImGui::BeginCombo("##alt", altLabel)) { - for (const auto& candidate : kNativeButtons) { - const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID; - const bool selected = candidate.nativeButton == altIt->nativeButton; - if (ImGui::Selectable(isNone ? "None" : candidate.label, selected)) { - const uint32_t port = static_cast(g_controllerPort); - PADSetAltButtonMapping( - port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton}); - writeBinding(i, mappingIt->nativeButton, candidate.nativeButton); - if (isNone) { - altRowExpanded[i] = false; - } - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); + ImGui::SetNextItemWidth(bindingWidth); + const std::string altCaption = std::string(altLabel) + "##alt"; + if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) { + BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true); } + drawThreshold(altIt, true); + ImGui::EndGroup(); } } ImGui::SameLine(); @@ -968,6 +1197,9 @@ void DrawTopBar() { if (ImGui::BeginMenu("Controller settings")) { DrawControllerSettings(); + // Nest capture under this menu so opening/closing the modal preserves + // the settings popup and its current port and scroll position. + DrawRebindPrompt(); ImGui::EndMenu(); } @@ -1074,7 +1306,7 @@ void HandleEvents(const AuroraEvent* events) noexcept { continue; } controller_mapping_wizard::HandleSdlEvent(ev->sdl); - if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { + if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { SetTopBarVisible(!g_topBarVisible); } if (IsMouseActivity(ev->sdl)) { @@ -1102,7 +1334,7 @@ void Draw() noexcept { DrawTopBar(); controller_mapping_wizard::Draw(); // The wizard captures raw presses; keep them out of the game. - const bool inputBlocked = controller_mapping_wizard::IsActive(); + const bool inputBlocked = controller_mapping_wizard::IsActive() || g_topBarVisible || g_rebind.active; PADBlockInput(inputBlocked); InputBindings::SetInputBlocked(inputBlocked); DrawStartupScreen();