mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
add keyboard support, analog triggers to digital input, and rebinding overhaul
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -319,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
||||
std::array<bool, PAD_CHANMAX> 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<SDL_GamepadAxis>(axis));
|
||||
if ((binding & 0x80u) != 0) value = -value;
|
||||
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
|
||||
}
|
||||
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
|
||||
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(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<u8>(std::min(static_cast<int>(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<SDL_GamepadButton>(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<SDL_GamepadButton>(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<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L));
|
||||
Sint16 tr = std::max(static_cast<Sint16>(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;
|
||||
}
|
||||
|
||||
@@ -48,8 +48,19 @@ struct NativeButtonItem {
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 12> 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) {
|
||||
|
||||
@@ -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<SDL_GamepadAxis>(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<SDL_GamepadButton>(native->nativeButton)) ? 1.0
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -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<SDL_Scancode>(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<bool, SDL_SCANCODE_COUNT> keys{};
|
||||
uint32_t mouse = 0;
|
||||
std::array<bool, SDL_GAMEPAD_BUTTON_COUNT> buttons{};
|
||||
std::array<bool, SDL_GAMEPAD_AXIS_COUNT> 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<uint32_t>(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<int>(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<SDL_GamepadButton>(i));
|
||||
for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i)
|
||||
g_rebind.axesReady[i] = std::abs(static_cast<int>(SDL_GetGamepadAxis(pad, static_cast<SDL_GamepadAxis>(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<int32_t>(value), capture.target});
|
||||
} else {
|
||||
PADSetKeyAxisBinding(capture.port, {static_cast<int32_t>(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<float>(g_rebind.deadline - Clock::now()).count();
|
||||
ImGui::Text("Unmapped in %d seconds", std::max(0, static_cast<int>(std::ceil(remaining))));
|
||||
if (remaining <= 0.0f) {
|
||||
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
|
||||
: static_cast<uint32_t>(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<int>(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<uint32_t>(-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<SDL_GamepadButton>(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<SDL_GamepadAxis>(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<int, PAD_BUTTON_COUNT> 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<int, PAD_AXIS_COUNT> 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<int>(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<int>(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<uint32_t>(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<uint32_t>(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<int>(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<uint32_t>(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<int>(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<uint32_t>(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<uint32_t>(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();
|
||||
|
||||
Reference in New Issue
Block a user