From e67e4934cc6a3ccbd49cedbda39ea14a5a541a2f Mon Sep 17 00:00:00 2001 From: staphen Date: Wed, 12 Oct 2022 01:25:41 -0400 Subject: [PATCH] Add padmapper for gamepad customization --- Source/controls/controller.cpp | 8 + Source/controls/controller.h | 1 + Source/controls/controller_buttons.h | 24 ++ Source/controls/controller_motion.cpp | 119 +++--- Source/controls/controller_motion.h | 4 +- Source/controls/game_controls.cpp | 433 ++++++-------------- Source/controls/game_controls.h | 17 +- Source/controls/menu_controls.cpp | 10 +- Source/controls/modifier_hints.cpp | 15 +- Source/controls/plrctrls.cpp | 121 +++++- Source/controls/plrctrls.h | 5 +- Source/cursor.cpp | 2 +- Source/diablo.cpp | 551 ++++++++++++++++++++++++++ Source/gmenu.cpp | 2 +- Source/items.cpp | 4 +- Source/miniwin/misc_msg.cpp | 135 +------ Source/options.cpp | 254 +++++++++++- Source/options.h | 69 +++- Source/track.cpp | 2 +- Source/utils/display.cpp | 3 + 20 files changed, 1228 insertions(+), 551 deletions(-) diff --git a/Source/controls/controller.cpp b/Source/controls/controller.cpp index f71b04c85..d24e9631d 100644 --- a/Source/controls/controller.cpp +++ b/Source/controls/controller.cpp @@ -73,6 +73,14 @@ bool IsControllerButtonPressed(ControllerButton button) return Joystick::IsPressedOnAnyJoystick(button); } +bool IsControllerButtonComboPressed(ControllerButtonCombo combo) +{ + bool comboPressed = IsControllerButtonPressed(combo.button); + if (combo.modifier != ControllerButton_NONE) + comboPressed = comboPressed && IsControllerButtonPressed(combo.modifier); + return comboPressed; +} + bool HandleControllerAddedOrRemovedEvent(const SDL_Event &event) { #ifndef USE_SDL1 diff --git a/Source/controls/controller.h b/Source/controls/controller.h index 8186a16fe..5118078ce 100644 --- a/Source/controls/controller.h +++ b/Source/controls/controller.h @@ -17,6 +17,7 @@ void UnlockControllerState(const SDL_Event &event); ControllerButtonEvent ToControllerButtonEvent(const SDL_Event &event); bool IsControllerButtonPressed(ControllerButton button); +bool IsControllerButtonComboPressed(ControllerButtonCombo combo); bool HandleControllerAddedOrRemovedEvent(const SDL_Event &event); diff --git a/Source/controls/controller_buttons.h b/Source/controls/controller_buttons.h index 0309bd8ff..75dfbc7e3 100644 --- a/Source/controls/controller_buttons.h +++ b/Source/controls/controller_buttons.h @@ -2,6 +2,7 @@ // Unifies joystick, gamepad, and keyboard controller APIs. #include +#include #include "utils/stdcompat/string_view.hpp" @@ -30,6 +31,29 @@ enum ControllerButton : uint8_t { ControllerButton_BUTTON_DPAD_RIGHT }; +struct ControllerButtonCombo { + constexpr ControllerButtonCombo() + : modifier(ControllerButton_NONE) + , button(ControllerButton_NONE) + { + } + + constexpr ControllerButtonCombo(ControllerButton button) + : modifier(ControllerButton_NONE) + , button(button) + { + } + + constexpr ControllerButtonCombo(ControllerButton modifier, ControllerButton button) + : modifier(modifier) + , button(button) + { + } + + ControllerButton modifier; + ControllerButton button; +}; + 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 4e769e9d3..f84afee10 100644 --- a/Source/controls/controller_motion.cpp +++ b/Source/controls/controller_motion.cpp @@ -16,7 +16,7 @@ namespace devilution { -bool SimulatingMouseWithSelectAndDPad; +bool SimulatingMouseWithPadmapper; namespace { @@ -66,11 +66,11 @@ void ScaleJoystickAxes(float *x, float *y, float deadzone) } } -void SetSimulatingMouseWithDpad(bool value) +void SetSimulatingMouseWithPadmapper(bool value) { - if (SimulatingMouseWithSelectAndDPad == value) + if (SimulatingMouseWithPadmapper == value) return; - SimulatingMouseWithSelectAndDPad = value; + SimulatingMouseWithPadmapper = value; if (value) { LogVerbose("Control: begin simulating mouse with D-Pad"); } else { @@ -81,65 +81,26 @@ void SetSimulatingMouseWithDpad(bool value) // SELECT + D-Pad to simulate right stick movement. bool SimulateRightStickWithDpad(ControllerButtonEvent ctrlEvent) { - if (sgOptions.Controller.bDpadHotkeys) - return false; - if (ctrlEvent.button == ControllerButton_NONE || ctrlEvent.button == ControllerButton_IGNORE) - return false; - if (ctrlEvent.button == ControllerButton_BUTTON_BACK) { - if (SimulatingMouseWithSelectAndDPad) { - if (ctrlEvent.up) { - rightStickX = rightStickY = 0; - } - return true; - } - return false; - } - - if (!IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) { - SetSimulatingMouseWithDpad(false); - return false; - } - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_DPAD_LEFT: - if (ctrlEvent.up) { - rightStickX = 0; - } else { - rightStickX = -1.F; - SetSimulatingMouseWithDpad(true); - } - break; - case ControllerButton_BUTTON_DPAD_RIGHT: - if (ctrlEvent.up) { - rightStickX = 0; - } else { - rightStickX = 1.F; - SetSimulatingMouseWithDpad(true); - } - break; - case ControllerButton_BUTTON_DPAD_UP: - if (ctrlEvent.up) { - rightStickY = 0; - } else { - rightStickY = 1.F; - SetSimulatingMouseWithDpad(true); - } - break; - case ControllerButton_BUTTON_DPAD_DOWN: - if (ctrlEvent.up) { - rightStickY = 0; - } else { - rightStickY = -1.F; - SetSimulatingMouseWithDpad(true); - } - break; - default: - if (!IsSimulatedMouseClickBinding(ctrlEvent)) { - SetSimulatingMouseWithDpad(false); - } - return false; - } - - return true; + ControllerButtonCombo upCombo = sgOptions.Padmapper.ButtonComboForAction("MouseUp"); + ControllerButtonCombo downCombo = sgOptions.Padmapper.ButtonComboForAction("MouseDown"); + ControllerButtonCombo leftCombo = sgOptions.Padmapper.ButtonComboForAction("MouseLeft"); + ControllerButtonCombo rightCombo = sgOptions.Padmapper.ButtonComboForAction("MouseRight"); + + int simulatedRightStickX = 0; + int simulatedRightStickY = 0; + if (IsControllerButtonComboPressed(upCombo)) + simulatedRightStickY += 1; + if (IsControllerButtonComboPressed(downCombo)) + simulatedRightStickY -= 1; + if (IsControllerButtonComboPressed(leftCombo)) + simulatedRightStickX -= 1; + if (IsControllerButtonComboPressed(rightCombo)) + simulatedRightStickX += 1; + SetSimulatingMouseWithPadmapper(simulatedRightStickX != 0 || simulatedRightStickY != 0); + + rightStickX += simulatedRightStickX; + rightStickY += simulatedRightStickY; + return SimulatingMouseWithPadmapper && IsAnyOf(ctrlEvent.button, upCombo.button, downCombo.button, leftCombo.button, rightCombo.button); } } // namespace @@ -179,14 +140,14 @@ bool ProcessControllerMotion(const SDL_Event &event, ControllerButtonEvent ctrlE GameController *const controller = GameController::Get(event); if (controller != nullptr && devilution::GameController::ProcessAxisMotion(event)) { ScaleJoysticks(); - SetSimulatingMouseWithDpad(false); + SetSimulatingMouseWithPadmapper(false); return true; } #endif Joystick *const joystick = Joystick::Get(event); if (joystick != nullptr && devilution::Joystick::ProcessAxisMotion(event)) { ScaleJoysticks(); - SetSimulatingMouseWithDpad(false); + SetSimulatingMouseWithPadmapper(false); return true; } #if HAS_KBCTRL == 1 @@ -198,19 +159,33 @@ bool ProcessControllerMotion(const SDL_Event &event, ControllerButtonEvent ctrlE return SimulateRightStickWithDpad(ctrlEvent); } -AxisDirection GetLeftStickOrDpadDirection(bool allowDpad) +AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper) { const float stickX = leftStickX; const float stickY = leftStickY; AxisDirection result { AxisDirectionX_NONE, AxisDirectionY_NONE }; - allowDpad = allowDpad && !IsControllerButtonPressed(ControllerButton_BUTTON_START); - - bool isUpPressed = stickY >= 0.5 || (allowDpad && IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_UP)); - bool isDownPressed = stickY <= -0.5 || (allowDpad && IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_DOWN)); - bool isLeftPressed = stickX <= -0.5 || (allowDpad && IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_LEFT)); - bool isRightPressed = stickX >= 0.5 || (allowDpad && IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_RIGHT)); + bool isUpPressed = stickY >= 0.5; + bool isDownPressed = stickY <= -0.5; + bool isLeftPressed = stickX <= -0.5; + bool isRightPressed = stickX >= 0.5; + + if (usePadmapper) { + ControllerButtonCombo upCombo = sgOptions.Padmapper.ButtonComboForAction("MoveUp"); + ControllerButtonCombo downCombo = sgOptions.Padmapper.ButtonComboForAction("MoveDown"); + ControllerButtonCombo leftCombo = sgOptions.Padmapper.ButtonComboForAction("MoveLeft"); + ControllerButtonCombo rightCombo = sgOptions.Padmapper.ButtonComboForAction("MoveRight"); + isUpPressed |= IsControllerButtonComboPressed(upCombo); + isDownPressed |= IsControllerButtonComboPressed(downCombo); + isLeftPressed |= IsControllerButtonComboPressed(leftCombo); + isRightPressed |= IsControllerButtonComboPressed(rightCombo); + } else { + isUpPressed |= IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_UP); + isDownPressed |= IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_DOWN); + isLeftPressed |= IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_LEFT); + isRightPressed |= IsControllerButtonPressed(ControllerButton_BUTTON_DPAD_RIGHT); + } #ifndef USE_SDL1 if (ControlMode == ControlTypes::VirtualGamepad) { diff --git a/Source/controls/controller_motion.h b/Source/controls/controller_motion.h index b0c8c4952..3e38c8e62 100644 --- a/Source/controls/controller_motion.h +++ b/Source/controls/controller_motion.h @@ -10,7 +10,7 @@ namespace devilution { // Whether we're currently simulating the mouse with SELECT + D-Pad. -extern bool SimulatingMouseWithSelectAndDPad; +extern bool SimulatingMouseWithPadmapper; // Raw axis values. extern float leftStickXUnscaled, leftStickYUnscaled, rightStickXUnscaled, rightStickYUnscaled; @@ -25,6 +25,6 @@ extern bool leftStickNeedsScaling, rightStickNeedsScaling; bool ProcessControllerMotion(const SDL_Event &event, ControllerButtonEvent ctrlEvent); // Returns direction of the left thumb stick or DPad (if allow_dpad = true). -AxisDirection GetLeftStickOrDpadDirection(bool allowDpad = true); +AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper); } // namespace devilution diff --git a/Source/controls/game_controls.cpp b/Source/controls/game_controls.cpp index dd6c0c0b0..db91eba64 100644 --- a/Source/controls/game_controls.cpp +++ b/Source/controls/game_controls.cpp @@ -2,47 +2,57 @@ #include -#include "controls/controller.h" #include "controls/controller_motion.h" #include "miniwin/misc_msg.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" #endif #include "controls/devices/joystick.h" -#include "controls/menu_controls.h" -#include "controls/modifier_hints.h" #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "doom.h" #include "gmenu.h" #include "options.h" -#include "qol/stash.h" #include "stores.h" namespace devilution { -bool start_modifier_active = false; -bool select_modifier_active = false; -const ControllerButton ControllerButtonPrimary = ControllerButton_BUTTON_B; -const ControllerButton ControllerButtonSecondary = ControllerButton_BUTTON_Y; -const ControllerButton ControllerButtonTertiary = ControllerButton_BUTTON_X; +bool PadMenuNavigatorActive = false; +bool PadHotspellMenuActive = false; +ControllerButton SuppressedButton = ControllerButton_NONE; namespace { -SDL_Keycode TranslateControllerButtonToKey(ControllerButton controllerButton) +SDL_Keycode TranslateControllerButtonToGameMenuKey(ControllerButton controllerButton) { - switch (controllerButton) { - case ControllerButton_BUTTON_A: // Bottom button - return QuestLogIsOpen ? SDLK_SPACE : SDLK_ESCAPE; - case ControllerButton_BUTTON_B: // Right button - return (sgpCurrentMenu != nullptr || stextflag != STORE_NONE || QuestLogIsOpen) ? SDLK_RETURN : SDLK_SPACE; - case ControllerButton_BUTTON_Y: // Top button + switch (TranslateTo(GamepadType, controllerButton)) { + case ControllerButton_BUTTON_A: + case ControllerButton_BUTTON_BACK: + case ControllerButton_BUTTON_START: + return SDLK_ESCAPE; + case ControllerButton_BUTTON_B: + case ControllerButton_BUTTON_Y: return SDLK_RETURN; case ControllerButton_BUTTON_LEFTSTICK: return SDLK_TAB; // Map + default: + return SDLK_UNKNOWN; + } +} + +SDL_Keycode TranslateControllerButtonToMenuKey(ControllerButton controllerButton) +{ + switch (TranslateTo(GamepadType, controllerButton)) { + case ControllerButton_BUTTON_A: case ControllerButton_BUTTON_BACK: case ControllerButton_BUTTON_START: return SDLK_ESCAPE; + case ControllerButton_BUTTON_B: + return SDLK_SPACE; + case ControllerButton_BUTTON_Y: + return SDLK_RETURN; + case ControllerButton_BUTTON_LEFTSTICK: + return SDLK_TAB; // Map case ControllerButton_BUTTON_DPAD_LEFT: return SDLK_LEFT; case ControllerButton_BUTTON_DPAD_RIGHT: @@ -56,50 +66,72 @@ SDL_Keycode TranslateControllerButtonToKey(ControllerButton controllerButton) } } -bool HandleStartAndSelect(const ControllerButtonEvent &ctrlEvent, GameAction *action) +SDL_Keycode TranslateControllerButtonToQuestLogKey(ControllerButton controllerButton) { - const bool inGameMenu = InGameMenu(); - - const bool startIsDown = IsControllerButtonPressed(ControllerButton_BUTTON_START); - const bool selectIsDown = IsControllerButtonPressed(ControllerButton_BUTTON_BACK); - start_modifier_active = !inGameMenu && startIsDown; - select_modifier_active = !inGameMenu && selectIsDown && !start_modifier_active; - - // Tracks whether we've received both START and SELECT down events. - // - // Using `IsControllerButtonPressed()` for this would be incorrect. - // If both buttons are pressed simultaneously, SDL sends 2 events for which both buttons are in the pressed state. - // This allows us to avoid triggering START+SELECT action twice in this case. - static bool startDownReceived = false; - static bool selectDownReceived = false; - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_BACK: - selectDownReceived = !ctrlEvent.up; - break; - case ControllerButton_BUTTON_START: - startDownReceived = !ctrlEvent.up; - break; + switch (TranslateTo(GamepadType, controllerButton)) { + case ControllerButton_BUTTON_A: + return SDLK_SPACE; + case ControllerButton_BUTTON_B: + case ControllerButton_BUTTON_Y: + return SDLK_RETURN; + case ControllerButton_BUTTON_LEFTSTICK: + return SDLK_TAB; // Map default: - return false; + return SDLK_UNKNOWN; } +} - if (startDownReceived && selectDownReceived) { - *action = GameActionSendKey { SDLK_ESCAPE, ctrlEvent.up }; - return true; +SDL_Keycode TranslateControllerButtonToSpellbookKey(ControllerButton controllerButton) +{ + switch (TranslateTo(GamepadType, controllerButton)) { + case ControllerButton_BUTTON_B: + return SDLK_SPACE; + case ControllerButton_BUTTON_Y: + return SDLK_RETURN; + case ControllerButton_BUTTON_LEFTSTICK: + return SDLK_TAB; // Map + case ControllerButton_BUTTON_DPAD_LEFT: + return SDLK_LEFT; + case ControllerButton_BUTTON_DPAD_RIGHT: + return SDLK_RIGHT; + case ControllerButton_BUTTON_DPAD_UP: + return SDLK_UP; + case ControllerButton_BUTTON_DPAD_DOWN: + return SDLK_DOWN; + default: + return SDLK_UNKNOWN; } +} - if (inGameMenu && (startIsDown || selectIsDown) && !ctrlEvent.up) { - // If both are down, do nothing because `both_received` will trigger soon. - if (startIsDown && selectIsDown) - return true; - *action = GameActionSendKey { SDLK_ESCAPE, ctrlEvent.up }; - return true; - } +} // namespace - return false; +ControllerButton TranslateTo(GamepadLayout layout, ControllerButton button) +{ + if (layout != GamepadLayout::Nintendo) + return button; + + switch (button) { + case ControllerButton_BUTTON_A: + return ControllerButton_BUTTON_B; + case ControllerButton_BUTTON_B: + return ControllerButton_BUTTON_A; + case ControllerButton_BUTTON_X: + return ControllerButton_BUTTON_Y; + case ControllerButton_BUTTON_Y: + return ControllerButton_BUTTON_X; + default: + return button; + } } -} // namespace +bool SkipsMovie(ControllerButtonEvent ctrlEvent) +{ + return IsAnyOf(ctrlEvent.button, + ControllerButton_BUTTON_A, + ControllerButton_BUTTON_B, + ControllerButton_BUTTON_START, + ControllerButton_BUTTON_BACK); +} bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, GameAction *action) { @@ -127,8 +159,8 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game if (VirtualGamepadState.primaryActionButton.isHeld && VirtualGamepadState.primaryActionButton.didStateChange) { if (!inGameMenu && !QuestLogIsOpen && !sbookflag) { *action = GameAction(GameActionType_PRIMARY_ACTION); - if (ControllerButtonHeld == ControllerButton_NONE) { - ControllerButtonHeld = ControllerButtonPrimary; + if (ControllerActionHeld == GameActionType_NONE) { + ControllerActionHeld = GameActionType_PRIMARY_ACTION; } } else if (sgpCurrentMenu != nullptr || stextflag != STORE_NONE || QuestLogIsOpen) { *action = GameActionSendKey { SDLK_RETURN, false }; @@ -140,16 +172,16 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game if (VirtualGamepadState.secondaryActionButton.isHeld && VirtualGamepadState.secondaryActionButton.didStateChange) { if (!inGameMenu && !QuestLogIsOpen && !sbookflag) { *action = GameAction(GameActionType_SECONDARY_ACTION); - if (ControllerButtonHeld == ControllerButton_NONE) - ControllerButtonHeld = ControllerButtonSecondary; + if (ControllerActionHeld == GameActionType_NONE) + ControllerActionHeld = GameActionType_SECONDARY_ACTION; } return true; } if (VirtualGamepadState.spellActionButton.isHeld && VirtualGamepadState.spellActionButton.didStateChange) { if (!inGameMenu && !QuestLogIsOpen && !sbookflag) { *action = GameAction(GameActionType_CAST_SPELL); - if (ControllerButtonHeld == ControllerButton_NONE) - ControllerButtonHeld = ControllerButtonTertiary; + if (ControllerActionHeld == GameActionType_NONE) + ControllerActionHeld = GameActionType_CAST_SPELL; } return true; } @@ -177,288 +209,53 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game return true; } } else if (event.type == SDL_FINGERUP) { - if ((!VirtualGamepadState.primaryActionButton.isHeld && ControllerButtonHeld == ControllerButtonPrimary) - || (!VirtualGamepadState.secondaryActionButton.isHeld && ControllerButtonHeld == ControllerButtonSecondary) - || (!VirtualGamepadState.spellActionButton.isHeld && ControllerButtonHeld == ControllerButtonTertiary)) { - ControllerButtonHeld = ControllerButton_NONE; + if ((!VirtualGamepadState.primaryActionButton.isHeld && ControllerActionHeld == GameActionType_PRIMARY_ACTION) + || (!VirtualGamepadState.secondaryActionButton.isHeld && ControllerActionHeld == GameActionType_SECONDARY_ACTION) + || (!VirtualGamepadState.spellActionButton.isHeld && ControllerActionHeld == GameActionType_CAST_SPELL)) { + ControllerActionHeld = GameActionType_NONE; LastMouseButtonAction = MouseActionType::None; } } } #endif - if (IsStashOpen && ctrlEvent.button == ControllerButton_BUTTON_BACK) { - StartGoldWithdraw(); - return false; - } - - if (HandleStartAndSelect(ctrlEvent, action)) - return true; - - // Stick clicks simulate the mouse both in menus and in-game. - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_LEFTSTICK: - if (select_modifier_active) { - if (!IsAutomapActive()) - *action = GameActionSendKey { SDL_BUTTON_LEFT | KeymapperMouseButtonMask, ctrlEvent.up }; - return true; - } - break; - case ControllerButton_BUTTON_RIGHTSTICK: - if (!IsAutomapActive()) { - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { SDL_BUTTON_RIGHT | KeymapperMouseButtonMask, ctrlEvent.up }; - else - *action = GameActionSendKey { SDL_BUTTON_LEFT | KeymapperMouseButtonMask, ctrlEvent.up }; - } - return true; - default: - break; - } - - if (!inGameMenu) { - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_LEFTSHOULDER: - if ((select_modifier_active && !sgOptions.Controller.bSwapShoulderButtonMode) || (sgOptions.Controller.bSwapShoulderButtonMode && !select_modifier_active)) { - if (!IsAutomapActive()) - *action = GameActionSendKey { SDL_BUTTON_LEFT | KeymapperMouseButtonMask, ctrlEvent.up }; - return true; - } - break; - case ControllerButton_BUTTON_RIGHTSHOULDER: - if ((select_modifier_active && !sgOptions.Controller.bSwapShoulderButtonMode) || (sgOptions.Controller.bSwapShoulderButtonMode && !select_modifier_active)) { - if (!IsAutomapActive()) - *action = GameActionSendKey { SDL_BUTTON_RIGHT | KeymapperMouseButtonMask, ctrlEvent.up }; - return true; - } - break; - case ControllerButton_AXIS_TRIGGERLEFT: // ZL (aka L2) - if (!ctrlEvent.up) { - if (select_modifier_active) - *action = GameAction(GameActionType_TOGGLE_QUEST_LOG); - else - *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - } - return true; - case ControllerButton_AXIS_TRIGGERRIGHT: // ZR (aka R2) - if (!ctrlEvent.up) { - if (select_modifier_active) - *action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); - else - *action = GameAction(GameActionType_TOGGLE_INVENTORY); - } - return true; - case ControllerButton_IGNORE: - case ControllerButton_BUTTON_START: - case ControllerButton_BUTTON_BACK: - return true; - default: - break; - } - if (sgOptions.Controller.bDpadHotkeys) { - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_DPAD_UP: - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell2"), ctrlEvent.up }; - else - *action = GameActionSendKey { SDLK_ESCAPE, ctrlEvent.up }; - return true; - case ControllerButton_BUTTON_DPAD_RIGHT: - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell4"), ctrlEvent.up }; - else if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_INVENTORY); - return true; - case ControllerButton_BUTTON_DPAD_DOWN: - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell3"), ctrlEvent.up }; - else - *action = GameActionSendKey { SDLK_TAB, ctrlEvent.up }; - return true; - case ControllerButton_BUTTON_DPAD_LEFT: - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell1"), ctrlEvent.up }; - else if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - return true; - default: - break; - } - } - if (start_modifier_active) { - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_DPAD_UP: - *action = GameActionSendKey { SDLK_ESCAPE, ctrlEvent.up }; - return true; - case ControllerButton_BUTTON_DPAD_RIGHT: - if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_INVENTORY); - return true; - case ControllerButton_BUTTON_DPAD_DOWN: - *action = GameActionSendKey { SDLK_TAB, ctrlEvent.up }; - return true; - case ControllerButton_BUTTON_DPAD_LEFT: - if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - return true; - case ControllerButton_BUTTON_Y: // Top button -#ifdef __3DS__ - if (!ctrlEvent.up) { - sgOptions.Graphics.zoom.SetValue(!*sgOptions.Graphics.zoom); - CalcViewportGeometry(); - } -#else - /* Not mapped. Reserved for future use. */ -#endif - return true; - case ControllerButton_BUTTON_B: // Right button - // Not mapped. TODO: map to attack in place. - return true; - case ControllerButton_BUTTON_A: // Bottom button - if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); - return true; - case ControllerButton_BUTTON_X: // Left button - if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_QUEST_LOG); - return true; - case ControllerButton_BUTTON_LEFTSHOULDER: - if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - return true; - case ControllerButton_BUTTON_RIGHTSHOULDER: - if (!ctrlEvent.up) - *action = GameAction(GameActionType_TOGGLE_INVENTORY); - return true; - default: - return true; - } - } - // Bottom button: Closes menus or opens quick spell book if nothing is open. - if (ctrlEvent.button == ControllerButton_BUTTON_A) { // Bottom button - if (ctrlEvent.up) - return true; - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell3"), ctrlEvent.up }; - else if (DoomFlag) - *action = GameActionSendKey { SDLK_ESCAPE, ctrlEvent.up }; - else if (invflag) - *action = GameAction(GameActionType_TOGGLE_INVENTORY); - else if (sbookflag) - *action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); - else if (QuestLogIsOpen) - *action = GameAction(GameActionType_TOGGLE_QUEST_LOG); - else if (chrflag) - *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - else - *action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); - return true; - } - - if (!QuestLogIsOpen && !sbookflag) { - switch (ctrlEvent.button) { - case ControllerButton_IGNORE: - return true; - case ControllerButton_BUTTON_B: // Right button - if (!ctrlEvent.up) { - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell4"), ctrlEvent.up }; - else - *action = GameAction(GameActionType_PRIMARY_ACTION); - } - return true; - case ControllerButton_BUTTON_Y: // Top button - if (!ctrlEvent.up) { - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell2"), ctrlEvent.up }; - else - *action = GameAction(GameActionType_SECONDARY_ACTION); - } - return true; - case ControllerButton_BUTTON_X: // Left button - if (!ctrlEvent.up) { - if (IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) - *action = GameActionSendKey { sgOptions.Keymapper.KeyForAction("QuickSpell1"), ctrlEvent.up }; - else - *action = GameAction(GameActionType_CAST_SPELL); - } - return true; - case ControllerButton_BUTTON_LEFTSHOULDER: - if (stextflag == STORE_NONE && !ctrlEvent.up) - *action = GameAction(GameActionType_USE_HEALTH_POTION); - return true; - case ControllerButton_BUTTON_RIGHTSHOULDER: - if (stextflag == STORE_NONE && !ctrlEvent.up) - *action = GameAction(GameActionType_USE_MANA_POTION); - return true; - case ControllerButton_BUTTON_DPAD_UP: - case ControllerButton_BUTTON_DPAD_DOWN: - case ControllerButton_BUTTON_DPAD_LEFT: - case ControllerButton_BUTTON_DPAD_RIGHT: - // The rest of D-Pad actions are handled in charMovement() on every game_logic() call. - return true; - default: - break; - } - } + if (PadMenuNavigatorActive || PadHotspellMenuActive) + return false; - if (ctrlEvent.button == ControllerButton_BUTTON_BACK) { - return true; // Ignore mod button - } - } + SDL_Keycode translation = SDLK_UNKNOWN; - // DPad navigation is handled separately for these. - if (gmenu_is_active() || QuestLogIsOpen || stextflag != STORE_NONE) { - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_DPAD_UP: - case ControllerButton_BUTTON_DPAD_DOWN: - case ControllerButton_BUTTON_DPAD_LEFT: - case ControllerButton_BUTTON_DPAD_RIGHT: - return true; - default: - break; - } - } + if (gmenu_is_active() || stextflag != STORE_NONE) + translation = TranslateControllerButtonToGameMenuKey(ctrlEvent.button); + else if (inGameMenu) + translation = TranslateControllerButtonToMenuKey(ctrlEvent.button); + else if (QuestLogIsOpen) + translation = TranslateControllerButtonToQuestLogKey(ctrlEvent.button); + else if (sbookflag) + translation = TranslateControllerButtonToSpellbookKey(ctrlEvent.button); - // By default, map to a keyboard key. - if (ctrlEvent.button != ControllerButton_NONE) { - *action = GameActionSendKey { static_cast(TranslateControllerButtonToKey(ctrlEvent.button)), - ctrlEvent.up }; + if (translation != SDLK_UNKNOWN) { + *action = GameActionSendKey { static_cast(translation), ctrlEvent.up }; return true; } -#ifndef USE_SDL1 - // Ignore unhandled joystick events where a GameController is open for this joystick. - // This is because SDL sends both game controller and joystick events in this case. - const Joystick *const joystick = Joystick::Get(event); - if (joystick != nullptr && GameController::Get(joystick->instance_id()) != nullptr) { - return true; - } - if (event.type == SDL_CONTROLLERAXISMOTION) { - return true; // Ignore releasing the trigger buttons - } -#endif - return false; } bool IsSimulatedMouseClickBinding(ControllerButtonEvent ctrlEvent) { - switch (ctrlEvent.button) { - case ControllerButton_BUTTON_LEFTSTICK: - case ControllerButton_BUTTON_LEFTSHOULDER: - case ControllerButton_BUTTON_RIGHTSHOULDER: - return select_modifier_active; - case ControllerButton_BUTTON_RIGHTSTICK: - return true; - default: - return false; - } + ControllerButtonCombo leftMouseClickBinding1 = sgOptions.Padmapper.ButtonComboForAction("LeftMouseClick1"); + ControllerButtonCombo leftMouseClickBinding2 = sgOptions.Padmapper.ButtonComboForAction("LeftMouseClick2"); + ControllerButtonCombo rightMouseClickBinding1 = sgOptions.Padmapper.ButtonComboForAction("RightMouseClick1"); + ControllerButtonCombo rightMouseClickBinding2 = sgOptions.Padmapper.ButtonComboForAction("RightMouseClick2"); + return (ctrlEvent.button == leftMouseClickBinding1.button && IsControllerButtonComboPressed(leftMouseClickBinding1)) + || (ctrlEvent.button == leftMouseClickBinding2.button && IsControllerButtonComboPressed(leftMouseClickBinding2)) + || (ctrlEvent.button == rightMouseClickBinding1.button && IsControllerButtonComboPressed(rightMouseClickBinding1)) + || (ctrlEvent.button == rightMouseClickBinding2.button && IsControllerButtonComboPressed(rightMouseClickBinding2)); } AxisDirection GetMoveDirection() { - return GetLeftStickOrDpadDirection(/*allowDpad=*/!sgOptions.Controller.bDpadHotkeys); + return GetLeftStickOrDpadDirection(true); } } // namespace devilution diff --git a/Source/controls/game_controls.h b/Source/controls/game_controls.h index 716ccca34..5c4c38911 100644 --- a/Source/controls/game_controls.h +++ b/Source/controls/game_controls.h @@ -59,16 +59,23 @@ struct GameAction { }; }; +ControllerButton TranslateTo(GamepadLayout layout, ControllerButton button); + +bool SkipsMovie(ControllerButtonEvent ctrlEvent); bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, GameAction *action); bool IsSimulatedMouseClickBinding(ControllerButtonEvent ctrlEvent); AxisDirection GetMoveDirection(); -extern bool start_modifier_active; -extern bool select_modifier_active; -extern const ControllerButton ControllerButtonPrimary; -extern const ControllerButton ControllerButtonSecondary; -extern const ControllerButton ControllerButtonTertiary; +extern bool PadMenuNavigatorActive; +extern bool PadHotspellMenuActive; + +// Tracks the button most recently used as a modifier for another button. +// +// If two buttons are pressed simultaneously, SDL sends two events for which both buttons are in the pressed state. +// The event processor may interpret the second event's button as a modifier for the action taken when processing the first event. +// The code for the modifier will be stored here, and the event processor can check this value when processing the second event to suppress it. +extern ControllerButton SuppressedButton; } // namespace devilution diff --git a/Source/controls/menu_controls.cpp b/Source/controls/menu_controls.cpp index 3e5d87a4c..152c7890a 100644 --- a/Source/controls/menu_controls.cpp +++ b/Source/controls/menu_controls.cpp @@ -13,7 +13,7 @@ namespace devilution { MenuAction GetMenuHeldUpDownAction() { static AxisDirectionRepeater repeater; - const AxisDirection dir = repeater.Get(GetLeftStickOrDpadDirection()); + const AxisDirection dir = repeater.Get(GetLeftStickOrDpadDirection(false)); switch (dir.y) { case AxisDirectionY_UP: return MenuAction_UP; @@ -35,16 +35,16 @@ MenuAction GetMenuAction(const SDL_Event &event) } if (!ctrlEvent.up) { - switch (ctrlEvent.button) { + switch (TranslateTo(GamepadType, ctrlEvent.button)) { case ControllerButton_IGNORE: return MenuAction_NONE; - case ControllerButton_BUTTON_B: // Right button + case ControllerButton_BUTTON_A: case ControllerButton_BUTTON_START: return MenuAction_SELECT; case ControllerButton_BUTTON_BACK: - case ControllerButton_BUTTON_A: // Bottom button + case ControllerButton_BUTTON_B: return MenuAction_BACK; - case ControllerButton_BUTTON_X: // Left button + case ControllerButton_BUTTON_X: return MenuAction_DELETE; case ControllerButton_BUTTON_DPAD_UP: case ControllerButton_BUTTON_DPAD_DOWN: diff --git a/Source/controls/modifier_hints.cpp b/Source/controls/modifier_hints.cpp index 7f6d4ede1..7ee509dda 100644 --- a/Source/controls/modifier_hints.cpp +++ b/Source/controls/modifier_hints.cpp @@ -145,9 +145,9 @@ void DrawSpellsCircleMenuHint(const Surface &out, const Point &origin) } } -void DrawStartModifierMenu(const Surface &out) +void DrawGamepadMenuNavigator(const Surface &out) { - if (!start_modifier_active) + if (!PadMenuNavigatorActive || SimulatingMouseWithPadmapper) return; static const CircleMenuHint DPad(/*top=*/HintIcon::IconMenu, /*right=*/HintIcon::IconInv, /*bottom=*/HintIcon::IconMap, /*left=*/HintIcon::IconChar); static const CircleMenuHint Buttons(/*top=*/HintIcon::IconNull, /*right=*/HintIcon::IconNull, /*bottom=*/HintIcon::IconSpells, /*left=*/HintIcon::IconQuests); @@ -156,15 +156,12 @@ void DrawStartModifierMenu(const Surface &out) DrawCircleMenuHint(out, Buttons, { mainPanel.position.x + mainPanel.size.width - HintBoxSize * 3 - CircleMarginX - HintBoxMargin * 2, mainPanel.position.y - CircleTop }); } -void DrawSelectModifierMenu(const Surface &out) +void DrawGamepadHotspellMenu(const Surface &out) { - if (!select_modifier_active || SimulatingMouseWithSelectAndDPad) + if (!PadHotspellMenuActive || SimulatingMouseWithPadmapper) return; const Rectangle &mainPanel = GetMainPanel(); - if (sgOptions.Controller.bDpadHotkeys) { - DrawSpellsCircleMenuHint(out, { mainPanel.position.x + CircleMarginX, mainPanel.position.y - CircleTop }); - } DrawSpellsCircleMenuHint(out, { mainPanel.position.x + mainPanel.size.width - HintBoxSize * 3 - CircleMarginX - HintBoxMargin * 2, mainPanel.position.y - CircleTop }); } @@ -186,8 +183,8 @@ void FreeModifierHints() void DrawControllerModifierHints(const Surface &out) { - DrawStartModifierMenu(out); - DrawSelectModifierMenu(out); + DrawGamepadMenuNavigator(out); + DrawGamepadHotspellMenu(out); } } // namespace devilution diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 844d06afc..3c2c7893d 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -45,7 +45,7 @@ namespace devilution { ControlTypes ControlMode = ControlTypes::None; ControlTypes ControlDevice = ControlTypes::None; -ControllerButton ControllerButtonHeld = ControllerButton_NONE; +GameActionType ControllerActionHeld = GameActionType_NONE; GamepadLayout GamepadType = #if defined(DEVILUTIONX_GAMEPAD_TYPE) GamepadLayout:: @@ -54,6 +54,8 @@ GamepadLayout GamepadType = GamepadLayout::Generic; #endif +bool StandToggle = false; + int pcurstrig = -1; Missile *pcursmissile = nullptr; quest_id pcursquest = Q_INVALID; @@ -488,8 +490,12 @@ void Interact() return; } - bool stand = false; + bool stand = StandToggle; #ifndef USE_SDL1 + if (ControlMode == ControlTypes::Gamepad) { + ControllerButtonCombo standGroundCombo = sgOptions.Padmapper.ButtonComboForAction("StandGround"); + stand = IsControllerButtonComboPressed(standGroundCombo); + } if (ControlMode == ControlTypes::VirtualGamepad) { stand = VirtualGamepadState.standButton.isHeld; } @@ -1057,6 +1063,28 @@ void CheckInventoryMove(AxisDirection dir) InventoryMove(dir); } +/** + * @brief Try to clean the inventory related cursor states. + * @return True if it is safe to close the inventory + */ +bool BlurInventory() +{ + if (!MyPlayer->HoldItem.isEmpty()) { + if (!TryDropItem()) { + MyPlayer->Say(HeroSpeech::WhereWouldIPutThis); + return false; + } + } + + CloseInventory(); + if (pcurs > CURSOR_HAND) + NewCursor(CURSOR_HAND); + if (chrflag) + FocusOnCharInfo(); + + return true; +} + void StashMove(AxisDirection dir) { static AxisDirectionRepeater repeater(/*min_interval_ms=*/150); @@ -1360,14 +1388,12 @@ void ProcessLeftStickOrDPadGameUI() { HandleLeftStickOrDPadFn handler = GetLeftStickOrDPadGameUIHandler(); if (handler != nullptr) - handler(GetLeftStickOrDpadDirection(true)); + handler(GetLeftStickOrDpadDirection(false)); } void Movement(size_t playerId) { - if (InGameMenu() - || IsControllerButtonPressed(ControllerButton_BUTTON_START) - || IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) + if (PadMenuNavigatorActive || PadHotspellMenuActive || InGameMenu()) return; if (GetLeftStickOrDPadGameUIHandler() == nullptr) { @@ -1469,7 +1495,7 @@ bool ContinueSimulatedMouseEvent(const SDL_Event &event, const ControllerButtonE return true; } - return SimulatingMouseWithSelectAndDPad || IsSimulatedMouseClickBinding(gamepadEvent); + return SimulatingMouseWithPadmapper || IsSimulatedMouseClickBinding(gamepadEvent); } string_view ControlTypeToString(ControlTypes controlType) @@ -1585,6 +1611,85 @@ void DetectInputMethod(const SDL_Event &event, const ControllerButtonEvent &game } } +void ProcessGameAction(const GameAction &action) +{ + switch (action.type) { + case GameActionType_NONE: + case GameActionType_SEND_KEY: + break; + case GameActionType_USE_HEALTH_POTION: + UseBeltItem(BLT_HEALING); + break; + case GameActionType_USE_MANA_POTION: + UseBeltItem(BLT_MANA); + break; + case GameActionType_PRIMARY_ACTION: + PerformPrimaryAction(); + break; + case GameActionType_SECONDARY_ACTION: + PerformSecondaryAction(); + break; + case GameActionType_CAST_SPELL: + PerformSpellAction(); + break; + case GameActionType_TOGGLE_QUICK_SPELL_MENU: + if (!invflag || BlurInventory()) { + if (!spselflag) + DoSpeedBook(); + else + spselflag = false; + chrflag = false; + QuestLogIsOpen = false; + sbookflag = false; + CloseGoldWithdraw(); + IsStashOpen = false; + } + break; + case GameActionType_TOGGLE_CHARACTER_INFO: + chrflag = !chrflag; + if (chrflag) { + QuestLogIsOpen = false; + CloseGoldWithdraw(); + IsStashOpen = false; + spselflag = false; + if (pcurs == CURSOR_DISARM) + NewCursor(CURSOR_HAND); + FocusOnCharInfo(); + } + break; + case GameActionType_TOGGLE_QUEST_LOG: + if (!QuestLogIsOpen) { + StartQuestlog(); + chrflag = false; + CloseGoldWithdraw(); + IsStashOpen = false; + spselflag = false; + } else { + QuestLogIsOpen = false; + } + break; + case GameActionType_TOGGLE_INVENTORY: + if (invflag) { + BlurInventory(); + } else { + sbookflag = false; + spselflag = false; + invflag = true; + if (pcurs == CURSOR_DISARM) + NewCursor(CURSOR_HAND); + FocusOnInventory(); + } + break; + case GameActionType_TOGGLE_SPELL_BOOK: + if (BlurInventory()) { + CloseInventory(); + spselflag = false; + sbookflag = !sbookflag; + } + break; + } +} + bool IsAutomapActive() { return AutomapActive && leveltype != DTYPE_TOWN; @@ -1667,7 +1772,7 @@ void plrctrls_after_check_curs_move() } // While holding the button down we should retain target (but potentially lose it if it dies, goes out of view, etc) - if (ControllerButtonHeld != ControllerButton_NONE && IsNoneOf(LastMouseButtonAction, MouseActionType::None, MouseActionType::Attack, MouseActionType::Spell)) { + if (ControllerActionHeld != GameActionType_NONE && IsNoneOf(LastMouseButtonAction, MouseActionType::None, MouseActionType::Attack, MouseActionType::Spell)) { InvalidateTargets(); if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == uint16_t(-1) && pcursplr == -1) { diff --git a/Source/controls/plrctrls.h b/Source/controls/plrctrls.h index 29630c748..04ce9c903 100644 --- a/Source/controls/plrctrls.h +++ b/Source/controls/plrctrls.h @@ -36,10 +36,12 @@ extern ControlTypes ControlMode; */ extern ControlTypes ControlDevice; -extern ControllerButton ControllerButtonHeld; +extern GameActionType ControllerActionHeld; extern GamepadLayout GamepadType; +extern bool StandToggle; + // Runs every frame. // Handles menu movement. void plrctrls_every_frame(); @@ -63,6 +65,7 @@ void SetPointAndClick(bool value); bool IsPointAndClick(); void DetectInputMethod(const SDL_Event &event, const ControllerButtonEvent &gamepadEvent); +void ProcessGameAction(const GameAction &action); // Whether the automap is being displayed. bool IsAutomapActive(); diff --git a/Source/cursor.cpp b/Source/cursor.cpp index bf0aeb1be..9d85f74ce 100644 --- a/Source/cursor.cpp +++ b/Source/cursor.cpp @@ -362,7 +362,7 @@ void CheckCursMove() const Point currentTile { mx, my }; // While holding the button down we should retain target (but potentially lose it if it dies, goes out of view, etc) - if ((sgbMouseDown != CLICK_NONE || ControllerButtonHeld != ControllerButton_NONE) && IsNoneOf(LastMouseButtonAction, MouseActionType::None, MouseActionType::Attack, MouseActionType::Spell)) { + if ((sgbMouseDown != CLICK_NONE || ControllerActionHeld != GameActionType_NONE) && IsNoneOf(LastMouseButtonAction, MouseActionType::None, MouseActionType::Attack, MouseActionType::Spell)) { InvalidateTargets(); if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == uint16_t(-1) && pcursplr == -1) { diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 6bb391d4a..fc772a855 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -597,6 +597,91 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) } } +void PressControllerButton(ControllerButton button) +{ + if (IsStashOpen) { + switch (button) { + case ControllerButton_BUTTON_BACK: + StartGoldWithdraw(); + return; + case ControllerButton_BUTTON_LEFTSHOULDER: + Stash.PreviousPage(); + return; + case ControllerButton_BUTTON_RIGHTSHOULDER: + Stash.NextPage(); + return; + } + } + + if (PadHotspellMenuActive) { + auto quickSpellAction = [](size_t slot) { + if (spselflag) { + SetSpeedSpell(slot); + return; + } + if (!*sgOptions.Gameplay.quickCast) + ToggleSpell(slot); + else + QuickCast(slot); + }; + switch (button) { + case devilution::ControllerButton_BUTTON_A: + quickSpellAction(2); + return; + case devilution::ControllerButton_BUTTON_B: + quickSpellAction(3); + return; + case devilution::ControllerButton_BUTTON_X: + quickSpellAction(0); + return; + case devilution::ControllerButton_BUTTON_Y: + quickSpellAction(1); + return; + default: + break; + } + } + + if (PadMenuNavigatorActive) { + switch (button) { + case devilution::ControllerButton_BUTTON_DPAD_UP: + PressEscKey(); + LastMouseButtonAction = MouseActionType::None; + PadHotspellMenuActive = false; + PadMenuNavigatorActive = false; + gamemenu_on(); + return; + case devilution::ControllerButton_BUTTON_DPAD_DOWN: + DoAutoMap(); + return; + case devilution::ControllerButton_BUTTON_DPAD_LEFT: + ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); + return; + case devilution::ControllerButton_BUTTON_DPAD_RIGHT: + ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); + return; + case devilution::ControllerButton_BUTTON_A: + ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); + return; + case devilution::ControllerButton_BUTTON_B: + return; + case devilution::ControllerButton_BUTTON_X: + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); + return; + case devilution::ControllerButton_BUTTON_Y: +#ifdef __3DS__ + sgOptions.Graphics.zoom.SetValue(!*sgOptions.Graphics.zoom); + CalcViewportGeometry(); +#endif + return; + default: + break; + } + } + + sgOptions.Padmapper.ButtonPressed(button); +} + void GameEventHandler(const SDL_Event &event, uint16_t modState) { switch (event.type) { @@ -642,6 +727,12 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) sgOptions.Keymapper.KeyReleased(event.button.button | KeymapperMouseButtonMask); } return; + case SDL_JOYBUTTONDOWN: + PressControllerButton(static_cast(event.jbutton.button)); + return; + case SDL_JOYBUTTONUP: + sgOptions.Padmapper.ButtonReleased(static_cast(event.jbutton.button)); + return; default: if (IsCustomEvent(event.type)) { if (gbIsMultiplayer) @@ -1690,6 +1781,465 @@ void InitKeymapActions() #endif } +void InitPadmapActions() +{ + for (int i = 0; i < 8; ++i) { + sgOptions.Padmapper.AddAction( + "BeltItem{}", + N_("Belt item {}"), + N_("Use Belt item."), + ControllerButton_NONE, + [i] { + Player &myPlayer = *MyPlayer; + if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { + UseInvItem(MyPlayerId, INVITEM_BELT_FIRST + i); + } + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + for (size_t i = 0; i < NumHotkeys; ++i) { + sgOptions.Padmapper.AddAction( + "QuickSpell{}", + N_("Quick spell {}"), + N_("Hotkey for skill or spell."), + ControllerButton_NONE, + [i]() { + if (spselflag) { + SetSpeedSpell(i); + return; + } + if (!*sgOptions.Gameplay.quickCast) + ToggleSpell(i); + else + QuickCast(i); + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + sgOptions.Padmapper.AddAction( + "PrimaryAction", + N_("Primary action"), + N_("Attack monsters, talk to towners, lift and place inventory items."), + ControllerButton_BUTTON_B, + [] { + ControllerActionHeld = GameActionType_PRIMARY_ACTION; + LastMouseButtonAction = MouseActionType::None; + PerformPrimaryAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastMouseButtonAction = MouseActionType::None; + }, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "SecondaryAction", + N_("Secondary action"), + N_("Open chests, interact with doors, pick up items."), + ControllerButton_BUTTON_Y, + [] { + ControllerActionHeld = GameActionType_SECONDARY_ACTION; + LastMouseButtonAction = MouseActionType::None; + PerformSecondaryAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastMouseButtonAction = MouseActionType::None; + }, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "SpellAction", + N_("Spell action"), + N_("Cast the active spell."), + ControllerButton_BUTTON_X, + [] { + ControllerActionHeld = GameActionType_CAST_SPELL; + LastMouseButtonAction = MouseActionType::None; + PerformSpellAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastMouseButtonAction = MouseActionType::None; + }, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "CancelAction", + N_("Cancel action"), + N_("Close menus."), + ControllerButton_BUTTON_A, + [] { + if (DoomFlag) { + doom_close(); + return; + } + + GameAction action; + if (spselflag) + action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); + else if (invflag) + action = GameAction(GameActionType_TOGGLE_INVENTORY); + else if (sbookflag) + action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); + else if (QuestLogIsOpen) + action = GameAction(GameActionType_TOGGLE_QUEST_LOG); + else if (chrflag) + action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); + ProcessGameAction(action); + }, + nullptr, + [] { return DoomFlag || spselflag || invflag || sbookflag || QuestLogIsOpen || chrflag; }); + sgOptions.Padmapper.AddAction( + "MoveUp", + N_("Move up"), + N_("Moves the player character up."), + ControllerButton_BUTTON_DPAD_UP, + [] {}); + sgOptions.Padmapper.AddAction( + "MoveDown", + N_("Move down"), + N_("Moves the player character down."), + ControllerButton_BUTTON_DPAD_DOWN, + [] {}); + sgOptions.Padmapper.AddAction( + "MoveLeft", + N_("Move left"), + N_("Moves the player character left."), + ControllerButton_BUTTON_DPAD_LEFT, + [] {}); + sgOptions.Padmapper.AddAction( + "MoveRight", + N_("Move right"), + N_("Moves the player character right."), + ControllerButton_BUTTON_DPAD_RIGHT, + [] {}); + sgOptions.Padmapper.AddAction( + "StandGround", + N_("Stand ground"), + N_("Hold to prevent the player from moving."), + ControllerButton_NONE, + [] {}); + sgOptions.Padmapper.AddAction( + "ToggleStandGround", + N_("Toggle stand ground"), + N_("Toggle whether the player moves."), + ControllerButton_NONE, + [] { StandToggle = true; }, + [] { StandToggle = false; }, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "UseHealthPotion", + N_("Use health potion"), + N_("Use health potions from belt."), + ControllerButton_BUTTON_LEFTSHOULDER, + [] { UseBeltItem(BLT_HEALING); }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "UseManaPotion", + N_("Use mana potion"), + N_("Use mana potions from belt."), + ControllerButton_BUTTON_RIGHTSHOULDER, + [] { UseBeltItem(BLT_MANA); }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "Character", + N_("Character"), + N_("Open Character screen."), + ControllerButton_AXIS_TRIGGERLEFT, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); + }); + sgOptions.Padmapper.AddAction( + "Inventory", + N_("Inventory"), + N_("Open Inventory screen."), + ControllerButton_AXIS_TRIGGERRIGHT, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); + }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "QuestLog", + N_("Quest log"), + N_("Open Quest log."), + { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERLEFT }, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); + }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "SpellBook", + N_("Spellbook"), + N_("Open Spellbook."), + { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERRIGHT }, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); + }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "DisplaySpells", + N_("Speedbook"), + N_("Open Speedbook."), + ControllerButton_BUTTON_A, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUICK_SPELL_MENU }); + }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "Toggle Automap", + N_("Toggle automap"), + N_("Toggles if automap is displayed."), + ControllerButton_BUTTON_LEFTSTICK, + DoAutoMap); + sgOptions.Padmapper.AddAction( + "MouseUp", + N_("Move mouse up"), + N_("Simulates upward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP }, + [] {}); + sgOptions.Padmapper.AddAction( + "MouseDown", + N_("Move mouse down"), + N_("Simulates downward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_DOWN }, + [] {}); + sgOptions.Padmapper.AddAction( + "MouseLeft", + N_("Move mouse left"), + N_("Simulates leftward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_LEFT }, + [] {}); + sgOptions.Padmapper.AddAction( + "MouseRight", + N_("Move mouse right"), + N_("Simulates rightward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_RIGHT }, + [] {}); + sgOptions.Padmapper.AddAction( + "LeftMouseClick1", + N_("Left mouse click"), + N_("Simulates the left mouse button."), + ControllerButton_BUTTON_RIGHTSTICK, + [] { + sgbMouseDown = CLICK_LEFT; + LeftMouseDown(KMOD_NONE); + }, + [] { + LastMouseButtonAction = MouseActionType::None; + sgbMouseDown = CLICK_NONE; + LeftMouseUp(KMOD_NONE); + }); + sgOptions.Padmapper.AddAction( + "LeftMouseClick2", + N_("Left mouse click"), + N_("Simulates the left mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_LEFTSHOULDER }, + [] { + sgbMouseDown = CLICK_LEFT; + LeftMouseDown(KMOD_NONE); + }, + [] { + LastMouseButtonAction = MouseActionType::None; + sgbMouseDown = CLICK_NONE; + LeftMouseUp(KMOD_NONE); + }); + sgOptions.Padmapper.AddAction( + "RightMouseClick1", + N_("Right mouse click"), + N_("Simulates the right mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSTICK }, + [] { + sgbMouseDown = CLICK_RIGHT; + RightMouseDown(false); + }, + [] { + LastMouseButtonAction = MouseActionType::None; + sgbMouseDown = CLICK_NONE; + }); + sgOptions.Padmapper.AddAction( + "RightMouseClick2", + N_("Right mouse click"), + N_("Simulates the right mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSHOULDER }, + [] { + sgbMouseDown = CLICK_RIGHT; + RightMouseDown(false); + }, + [] { + LastMouseButtonAction = MouseActionType::None; + sgbMouseDown = CLICK_NONE; + }); + sgOptions.Padmapper.AddAction( + "PadHotspellMenu", + N_("Gamepad hotspell menu"), + N_("Hold to set or use spell hotkeys."), + ControllerButton_BUTTON_BACK, + [] { PadHotspellMenuActive = true; }, + [] { PadHotspellMenuActive = false; }); + sgOptions.Padmapper.AddAction( + "PadMenuNavigator", + N_("Gamepad menu navigator"), + N_("Hold to access gamepad menu navigation."), + ControllerButton_BUTTON_START, + [] { PadMenuNavigatorActive = true; }, + [] { PadMenuNavigatorActive = false; }); + auto toggleGameMenu = [] { + bool inMenu = gmenu_is_active(); + PressEscKey(); + LastMouseButtonAction = MouseActionType::None; + PadHotspellMenuActive = false; + PadMenuNavigatorActive = false; + if (!inMenu) + gamemenu_on(); + }; + sgOptions.Padmapper.AddAction( + "ToggleGameMenu1", + N_("Toggle game menu"), + N_("Opens the game menu."), + { + ControllerButton_BUTTON_BACK, + ControllerButton_BUTTON_START, + }, + toggleGameMenu); + sgOptions.Padmapper.AddAction( + "ToggleGameMenu2", + N_("Toggle game menu"), + N_("Opens the game menu."), + { + ControllerButton_BUTTON_START, + ControllerButton_BUTTON_BACK, + }, + toggleGameMenu); + sgOptions.Padmapper.AddAction( + "QuickSave", + N_("Quick save"), + N_("Saves the game."), + ControllerButton_NONE, + [] { gamemenu_save_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); + sgOptions.Padmapper.AddAction( + "QuickLoad", + N_("Quick load"), + N_("Loads the game."), + ControllerButton_NONE, + [] { gamemenu_load_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && gbValidSaveFile && stextflag == STORE_NONE && IsGameRunning(); }); + sgOptions.Padmapper.AddAction( + "Item Highlighting", + N_("Item highlighting"), + N_("Show/hide items on ground."), + ControllerButton_NONE, + [] { AltPressed(true); }, + [] { AltPressed(false); }); + sgOptions.Padmapper.AddAction( + "Toggle Item Highlighting", + N_("Toggle item highlighting"), + N_("Permanent show/hide items on ground."), + ControllerButton_NONE, + nullptr, + [] { ToggleItemLabelHighlight(); }); + sgOptions.Padmapper.AddAction( + "Hide Info Screens", + N_("Hide Info Screens"), + N_("Hide all info screens."), + ControllerButton_NONE, + [] { + ClosePanels(); + HelpFlag = false; + ChatLogFlag = false; + spselflag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + AutomapActive = false; + CancelCurrentDiabloMsg(); + gamemenu_off(); + doom_close(); + }, + nullptr, + IsGameRunning); + sgOptions.Padmapper.AddAction( + "Zoom", + N_("Zoom"), + N_("Zoom Game Screen."), + ControllerButton_NONE, + [] { + sgOptions.Graphics.zoom.SetValue(!*sgOptions.Graphics.zoom); + CalcViewportGeometry(); + }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "Pause Game", + N_("Pause Game"), + N_("Pauses the game."), + ControllerButton_NONE, + diablo_pause_game); + sgOptions.Padmapper.AddAction( + "DecreaseGamma", + N_("Decrease Gamma"), + N_("Reduce screen brightness."), + ControllerButton_NONE, + DecreaseGamma, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "IncreaseGamma", + N_("Increase Gamma"), + N_("Increase screen brightness."), + ControllerButton_NONE, + IncreaseGamma, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "Help", + N_("Help"), + N_("Open Help Screen."), + ControllerButton_NONE, + HelpKeyPressed, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "Screenshot", + N_("Screenshot"), + N_("Takes a screenshot."), + ControllerButton_NONE, + nullptr, + CaptureScreen); + sgOptions.Padmapper.AddAction( + "GameInfo", + N_("Game info"), + N_("Displays game infos."), + ControllerButton_NONE, + [] { + EventPlrMsg(fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. */ "{:s} {:s}")), + PROJECT_NAME, + PROJECT_VERSION), + UiFlags::ColorWhite); + }, + nullptr, + CanPlayerTakeAction); + sgOptions.Padmapper.AddAction( + "ChatLog", + N_("Chat Log"), + N_("Displays chat log."), + ControllerButton_NONE, + [] { + ToggleChatLog(); + }); +} + void FreeGameMem() { pDungeonCels = nullptr; @@ -1778,6 +2328,7 @@ int DiabloMain(int argc, char **argv) DiabloParseFlags(argc, argv); InitKeymapActions(); + InitPadmapActions(); // Need to ensure devilutionx.mpq (and fonts.mpq if available) are loaded before attempting to read translation settings LoadCoreArchives(); diff --git a/Source/gmenu.cpp b/Source/gmenu.cpp index f10508319..e856e174f 100644 --- a/Source/gmenu.cpp +++ b/Source/gmenu.cpp @@ -139,7 +139,7 @@ void GmenuDrawMenuItem(const Surface &out, TMenuItem *pItem, int y) void GameMenuMove() { static AxisDirectionRepeater repeater; - const AxisDirection moveDir = repeater.Get(GetLeftStickOrDpadDirection()); + const AxisDirection moveDir = repeater.Get(GetLeftStickOrDpadDirection(false)); if (moveDir.x != AxisDirectionX_NONE) GmenuLeftRight(moveDir.x == AxisDirectionX_RIGHT); if (moveDir.y != AxisDirectionY_NONE) diff --git a/Source/items.cpp b/Source/items.cpp index 402c6bcda..e0dcf0b8f 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -1783,8 +1783,8 @@ void printItemMiscGamepad(const Item &item, bool isOil, bool isCastOnTarget) castButton = controller_button_icon::Playstation_Square; break; case GamepadLayout::Nintendo: - activateButton = controller_button_icon::Nintendo_Y; - castButton = controller_button_icon::Nintendo_X; + activateButton = controller_button_icon::Nintendo_X; + castButton = controller_button_icon::Nintendo_Y; break; } diff --git a/Source/miniwin/misc_msg.cpp b/Source/miniwin/misc_msg.cpp index ef6e4c4a5..e0b9fe447 100644 --- a/Source/miniwin/misc_msg.cpp +++ b/Source/miniwin/misc_msg.cpp @@ -101,113 +101,6 @@ bool FalseAvail(const char *name, int value) return true; } -/** - * @brief Try to clean the inventory related cursor states. - * @return True if it is safe to close the inventory - */ -bool BlurInventory() -{ - if (!MyPlayer->HoldItem.isEmpty()) { - if (!TryDropItem()) { - MyPlayer->Say(HeroSpeech::WhereWouldIPutThis); - return false; - } - } - - CloseInventory(); - if (pcurs > CURSOR_HAND) - NewCursor(CURSOR_HAND); - if (chrflag) - FocusOnCharInfo(); - - return true; -} - -void ProcessGamepadEvents(GameAction &action) -{ - switch (action.type) { - case GameActionType_NONE: - case GameActionType_SEND_KEY: - break; - case GameActionType_USE_HEALTH_POTION: - if (IsStashOpen) - Stash.PreviousPage(); - else - UseBeltItem(BLT_HEALING); - break; - case GameActionType_USE_MANA_POTION: - if (IsStashOpen) - Stash.NextPage(); - else - UseBeltItem(BLT_MANA); - break; - case GameActionType_PRIMARY_ACTION: - PerformPrimaryAction(); - break; - case GameActionType_SECONDARY_ACTION: - PerformSecondaryAction(); - break; - case GameActionType_CAST_SPELL: - PerformSpellAction(); - break; - case GameActionType_TOGGLE_QUICK_SPELL_MENU: - if (!invflag || BlurInventory()) { - if (!spselflag) - DoSpeedBook(); - else - spselflag = false; - chrflag = false; - QuestLogIsOpen = false; - sbookflag = false; - CloseGoldWithdraw(); - IsStashOpen = false; - } - break; - case GameActionType_TOGGLE_CHARACTER_INFO: - chrflag = !chrflag; - if (chrflag) { - QuestLogIsOpen = false; - CloseGoldWithdraw(); - IsStashOpen = false; - spselflag = false; - if (pcurs == CURSOR_DISARM) - NewCursor(CURSOR_HAND); - FocusOnCharInfo(); - } - break; - case GameActionType_TOGGLE_QUEST_LOG: - if (!QuestLogIsOpen) { - StartQuestlog(); - chrflag = false; - CloseGoldWithdraw(); - IsStashOpen = false; - spselflag = false; - } else { - QuestLogIsOpen = false; - } - break; - case GameActionType_TOGGLE_INVENTORY: - if (invflag) { - BlurInventory(); - } else { - sbookflag = false; - spselflag = false; - invflag = true; - if (pcurs == CURSOR_DISARM) - NewCursor(CURSOR_HAND); - FocusOnInventory(); - } - break; - case GameActionType_TOGGLE_SPELL_BOOK: - if (BlurInventory()) { - CloseInventory(); - spselflag = false; - sbookflag = !sbookflag; - } - break; - } -} - } // namespace bool FetchMessage_Real(SDL_Event *event, uint16_t *modState) @@ -269,20 +162,26 @@ bool FetchMessage_Real(SDL_Event *event, uint16_t *modState) return true; } - if (IsAnyOf(ctrlEvent.button, ControllerButtonPrimary, ControllerButtonSecondary, ControllerButtonTertiary) && IsAnyOf(ControllerButtonHeld, ctrlEvent.button, ControllerButton_NONE)) { - ControllerButtonHeld = (ctrlEvent.up || IsControllerButtonPressed(ControllerButton_BUTTON_BACK)) ? ControllerButton_NONE : ctrlEvent.button; - LastMouseButtonAction = MouseActionType::None; + if (movie_playing && SkipsMovie(ctrlEvent)) { + event->type = SDL_KEYDOWN; + return true; + } + + if (ctrlEvent.button != ControllerButton_NONE) { + if (ctrlEvent.button == SuppressedButton) { + if (!ctrlEvent.up) + return true; + SuppressedButton = ControllerButton_NONE; + } + + event->type = ctrlEvent.up ? SDL_JOYBUTTONUP : SDL_JOYBUTTONDOWN; + event->jbutton.button = ctrlEvent.button; + event->jbutton.state = ctrlEvent.up ? SDL_RELEASED : SDL_PRESSED; } GameAction action; if (GetGameAction(e, ctrlEvent, &action)) { - if (movie_playing) { - if (action.type != GameActionType_NONE) { - event->type = SDL_KEYDOWN; - if (action.type == GameActionType_SEND_KEY) - event->key.keysym.sym = static_cast(action.send_key.vk_code); - } - } else if (action.type == GameActionType_SEND_KEY) { + if (action.type == GameActionType_SEND_KEY) { if ((action.send_key.vk_code & KeymapperMouseButtonMask) != 0) { const unsigned button = action.send_key.vk_code & ~KeymapperMouseButtonMask; SetMouseButtonEvent(*event, action.send_key.up ? SDL_MOUSEBUTTONUP : SDL_MOUSEBUTTONDOWN, static_cast(button), MousePosition); @@ -292,7 +191,7 @@ bool FetchMessage_Real(SDL_Event *event, uint16_t *modState) event->key.keysym.sym = static_cast(action.send_key.vk_code); } } else { - ProcessGamepadEvents(action); + ProcessGameAction(action); } return true; } diff --git a/Source/options.cpp b/Source/options.cpp index 7960b13cb..cda18c9bd 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -14,6 +14,8 @@ #include #include "control.h" +#include "controls/controller.h" +#include "controls/game_controls.h" #include "discord/discord.h" #include "engine/demomode.h" #include "engine/sound_defs.hpp" @@ -29,6 +31,7 @@ #include "utils/paths.h" #include "utils/stdcompat/algorithm.hpp" #include "utils/str_cat.hpp" +#include "utils/str_split.hpp" #include "utils/utf8.hpp" namespace devilution { @@ -352,8 +355,6 @@ void LoadOptions() GetIniStringVector("NetMsg", QuickMessages[i].key, sgOptions.Chat.szHotKeyMsgs[i]); GetIniValue("Controller", "Mapping", sgOptions.Controller.szMapping, sizeof(sgOptions.Controller.szMapping), ""); - sgOptions.Controller.bSwapShoulderButtonMode = GetIniBool("Controller", "Swap Shoulder Button Mode", false); - sgOptions.Controller.bDpadHotkeys = GetIniBool("Controller", "Dpad Hotkeys", false); sgOptions.Controller.fDeadzone = GetIniFloat("Controller", "deadzone", 0.07F); #ifdef __vita__ sgOptions.Controller.bRearTouch = GetIniBool("Controller", "Enable Rear Touchpad", true); @@ -384,8 +385,6 @@ void SaveOptions() SetIniValue("NetMsg", QuickMessages[i].key, sgOptions.Chat.szHotKeyMsgs[i]); SetIniValue("Controller", "Mapping", sgOptions.Controller.szMapping); - SetIniValue("Controller", "Swap Shoulder Button Mode", sgOptions.Controller.bSwapShoulderButtonMode); - SetIniValue("Controller", "Dpad Hotkeys", sgOptions.Controller.bDpadHotkeys); SetIniValue("Controller", "deadzone", sgOptions.Controller.fDeadzone); #ifdef __vita__ SetIniValue("Controller", "Enable Rear Touchpad", sgOptions.Controller.bRearTouch); @@ -1419,6 +1418,253 @@ uint32_t KeymapperOptions::KeyForAction(string_view actionName) const return SDLK_UNKNOWN; } +PadmapperOptions::PadmapperOptions() + : OptionCategoryBase("Padmapping", N_("Padmapping"), N_("Padmapping Settings")) +{ + buttonToButtonName.emplace(ControllerButton_AXIS_TRIGGERLEFT, "LT"); + buttonToButtonName.emplace(ControllerButton_AXIS_TRIGGERRIGHT, "RT"); + buttonToButtonName.emplace(ControllerButton_BUTTON_A, "A"); + buttonToButtonName.emplace(ControllerButton_BUTTON_B, "B"); + buttonToButtonName.emplace(ControllerButton_BUTTON_X, "X"); + buttonToButtonName.emplace(ControllerButton_BUTTON_Y, "Y"); + buttonToButtonName.emplace(ControllerButton_BUTTON_LEFTSTICK, "LS"); + buttonToButtonName.emplace(ControllerButton_BUTTON_RIGHTSTICK, "RS"); + buttonToButtonName.emplace(ControllerButton_BUTTON_LEFTSHOULDER, "LB"); + buttonToButtonName.emplace(ControllerButton_BUTTON_RIGHTSHOULDER, "RB"); + buttonToButtonName.emplace(ControllerButton_BUTTON_START, "Start"); + buttonToButtonName.emplace(ControllerButton_BUTTON_BACK, "Select"); + buttonToButtonName.emplace(ControllerButton_BUTTON_DPAD_UP, "Up"); + buttonToButtonName.emplace(ControllerButton_BUTTON_DPAD_DOWN, "Down"); + buttonToButtonName.emplace(ControllerButton_BUTTON_DPAD_LEFT, "Left"); + buttonToButtonName.emplace(ControllerButton_BUTTON_DPAD_RIGHT, "Right"); + + buttonNameToButton.reserve(buttonToButtonName.size()); + for (const auto &kv : buttonToButtonName) { + buttonNameToButton.emplace(kv.second, kv.first); + } +} + +std::forward_list &PadmapperOptions::GetActions() +{ + if (!reversed) { + actions.reverse(); + reversed = true; + } + return actions; +} + +std::vector PadmapperOptions::GetEntries() +{ + std::vector entries; + for (Action &action : GetActions()) { + entries.push_back(&action); + } + return entries; +} + +PadmapperOptions::Action::Action(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::Invisible, name, description) + , defaultInput(defaultInput) + , actionPressed(std::move(actionPressed)) + , actionReleased(std::move(actionReleased)) + , enable(std::move(enable)) + , dynamicIndex(index) +{ + if (index != 0) { + dynamicKey = fmt::format(fmt::runtime(fmt::string_view(key.data(), key.size())), index); + this->key = dynamicKey; + } +} + +string_view PadmapperOptions::Action::GetName() const +{ + if (dynamicIndex == 0) + return _(name); + dynamicName = fmt::format(fmt::runtime(_(name)), dynamicIndex); + return dynamicName; +} + +void PadmapperOptions::Action::LoadFromIni(string_view category) +{ + std::array result; + if (!GetIniValue(category.data(), key.data(), result.data(), result.size())) { + SetValue(defaultInput); + return; // Use the default button combo if no mapping has been set. + } + + std::string modName; + std::string buttonName; + for (string_view name : SplitByChar(result.data(), '+')) { + modName = buttonName; + buttonName = name; + } + + if (buttonName.empty()) { + SetValue(ControllerButtonCombo {}); + return; + } + + ControllerButtonCombo input {}; + if (!modName.empty()) { + auto modifierIt = sgOptions.Padmapper.buttonNameToButton.find(modName); + if (modifierIt == sgOptions.Padmapper.buttonNameToButton.end()) { + // Use the default button combo if the modifier name is unknown. + LogWarn("Padmapper: unknown button '{}'", modName); + SetValue(defaultInput); + return; + } + input.modifier = modifierIt->second; + } + + auto buttonIt = sgOptions.Padmapper.buttonNameToButton.find(buttonName); + if (buttonIt == sgOptions.Padmapper.buttonNameToButton.end()) { + // Use the default button combo if the button name is unknown. + LogWarn("Padmapper: unknown button '{}'", buttonName); + SetValue(defaultInput); + return; + } + input.button = buttonIt->second; + + // Store the input in action.boundInput and in the map so we can save() + // the actions while keeping the same order as they have been added. + SetValue(input); +} +void PadmapperOptions::Action::SaveToIni(string_view category) const +{ + if (boundInput.button == ControllerButton_NONE) { + // Just add an empty config entry if the action is unbound. + SetIniValue(category.data(), key.data(), ""); + return; + } + auto buttonNameIt = sgOptions.Padmapper.buttonToButtonName.find(boundInput.button); + if (buttonNameIt == sgOptions.Padmapper.buttonToButtonName.end()) { + LogVerbose("Padmapper: no name found for key '{}'", key); + return; + } + std::string inputName = buttonNameIt->second; + if (boundInput.modifier != ControllerButton_NONE) { + auto modifierNameIt = sgOptions.Padmapper.buttonToButtonName.find(boundInput.modifier); + if (modifierNameIt == sgOptions.Padmapper.buttonToButtonName.end()) { + LogVerbose("Padmapper: no name found for key '{}'", key); + return; + } + inputName = StrCat(modifierNameIt->second, "+", inputName); + } + SetIniValue(category.data(), key.data(), inputName.data()); +} + +string_view PadmapperOptions::Action::GetValueDescription() const +{ + return boundInputDescription; +} + +bool PadmapperOptions::Action::SetValue(ControllerButtonCombo value) +{ + auto modifierNameIt = sgOptions.Padmapper.buttonToButtonName.find(value.modifier); + auto buttonNameIt = sgOptions.Padmapper.buttonToButtonName.find(value.button); + auto notFoundIt = sgOptions.Padmapper.buttonToButtonName.end(); + if ((value.modifier != ControllerButton_NONE && modifierNameIt == notFoundIt) || (value.button != ControllerButton_NONE && buttonNameIt == notFoundIt)) { + // Ignore invalid button combos + return false; + } + + // Remove old button combo + if (boundInput.button != ControllerButton_NONE) { + boundInput = {}; + boundInputDescription = ""; + } + + // Add new button combo + if (value.button != ControllerButton_NONE) { + boundInput = value; + boundInputDescription = buttonNameIt->second; + + if (modifierNameIt != notFoundIt) { + boundInputDescription = StrCat(modifierNameIt->second, "+", boundInputDescription); + } + } + + return true; +} + +void PadmapperOptions::AddAction(string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index) +{ + actions.push_front(Action { key, name, description, defaultInput, std::move(actionPressed), std::move(actionReleased), std::move(enable), index }); +} + +void PadmapperOptions::ButtonPressed(ControllerButton button) +{ + // To give preference to button combinations, + // first pass ignores mappings where no modifier is bound + for (Action &action : GetActions()) { + ControllerButtonCombo combo = action.boundInput; + if (combo.modifier == ControllerButton_NONE) + continue; + if (button != combo.button) + continue; + if (!IsControllerButtonPressed(combo.modifier)) + continue; + if (action.enable && !action.enable()) + continue; + if (action.actionPressed) + action.actionPressed(); + SuppressedButton = combo.modifier; + buttonToReleaseAction.insert_or_assign(combo.button, action); + return; + } + + for (Action &action : GetActions()) { + ControllerButtonCombo combo = action.boundInput; + if (combo.modifier != ControllerButton_NONE) + continue; + if (button != combo.button) + continue; + if (action.enable && !action.enable()) + continue; + if (action.actionPressed) + action.actionPressed(); + SuppressedButton = combo.modifier; + buttonToReleaseAction.insert_or_assign(combo.button, action); + return; + } +} + +void PadmapperOptions::ButtonReleased(ControllerButton button) +{ + auto it = buttonToReleaseAction.find(button); + if (it == buttonToReleaseAction.end()) + return; // Ignore unmapped buttons. + + const Action &action = it->second.get(); + + // Check that the action can be triggered. + if (!action.actionReleased || (action.enable && !action.enable())) + return; + + action.actionReleased(); + buttonToReleaseAction.erase(button); +} + +string_view PadmapperOptions::InputNameForAction(string_view actionName) const +{ + for (const Action &action : actions) { + if (action.key == actionName && action.boundInput.button != ControllerButton_NONE) { + return action.GetValueDescription(); + } + } + return ""; +} + +ControllerButtonCombo PadmapperOptions::ButtonComboForAction(string_view actionName) const +{ + for (const auto &action : actions) { + if (action.key == actionName && action.boundInput.button != ControllerButton_NONE) { + return action.boundInput; + } + } + return ControllerButton_NONE; +} + namespace { constexpr char ResamplerSpeex[] = "Speex"; constexpr char ResamplerSDL[] = "SDL"; diff --git a/Source/options.h b/Source/options.h index 7812b2703..192a89f61 100644 --- a/Source/options.h +++ b/Source/options.h @@ -2,10 +2,12 @@ #include #include +#include #include #include +#include "controls/controller_buttons.h" #include "engine/sound_defs.hpp" #include "miniwin/misc_msg.h" #include "pack.h" @@ -60,6 +62,7 @@ enum class OptionEntryType : uint8_t { Boolean, List, Key, + PadButton, }; enum class OptionEntryFlags : uint8_t { @@ -583,10 +586,6 @@ struct ControllerOptions : OptionCategoryBase { /** @brief SDL Controller mapping, see SDL_GameControllerDB. */ char szMapping[1024]; - /** @brief Use dpad for spell hotkeys without holding "start" */ - bool bDpadHotkeys; - /** @brief Shoulder gamepad shoulder buttons act as potions by default */ - bool bSwapShoulderButtonMode; /** @brief Configure gamepad joysticks deadzone */ float fDeadzone; #ifdef __vita__ @@ -682,6 +681,66 @@ private: std::unordered_map keyNameToKeyID; }; +/** The Padmapper maps gamepad buttons to actions. */ +struct PadmapperOptions : OptionCategoryBase { + /** + * Action represents an action that can be triggered using a gamepad + * button combo. + */ + class Action final : public OptionEntryBase { + public: + [[nodiscard]] string_view GetName() const override; + [[nodiscard]] OptionEntryType GetType() const override + { + return OptionEntryType::PadButton; + } + + void LoadFromIni(string_view category) override; + void SaveToIni(string_view category) const override; + + [[nodiscard]] string_view GetValueDescription() const override; + + bool SetValue(ControllerButtonCombo value); + + private: + Action(string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index); + ControllerButtonCombo defaultInput; + std::function actionPressed; + std::function actionReleased; + std::function enable; + ControllerButtonCombo boundInput {}; + std::string boundInputDescription; + unsigned dynamicIndex; + std::string dynamicKey; + mutable std::string dynamicName; + + friend struct PadmapperOptions; + }; + + PadmapperOptions(); + std::vector GetEntries() override; + + void AddAction( + string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, + std::function actionPressed, + std::function actionReleased = nullptr, + std::function enable = nullptr, + unsigned index = 0); + void ButtonPressed(ControllerButton button); + void ButtonReleased(ControllerButton button); + string_view InputNameForAction(string_view actionName) const; + ControllerButtonCombo ButtonComboForAction(string_view actionName) const; + +private: + std::forward_list actions; + std::unordered_map> buttonToReleaseAction; + std::unordered_map buttonToButtonName; + std::unordered_map buttonNameToButton; + bool reversed = false; + + std::forward_list &GetActions(); +}; + struct Options { StartUpOptions StartUp; DiabloOptions Diablo; @@ -694,6 +753,7 @@ struct Options { ChatOptions Chat; LanguageOptions Language; KeymapperOptions Keymapper; + PadmapperOptions Padmapper; [[nodiscard]] std::vector GetCategories() { @@ -709,6 +769,7 @@ struct Options { &Network, &Chat, &Keymapper, + &Padmapper, }; } }; diff --git a/Source/track.cpp b/Source/track.cpp index ac8e962ca..3c119b907 100644 --- a/Source/track.cpp +++ b/Source/track.cpp @@ -63,7 +63,7 @@ void RepeatMouseAction() if (pcurs != CURSOR_HAND) return; - if (sgbMouseDown == CLICK_NONE && ControllerButtonHeld == ControllerButton_NONE) + if (sgbMouseDown == CLICK_NONE && ControllerActionHeld == GameActionType_NONE) return; if (stextflag != STORE_NONE) diff --git a/Source/utils/display.cpp b/Source/utils/display.cpp index fb09cf37e..23a06aaac 100644 --- a/Source/utils/display.cpp +++ b/Source/utils/display.cpp @@ -262,6 +262,9 @@ bool SpawnWindow(const char *lpWindowName) #if SDL_VERSION_ATLEAST(2, 0, 2) SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); #endif +#if SDL_VERSION_ATLEAST(2, 0, 12) + SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); +#endif int initFlags = SDL_INIT_VIDEO | SDL_INIT_JOYSTICK; #ifndef NOSOUND