diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index f9d0ac170..1a686e455 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -219,6 +219,14 @@ target_link_dependencies(libdevilutionx_controller_buttons DevilutionX::SDL ) +add_devilutionx_object_library(libdevilutionx_padmapper + controls/padmapper.cpp +) +target_link_dependencies(libdevilutionx_padmapper PUBLIC + libdevilutionx_controller_buttons + libdevilutionx_options +) + add_devilutionx_object_library(libdevilutionx_crawl crawl.cpp ) @@ -662,6 +670,7 @@ target_link_dependencies(libdevilutionx PUBLIC libdevilutionx_mpq libdevilutionx_multiplayer libdevilutionx_options + libdevilutionx_padmapper libdevilutionx_parse_int libdevilutionx_pathfinding libdevilutionx_pkware_encrypt diff --git a/Source/controls/controller.h b/Source/controls/controller.h index bef3d210b..e8185d838 100644 --- a/Source/controls/controller.h +++ b/Source/controls/controller.h @@ -7,17 +7,6 @@ namespace devilution { -struct ControllerButtonEvent { - ControllerButtonEvent(ControllerButton button, bool up) - : button(button) - , up(up) - { - } - - ControllerButton button; - bool up; -}; - // Must be called exactly once at the start of each SDL input event. void UnlockControllerState(const SDL_Event &event); diff --git a/Source/controls/controller_buttons.h b/Source/controls/controller_buttons.h index bfe02a003..b27894a8b 100644 --- a/Source/controls/controller_buttons.h +++ b/Source/controls/controller_buttons.h @@ -55,6 +55,17 @@ struct ControllerButtonCombo { ControllerButton button; }; +struct ControllerButtonEvent { + ControllerButtonEvent(ControllerButton button, bool up) + : button(button) + , up(up) + { + } + + ControllerButton button; + bool up; +}; + inline bool IsDPadButton(ControllerButton button) { return button == ControllerButton_BUTTON_DPAD_UP diff --git a/Source/controls/controller_motion.cpp b/Source/controls/controller_motion.cpp index f26e64dca..3ef886fd5 100644 --- a/Source/controls/controller_motion.cpp +++ b/Source/controls/controller_motion.cpp @@ -9,6 +9,7 @@ #endif #include "controls/devices/joystick.h" #include "controls/game_controls.h" +#include "controls/padmapper.hpp" #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "engine/demomode.h" @@ -71,16 +72,15 @@ void ScaleJoystickAxes(float *x, float *y, float deadzone) bool IsMovementOverriddenByPadmapper(ControllerButton button) { ControllerButtonEvent releaseEvent { button, true }; - const Options &options = GetOptions(); - std::string_view actionName = options.Padmapper.ActionNameTriggeredByButtonEvent(releaseEvent); - ControllerButtonCombo buttonCombo = options.Padmapper.ButtonComboForAction(actionName); + std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(releaseEvent); + ControllerButtonCombo buttonCombo = GetOptions().Padmapper.ButtonComboForAction(actionName); return buttonCombo.modifier != ControllerButton_NONE; } bool TriggersQuickSpellAction(ControllerButton button) { ControllerButtonEvent releaseEvent { button, true }; - std::string_view actionName = GetOptions().Padmapper.ActionNameTriggeredByButtonEvent(releaseEvent); + std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(releaseEvent); std::string_view prefix { "QuickSpell" }; if (actionName.size() < prefix.size()) @@ -211,12 +211,11 @@ AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper) bool isLeftPressed = stickX <= -0.5; bool isRightPressed = stickX >= 0.5; - const Options &options = GetOptions(); if (usePadmapper) { - isUpPressed |= options.Padmapper.IsActive("MoveUp"); - isDownPressed |= options.Padmapper.IsActive("MoveDown"); - isLeftPressed |= options.Padmapper.IsActive("MoveLeft"); - isRightPressed |= options.Padmapper.IsActive("MoveRight"); + isUpPressed |= PadmapperIsActionActive("MoveUp"); + isDownPressed |= PadmapperIsActionActive("MoveDown"); + isLeftPressed |= PadmapperIsActionActive("MoveLeft"); + isRightPressed |= PadmapperIsActionActive("MoveRight"); } else if (!SimulatingMouseWithPadmapper) { isUpPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_UP); isDownPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_DOWN); @@ -255,8 +254,7 @@ void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent) if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton) return; - const Options &options = GetOptions(); - std::string_view actionName = options.Padmapper.ActionNameTriggeredByButtonEvent(ctrlEvent); + std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(ctrlEvent); bool upTriggered = actionName == "MouseUp"; bool downTriggered = actionName == "MouseDown"; bool leftTriggered = actionName == "MouseLeft"; @@ -267,10 +265,10 @@ void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent) return; } - bool upActive = (upTriggered && !ctrlEvent.up) || (!upTriggered && options.Padmapper.IsActive("MouseUp")); - bool downActive = (downTriggered && !ctrlEvent.up) || (!downTriggered && options.Padmapper.IsActive("MouseDown")); - bool leftActive = (leftTriggered && !ctrlEvent.up) || (!leftTriggered && options.Padmapper.IsActive("MouseLeft")); - bool rightActive = (rightTriggered && !ctrlEvent.up) || (!rightTriggered && options.Padmapper.IsActive("MouseRight")); + bool upActive = (upTriggered && !ctrlEvent.up) || (!upTriggered && PadmapperIsActionActive("MouseUp")); + bool downActive = (downTriggered && !ctrlEvent.up) || (!downTriggered && PadmapperIsActionActive("MouseDown")); + bool leftActive = (leftTriggered && !ctrlEvent.up) || (!leftTriggered && PadmapperIsActionActive("MouseLeft")); + bool rightActive = (rightTriggered && !ctrlEvent.up) || (!rightTriggered && PadmapperIsActionActive("MouseRight")); rightStickX = 0; rightStickY = 0; diff --git a/Source/controls/game_controls.cpp b/Source/controls/game_controls.cpp index 32d13e410..21fafcde8 100644 --- a/Source/controls/game_controls.cpp +++ b/Source/controls/game_controls.cpp @@ -7,6 +7,7 @@ #include "controls/devices/game_controller.h" #endif #include "controls/devices/joystick.h" +#include "controls/padmapper.hpp" #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "doom.h" @@ -214,6 +215,28 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game return false; } +bool CanDeferToMovementHandler(const PadmapperOptions::Action &action) +{ + if (action.boundInput.modifier != ControllerButton_NONE) + return false; + + if (SpellSelectFlag) { + const std::string_view prefix { "QuickSpell" }; + const std::string_view key { action.key }; + if (key.size() >= prefix.size()) { + const std::string_view truncated { key.data(), prefix.size() }; + if (truncated == prefix) + return false; + } + } + + return IsAnyOf(action.boundInput.button, + ControllerButton_BUTTON_DPAD_UP, + ControllerButton_BUTTON_DPAD_DOWN, + ControllerButton_BUTTON_DPAD_LEFT, + ControllerButton_BUTTON_DPAD_RIGHT); +} + void PressControllerButton(ControllerButton button) { if (IsStashOpen) { @@ -298,7 +321,10 @@ void PressControllerButton(ControllerButton button) } } - GetOptions().Padmapper.ButtonPressed(button); + const PadmapperOptions::Action *action = GetOptions().Padmapper.findAction(button, IsControllerButtonPressed); + if (action == nullptr) return; + if (IsMovementHandlerActive() && CanDeferToMovementHandler(*action)) return; + PadmapperPress(button, *action); } } // namespace @@ -337,7 +363,7 @@ bool IsSimulatedMouseClickBinding(ControllerButtonEvent ctrlEvent) return false; if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton) return false; - std::string_view actionName = GetOptions().Padmapper.ActionNameTriggeredByButtonEvent(ctrlEvent); + const std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(ctrlEvent); return IsAnyOf(actionName, "LeftMouseClick1", "LeftMouseClick2", "RightMouseClick1", "RightMouseClick2"); } @@ -355,8 +381,7 @@ bool HandleControllerButtonEvent(const SDL_Event &event, const ControllerButtonE struct ButtonReleaser { ~ButtonReleaser() { - if (ctrlEvent.up) - GetOptions().Padmapper.ButtonReleased(ctrlEvent.button, false); + if (ctrlEvent.up) PadmapperRelease(ctrlEvent.button, /*invokeAction=*/false); } ControllerButtonEvent ctrlEvent; }; @@ -377,10 +402,10 @@ bool HandleControllerButtonEvent(const SDL_Event &event, const ControllerButtonE SuppressedButton = ControllerButton_NONE; } - if (ctrlEvent.up && GetOptions().Padmapper.ActionNameTriggeredByButtonEvent(ctrlEvent) != "") { + if (ctrlEvent.up && !PadmapperActionNameTriggeredByButtonEvent(ctrlEvent).empty()) { // Button press may have brought up a menu; // don't confuse release of that button with intent to interact with the menu - GetOptions().Padmapper.ButtonReleased(ctrlEvent.button); + PadmapperRelease(ctrlEvent.button, /*invokeAction=*/true); return true; } else if (GetGameAction(event, ctrlEvent, &action)) { ProcessGameAction(action); diff --git a/Source/controls/padmapper.cpp b/Source/controls/padmapper.cpp new file mode 100644 index 000000000..3250d3185 --- /dev/null +++ b/Source/controls/padmapper.cpp @@ -0,0 +1,66 @@ +#include "controls/padmapper.hpp" + +#include + +#include "options.h" + +namespace devilution { + +namespace { +std::array::value> ButtonToReleaseAction; +} // namespace + +void PadmapperPress(ControllerButton button, const PadmapperOptions::Action &action) +{ + if (action.actionPressed) action.actionPressed(); + SuppressedButton = action.boundInput.modifier; + ButtonToReleaseAction[static_cast(button)] = &action; +} + +void PadmapperRelease(ControllerButton button, bool invokeAction) +{ + if (invokeAction) { + const PadmapperOptions::Action *action = ButtonToReleaseAction[static_cast(button)]; + if (action == nullptr) + return; // Ignore unmapped buttons. + + // Check that the action can be triggered. + if (action->actionReleased && action->isEnabled()) + action->actionReleased(); + } + ButtonToReleaseAction[static_cast(button)] = nullptr; +} + +bool PadmapperIsActionActive(std::string_view actionName) +{ + for (const PadmapperOptions::Action &action : GetOptions().Padmapper.actions) { + if (action.key != actionName) + continue; + const PadmapperOptions::Action *releaseAction = ButtonToReleaseAction[static_cast(action.boundInput.button)]; + return releaseAction != nullptr && releaseAction->key == actionName; + } + return false; +} + +void PadmapperReleaseAllActiveButtons() +{ + for (const PadmapperOptions::Action *action : ButtonToReleaseAction) { + if (action != nullptr) { + PadmapperRelease(action->boundInput.button, /*invokeAction=*/true); + } + } +} + +std::string_view PadmapperActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent) +{ + if (!ctrlEvent.up) { + const PadmapperOptions::Action *pressAction = GetOptions().Padmapper.findAction(ctrlEvent.button, IsControllerButtonPressed); + if (pressAction == nullptr) return {}; + return pressAction->key; + } + const PadmapperOptions::Action *releaseAction = ButtonToReleaseAction[static_cast(ctrlEvent.button)]; + if (releaseAction == nullptr) return {}; + return releaseAction->key; +} + +} // namespace devilution diff --git a/Source/controls/padmapper.hpp b/Source/controls/padmapper.hpp new file mode 100644 index 000000000..563321cb4 --- /dev/null +++ b/Source/controls/padmapper.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "controls/controller_buttons.h" +#include "options.h" + +namespace devilution { + +void PadmapperPress(ControllerButton button, const PadmapperOptions::Action &action); +void PadmapperRelease(ControllerButton button, bool invokeAction); +void PadmapperReleaseAllActiveButtons(); +[[nodiscard]] bool PadmapperIsActionActive(std::string_view actionName); +[[nodiscard]] std::string_view PadmapperActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent); + +} // namespace devilution diff --git a/Source/engine/events.cpp b/Source/engine/events.cpp index 7652dd668..d4492bac9 100644 --- a/Source/engine/events.cpp +++ b/Source/engine/events.cpp @@ -3,6 +3,7 @@ #include #include "controls/input.h" +#include "controls/padmapper.hpp" #include "engine/demomode.h" #include "engine/render/primitive_render.hpp" #include "interfac.h" @@ -143,7 +144,7 @@ EventHandler CurrentEventHandler; EventHandler SetEventHandler(EventHandler eventHandler) { - GetOptions().Padmapper.ReleaseAllActiveButtons(); + PadmapperReleaseAllActiveButtons(); EventHandler previousHandler = CurrentEventHandler; CurrentEventHandler = eventHandler; diff --git a/Source/options.cpp b/Source/options.cpp index ce930fca7..45aa0bfc0 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -1387,9 +1387,9 @@ std::vector PadmapperOptions::GetEntries() PadmapperOptions::Action::Action(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index) : OptionEntryBase(key, OptionEntryFlags::None, name, description) - , defaultInput(defaultInput) , actionPressed(std::move(actionPressed)) , actionReleased(std::move(actionReleased)) + , defaultInput(defaultInput) , enable(std::move(enable)) , dynamicIndex(index) { @@ -1548,66 +1548,6 @@ void PadmapperOptions::CommitActions() committed = true; } -void PadmapperOptions::ButtonPressed(ControllerButton button) -{ - const Action *action = FindAction(button); - if (action == nullptr) - return; - if (IsMovementHandlerActive() && CanDeferToMovementHandler(*action)) - return; - if (action->actionPressed) - action->actionPressed(); - SuppressedButton = action->boundInput.modifier; - buttonToReleaseAction[static_cast(button)] = action; -} - -void PadmapperOptions::ButtonReleased(ControllerButton button, bool invokeAction) -{ - if (invokeAction) { - const Action *action = buttonToReleaseAction[static_cast(button)]; - if (action == nullptr) - return; // Ignore unmapped buttons. - - // Check that the action can be triggered. - if (action->actionReleased && (!action->enable || action->enable())) - action->actionReleased(); - } - buttonToReleaseAction[static_cast(button)] = nullptr; -} - -void PadmapperOptions::ReleaseAllActiveButtons() -{ - for (auto *action : buttonToReleaseAction) { - if (action == nullptr) - continue; - ControllerButton button = action->boundInput.button; - ButtonReleased(button, true); - } -} - -bool PadmapperOptions::IsActive(std::string_view actionName) const -{ - for (const Action &action : actions) { - if (action.key != actionName) - continue; - const Action *releaseAction = buttonToReleaseAction[static_cast(action.boundInput.button)]; - return releaseAction != nullptr && releaseAction->key == actionName; - } - return false; -} - -std::string_view PadmapperOptions::ActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent) const -{ - if (!ctrlEvent.up) { - const Action *pressAction = FindAction(ctrlEvent.button); - return pressAction != nullptr ? pressAction->key : ""; - } - const Action *releaseAction = buttonToReleaseAction[static_cast(ctrlEvent.button)]; - if (releaseAction == nullptr) - return ""; - return releaseAction->key; -} - std::string_view PadmapperOptions::InputNameForAction(std::string_view actionName, bool useShortName) const { for (const Action &action : actions) { @@ -1628,7 +1568,7 @@ ControllerButtonCombo PadmapperOptions::ButtonComboForAction(std::string_view ac return ControllerButton_NONE; } -const PadmapperOptions::Action *PadmapperOptions::FindAction(ControllerButton button) const +const PadmapperOptions::Action *PadmapperOptions::findAction(ControllerButton button, tl::function_ref isModifierPressed) const { // To give preference to button combinations, // first pass ignores mappings where no modifier is bound @@ -1638,7 +1578,7 @@ const PadmapperOptions::Action *PadmapperOptions::FindAction(ControllerButton bu continue; if (button != combo.button) continue; - if (!IsControllerButtonPressed(combo.modifier)) + if (!isModifierPressed(combo.modifier)) continue; if (action.enable && !action.enable()) continue; @@ -1659,28 +1599,6 @@ const PadmapperOptions::Action *PadmapperOptions::FindAction(ControllerButton bu return nullptr; } -bool PadmapperOptions::CanDeferToMovementHandler(const Action &action) const -{ - if (action.boundInput.modifier != ControllerButton_NONE) - return false; - - if (SpellSelectFlag) { - const std::string_view prefix { "QuickSpell" }; - const std::string_view key { action.key }; - if (key.size() >= prefix.size()) { - const std::string_view truncated { key.data(), prefix.size() }; - if (truncated == prefix) - return false; - } - } - - return IsAnyOf(action.boundInput.button, - ControllerButton_BUTTON_DPAD_UP, - ControllerButton_BUTTON_DPAD_DOWN, - ControllerButton_BUTTON_DPAD_LEFT, - ControllerButton_BUTTON_DPAD_RIGHT); -} - ModOptions::ModOptions() : OptionCategoryBase("Mods", N_("Mods"), N_("Mod Settings")) { diff --git a/Source/options.h b/Source/options.h index 6f7fc300a..fa0008898 100644 --- a/Source/options.h +++ b/Source/options.h @@ -131,8 +131,10 @@ public: OptionEntryFlags flags; -protected: +public: std::string_view key; + +protected: const char *name; const char *description; void NotifyValueChanged(); @@ -763,12 +765,15 @@ struct PadmapperOptions : OptionCategoryBase { bool SetValue(ControllerButtonCombo value); - private: - ControllerButtonCombo defaultInput; + [[nodiscard]] bool isEnabled() const { return !enable || enable(); } + std::function actionPressed; std::function actionReleased; + ControllerButtonCombo boundInput; + + private: + ControllerButtonCombo defaultInput; std::function enable; - ControllerButtonCombo boundInput {}; mutable GamepadLayout boundInputDescriptionType = GamepadLayout::Generic; mutable std::string boundInputDescription; mutable std::string boundInputShortDescription; @@ -792,23 +797,17 @@ struct PadmapperOptions : OptionCategoryBase { std::function enable = nullptr, unsigned index = 0); void CommitActions(); - void ButtonPressed(ControllerButton button); - void ButtonReleased(ControllerButton button, bool invokeAction = true); - void ReleaseAllActiveButtons(); - bool IsActive(std::string_view actionName) const; - std::string_view ActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent) const; std::string_view InputNameForAction(std::string_view actionName, bool useShortName = false) const; ControllerButtonCombo ButtonComboForAction(std::string_view actionName) const; -private: + [[nodiscard]] const Action *findAction(ControllerButton button, tl::function_ref isModifierPressed) const; + std::forward_list actions; - std::array::value> buttonToReleaseAction; + +private: std::array::value> buttonToButtonName; ankerl::unordered_dense::segmented_map buttonNameToButton; bool committed = false; - - const Action *FindAction(ControllerButton button) const; - bool CanDeferToMovementHandler(const Action &action) const; }; struct ModOptions : OptionCategoryBase {