From ef43b943704ea0fce9cfc1e85c5d2840ca7b54b3 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Tue, 12 May 2026 21:36:07 -0400 Subject: [PATCH] Add options for binding custom buttons to specific actions (#1141) * custom action framework and first person custom action * add bind for midna call * custom binding for opening dusklight menu * turbo speed button action * text descriptions * fix not stopping default GC controller menu combo * more explanation text * block bind actions when in the dusklight menu --- files.cmake | 2 + include/dusk/action_bindings.h | 43 +++++ include/dusk/config.hpp | 7 + include/dusk/config_var.hpp | 2 + include/dusk/settings.h | 10 + libs/JSystem/src/JUtility/JUTGamePad.cpp | 7 + src/d/actor/d_a_alink.cpp | 9 + src/d/actor/d_a_alink_link.inc | 5 +- src/d/d_camera.cpp | 20 +- src/dusk/action_bindings.cpp | 96 ++++++++++ src/dusk/config.cpp | 8 + src/dusk/imgui/ImGuiConsole.cpp | 4 +- src/dusk/settings.cpp | 45 +++++ src/dusk/ui/controller_config.cpp | 231 ++++++++++++++++------- src/dusk/ui/controller_config.hpp | 5 + src/dusk/ui/input.cpp | 26 ++- src/dusk/ui/overlay.cpp | 14 +- 17 files changed, 456 insertions(+), 78 deletions(-) create mode 100644 include/dusk/action_bindings.h create mode 100644 src/dusk/action_bindings.cpp diff --git a/files.cmake b/files.cmake index 47527da735..450fae3b49 100644 --- a/files.cmake +++ b/files.cmake @@ -1411,6 +1411,7 @@ set(DOLPHIN_FILES ) set(DUSK_FILES + include/dusk/action_bindings.h include/dusk/endian_gx.hpp include/dusk/config.hpp include/dusk/dvd_asset.hpp @@ -1522,6 +1523,7 @@ set(DUSK_FILES src/dusk/discord.hpp src/dusk/discord_presence.cpp src/dusk/version.cpp + src/dusk/action_bindings.cpp ) set(DUSK_HTTP_BACKEND_FILES diff --git a/include/dusk/action_bindings.h b/include/dusk/action_bindings.h new file mode 100644 index 0000000000..7eba412fe8 --- /dev/null +++ b/include/dusk/action_bindings.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "dusk/config_var.hpp" + +namespace dusk { + +enum class ActionBinds { + FIRST_PERSON_CAMERA, + CALL_MIDNA, + OPEN_DUSKLIGHT_MENU, + TURBO_SPEED_BUTTON, + COUNT, +}; + +struct ActionBindData { + std::array* configVars{}; + std::string actionName{}; +}; + +struct ActionBindPressData { + bool pressedCurFrame{false}; + bool pressedPrevFrame{false}; +}; + +using ActionBindsMap = std::unordered_map; + +ActionBindsMap& getActionBinds(); + +bool isActionBound(ActionBinds action, u32 port); + +void updateActionBindings(); + +bool getActionBindTrig(ActionBinds action, u32 port); + +bool getActionBindHold(ActionBinds action, u32 port); + +bool getActionBindHoldAnyPort(ActionBinds action); + +int getActionBindButton(ActionBinds action, u32 port); + +} diff --git a/include/dusk/config.hpp b/include/dusk/config.hpp index 72ec449b50..382c4c24c6 100644 --- a/include/dusk/config.hpp +++ b/include/dusk/config.hpp @@ -112,6 +112,13 @@ void Save(); */ ConfigVarBase* GetConfigVar(std::string_view name); +/** + * \brief Resets all custom action bindings for a specific port to nothing + * + * @param port The port to be cleared of action bindings + */ +void ClearAllActionBindings(int port); + /** * \brief Call a function on every registered CVar. */ diff --git a/include/dusk/config_var.hpp b/include/dusk/config_var.hpp index b16409c7f3..cc8c700ed4 100644 --- a/include/dusk/config_var.hpp +++ b/include/dusk/config_var.hpp @@ -287,6 +287,8 @@ public: } }; +using ActionBindConfigVar = ConfigVar; + } #endif // DUSK_CONFIG_VAR_HPP diff --git a/include/dusk/settings.h b/include/dusk/settings.h index d57b15b9c1..25fb08d246 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -1,6 +1,8 @@ #ifndef DUSK_CONFIG_H #define DUSK_CONFIG_H +#include + #include "dusk/config_var.hpp" namespace dusk { @@ -197,6 +199,14 @@ struct UserSettings { ConfigVar cardFileType; ConfigVar enableAdvancedSettings; } backend; + + // Arrays of size 4 for 4 ports + struct { + std::array firstPersonCamera; + std::array callMidna; + std::array openDusklightMenu; + std::array turboSpeedButton; + } actionBindings; }; UserSettings& getSettings(); diff --git a/libs/JSystem/src/JUtility/JUTGamePad.cpp b/libs/JSystem/src/JUtility/JUTGamePad.cpp index 2ddf4397dc..48ed5f130e 100644 --- a/libs/JSystem/src/JUtility/JUTGamePad.cpp +++ b/libs/JSystem/src/JUtility/JUTGamePad.cpp @@ -4,6 +4,10 @@ #include #include "os_report.h" +#if TARGET_PC +#include "dusk/action_bindings.h" +#endif + u32 JUTGamePad::CRumble::sChannelMask[4] = { PAD_CHAN0_BIT, PAD_CHAN1_BIT, @@ -85,6 +89,9 @@ u32 JUTGamePad::sRumbleSupported; u32 JUTGamePad::read() { sRumbleSupported = PADRead(mPadStatus); +#if TARGET_PC + dusk::updateActionBindings(); +#endif switch (sClampMode) { case EClampStick: diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 0e0f0d06c7..498d3de173 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -51,10 +51,13 @@ #include "d/actor/d_a_ni.h" #include "d/d_s_play.h" +#if TARGET_PC +#include "dusk/action_bindings.h" #include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "res/Object/Alink.h" #include +#endif static int daAlink_Create(fopAc_ac_c* i_this); static int daAlink_Delete(daAlink_c* i_this); @@ -9363,6 +9366,12 @@ BOOL daAlink_c::spActionTrigger() { } BOOL daAlink_c::midnaTalkTrigger() const { +#if TARGET_PC + // If we have a custom bind for Midna, check that instead + if (dusk::isActionBound(dusk::ActionBinds::CALL_MIDNA, 0)) { + return dusk::getActionBindTrig(dusk::ActionBinds::CALL_MIDNA, 0); + } +#endif return mItemTrigger & BTN_Z; } diff --git a/src/d/actor/d_a_alink_link.inc b/src/d/actor/d_a_alink_link.inc index a834535426..636aabdffe 100644 --- a/src/d/actor/d_a_alink_link.inc +++ b/src/d/actor/d_a_alink_link.inc @@ -12,6 +12,7 @@ #if TARGET_PC #include "dusk/gyro.h" +#include "dusk/action_bindings.h" #endif bool daAlink_c::checkNoSubjectModeCamera() { @@ -192,7 +193,9 @@ BOOL daAlink_c::subjectCancelTrigger() { BOOL daAlink_c::checkSubjectEnd(BOOL i_isPlaySe) { setDoStatus(BUTTON_STATUS_BACK); - if (checkEventRun() || checkEquipAnime() || doTrigger() || checkSetItemTrigger(dItemNo_HAWK_EYE_e) || subjectCancelTrigger() || checkEndResetFlg0(ERFLG0_FORCE_SUBJECT_CANCEL) || dComIfGp_checkCameraAttentionStatus(field_0x317c, 0x2000)) { + // Allow pressing the first person binding to also leave first person + if (IF_DUSK(dusk::getActionBindTrig(dusk::ActionBinds::FIRST_PERSON_CAMERA, 0)) || + checkEventRun() || checkEquipAnime() || doTrigger() || checkSetItemTrigger(dItemNo_HAWK_EYE_e) || subjectCancelTrigger() || checkEndResetFlg0(ERFLG0_FORCE_SUBJECT_CANCEL) || dComIfGp_checkCameraAttentionStatus(field_0x317c, 0x2000)) { if (i_isPlaySe) { seStartSystem(Z2SE_SUBJ_VIEW_OUT); } diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 99274f133d..bfc4c809c9 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -31,6 +31,7 @@ #if TARGET_PC #include "dusk/frame_interpolation.h" #include "dusk/logging.h" +#include "dusk/action_bindings.h" #include "imgui.h" #endif @@ -838,6 +839,12 @@ void dCamera_c::updatePad() { mTrigB = mDoCPd_c::getTrigB(mPadID) ? true : false; #if TARGET_PC + // If our custom action binding is triggered, and we're not already in first person, go into first person + if (dusk::getActionBindTrig(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID) && mGear != -1) { + setComStat(0x1000); + mGear = 0; + } + if (mCamParam.mManualMode) { return; } @@ -877,7 +884,8 @@ void dCamera_c::updatePad() { if (mPadInfo.mCStick.mLastPosY < -mCamSetup.mCStick.SwTHH()) { if (mCStickYState != -1) { - if (mGear == -1 && mCurMode == 4) { + // Don't use regular first person trigger if custom mapping is set + if (mGear == -1 && mCurMode == 4 IF_DUSK(&& !dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID))) { mGear = 0; setComStat(0x2000); } else if (mGear == 0 && sp6C) { @@ -888,7 +896,8 @@ void dCamera_c::updatePad() { mCStickYState = -1; } else if (mPadInfo.mCStick.mLastPosY > mCamSetup.mCStick.SwTHH()) { if (mCStickYState != 1) { - if (mGear == 0 && sp6B) { + // Don't use regular first person trigger if custom mapping is set + if (mGear == 0 && sp6B IF_DUSK(&& !dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID))) { setComStat(0x1000); } else if (mGear == 1) { mGear = 0; @@ -7649,9 +7658,10 @@ bool dCamera_c::freeCamera() { f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY); // If we aren't in manual cam mode, don't trigger it if the player tries to hit C-up - // for first person - if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY < 0 || - (mCamParam.mManualMode == 1 && mPadInfo.mCStick.mLastPosY != 0)) { + // for first person unless they have first person bound to a custom binding + if ((dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID) && mPadInfo.mCStick.mLastPosY != 0) || + mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY < 0 || (mCamParam.mManualMode == 1 && mPadInfo.mCStick.mLastPosY != 0)) + { mCamParam.mManualMode = 1; camMovement = camMovement.normalize(); camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f; diff --git a/src/dusk/action_bindings.cpp b/src/dusk/action_bindings.cpp new file mode 100644 index 0000000000..204f219558 --- /dev/null +++ b/src/dusk/action_bindings.cpp @@ -0,0 +1,96 @@ +#include "dusk/action_bindings.h" + +#include "aurora/lib/input.hpp" +#include "dusk/settings.h" +#include "dusk/ui/ui.hpp" + +namespace dusk { + +static std::array(ActionBinds::COUNT)>, PAD_CHANMAX> actionPressData{}; + +ActionBindsMap& getActionBinds() { + static ActionBindsMap actionBinds = { + {ActionBinds::FIRST_PERSON_CAMERA, {&getSettings().actionBindings.firstPersonCamera, "First Person Camera"}}, + {ActionBinds::CALL_MIDNA, {&getSettings().actionBindings.callMidna, "Call Midna"}}, + {ActionBinds::OPEN_DUSKLIGHT_MENU, {&getSettings().actionBindings.openDusklightMenu, "Open Dusklight Menu"}}, + {ActionBinds::TURBO_SPEED_BUTTON, {&getSettings().actionBindings.turboSpeedButton, "Turbo Speed Button"}}, + }; + return actionBinds; +} + +bool isActionBound(ActionBinds action, u32 port) { + auto& actionBinds = getActionBinds(); + // Check to make sure action is properly bound + if (!actionBinds.contains(action)) { + return false; + } + + return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID; +} + +void updateActionBindings() { + for (u32 port = 0; port < PAD_CHANMAX; ++port) { + // Move the current press to the previous frame + for (auto& pressData : actionPressData[port]) { + pressData.pressedPrevFrame = pressData.pressedCurFrame; + pressData.pressedCurFrame = false; + } + + // Update current frame with whether action button is pressed + for (auto& [action, boundAction] : getActionBinds()) { + // If the action isn't bound, or if documents are visible and the action isn't + // opening the dusklight menu, don't update. Otherwise, we may accidentally + // perform actions while the dusklight menu is open. + if (!isActionBound(action, port) || + (ui::any_document_visible() && action != ActionBinds::OPEN_DUSKLIGHT_MENU)) { + continue; + } + + int button = boundAction.configVars->at(port); + + // If keyboard is active for this port + u32 count = 0; + if (PADGetKeyButtonBindings(port, &count) != nullptr) { + int numKeys = 0; + const bool* kbState = SDL_GetKeyboardState(&numKeys); + if (kbState[button]) { + actionPressData[port][static_cast(action)].pressedCurFrame = true; + } + } else { + // If controller is active + auto controller = aurora::input::get_controller_for_player(port); + if (controller) { + if (SDL_GetGamepadButton(controller->m_controller, static_cast(button))) { + actionPressData[port][static_cast(action)].pressedCurFrame = true; + } + } + } + } + } +} + +bool getActionBindTrig(ActionBinds action, u32 port) { + return isActionBound(action, port) && + actionPressData[port][static_cast(action)].pressedCurFrame && + !actionPressData[port][static_cast(action)].pressedPrevFrame; +} + +bool getActionBindHold(ActionBinds action, u32 port) { + return isActionBound(action, port) && + actionPressData[port][static_cast(action)].pressedCurFrame && + actionPressData[port][static_cast(action)].pressedPrevFrame; +} + +bool getActionBindHoldAnyPort(ActionBinds action) { + for (u32 port = 0; port < PAD_CHANMAX; ++port) { + if (getActionBindHold(action, port)) { + return true; + } + } + return false; +} + +int getActionBindButton(ActionBinds action, u32 port) { + return (*getActionBinds()[action].configVars)[port]; +} +} diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 46a689e287..de4cc227a3 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -11,6 +11,7 @@ #include #include "dusk/main.h" +#include "dusk/action_bindings.h" using namespace dusk::config; @@ -256,6 +257,13 @@ void dusk::config::Save() { io::FileStream::WriteAllText(reinterpret_cast(configJsonPath.c_str()), j.dump(4)); } +void dusk::config::ClearAllActionBindings(int port) { + for (auto& actionBinding : getActionBinds() | std::views::values) { + actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID); + } + Save(); +} + ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { const auto configVar = RegisteredConfigVars.find(name); if (configVar != RegisteredConfigVars.end()) { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 7d1cb98524..9ba323eaad 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -13,6 +13,7 @@ #include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" #include "SDL3/SDL_mouse.h" +#include "dusk/action_bindings.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/data.hpp" @@ -239,7 +240,8 @@ namespace dusk { } void ImGuiConsole::UpdateSettings() { - getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab); + getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && + (ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON)); if (dusk::frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 7475e5d48a..80f64a19d4 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -133,6 +133,34 @@ UserSettings g_userSettings = { .checkForUpdates {"backend.checkForUpdates", true}, .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)}, .enableAdvancedSettings {"backend.enableAdvancedSettings", false}, + }, + + // Not sure if there's a better way to declare this + .actionBindings = { + .firstPersonCamera { + ActionBindConfigVar{"actionBindings.firstPersonCamera_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.firstPersonCamera_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.firstPersonCamera_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.firstPersonCamera_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .callMidna { + ActionBindConfigVar{"actionBindings.callMidna_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.callMidna_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.callMidna_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.callMidna_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .openDusklightMenu { + ActionBindConfigVar{"actionBindings.openDusklightMenu_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openDusklightMenu_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openDusklightMenu_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openDusklightMenu_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .turboSpeedButton { + ActionBindConfigVar{"actionBindings.turboButton_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.turboButton_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.turboButton_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.turboButton_port3", PAD_NATIVE_BUTTON_INVALID}, + }, } }; @@ -248,6 +276,23 @@ void registerSettings() { Register(g_userSettings.backend.checkForUpdates); Register(g_userSettings.backend.cardFileType); Register(g_userSettings.backend.enableAdvancedSettings); + + Register(g_userSettings.actionBindings.firstPersonCamera[0]); + Register(g_userSettings.actionBindings.firstPersonCamera[1]); + Register(g_userSettings.actionBindings.firstPersonCamera[2]); + Register(g_userSettings.actionBindings.firstPersonCamera[3]); + Register(g_userSettings.actionBindings.callMidna[0]); + Register(g_userSettings.actionBindings.callMidna[1]); + Register(g_userSettings.actionBindings.callMidna[2]); + Register(g_userSettings.actionBindings.callMidna[3]); + Register(g_userSettings.actionBindings.openDusklightMenu[0]); + Register(g_userSettings.actionBindings.openDusklightMenu[1]); + Register(g_userSettings.actionBindings.openDusklightMenu[2]); + Register(g_userSettings.actionBindings.openDusklightMenu[3]); + Register(g_userSettings.actionBindings.turboSpeedButton[0]); + Register(g_userSettings.actionBindings.turboSpeedButton[1]); + Register(g_userSettings.actionBindings.turboSpeedButton[2]); + Register(g_userSettings.actionBindings.turboSpeedButton[3]); } // Transient settings diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp index 2c1d815ceb..aef911f996 100644 --- a/src/dusk/ui/controller_config.cpp +++ b/src/dusk/ui/controller_config.cpp @@ -15,6 +15,9 @@ #include #include +#include "dusk/action_bindings.h" +#include "dusk/config.hpp" + namespace dusk::ui { namespace { @@ -108,68 +111,6 @@ const std::vector kGamepadButtonNames = { }; // clang-format on -Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) { - if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) { - return "Not bound"; - } - - auto button = static_cast(buttonUntyped); - if (gamepad != nullptr) { - switch (SDL_GetGamepadButtonLabel(gamepad, button)) { - case SDL_GAMEPAD_BUTTON_LABEL_A: - return "A"; - case SDL_GAMEPAD_BUTTON_LABEL_B: - return "B"; - case SDL_GAMEPAD_BUTTON_LABEL_X: - return "X"; - case SDL_GAMEPAD_BUTTON_LABEL_Y: - return "Y"; - case SDL_GAMEPAD_BUTTON_LABEL_CROSS: - return "Cross"; - case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: - return "Circle"; - case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: - return "Triangle"; - case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: - return "Square"; - default: - break; - } - } - - const SDL_GamepadType type = - gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN; - for (const auto& buttonNames : kGamepadButtonNames) { - if (buttonNames.button != button) { - continue; - } - - for (const auto& name : buttonNames.names) { - if (name.type == type) { - return name.name; - } - } - } - - switch (button) { - case SDL_GAMEPAD_BUTTON_DPAD_LEFT: - return "D-pad left"; - case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: - return "D-pad right"; - case SDL_GAMEPAD_BUTTON_DPAD_UP: - return "D-pad up"; - case SDL_GAMEPAD_BUTTON_DPAD_DOWN: - return "D-pad down"; - default: - break; - } - - if (const char* name = PADGetNativeButtonName(buttonUntyped)) { - return name; - } - return "Unknown"; -} - Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad) { if (mapping.nativeAxis.nativeAxis != -1) { Rml::String value = PADGetNativeAxisName(mapping.nativeAxis); @@ -367,6 +308,7 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) { addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }, [] { return false; }); addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }, [] { return false; }); addPageButton(Page::Rumble, "Rumble", [] { return Rml::String(">"); }, [port] { return !PADSupportsRumbleIntensity(static_cast(port)); }); + addPageButton(Page::Actions, "Custom Action Bindings", [] {return Rml::String(">"); }, [] { return false; }); leftPane.add_section("Options"); leftPane.register_control(leftPane.add_child(BoolButton::Props{ @@ -428,6 +370,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADClearPort(port); PADSetKeyboardActive(static_cast(port), FALSE); PADSerializeMappings(); + ClearAllActionBindings(port); }); pane.add_button({ @@ -440,6 +383,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADClearPort(port); PADSetKeyboardActive(static_cast(port), TRUE); PADSerializeMappings(); + ClearAllActionBindings(port); }); const u32 controllerCount = PADCount(); @@ -461,6 +405,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADSetKeyboardActive(static_cast(port), FALSE); PADSetPortForIndex(i, port); PADSerializeMappings(); + ClearAllActionBindings(port); }); } break; @@ -946,6 +891,77 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { pane.add_text("Configure your desired rumble intensities, then run a test to check how they feel."); break; } + case Page::Actions: { + if (keyboard_active(port)) { + auto addActionBinding = [&](auto actionBind, const std::string& key) { + pane.add_select_button( + { + .key = key, + .getValue = + [this, actionBind] { + if (mPendingActionBinding == actionBind) { + return pending_key_label(); + } + + return keyboard_key_name(actionBind->getValue()); + }, + }) + .on_pressed([this, port, actionBind] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingActionBinding = actionBind; + }); + }; + + pane.add_section("Custom Action Bindings"); + pane.add_text("A key bound to any action here will REPLACE the default control for" + " that action. Only bind buttons here that aren't used anywhere else."); + for (auto& [configVars, actionName] : getActionBinds() | std::views::values) { + addActionBinding(&configVars->at(port), actionName); + } + break; + } + + u32 buttonCount = 0; + PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount); + if (mappings == nullptr) { + pane.add_text("No controller selected"); + break; + } + + SDL_Gamepad* gamepad = gamepad_for_port(port); + pane.add_section("Custom Action Bindings"); + pane.add_text("A button bound to any action here will REPLACE the default control for" + " that action. Only bind buttons here that aren't used anywhere else. The glyphs" + " shown for in game actions will not change. This is not recommended for " + " regular Gamecube controllers."); + auto addActionBinding = [&](auto actionBind, const std::string& key) { + pane.add_select_button({ + .key = key, + .getValue = + [this, gamepad, actionBind] { + if (mPendingActionBinding == actionBind) { + return pending_button_label(); + } + + return native_button_name( + gamepad, actionBind->getValue()); + }, + }) + .on_pressed([this, port, actionBind] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingActionBinding = actionBind; + }); + }; + + for (auto& [configVars, actionName] : getActionBinds() | std::views::values) { + addActionBinding(&configVars->at(port), actionName); + } + break; + } } } @@ -1020,12 +1036,31 @@ void ControllerConfigWindow::poll_pending_binding() { mPendingAxisMapping->nativeButton = nativeButton; finish_pending_binding(completedPort); } + return; + } + + if (mPendingActionBinding != nullptr) { + int button{}; + if (keyboard_active(mPendingPort)) { + button = keyboard_key_pressed(); + } else { + button = PADGetNativeButtonPressed(mPendingPort); + } + + if (button != -1) { + const int completedPort = mPendingPort; + mPendingActionBinding->setValue(button); + config::Save(); + finish_pending_binding(completedPort); + } + return; } } void ControllerConfigWindow::finish_pending_binding(int completedPort) { mPendingButtonMapping = nullptr; mPendingAxisMapping = nullptr; + mPendingActionBinding = nullptr; mPendingPort = -1; mPendingBindingArmed = false; mSuppressNavigationUntilNeutral = true; @@ -1035,7 +1070,7 @@ void ControllerConfigWindow::finish_pending_binding(int completedPort) { void ControllerConfigWindow::unmap_pending_binding() { if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && - mPendingKeyButton < 0 && mPendingKeyAxis < 0) + mPendingActionBinding == nullptr && mPendingKeyButton < 0 && mPendingKeyAxis < 0) { return; } @@ -1048,6 +1083,9 @@ void ControllerConfigWindow::unmap_pending_binding() { mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; mPendingAxisMapping->nativeButton = -1; finish_pending_binding(completedPort); + } else if (mPendingActionBinding != nullptr) { + mPendingActionBinding->setValue(PAD_NATIVE_BUTTON_INVALID); + finish_pending_binding(completedPort); } else if (mPendingKeyButton >= 0) { PADSetKeyButtonBinding(static_cast(completedPort), {PAD_KEY_INVALID, static_cast(mPendingKeyButton)}); @@ -1061,7 +1099,7 @@ void ControllerConfigWindow::unmap_pending_binding() { bool ControllerConfigWindow::capture_active() const { return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr || - mPendingKeyButton >= 0 || mPendingKeyAxis >= 0; + mPendingActionBinding != nullptr || mPendingKeyButton >= 0 || mPendingKeyAxis >= 0; } bool ControllerConfigWindow::pending_input_neutral() const { @@ -1080,13 +1118,14 @@ Rml::String ControllerConfigWindow::pending_axis_label() const { } void ControllerConfigWindow::cancel_pending_binding() { - if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && + if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && mPendingActionBinding == nullptr && !mSuppressNavigationUntilNeutral && mPendingKeyButton < 0 && mPendingKeyAxis < 0) { return; } mPendingButtonMapping = nullptr; mPendingAxisMapping = nullptr; + mPendingActionBinding = nullptr; mPendingKeyButton = -1; mPendingKeyAxis = -1; mPendingPort = -1; @@ -1118,4 +1157,66 @@ void ControllerConfigWindow::stop_rumble_test() { mRumbleTestPort = -1; } +Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) { + if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) { + return "Not bound"; + } + + auto button = static_cast(buttonUntyped); + if (gamepad != nullptr) { + switch (SDL_GetGamepadButtonLabel(gamepad, button)) { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return "A"; + case SDL_GAMEPAD_BUTTON_LABEL_B: + return "B"; + case SDL_GAMEPAD_BUTTON_LABEL_X: + return "X"; + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return "Y"; + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return "Cross"; + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return "Circle"; + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return "Triangle"; + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return "Square"; + default: + break; + } + } + + const SDL_GamepadType type = + gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN; + for (const auto& buttonNames : kGamepadButtonNames) { + if (buttonNames.button != button) { + continue; + } + + for (const auto& name : buttonNames.names) { + if (name.type == type) { + return name.name; + } + } + } + + switch (button) { + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + return "D-pad left"; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + return "D-pad right"; + case SDL_GAMEPAD_BUTTON_DPAD_UP: + return "D-pad up"; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + return "D-pad down"; + default: + break; + } + + if (const char* name = PADGetNativeButtonName(buttonUntyped)) { + return name; + } + return "Unknown"; +} + } // namespace dusk::ui diff --git a/src/dusk/ui/controller_config.hpp b/src/dusk/ui/controller_config.hpp index df8533a492..b5a50ad8b1 100644 --- a/src/dusk/ui/controller_config.hpp +++ b/src/dusk/ui/controller_config.hpp @@ -1,6 +1,7 @@ #pragma once #include "window.hpp" +#include "dusk/config_var.hpp" #include @@ -20,6 +21,7 @@ private: Triggers, Sticks, Rumble, + Actions, }; void build_port_tab(Rml::Element* content, int port); @@ -50,6 +52,9 @@ private: int mPendingKeyAxis = -1; bool mRumbleTestActive = false; int mRumbleTestPort = -1; + ActionBindConfigVar* mPendingActionBinding = nullptr; }; +Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped); + } // namespace dusk::ui diff --git a/src/dusk/ui/input.cpp b/src/dusk/ui/input.cpp index cdaa2360c0..02a15e9a0f 100644 --- a/src/dusk/ui/input.cpp +++ b/src/dusk/ui/input.cpp @@ -12,6 +12,8 @@ #include #include +#include "dusk/action_bindings.h" + namespace dusk::ui::input { namespace { @@ -203,6 +205,9 @@ Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexc case SDL_GAMEPAD_BUTTON_SOUTH: return Rml::Input::KI_RETURN; case SDL_GAMEPAD_BUTTON_BACK: + if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)) { + return Rml::Input::KI_UNKNOWN; + } return Rml::Input::KI_F1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Rml::Input::KI_NEXT; @@ -216,6 +221,9 @@ Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexc Rml::Input::KeyIdentifier map_raw_button_alias(SDL_GamepadButton button) noexcept { switch (button) { case SDL_GAMEPAD_BUTTON_BACK: + if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)) { + return Rml::Input::KI_UNKNOWN; + } return Rml::Input::KI_F1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Rml::Input::KI_NEXT; @@ -318,12 +326,20 @@ bool find_event_pad_button( Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event) noexcept { const auto nativeButton = static_cast(event.button); - if (nativeButton == SDL_GAMEPAD_BUTTON_BACK) { + u32 port = 0; + bool foundEventPort = find_event_port(event.which, port); + if (foundEventPort) { + int openMenuButton = getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, port); + if (openMenuButton != PAD_NATIVE_BUTTON_INVALID && openMenuButton == nativeButton) { + return Rml::Input::KI_F1; + } + } + + if (nativeButton == SDL_GAMEPAD_BUTTON_BACK && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port)) { return Rml::Input::KI_F1; } - u32 port = 0; - if (!find_event_port(event.which, port)) { + if (!foundEventPort) { return map_raw_gamepad_button(nativeButton); } @@ -631,7 +647,7 @@ void process_axis_direction( if (chorded) { consume_menu_chord(port, context); } - const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign); + const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign); if (key == Rml::Input::KI_UNKNOWN) { return; } @@ -719,7 +735,7 @@ void handle_event(const SDL_Event& event) noexcept { if (chorded) { consume_menu_chord(port, *context); } - const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton); + const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton); if (key != Rml::Input::KI_UNKNOWN) { bool deferred = false; if (repeat != nullptr) { diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 9a3ef78021..5bcd21c401 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -2,6 +2,8 @@ #include "aurora/lib/logging.hpp" #include "dusk/achievements.h" +#include "dusk/action_bindings.h" +#include "controller_config.hpp" #include "dusk/livesplit.h" #include "dusk/speedrun.h" #include "fmt/format.h" @@ -152,12 +154,22 @@ Rml::Element* create_menu_notification(Rml::Element* parent) { auto* elem = append(parent, "toast"); elem->SetClass("menu-notification", true); + // Get name of button for action binding if the action is bound + Rml::String padButton{}; + SDL_Gamepad* gamepad = gamepad_for_port(PAD_CHAN0); + if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0) && gamepad != nullptr) { + padButton = native_button_name(gamepad, + getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)); + } else { + padButton = back_button_name(); + } + auto* message = append(elem, "message"); auto* row = append(message, "row"); append(row, "span")->SetInnerRML(kMenuNotificationPrefix); auto* icon = append(row, "icon"); icon->SetClass("controller", true); - append(row, "span")->SetInnerRML(escape(back_button_name())); + append(row, "span")->SetInnerRML(escape(padButton)); append(row, "span")->SetInnerRML("to open menu"); return elem;