Browse Source

Add padmapper for gamepad customization

pull/5419/head
staphen 3 years ago committed by Anders Jenbo
parent
commit
e67e4934cc
  1. 8
      Source/controls/controller.cpp
  2. 1
      Source/controls/controller.h
  3. 24
      Source/controls/controller_buttons.h
  4. 119
      Source/controls/controller_motion.cpp
  5. 4
      Source/controls/controller_motion.h
  6. 433
      Source/controls/game_controls.cpp
  7. 17
      Source/controls/game_controls.h
  8. 10
      Source/controls/menu_controls.cpp
  9. 15
      Source/controls/modifier_hints.cpp
  10. 121
      Source/controls/plrctrls.cpp
  11. 5
      Source/controls/plrctrls.h
  12. 2
      Source/cursor.cpp
  13. 551
      Source/diablo.cpp
  14. 2
      Source/gmenu.cpp
  15. 4
      Source/items.cpp
  16. 135
      Source/miniwin/misc_msg.cpp
  17. 254
      Source/options.cpp
  18. 69
      Source/options.h
  19. 2
      Source/track.cpp
  20. 3
      Source/utils/display.cpp

8
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

1
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);

24
Source/controls/controller_buttons.h

@ -2,6 +2,7 @@
// Unifies joystick, gamepad, and keyboard controller APIs.
#include <cstdint>
#include <functional>
#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

119
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) {

4
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

433
Source/controls/game_controls.cpp

@ -2,47 +2,57 @@
#include <cstdint>
#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<uint32_t>(TranslateControllerButtonToKey(ctrlEvent.button)),
ctrlEvent.up };
if (translation != SDLK_UNKNOWN) {
*action = GameActionSendKey { static_cast<uint32_t>(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

17
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

10
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:

15
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

121
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) {

5
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();

2
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) {

551
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<ControllerButton>(event.jbutton.button));
return;
case SDL_JOYBUTTONUP:
sgOptions.Padmapper.ButtonReleased(static_cast<ControllerButton>(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();

2
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)

4
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;
}

135
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<SDL_Keycode>(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<uint8_t>(button), MousePosition);
@ -292,7 +191,7 @@ bool FetchMessage_Real(SDL_Event *event, uint16_t *modState)
event->key.keysym.sym = static_cast<SDL_Keycode>(action.send_key.vk_code);
}
} else {
ProcessGamepadEvents(action);
ProcessGameAction(action);
}
return true;
}

254
Source/options.cpp

@ -14,6 +14,8 @@
#include <SimpleIni.h>
#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::Action> &PadmapperOptions::GetActions()
{
if (!reversed) {
actions.reverse();
reversed = true;
}
return actions;
}
std::vector<OptionEntryBase *> PadmapperOptions::GetEntries()
{
std::vector<OptionEntryBase *> 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<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> 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<char, 64> 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<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> 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";

69
Source/options.h

@ -2,10 +2,12 @@
#include <cstddef>
#include <cstdint>
#include <forward_list>
#include <unordered_map>
#include <SDL_version.h>
#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<std::string, uint32_t> 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<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index);
ControllerButtonCombo defaultInput;
std::function<void()> actionPressed;
std::function<void()> actionReleased;
std::function<bool()> enable;
ControllerButtonCombo boundInput {};
std::string boundInputDescription;
unsigned dynamicIndex;
std::string dynamicKey;
mutable std::string dynamicName;
friend struct PadmapperOptions;
};
PadmapperOptions();
std::vector<OptionEntryBase *> GetEntries() override;
void AddAction(
string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput,
std::function<void()> actionPressed,
std::function<void()> actionReleased = nullptr,
std::function<bool()> 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<Action> actions;
std::unordered_map<ControllerButton, std::reference_wrapper<Action>> buttonToReleaseAction;
std::unordered_map<ControllerButton, std::string> buttonToButtonName;
std::unordered_map<std::string, ControllerButton> buttonNameToButton;
bool reversed = false;
std::forward_list<Action> &GetActions();
};
struct Options {
StartUpOptions StartUp;
DiabloOptions Diablo;
@ -694,6 +753,7 @@ struct Options {
ChatOptions Chat;
LanguageOptions Language;
KeymapperOptions Keymapper;
PadmapperOptions Padmapper;
[[nodiscard]] std::vector<OptionCategoryBase *> GetCategories()
{
@ -709,6 +769,7 @@ struct Options {
&Network,
&Chat,
&Keymapper,
&Padmapper,
};
}
};

2
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)

3
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

Loading…
Cancel
Save