10 Commits

Author SHA1 Message Date
patchzyy 352c4ca02e Update settings_overlay.cpp 2026-09-10 17:35:29 +02:00
Cristian Boehm cc286f3060 Merge branch 'patchzyy:main' into main 2026-09-09 13:17:50 -04:00
Cristian Boehm b034f7c4cc Merge branch 'patchzyy:main' into main 2026-09-07 14:45:55 -04:00
GalaxisBeast a51fc25daf Merge branch 'main' of https://github.com/GalaxisBeast/Wiicompiled into input-remapping-restored 2026-09-06 23:39:15 -04:00
GalaxisBeast f41d1f1393 add dimming when in settings and add clear mapping button 2026-09-06 23:39:11 -04:00
Cristian Boehm 6242926af9 Merge branch 'patchzyy:main' into main 2026-09-06 23:27:11 -04:00
GalaxisBeast 88643d6884 implemented code rabbits suggestions
- Preserved NSO GameCube analog triggers.
  - Made modal closure and Escape cancel every rebind kind.
  - Deduced the native button array size; <array> already existed.
  - Centralized axis/sign decoding.
  - Kept threshold updates live, saving only when editing ends.
2026-09-05 13:59:25 -04:00
GalaxisBeast 5381c4ecf2 Update README.md
readme typo
2026-09-05 13:44:56 -04:00
GalaxisBeast e5258af5ec Update README.md 2026-09-05 13:44:06 -04:00
GalaxisBeast c57ed5f69b add keyboard support, analog triggers to digital input, and rebinding overhaul 2026-09-05 13:42:29 -04:00
6 changed files with 399 additions and 59 deletions
+14
View File
@@ -54,10 +54,24 @@ Press **F10** while the game window has focus:
Everything you change is saved to `Config.toml` on the spot and restored next launch. Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Real controller support.** **Real controller support.**
Controllers are fed to the game as a GameCube controller. Controllers are fed to the game as a GameCube controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the 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 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. 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 (1100%, 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"`).
**Keyboard and Mouse 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.
**Dolphin-compatible input expressions.** **Dolphin-compatible input expressions.**
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
+16
View File
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
PADButton padButton; PADButton padButton;
} PADButtonMapping; } 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 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
constexpr u32 PADAxisButtonIdentity(u32 binding) {
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
}
typedef struct PADAxisMapping { typedef struct PADAxisMapping {
PADSignedNativeAxis nativeAxis; PADSignedNativeAxis nativeAxis;
s32 nativeButton; s32 nativeButton;
+39 -10
View File
@@ -319,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{}; std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; } 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 = PADAxisButtonAxis(binding);
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 (PADAxisButtonNegative(binding)) 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) { bool is_mouse_button_pressed(const s32 scancode) {
const int32_t buttonNum = -(scancode + 1); const int32_t buttonNum = -(scancode + 1);
if (buttonNum < 1 || buttonNum > 5) { if (buttonNum < 1 || buttonNum > 5) {
@@ -724,10 +736,10 @@ u32 PADRead(PADStatus* status) {
} }
status[i].err = PAD_ERR_NONE; 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( std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) { g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) { if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
status[i].button |= mapping.padButton; 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)); 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); EnsureMappingLoaded(controller);
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts // 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; bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) { &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; status[i].button |= mapping.padButton;
} }
@@ -852,7 +864,7 @@ u32 PADRead(PADStatus* status) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return; 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; status[i].button |= mapping.padButton;
} }
@@ -946,6 +958,17 @@ u32 PADRead(PADStatus* status) {
Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L)); 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)); 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 ||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
controller->m_pid == 0x2073))) {
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 (controller->m_deadZones.emulateTriggers) {
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) { if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
status[i].button |= PAD_TRIGGER_L; status[i].button |= PAD_TRIGGER_L;
@@ -990,12 +1013,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
} }
if (controller->m_isGameCube) { if (controller->m_isGameCube) {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 1, 0); // 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) { } else if (cmd == PAD_MOTOR_RUMBLE) {
aurora::input::controller_rumble(instance, 1, 1, 0); aurora::input::controller_rumble(instance, 1, 1, 0);
} else if (cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 0, 0);
} }
} else { } else {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP) {
@@ -1278,6 +1302,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
} }
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) { 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) { if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
return nullptr; return nullptr;
} }
+17 -6
View File
@@ -48,8 +48,19 @@ struct NativeButtonItem {
uint32_t nativeButton; uint32_t nativeButton;
}; };
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{ inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID}, {"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}, {"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST}, {"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST}, {"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
@@ -76,7 +87,7 @@ inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNat
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4}, {"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5}, {"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6}, {"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
}}; });
inline std::string TrimToken(std::string_view token) { inline std::string TrimToken(std::string_view token) {
const size_t begin = token.find_first_not_of(" \t"); const size_t begin = token.find_first_not_of(" \t");
@@ -88,7 +99,7 @@ inline std::string TrimToken(std::string_view token) {
} }
inline const NativeButtonItem* FindNativeButton(std::string_view configName) { 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 auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return name == item.configName; }); [&](const NativeButtonItem& item) { return name == item.configName; });
return it == kNativeButtons.end() ? nullptr : &*it; 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. // Falls back to the "unmapped" entry so callers always have a label to draw.
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) { inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; }); [&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
return it == kNativeButtons.end() ? kNativeButtons.front() : *it; return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
} }
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) { inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
+6 -1
View File
@@ -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 // Fall back to this project's own positional names, so a binding written
// here does not have to use Dolphin vocabulary. // here does not have to use Dolphin vocabulary.
if (const auto* native = ControllerNames::FindNativeButton(name)) { 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>(PADAxisButtonAxis(native->nativeButton));
const double sign = PADAxisButtonNegative(native->nativeButton) ? -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 return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
: 0.0; : 0.0;
} }
+307 -42
View File
@@ -20,6 +20,7 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <cctype> #include <cctype>
#include <charconv>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -186,6 +187,18 @@ void LimitResolutionForFrameRate() {
using ControllerNames::FindNativeButton; 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 { struct ControllerBindingPair {
std::string primary; std::string primary;
std::string secondary; std::string secondary;
@@ -204,6 +217,13 @@ ControllerBindingPair SplitControllerBinding(const std::string& value) {
using ControllerNames::NativeButtonForValue; 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) { void SetTopBarVisible(bool visible) {
if (g_topBarVisible == visible) { if (g_topBarVisible == visible) {
return; return;
@@ -242,7 +262,7 @@ void ApplyConfiguredMappings() {
} }
const ControllerBindingPair binding = SplitControllerBinding(*configured); const ControllerBindingPair binding = SplitControllerBinding(*configured);
if (const NativeButtonItem* native = FindNativeButton(binding.primary)) { 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 { } else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " button '" << binding.primary << "'" << std::endl; << " button '" << binding.primary << "'" << std::endl;
@@ -250,7 +270,7 @@ void ApplyConfiguredMappings() {
uint32_t altNative = PAD_NATIVE_BUTTON_INVALID; uint32_t altNative = PAD_NATIVE_BUTTON_INVALID;
if (!binding.secondary.empty()) { if (!binding.secondary.empty()) {
if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) { if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) {
altNative = native->nativeButton; altNative = ConfiguredNativeButton(*native, binding.secondary);
} else { } else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " secondary button '" << binding.secondary << "'" << std::endl; << " secondary button '" << binding.secondary << "'" << std::endl;
@@ -395,6 +415,219 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
ImGui::EndMenu(); 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", &g_rebind.active, ImGuiWindowFlags_AlwaysAutoResize)) {
g_rebind.active = false;
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. Backspace or Delete clears the mapping.");
ImGui::TextUnformatted("Escape can be bound. 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))));
const bool clear = ImGui::Button("Clear mapping");
ImGui::SameLine();
if (ImGui::Button("Cancel")) g_rebind.active = false;
// UI clicks must not become mouse bindings (buttons activate on release).
const bool overControl = ImGui::IsAnyItemHovered();
if (g_rebind.active && (clear || remaining <= 0.0f)) {
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
: static_cast<uint32_t>(PAD_KEY_INVALID));
} else if (g_rebind.active && 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 (!overControl && (mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast<uint32_t>(-i - 1));
g_rebind.mouse = mouse;
} else if (g_rebind.active && 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. // Controller settings menu: port selection, controller assignment and button mapping.
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) { int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
@@ -501,6 +734,10 @@ void DrawControllerSettings() {
ImGui::Separator(); ImGui::Separator();
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort); const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
if (DrawKeyboardSettings(selectedGamePort)) {
return;
}
ImGui::Separator();
const char* currentName = PADGetName(selectedGamePort); const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None"); ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
if (ImGui::MenuItem("Unassign controller")) { if (ImGui::MenuItem("Unassign controller")) {
@@ -542,10 +779,10 @@ void DrawControllerSettings() {
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount); PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) { 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) { if (altNative != PAD_NATIVE_BUTTON_INVALID) {
value += ','; value += ',';
value += NativeButtonForValue(altNative).configName; value += NativeBindingConfig(altNative);
} }
RuntimeConfigFile::SetControllerButton(index, value); RuntimeConfigFile::SetControllerButton(index, value);
}; };
@@ -603,6 +840,11 @@ void DrawControllerSettings() {
} }
ImGui::SeparatorText("Button mapping"); 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) { for (size_t i = 0; i < kControllerButtons.size(); ++i) {
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) { auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
return mapping.padButton == kControllerButtons[i].padButton; return mapping.padButton == kControllerButtons[i].padButton;
@@ -622,30 +864,40 @@ void DrawControllerSettings() {
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton); const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
ImGui::SetNextItemWidth(190.0f); const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) {
if (ImGui::BeginCombo("##primary", current.label)) { if (!PADIsAxisButton(mapping->nativeButton)) return;
for (const auto& candidate : kNativeButtons) { int threshold = static_cast<int>(PADAxisButtonThreshold(mapping->nativeButton));
const bool selected = candidate.nativeButton == mappingIt->nativeButton; ImGui::SetNextItemWidth(bindingWidth);
if (ImGui::Selectable(candidate.label, selected)) { if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold,
const uint32_t port = static_cast<uint32_t>(g_controllerPort); 1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) {
PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton}); const PADButtonMapping updated = {
writeBinding(i, candidate.nativeButton, PADAxisButtonIdentity(mapping->nativeButton) | (static_cast<uint32_t>(threshold) << 8),
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID); mapping->padButton,
PADSerializeMappings(); };
mappings = PADGetButtonMappings(port, &mappingCount); if (secondary) PADSetAltButtonMapping(selectedGamePort, updated);
} else PADSetButtonMapping(selectedGamePort, updated);
if (selected) {
ImGui::SetItemDefaultFocus();
}
} }
ImGui::EndCombo(); if (ImGui::IsItemDeactivatedAfterEdit()) {
writeBinding(i, mappingIt->nativeButton,
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
PADSerializeMappings();
}
};
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) { if (altIt != nullptr) {
const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID; const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID;
if (!altBound && !altRowExpanded[i]) { if (!altBound && !altRowExpanded[i]) {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("+")) { if (ImGui::SmallButton("+")) {
altRowExpanded[i] = true; altRowExpanded[i] = true;
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Add a second binding; pressing either one works"); ImGui::SetTooltip("Add a second binding; pressing either one works");
@@ -654,27 +906,15 @@ void DrawControllerSettings() {
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextUnformatted("or"); ImGui::TextUnformatted("or");
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginGroup();
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None"; const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
ImGui::SetNextItemWidth(190.0f); ImGui::SetNextItemWidth(bindingWidth);
if (ImGui::BeginCombo("##alt", altLabel)) { const std::string altCaption = std::string(altLabel) + "##alt";
for (const auto& candidate : kNativeButtons) { if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID; BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
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();
} }
drawThreshold(altIt, true);
ImGui::EndGroup();
} }
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -936,10 +1176,27 @@ void DrawStartupScreen() {
} }
void DrawTopBar() { void DrawTopBar() {
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) { if (!g_topBarVisible) {
return; return;
} }
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::GetBackgroundDrawList()->AddRectFilled(viewport->Pos,
ImVec2(viewport->Pos.x + viewport->Size.x, viewport->Pos.y + viewport->Size.y),
IM_COL32(0, 0, 0, 70));
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
viewport->Pos.y + viewport->Size.y - 24.0f),
ImGuiCond_Always, ImVec2(0.5f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.85f);
if (ImGui::Begin("Settings input hint", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::TextUnformatted("Settings open - game controls disabled. Press F10 to return to the game.");
}
ImGui::End();
if (!ImGui::BeginMainMenuBar()) return;
ImGui::TextUnformatted("WiiCompiled"); ImGui::TextUnformatted("WiiCompiled");
ImGui::Separator(); ImGui::Separator();
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) { const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
@@ -968,6 +1225,9 @@ void DrawTopBar() {
if (ImGui::BeginMenu("Controller settings")) { if (ImGui::BeginMenu("Controller settings")) {
DrawControllerSettings(); 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(); ImGui::EndMenu();
} }
@@ -1074,7 +1334,12 @@ void HandleEvents(const AuroraEvent* events) noexcept {
continue; continue;
} }
controller_mapping_wizard::HandleSdlEvent(ev->sdl); controller_mapping_wizard::HandleSdlEvent(ev->sdl);
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { if (g_rebind.active && (IsToggleKey(ev->sdl, SDL_SCANCODE_BACKSPACE) ||
IsToggleKey(ev->sdl, SDL_SCANCODE_DELETE))) {
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
: static_cast<uint32_t>(PAD_KEY_INVALID));
}
if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
SetTopBarVisible(!g_topBarVisible); SetTopBarVisible(!g_topBarVisible);
} }
if (IsMouseActivity(ev->sdl)) { if (IsMouseActivity(ev->sdl)) {
@@ -1102,7 +1367,7 @@ void Draw() noexcept {
DrawTopBar(); DrawTopBar();
controller_mapping_wizard::Draw(); controller_mapping_wizard::Draw();
// The wizard captures raw presses; keep them out of the game. // 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_rebind.active;
PADBlockInput(inputBlocked); PADBlockInput(inputBlocked);
InputBindings::SetInputBlocked(inputBlocked); InputBindings::SetInputBlocked(inputBlocked);
DrawStartupScreen(); DrawStartupScreen();