From a2e821d2413d6ed2bb254b8798e6fd3b5d95aed1 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 26 Oct 2019 14:32:29 +0100 Subject: [PATCH] Game controller support Initial game controller support. Actions are based on the Switch branch but the controller code itself is implemented differently, allowing for easy remapping and minimizing changes to the Source/ directory. Many subtle and not so subtle controller bugs have been fixed in this implementation, including: 1. Smoother & more responsive movement with the joysticks. 2. Consistent controls for all the menus in the game (stores, quest log, etc). 3. Cursor appearance / disappearance at appropriate times. Low-level controls are abstracted and 3 SDL interfaces are supported: game controller, joystick, and keyboard. See SourceX/controls/ for more details on this. Wishlist for the future: 1. Primary button and use button should attack continously. This is hard as it requires checking the cooldowns / attack speed. 2. Quick spell menu navigation is very buggy. It is also buggy in the switch branch. I haven't had a change to investigate. --- CMakeLists.txt | 8 + Source/diablo.cpp | 4 + Source/inv.h | 1 + Source/scrollrt.cpp | 2 +- Source/scrollrt.h | 1 + SourceS/sdl2_to_1_2_backports.h | 12 +- SourceS/sdl_compat.h | 28 ++ SourceX/DiabloUI/credits.cpp | 10 + SourceX/DiabloUI/diabloui.cpp | 68 ++- SourceX/DiabloUI/dialogs.cpp | 23 +- SourceX/DiabloUI/title.cpp | 5 + SourceX/controls/README.md | 20 + SourceX/controls/controller.cpp | 65 +++ SourceX/controls/controller.h | 18 + SourceX/controls/controller_buttons.h | 30 ++ SourceX/controls/controller_motion.cpp | 91 ++++ SourceX/controls/controller_motion.h | 23 + SourceX/controls/devices/game_controller.cpp | 159 +++++++ SourceX/controls/devices/game_controller.h | 22 + SourceX/controls/devices/joystick.cpp | 193 +++++++++ SourceX/controls/devices/joystick.h | 19 + SourceX/controls/devices/kbcontroller.cpp | 202 +++++++++ SourceX/controls/devices/kbcontroller.h | 23 + SourceX/controls/game_controls.cpp | 156 +++++++ SourceX/controls/game_controls.h | 79 ++++ SourceX/controls/menu_controls.cpp | 74 ++++ SourceX/controls/menu_controls.h | 24 ++ SourceX/controls/plrctrls.cpp | 416 ++++++++++++------- SourceX/controls/plrctrls.h | 44 +- SourceX/miniwin/misc.cpp | 3 + SourceX/miniwin/misc_dx.cpp | 2 + SourceX/miniwin/misc_msg.cpp | 218 ++++++++-- 32 files changed, 1784 insertions(+), 259 deletions(-) create mode 100644 SourceX/controls/README.md create mode 100644 SourceX/controls/controller.cpp create mode 100644 SourceX/controls/controller.h create mode 100644 SourceX/controls/controller_buttons.h create mode 100644 SourceX/controls/controller_motion.cpp create mode 100644 SourceX/controls/controller_motion.h create mode 100644 SourceX/controls/devices/game_controller.cpp create mode 100644 SourceX/controls/devices/game_controller.h create mode 100644 SourceX/controls/devices/joystick.cpp create mode 100644 SourceX/controls/devices/joystick.h create mode 100644 SourceX/controls/devices/kbcontroller.cpp create mode 100644 SourceX/controls/devices/kbcontroller.h create mode 100644 SourceX/controls/game_controls.cpp create mode 100644 SourceX/controls/game_controls.h create mode 100644 SourceX/controls/menu_controls.cpp create mode 100644 SourceX/controls/menu_controls.h diff --git a/CMakeLists.txt b/CMakeLists.txt index edce03ce6..3aa0258aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -208,6 +208,14 @@ add_library(devilution STATIC set(devilutionx_SRCS SourceX/dx.cpp + SourceX/controls/devices/game_controller.cpp + SourceX/controls/devices/joystick.cpp + SourceX/controls/devices/kbcontroller.cpp + SourceX/controls/controller.cpp + SourceX/controls/controller_motion.cpp + SourceX/controls/game_controls.cpp + SourceX/controls/menu_controls.cpp + SourceX/controls/plrctrls.cpp SourceX/miniwin/ddraw.cpp SourceX/miniwin/misc.cpp SourceX/miniwin/misc_io.cpp diff --git a/Source/diablo.cpp b/Source/diablo.cpp index ba6627d6a..ce5ee181d 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1675,6 +1675,8 @@ void game_loop(BOOL bStartup) } } +extern void plrctrls_game_logic(); + void game_logic() { if (PauseMode == 2) { @@ -1720,6 +1722,8 @@ void game_logic() CheckQuests(); force_redraw |= 1; pfile_update(FALSE); + + plrctrls_game_logic(); } void timeout_cursor(BOOL bTimeout) diff --git a/Source/inv.h b/Source/inv.h index 0ef9c39ca..77dc3509b 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -4,6 +4,7 @@ extern BOOL invflag; extern BOOL drawsbarflag; +extern const InvXY InvRect[73]; void FreeInvGFX(); void InitInv(); diff --git a/Source/scrollrt.cpp b/Source/scrollrt.cpp index 522adf697..fb1096964 100644 --- a/Source/scrollrt.cpp +++ b/Source/scrollrt.cpp @@ -102,7 +102,7 @@ static void scrollrt_draw_cursor_item() assert(! sgdwCursWdt); - if (pcurs <= 0 || cursW == 0 || cursH == 0) { + if (pcurs <= 0 || cursW == 0 || cursH == 0 || sgbControllerActive) { return; } diff --git a/Source/scrollrt.h b/Source/scrollrt.h index 600af6555..e3fd649ce 100644 --- a/Source/scrollrt.h +++ b/Source/scrollrt.h @@ -2,6 +2,7 @@ #ifndef __SCROLLRT_H__ #define __SCROLLRT_H__ +extern bool sgbControllerActive; extern int light_table_index; extern BYTE *gpBufStart; extern BYTE *gpBufEnd; diff --git a/SourceS/sdl2_to_1_2_backports.h b/SourceS/sdl2_to_1_2_backports.h index c24a00e9b..938964d8a 100644 --- a/SourceS/sdl2_to_1_2_backports.h +++ b/SourceS/sdl2_to_1_2_backports.h @@ -17,7 +17,6 @@ #define SDL_zero(x) SDL_memset(&(x), 0, sizeof((x))) #define SDL_InvalidParamError(param) SDL_SetError("Parameter '%s' is invalid", (param)) -#define SDL_Log puts #define SDL_floor floor //== Events handling @@ -47,6 +46,17 @@ // For now we only process ASCII input when using SDL1. #define SDL_TEXTINPUTEVENT_TEXT_SIZE 2 +#define SDL_JoystickNameForIndex SDL_JoystickName + +inline void SDL_Log(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + puts(""); +} + static SDL_bool SDLBackport_IsTextInputActive = SDL_FALSE; inline SDL_bool SDL_IsTextInputActive() diff --git a/SourceS/sdl_compat.h b/SourceS/sdl_compat.h index 69941d782..39fdee029 100644 --- a/SourceS/sdl_compat.h +++ b/SourceS/sdl_compat.h @@ -1,6 +1,34 @@ // Compatibility wrappers for SDL 1 & 2. +#pragma once #include +#ifndef USE_SDL1 +#define SDLC_KEYSTATE_LEFTSHIFT SDL_SCANCODE_LSHIFT +#define SDLC_KEYSTATE_RIGHTSHIFT SDL_SCANCODE_RSHIFT +#define SDLC_KEYSTATE_MENU SDL_SCANCODE_MENU +#define SDLC_KEYSTATE_UP SDL_SCANCODE_UP +#define SDLC_KEYSTATE_DOWN SDL_SCANCODE_DOWN +#define SDLC_KEYSTATE_LEFT SDL_SCANCODE_LEFT +#define SDLC_KEYSTATE_RIGHT SDL_SCANCODE_RIGHT +#else +#define SDLC_KEYSTATE_LEFTSHIFT SDLK_LSHIFT +#define SDLC_KEYSTATE_RIGHTSHIFT SDLK_LSHIFT +#define SDLC_KEYSTATE_MENU SDLK_MENU +#define SDLC_KEYSTATE_UP SDLK_UP +#define SDLC_KEYSTATE_DOWN SDLK_DOWN +#define SDLC_KEYSTATE_LEFT SDLK_LEFT +#define SDLC_KEYSTATE_RIGHT SDLK_RIGHT +#endif + +inline const Uint8 *SDLC_GetKeyState() +{ +#ifndef USE_SDL1 + return SDL_GetKeyboardState(nullptr); +#else + return SDL_GetKeyState(nullptr); +#endif +} + inline int SDLC_SetColorKey(SDL_Surface *surface, Uint32 key) { #ifdef USE_SDL1 diff --git a/SourceX/DiabloUI/credits.cpp b/SourceX/DiabloUI/credits.cpp index 78ada6966..82dc9c731 100644 --- a/SourceX/DiabloUI/credits.cpp +++ b/SourceX/DiabloUI/credits.cpp @@ -2,6 +2,7 @@ #include #include +#include "controls/menu_controls.h" #include "devilution.h" #include "miniwin/ddraw.h" @@ -259,6 +260,15 @@ BOOL UiCreditsDialog(int a1) break; case SDL_QUIT: exit(0); + default: + switch (GetMenuAction(event)) { + case MenuAction::BACK: + case MenuAction::SELECT: + endMenu = true; + break; + default: + break; + } } } } while (!endMenu && !credits_renderer.Finished()); diff --git a/SourceX/DiabloUI/diabloui.cpp b/SourceX/DiabloUI/diabloui.cpp index 8276fad3b..6dee45478 100644 --- a/SourceX/DiabloUI/diabloui.cpp +++ b/SourceX/DiabloUI/diabloui.cpp @@ -5,6 +5,8 @@ #include #include +#include "controls/menu_controls.h" + #include "DiabloUI/scrollbar.h" #include "DiabloUI/diabloui.h" @@ -181,6 +183,34 @@ bool UiFocusNavigation(SDL_Event *event) if (event->type == SDL_QUIT) exit(0); + switch (GetMenuAction(*event)) { + case MenuAction::SELECT: + UiFocusNavigationSelect(); + return true; + case MenuAction::UP: + UiFocus(SelectedItem - 1, UiItemsWraps); + return true; + case MenuAction::DOWN: + UiFocus(SelectedItem + 1, UiItemsWraps); + return true; + case MenuAction::PAGE_UP: + UiFocusPageUp(); + return true; + case MenuAction::PAGE_DOWN: + UiFocusPageDown(); + return true; + case MenuAction::DELETE: + UiFocusNavigationYesNo(); + return true; + case MenuAction::BACK: + if (!gfnListEsc) + break; + UiFocusNavigationEsc(); + return true; + default: + break; + } + switch (event->type) { case SDL_KEYUP: case SDL_MOUSEBUTTONUP: @@ -203,39 +233,6 @@ bool UiFocusNavigation(SDL_Event *event) mainmenu_restart_repintro(); } - if (event->type == SDL_KEYDOWN) { - switch (event->key.keysym.sym) { - case SDLK_UP: - UiFocus(SelectedItem - 1, UiItemsWraps); - return true; - case SDLK_DOWN: - UiFocus(SelectedItem + 1, UiItemsWraps); - return true; - case SDLK_TAB: - if (SDL_GetModState() & KMOD_SHIFT) - UiFocus(SelectedItem - 1, UiItemsWraps); - else - UiFocus(SelectedItem + 1, UiItemsWraps); - return true; - case SDLK_PAGEUP: - UiFocusPageUp(); - return true; - case SDLK_PAGEDOWN: - UiFocusPageDown(); - return true; - case SDLK_RETURN: - case SDLK_KP_ENTER: - case SDLK_SPACE: - UiFocusNavigationSelect(); - return true; - case SDLK_DELETE: - UiFocusNavigationYesNo(); - return true; - default: - break; - } - } - if (SDL_IsTextInputActive()) { switch (event->type) { case SDL_KEYDOWN: { @@ -289,11 +286,6 @@ bool UiFocusNavigation(SDL_Event *event) if (UiItemMouseEvents(event, gUiItems, gUiItemCnt)) return true; - if (gfnListEsc && event->type == SDL_KEYDOWN && event->key.keysym.sym == SDLK_ESCAPE) { - UiFocusNavigationEsc(); - return true; - } - return false; } diff --git a/SourceX/DiabloUI/dialogs.cpp b/SourceX/DiabloUI/dialogs.cpp index a5b67620c..7cc5a76e7 100644 --- a/SourceX/DiabloUI/dialogs.cpp +++ b/SourceX/DiabloUI/dialogs.cpp @@ -1,5 +1,6 @@ #include "DiabloUI/dialogs.h" +#include "controls/menu_controls.h" #include "devilution.h" #include "dx.h" #include "DiabloUI/diabloui.h" @@ -257,24 +258,22 @@ void DialogLoop(UiItem *items, std::size_t num_items, UiItem *render_behind, std do { while (SDL_PollEvent(&event)) { switch (event.type) { - case SDL_KEYDOWN: - switch (event.key.keysym.sym) { - case SDLK_ESCAPE: - case SDLK_RETURN: - case SDLK_KP_ENTER: - case SDLK_SPACE: - state = State::OK; - break; - default: - break; - } - break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: UiItemMouseEvents(&event, items, num_items); break; case SDL_QUIT: exit(0); + default: + switch (GetMenuAction(event)) { + case MenuAction::BACK: + case MenuAction::SELECT: + state = State::OK; + break; + default: + break; + } + break; } } diff --git a/SourceX/DiabloUI/title.cpp b/SourceX/DiabloUI/title.cpp index 665e279bc..6bdb43b64 100644 --- a/SourceX/DiabloUI/title.cpp +++ b/SourceX/DiabloUI/title.cpp @@ -1,4 +1,5 @@ #include "devilution.h" +#include "controls/menu_controls.h" #include "DiabloUI/diabloui.h" namespace dvl { @@ -34,6 +35,10 @@ void UiTitleDialog() UiFadeIn(); while (SDL_PollEvent(&event)) { + if (GetMenuAction(event) != MenuAction::NONE) { + endMenu = true; + break; + } switch (event.type) { case SDL_KEYDOWN: /* To match the original uncomment this if (event.key.keysym.sym == SDLK_UP diff --git a/SourceX/controls/README.md b/SourceX/controls/README.md new file mode 100644 index 000000000..8689f41c3 --- /dev/null +++ b/SourceX/controls/README.md @@ -0,0 +1,20 @@ +# Controls handling + +DevilutionX supports mouse & keyboard and gamepad input. + +This directory currently mostly handles gamepad input. + +Low-level gamepad handling is abstracted and 3 implementations are provided: + +1. SDL2 controller API. + +2. SDL 1&2 joystick API. + + This can be used in SDL1 joystick platforms and for mapping additional + buttons not defined by SDL2 controller mappings (e.g. additional Nintendo + Switch arrows). + +3. Keyboard keys acting as controller buttons. + + This can be used for testing, or on devices where this is the + only or the easiest API to use (e.g. RetroFW). diff --git a/SourceX/controls/controller.cpp b/SourceX/controls/controller.cpp new file mode 100644 index 000000000..4a3eb4779 --- /dev/null +++ b/SourceX/controls/controller.cpp @@ -0,0 +1,65 @@ +#include "controls/controller.h" + +#include "controls/devices/kbcontroller.h" +#include "controls/devices/joystick.h" +#include "controls/devices/game_controller.h" + +namespace dvl { + +ControllerButtonEvent ToControllerButtonEvent(const SDL_Event &event) +{ + ControllerButtonEvent result{ ControllerButton::NONE, false }; + switch (event.type) { +#ifndef USE_SDL1 + case SDL_CONTROLLERAXISMOTION: + result.up = event.caxis.value == 0; + break; + case SDL_CONTROLLERBUTTONUP: +#endif + case SDL_JOYBUTTONUP: + case SDL_KEYUP: + result.up = true; + break; + default: + break; + } + +#if HAS_KBCTRL == 1 + result.button = KbCtrlToControllerButton(event); + if (result.button != ControllerButton::NONE) + return result; +#endif + +#ifndef USE_SDL1 + result.button = GameControllerToControllerButton(event); + if (result.button != ControllerButton::NONE) + return result; +#endif + + result.button = JoyButtonToControllerButton(event); + + return result; +} + +bool IsControllerButtonPressed(ControllerButton button) +{ + bool result = false; +#ifndef USE_SDL1 + result = result || IsGameControllerButtonPressed(button); +#endif +#if HAS_KBCTRL == 1 + result = result || IsKbCtrlButtonPressed(button); +#endif + result = result || IsJoystickButtonPressed(button); + return result; +} + +void InitController() +{ + InitJoystick(); +#ifndef USE_SDL1 + InitGameController(); +#endif +} + +} // namespace dvl diff --git a/SourceX/controls/controller.h b/SourceX/controls/controller.h new file mode 100644 index 000000000..4a46d021c --- /dev/null +++ b/SourceX/controls/controller.h @@ -0,0 +1,18 @@ +#pragma once + +#include "controls/controller_buttons.h" + +namespace dvl { + +struct ControllerButtonEvent { + ControllerButton button; + bool up; +}; + +ControllerButtonEvent ToControllerButtonEvent(const SDL_Event &event); + +bool IsControllerButtonPressed(ControllerButton button); + +void InitController(); + +} // namespace dvl diff --git a/SourceX/controls/controller_buttons.h b/SourceX/controls/controller_buttons.h new file mode 100644 index 000000000..4f5a136fa --- /dev/null +++ b/SourceX/controls/controller_buttons.h @@ -0,0 +1,30 @@ +#pragma once +// Unifies joystick, gamepad, and keyboard controller APIs. + +#include "devilution.h" + +namespace dvl { + +// NOTE: A, B, X, Y refer to physical positions on an XBox 360 controller. +// A<->B and X<->Y are reversed on a Nintendo controller. +enum class ControllerButton { + NONE = 0, + AXIS_TRIGGERLEFT, // ZL (aka L2) + AXIS_TRIGGERRIGHT, // ZR (aka R2) + BUTTON_A, // Bottom button + BUTTON_B, // Right button + BUTTON_X, // Left button + BUTTON_Y, // TOP button + BUTTON_LEFTSTICK, + BUTTON_RIGHTSTICK, + BUTTON_LEFTSHOULDER, + BUTTON_RIGHTSHOULDER, + BUTTON_START, + BUTTON_BACK, + BUTTON_DPAD_UP, + BUTTON_DPAD_DOWN, + BUTTON_DPAD_LEFT, + BUTTON_DPAD_RIGHT +}; + +} // namespace dvl diff --git a/SourceX/controls/controller_motion.cpp b/SourceX/controls/controller_motion.cpp new file mode 100644 index 000000000..d97df7c1f --- /dev/null +++ b/SourceX/controls/controller_motion.cpp @@ -0,0 +1,91 @@ +#include "controls/controller_motion.h" + +#include "controls/devices/game_controller.h" + +namespace dvl { + +namespace { + +void ScaleJoystickAxes(float *x, float *y, float deadzone) +{ + //radial and scaled dead_zone + //http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html + //input values go from -32767.0...+32767.0, output values are from -1.0 to 1.0; + + if (deadzone == 0) { + return; + } + if (deadzone >= 1.0) { + *x = 0; + *y = 0; + return; + } + + const float maximum = 32767.0f; + float analog_x = *x; + float analog_y = *y; + float dead_zone = deadzone * maximum; + + float magnitude = sqrtf(analog_x * analog_x + analog_y * analog_y); + if (magnitude >= dead_zone) { + // find scaled axis values with magnitudes between zero and maximum + float scalingFactor = 1.0 / magnitude * (magnitude - dead_zone) / (maximum - dead_zone); + analog_x = (analog_x * scalingFactor); + analog_y = (analog_y * scalingFactor); + + // clamp to ensure results will never exceed the max_axis value + float clamping_factor = 1.0f; + float abs_analog_x = fabs(analog_x); + float abs_analog_y = fabs(analog_y); + if (abs_analog_x > 1.0 || abs_analog_y > 1.0) { + if (abs_analog_x > abs_analog_y) { + clamping_factor = 1 / abs_analog_x; + } else { + clamping_factor = 1 / abs_analog_y; + } + } + *x = (clamping_factor * analog_x); + *y = (clamping_factor * analog_y); + } else { + *x = 0; + *y = 0; + } +} + +} // namespace + +float leftStickX, leftStickY, rightStickX, rightStickY; +float leftStickXUnscaled, leftStickYUnscaled, rightStickXUnscaled, rightStickYUnscaled; +bool leftStickNeedsScaling, rightStickNeedsScaling; + +void ScaleJoysticks() +{ + constexpr float rightDeadzone = 0.07; + constexpr float leftDeadzone = 0.07; + + if (leftStickNeedsScaling) { + leftStickX = leftStickXUnscaled; + leftStickY = leftStickYUnscaled; + ScaleJoystickAxes(&leftStickX, &leftStickY, leftDeadzone); + leftStickNeedsScaling = false; + } + + if (rightStickNeedsScaling) { + rightStickX = rightStickXUnscaled; + rightStickY = rightStickYUnscaled; + ScaleJoystickAxes(&rightStickX, &rightStickY, rightDeadzone); + rightStickNeedsScaling = false; + } +} + +// Updates motion state for mouse and joystick sticks. +bool ProcessControllerMotion(const SDL_Event &event) +{ +#ifndef USE_SDL1 + if (ProcessGameControllerAxisMotion(event)) + return true; +#endif + return false; +} + +} // namespace dvl diff --git a/SourceX/controls/controller_motion.h b/SourceX/controls/controller_motion.h new file mode 100644 index 000000000..e2dfca1a4 --- /dev/null +++ b/SourceX/controls/controller_motion.h @@ -0,0 +1,23 @@ +#pragma once + +// Processes and stores mouse and joystick motion. + +#include "devilution.h" + +namespace dvl { + +// Raw axis values. +extern float leftStickXUnscaled, leftStickYUnscaled, rightStickXUnscaled, rightStickYUnscaled; + +// Axis values scaled to [-1, 1] range and clamped to a deadzone. +extern float leftStickX, leftStickY, rightStickX, rightStickY; + +// Whether stick positions have been updated and need rescaling. +extern bool leftStickNeedsScaling, rightStickNeedsScaling; + +void ScaleJoysticks(); + +// Updates motion state for mouse and joystick sticks. +bool ProcessControllerMotion(const SDL_Event &event); + +} // namespace dvl diff --git a/SourceX/controls/devices/game_controller.cpp b/SourceX/controls/devices/game_controller.cpp new file mode 100644 index 000000000..ce4f05b3a --- /dev/null +++ b/SourceX/controls/devices/game_controller.cpp @@ -0,0 +1,159 @@ +#include "controls/devices/game_controller.h" + +#ifndef USE_SDL1 +#include "controls/controller_motion.h" +#include "controls/devices/joystick.h" +#include "stubs.h" + +namespace dvl { + +static SDL_GameController *current_game_controller = nullptr; + +ControllerButton GameControllerToControllerButton(const SDL_Event &event) +{ + switch (event.type) { + case SDL_CONTROLLERAXISMOTION: + switch (event.caxis.axis) { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return ControllerButton::AXIS_TRIGGERLEFT; + break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return ControllerButton::AXIS_TRIGGERRIGHT; + break; + } + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + switch (event.cbutton.button) { + case SDL_CONTROLLER_BUTTON_A: + return ControllerButton::BUTTON_A; + case SDL_CONTROLLER_BUTTON_B: + return ControllerButton::BUTTON_B; + case SDL_CONTROLLER_BUTTON_X: + return ControllerButton::BUTTON_X; + case SDL_CONTROLLER_BUTTON_Y: + return ControllerButton::BUTTON_Y; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return ControllerButton::BUTTON_LEFTSTICK; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return ControllerButton::BUTTON_RIGHTSTICK; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return ControllerButton::BUTTON_LEFTSHOULDER; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return ControllerButton::BUTTON_RIGHTSHOULDER; + case SDL_CONTROLLER_BUTTON_START: + return ControllerButton::BUTTON_START; + case SDL_CONTROLLER_BUTTON_BACK: + return ControllerButton::BUTTON_BACK; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + return ControllerButton::BUTTON_DPAD_UP; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + return ControllerButton::BUTTON_DPAD_DOWN; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + return ControllerButton::BUTTON_DPAD_LEFT; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + return ControllerButton::BUTTON_DPAD_RIGHT; + default: + break; + } + default: + break; + } + return ControllerButton::NONE; +} + +namespace { + +SDL_GameControllerButton ControllerButtonToGameControllerButton(ControllerButton button) +{ + if (button == ControllerButton::AXIS_TRIGGERLEFT || button == ControllerButton::AXIS_TRIGGERRIGHT) + UNIMPLEMENTED(); + switch (button) { + case ControllerButton::BUTTON_A: + return SDL_CONTROLLER_BUTTON_A; + case ControllerButton::BUTTON_B: + return SDL_CONTROLLER_BUTTON_B; + case ControllerButton::BUTTON_X: + return SDL_CONTROLLER_BUTTON_X; + case ControllerButton::BUTTON_Y: + return SDL_CONTROLLER_BUTTON_Y; + case ControllerButton::BUTTON_BACK: + return SDL_CONTROLLER_BUTTON_BACK; + case ControllerButton::BUTTON_START: + return SDL_CONTROLLER_BUTTON_START; + case ControllerButton::BUTTON_LEFTSTICK: + return SDL_CONTROLLER_BUTTON_LEFTSTICK; + case ControllerButton::BUTTON_RIGHTSTICK: + return SDL_CONTROLLER_BUTTON_RIGHTSTICK; + case ControllerButton::BUTTON_LEFTSHOULDER: + return SDL_CONTROLLER_BUTTON_LEFTSHOULDER; + case ControllerButton::BUTTON_RIGHTSHOULDER: + return SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; + case ControllerButton::BUTTON_DPAD_UP: + return SDL_CONTROLLER_BUTTON_DPAD_UP; + case ControllerButton::BUTTON_DPAD_DOWN: + return SDL_CONTROLLER_BUTTON_DPAD_DOWN; + case ControllerButton::BUTTON_DPAD_LEFT: + return SDL_CONTROLLER_BUTTON_DPAD_LEFT; + case ControllerButton::BUTTON_DPAD_RIGHT: + return SDL_CONTROLLER_BUTTON_DPAD_RIGHT; + default: + return SDL_CONTROLLER_BUTTON_INVALID; + } +} + +} // namespace + +bool IsGameControllerButtonPressed(ControllerButton button) +{ + if (current_game_controller == nullptr) + return false; + const SDL_GameControllerButton gc_button = ControllerButtonToGameControllerButton(button); + return gc_button != SDL_CONTROLLER_BUTTON_INVALID && SDL_GameControllerGetButton(current_game_controller, gc_button); +} + +bool ProcessGameControllerAxisMotion(const SDL_Event &event) +{ + if (event.type != SDL_CONTROLLERAXISMOTION) + return false; + switch (event.caxis.axis) { + case SDL_CONTROLLER_AXIS_LEFTX: + leftStickXUnscaled = event.caxis.value; + leftStickNeedsScaling = true; + break; + case SDL_CONTROLLER_AXIS_LEFTY: + leftStickYUnscaled = -event.caxis.value; + leftStickNeedsScaling = true; + break; + case SDL_CONTROLLER_AXIS_RIGHTX: + rightStickXUnscaled = event.caxis.value; + rightStickNeedsScaling = true; + break; + case SDL_CONTROLLER_AXIS_RIGHTY: + rightStickYUnscaled = -event.caxis.value; + rightStickNeedsScaling = true; + break; + default: + return false; + } + return true; +} + +SDL_GameController *CurrentGameController() +{ + return current_game_controller; +} + +void InitGameController() +{ + if (CurrentJoystickIndex() == -1) + return; + const SDL_JoystickGUID guid = SDL_JoystickGetGUID(CurrentJoystick()); + SDL_Log("Opening gamepad %d: %s", CurrentJoystickIndex(), SDL_GameControllerMappingForGUID(guid)); + current_game_controller = SDL_GameControllerOpen(CurrentJoystickIndex()); + if (current_game_controller == nullptr) + SDL_Log(SDL_GetError()); +} + +} // namespace dvl +#endif diff --git a/SourceX/controls/devices/game_controller.h b/SourceX/controls/devices/game_controller.h new file mode 100644 index 000000000..864ebc9c5 --- /dev/null +++ b/SourceX/controls/devices/game_controller.h @@ -0,0 +1,22 @@ + +#pragma once + +#include +#include "controls/controller_buttons.h" + +#ifndef USE_SDL1 +namespace dvl { + +ControllerButton GameControllerToControllerButton(const SDL_Event &event); + +bool IsGameControllerButtonPressed(ControllerButton button); + +bool ProcessGameControllerAxisMotion(const SDL_Event &event); + +SDL_GameController *CurrentGameController(); + +// Must be called after InitJoystick(). +void InitGameController(); + +} // namespace dvl +#endif diff --git a/SourceX/controls/devices/joystick.cpp b/SourceX/controls/devices/joystick.cpp new file mode 100644 index 000000000..bd48c6414 --- /dev/null +++ b/SourceX/controls/devices/joystick.cpp @@ -0,0 +1,193 @@ + +#include "controls/devices/joystick.h" + +#include "stubs.h" + +#ifdef SWITCH +#define JOY_BUTTON_DPAD_LEFT 16 +#define JOY_BUTTON_DPAD_UP 17 +#define JOY_BUTTON_DPAD_RIGHT 18 +#define JOY_BUTTON_DPAD_DOWN 19 +#endif + +namespace dvl { + +ControllerButton JoyButtonToControllerButton(const SDL_Event &event) +{ + switch (event.type) { + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + switch (event.jbutton.button) { +#ifdef JOY_BUTTON_A + case JOY_BUTTON_A: + return ControllerButton::BUTTON_A; +#endif +#ifdef JOY_BUTTON_B + case JOY_BUTTON_B: + return ControllerButton::BUTTON_B; +#endif +#ifdef JOY_BUTTON_X + case JOY_BUTTON_X: + return ControllerButton::BUTTON_X; +#endif +#ifdef JOY_BUTTON_Y + case JOY_BUTTON_Y: + return ControllerButton::BUTTON_Y; +#endif +#ifdef JOY_BUTTON_LEFTSTICK + case JOY_BUTTON_LEFTSTICK: + return ControllerButton::BUTTON_LEFTSTICK; +#endif +#ifdef JOY_BUTTON_RIGHTSTICK + case JOY_BUTTON_RIGHTSTICK: + return ControllerButton::BUTTON_RIGHTSTICK; +#endif +#ifdef JOY_BUTTON_LEFTSHOULDER + case JOY_BUTTON_LEFTSHOULDER: + return ControllerButton::BUTTON_LEFTSHOULDER; +#endif +#ifdef JOY_BUTTON_RIGHTSHOULDER + case JOY_BUTTON_RIGHTSHOULDER: + return ControllerButton::BUTTON_RIGHTSHOULDER; +#endif +#ifdef JOY_BUTTON_START + case JOY_BUTTON_START: + return ControllerButton::BUTTON_START; +#endif +#ifdef JOY_BUTTON_BACK + case JOY_BUTTON_BACK: + return ControllerButton::BUTTON_BACK; +#endif +#ifdef JOY_BUTTON_DPAD_LEFT + case JOY_BUTTON_DPAD_LEFT: + return ControllerButton::BUTTON_DPAD_LEFT; +#endif +#ifdef JOY_BUTTON_DPAD_UP + case JOY_BUTTON_DPAD_UP: + return ControllerButton::BUTTON_DPAD_UP; +#endif +#ifdef JOY_BUTTON_DPAD_RIGHT + case JOY_BUTTON_DPAD_RIGHT: + return ControllerButton::BUTTON_DPAD_RIGHT; +#endif +#ifdef JOY_BUTTON_DPAD_DOWN + case JOY_BUTTON_DPAD_DOWN: + return ControllerButton::BUTTON_DPAD_DOWN; +#endif + default: + break; + } + break; + } + return ControllerButton::NONE; +} + +int JoyButtonToControllerButton(ControllerButton button) { + if (button == ControllerButton::AXIS_TRIGGERLEFT || button == ControllerButton::AXIS_TRIGGERRIGHT) + UNIMPLEMENTED(); + switch (button) { +#ifdef JOY_BUTTON_A + case ControllerButton::BUTTON_A: + return JOY_BUTTON_A; +#endif +#ifdef JOY_BUTTON_B + case ControllerButton::BUTTON_B: + return JOY_BUTTON_B; +#endif +#ifdef JOY_BUTTON_X + case ControllerButton::BUTTON_X: + return JOY_BUTTON_X; +#endif +#ifdef JOY_BUTTON_Y + case ControllerButton::BUTTON_Y: + return JOY_BUTTON_Y; +#endif +#ifdef JOY_BUTTON_BACK + case ControllerButton::BUTTON_BACK: + return JOY_BUTTON_BACK; +#endif +#ifdef JOY_BUTTON_START + case ControllerButton::BUTTON_START: + return JOY_BUTTON_START; +#endif +#ifdef JOY_BUTTON_LEFTSTICK + case ControllerButton::BUTTON_LEFTSTICK: + return JOY_BUTTON_LEFTSTICK; +#endif +#ifdef JOY_BUTTON_RIGHTSTICK + case ControllerButton::BUTTON_RIGHTSTICK: + return JOY_BUTTON_RIGHTSTICK; +#endif +#ifdef JOY_BUTTON_LEFTSHOULDER + case ControllerButton::BUTTON_LEFTSHOULDER: + return JOY_BUTTON_LEFTSHOULDER; +#endif +#ifdef JOY_BUTTON_RIGHTSHOULDER + case ControllerButton::BUTTON_RIGHTSHOULDER: + return JOY_BUTTON_RIGHTSHOULDER; +#endif +#ifdef JOY_BUTTON_DPAD_UP + case ControllerButton::BUTTON_DPAD_UP: + return JOY_BUTTON_DPAD_UP; +#endif +#ifdef JOY_BUTTON_DPAD_DOWN + case ControllerButton::BUTTON_DPAD_DOWN: + return JOY_BUTTON_DPAD_DOWN; +#endif +#ifdef JOY_BUTTON_DPAD_LEFT + case ControllerButton::BUTTON_DPAD_LEFT: + return JOY_BUTTON_DPAD_LEFT; +#endif +#ifdef JOY_BUTTON_DPAD_RIGHT + case ControllerButton::BUTTON_DPAD_RIGHT: + return JOY_BUTTON_DPAD_RIGHT; +#endif + default: + return -1; + } +} + +bool IsJoystickButtonPressed(ControllerButton button) { + if (CurrentJoystick() == nullptr) + return false; + const int joy_button = JoyButtonToControllerButton(button); + return joy_button != -1 && SDL_JoystickGetButton(CurrentJoystick(), joy_button); +} + +static SDL_Joystick *current_joystick = nullptr; + +SDL_Joystick *CurrentJoystick() +{ + return current_joystick; +} + +static int current_joystick_index = -1; + +int CurrentJoystickIndex() +{ + return current_joystick_index; +} + +void InitJoystick() +{ + if (SDL_NumJoysticks() == 0) + return; + + // Get the first available controller. + for (int i = 0; i < SDL_NumJoysticks(); ++i) { +#ifndef USE_SDL1 + if (!SDL_IsGameController(i)) + continue; +#endif + SDL_Log("Initializing joystick %d: %s", i, SDL_JoystickNameForIndex(i)); + current_joystick = SDL_JoystickOpen(i); + if (current_joystick == nullptr) { + SDL_Log(SDL_GetError()); + continue; + } + current_joystick_index = i; + break; + } +} + +} // namespace dvl diff --git a/SourceX/controls/devices/joystick.h b/SourceX/controls/devices/joystick.h new file mode 100644 index 000000000..815fdc74b --- /dev/null +++ b/SourceX/controls/devices/joystick.h @@ -0,0 +1,19 @@ +#pragma once + +// Joystick mappings for SDL1 and additional buttons on SDL2. + +#include +#include "controls/controller_buttons.h" + +namespace dvl { + +ControllerButton JoyButtonToControllerButton(const SDL_Event &event); + +bool IsJoystickButtonPressed(ControllerButton button); + +SDL_Joystick *CurrentJoystick(); +int CurrentJoystickIndex(); + +void InitJoystick(); + +} // namespace dvl diff --git a/SourceX/controls/devices/kbcontroller.cpp b/SourceX/controls/devices/kbcontroller.cpp new file mode 100644 index 000000000..6653c5297 --- /dev/null +++ b/SourceX/controls/devices/kbcontroller.cpp @@ -0,0 +1,202 @@ +#include "controls/devices/kbcontroller.h" + +#if defined(RETROFW) +#define HAS_KBCTRL 1 + +#define KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT +#define KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT +#define KBCTRL_BUTTON_DPAD_UP SDLK_UP +#define KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN + +#define KBCTRL_BUTTON_B SDLK_LCTRL +#define KBCTRL_BUTTON_A SDLK_LALT +#define KBCTRL_BUTTON_Y SDLK_SPACE +#define KBCTRL_BUTTON_X SDLK_LSHIFT +#define KBCTRL_BUTTON_RIGHTSHOULDER SDLK_BACKSPACE +#define KBCTRL_BUTTON_LEFTSHOULDER SDLK_TAB +#define KBCTRL_BUTTON_START SDLK_RETURN +#define KBCTRL_BUTTON_BACK SDLK_ESCAPE +#define KBCTRL_MODIFIER_KEY SDLK_END // The suspend key on RG300 +#endif + +#if HAS_KBCTRL == 1 + +#include "sdl2_to_1_2_backports.h" +#include "sdl_compat.h" +#include "stubs.h" + +namespace dvl { + +namespace { + +bool IsModifierKey() +{ +#ifdef KBCTRL_MODIFIER_KEY + return SDLC_GetKeyboardState(nullptr)[KBCTRL_MODIFIER_KEY]; +#else + return false; +#endif +} + +} // namespace + +ControllerButton KbCtrlToControllerButton(const SDL_Event &event) +{ + switch (event.type) { + case SDL_KEYDOWN: + case SDL_KEYUP: + switch (event.key.keysym.sym) { +#ifdef KBCTRL_BUTTON_A + case KBCTRL_BUTTON_A: + return ControllerButton::BUTTON_A; +#endif +#ifdef KBCTRL_BUTTON_B + case KBCTRL_BUTTON_B: + return ControllerButton::BUTTON_B; +#endif +#ifdef KBCTRL_BUTTON_X + case KBCTRL_BUTTON_X: + if (IsModifierKey()) + return ControllerButton::BUTTON_LEFTSTICK; + return ControllerButton::BUTTON_X; +#endif +#ifdef KBCTRL_BUTTON_Y + case KBCTRL_BUTTON_Y: + if (IsModifierKey()) + return ControllerButton::BUTTON_RIGHTSTICK; + return ControllerButton::BUTTON_Y; +#endif +#ifdef KBCTRL_BUTTON_LEFTSTICK + case KBCTRL_BUTTON_LEFTSTICK: + return ControllerButton::BUTTON_LEFTSTICK; +#endif +#ifdef KBCTRL_BUTTON_RIGHTSTICK + case KBCTRL_BUTTON_RIGHTSTICK: + return ControllerButton::BUTTON_RIGHTSTICK; +#endif +#ifdef KBCTRL_BUTTON_LEFTSHOULDER + case KBCTRL_BUTTON_LEFTSHOULDER: + if (IsModifierKey()) + return ControllerButton::AXIS_TRIGGERLEFT; + return ControllerButton::BUTTON_LEFTSHOULDER; +#endif +#ifdef KBCTRL_BUTTON_RIGHTSHOULDER + case KBCTRL_BUTTON_RIGHTSHOULDER: + if (IsModifierKey()) + return ControllerButton::AXIS_TRIGGERRIGHT; + return ControllerButton::BUTTON_RIGHTSHOULDER; +#endif +#ifdef KBCTRL_BUTTON_START + case KBCTRL_BUTTON_START: + return ControllerButton::BUTTON_START; +#endif +#ifdef KBCTRL_BUTTON_BACK + case KBCTRL_BUTTON_BACK: + return ControllerButton::BUTTON_BACK; +#endif +#ifdef KBCTRL_BUTTON_DPAD_UP + case KBCTRL_BUTTON_DPAD_UP: + return ControllerButton::BUTTON_DPAD_UP; +#endif +#ifdef KBCTRL_BUTTON_DPAD_DOWN + case KBCTRL_BUTTON_DPAD_DOWN: + return ControllerButton::BUTTON_DPAD_DOWN; +#endif +#ifdef KBCTRL_BUTTON_DPAD_LEFT + case KBCTRL_BUTTON_DPAD_LEFT: + return ControllerButton::BUTTON_DPAD_LEFT; +#endif +#ifdef KBCTRL_BUTTON_DPAD_RIGHT + case KBCTRL_BUTTON_DPAD_RIGHT: + return ControllerButton::BUTTON_DPAD_RIGHT; +#endif + default: + return ControllerButton::NONE; + } + } +} + +namespace { + +int ControllerButtonToKbCtrlKeyCode(ControllerButton button) +{ + if (button == ControllerButton::AXIS_TRIGGERLEFT || button == ControllerButton::AXIS_TRIGGERRIGHT) + UNIMPLEMENTED(); + switch (button) { +#ifdef KBCTRL_BUTTON_A + case ControllerButton::BUTTON_A: + return KBCTRL_BUTTON_A; +#endif +#ifdef KBCTRL_BUTTON_B + case ControllerButton::BUTTON_B: + return KBCTRL_BUTTON_B; +#endif +#ifdef KBCTRL_BUTTON_X + case ControllerButton::BUTTON_X: + return KBCTRL_BUTTON_X; +#endif +#ifdef KBCTRL_BUTTON_Y + case ControllerButton::BUTTON_Y: + return KBCTRL_BUTTON_Y; +#endif +#ifdef KBCTRL_BUTTON_BACK + case ControllerButton::BUTTON_BACK: + return KBCTRL_BUTTON_BACK; +#endif +#ifdef KBCTRL_BUTTON_START + case ControllerButton::BUTTON_START: + return KBCTRL_BUTTON_START; +#endif +#ifdef KBCTRL_BUTTON_LEFTSTICK + case ControllerButton::BUTTON_LEFTSTICK: + return KBCTRL_BUTTON_LEFTSTICK; +#endif +#ifdef KBCTRL_BUTTON_RIGHTSTICK + case ControllerButton::BUTTON_RIGHTSTICK: + return KBCTRL_BUTTON_RIGHTSTICK; +#endif +#ifdef KBCTRL_BUTTON_LEFTSHOULDER + case ControllerButton::BUTTON_LEFTSHOULDER: + return KBCTRL_BUTTON_LEFTSHOULDER; +#endif +#ifdef KBCTRL_BUTTON_RIGHTSHOULDER + case ControllerButton::BUTTON_RIGHTSHOULDER: + return KBCTRL_BUTTON_RIGHTSHOULDER; +#endif +#ifdef KBCTRL_BUTTON_DPAD_UP + case ControllerButton::BUTTON_DPAD_UP: + return KBCTRL_BUTTON_DPAD_UP; +#endif +#ifdef KBCTRL_BUTTON_DPAD_DOWN + case ControllerButton::BUTTON_DPAD_DOWN: + return KBCTRL_BUTTON_DPAD_DOWN; +#endif +#ifdef KBCTRL_BUTTON_DPAD_LEFT + case ControllerButton::BUTTON_DPAD_LEFT: + return KBCTRL_BUTTON_DPAD_LEFT; +#endif +#ifdef KBCTRL_BUTTON_DPAD_RIGHT + case ControllerButton::BUTTON_DPAD_RIGHT: + return KBCTRL_BUTTON_DPAD_RIGHT; +#endif + default: + return -1; + } +} + +} // namespace + +bool IsKbCtrlButtonPressed(ControllerButton button) +{ + int key_code = ControllerButtonToKbCtrlKeyCode(button); + if (key_code == -1) + return false; +#ifndef USE_SDL1 + return SDL_GetKeyboardState(nullptr)[SDL_GetScancodeFromKey(key_code)]; +#else + return SDL_GetKeyState(nullptr)[key_code]; +#endif +} + +} // namespace dvl +#endif diff --git a/SourceX/controls/devices/kbcontroller.h b/SourceX/controls/devices/kbcontroller.h new file mode 100644 index 000000000..663979916 --- /dev/null +++ b/SourceX/controls/devices/kbcontroller.h @@ -0,0 +1,23 @@ +#pragma once + +// Keyboard keys acting like gamepad buttons +#ifndef HAS_KBCTRL +#define HAS_KBCTRL 0 +#endif + +#if defined(RETROFW) +#define HAS_KBCTRL 1 +#endif + +#if HAS_KBCTRL == 1 +#include +#include "controls/controller_buttons.h" + +namespace dvl { + +ControllerButton KbCtrlToControllerButton(const SDL_Event &event); + +bool IsKbCtrlButtonPressed(ControllerButton button); + +} // namespace dvl +#endif diff --git a/SourceX/controls/game_controls.cpp b/SourceX/controls/game_controls.cpp new file mode 100644 index 000000000..3b0bab320 --- /dev/null +++ b/SourceX/controls/game_controls.cpp @@ -0,0 +1,156 @@ +#include "controls/game_controls.h" + +#include + +#include "controls/controller.h" +#include "controls/controller_motion.h" +#include "controls/devices/game_controller.h" +#include "controls/devices/joystick.h" +#include "controls/menu_controls.h" +#include "controls/plrctrls.h" + +namespace dvl { + +namespace { + +DWORD translate_controller_button_to_key(ControllerButton controller_button) +{ + switch (controller_button) { + case ControllerButton::BUTTON_A: // Bottom button + return questlog ? DVL_VK_SPACE : DVL_VK_ESCAPE; + case ControllerButton::BUTTON_B: // Right button + return sgpCurrentMenu || stextflag || questlog ? DVL_VK_RETURN : DVL_VK_SPACE; + case ControllerButton::BUTTON_X: // Left button + return 'X'; + case ControllerButton::BUTTON_Y: // Top button + return DVL_VK_RETURN; + case ControllerButton::BUTTON_LEFTSTICK: + return 'Q'; // Quest log + case ControllerButton::BUTTON_START: + return DVL_VK_ESCAPE; + case ControllerButton::BUTTON_BACK: + return DVL_VK_TAB; // Map + case ControllerButton::BUTTON_DPAD_LEFT: + return DVL_VK_LEFT; + case ControllerButton::BUTTON_DPAD_RIGHT: + return DVL_VK_RIGHT; + case ControllerButton::BUTTON_DPAD_UP: + return DVL_VK_UP; + case ControllerButton::BUTTON_DPAD_DOWN: + return DVL_VK_DOWN; + default: + return 0; + } +} + +} // namespace + +bool GetGameAction(const SDL_Event &event, GameAction *action) +{ + const ControllerButtonEvent ctrl_event = ToControllerButtonEvent(event); + switch (ctrl_event.button) { + case ControllerButton::AXIS_TRIGGERLEFT: // ZL (aka L2) + if (!ctrl_event.up) + *action = GameAction(GameActionType::USE_HEALTH_POTION); + return true; + case ControllerButton::AXIS_TRIGGERRIGHT: // ZR (aka R2) + if (!ctrl_event.up) + *action = GameAction(GameActionType::USE_MANA_POTION); + return true; + case ControllerButton::BUTTON_B: // Right button + if (InGameMenu()) + break; // Map to keyboard key + if (!ctrl_event.up) + *action = GameAction(GameActionType::PRIMARY_ACTION); + return true; + case ControllerButton::BUTTON_Y: // Top button + if (InGameMenu()) + break; // Map to keyboard key + if (!ctrl_event.up) + *action = GameAction(GameActionType::SECONDARY_ACTION); + return true; + case ControllerButton::BUTTON_A: // Bottom button + if (InGameMenu()) + break; // Map to keyboard key + if (!ctrl_event.up) + *action = GameAction(GameActionType::TOGGLE_QUICK_SPELL_MENU); + return true; + case ControllerButton::BUTTON_LEFTSHOULDER: + if (!stextflag && !ctrl_event.up) + *action = GameAction(GameActionType::TOGGLE_CHARACTER_INFO); + return true; + case ControllerButton::BUTTON_RIGHTSHOULDER: + if (!stextflag && !ctrl_event.up) + *action = GameAction(GameActionType::TOGGLE_INVENTORY); + return true; + case ControllerButton::BUTTON_DPAD_UP: + case ControllerButton::BUTTON_DPAD_DOWN: + case ControllerButton::BUTTON_DPAD_LEFT: + case ControllerButton::BUTTON_DPAD_RIGHT: + if (InGameMenu()) + break; + // The rest is handled in charMovement() on every game_logic() call. + return true; + case ControllerButton::BUTTON_RIGHTSTICK: + *action = GameActionSendMouseLeftClick { ctrl_event.up }; + return true; + default: + break; + } + + // By default, map to a keyboard key. + if (ctrl_event.button != ControllerButton::NONE) { + *action = GameActionSendKey{ translate_controller_button_to_key(ctrl_event.button), + ctrl_event.up }; + return true; + } + +#ifndef USE_SDL1 + // Ignore unhandled joystick events if gamepad is active. + // We receive the same events as gamepad events. + if (CurrentGameController() != nullptr && event.type >= SDL_JOYAXISMOTION && event.type <= SDL_JOYBUTTONUP) { + return true; + } +#endif + + return false; +} + +bool ShouldSkipMovie(const SDL_Event &event) +{ + if (GetMenuAction(event) != MenuAction::NONE) + return true; + switch (event.type) { + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + return event.button.button == SDL_BUTTON_LEFT; + case SDL_KEYDOWN: + case SDL_KEYUP: + return true; + default: + return false; + } +} + +MoveDirection GetMoveDirection() +{ + const float stickX = leftStickX; + const float stickY = leftStickY; + MoveDirection result{ MoveDirectionX::NONE, MoveDirectionY::NONE }; + + if (stickY >= 0.5 || IsControllerButtonPressed(ControllerButton::BUTTON_DPAD_UP)) { + result.y = MoveDirectionY::UP; + } else if (stickY <= -0.5 || IsControllerButtonPressed(ControllerButton::BUTTON_DPAD_DOWN)) { + result.y = MoveDirectionY::DOWN; + } + + if (stickX <= -0.5 || IsControllerButtonPressed(ControllerButton::BUTTON_DPAD_LEFT)) { + result.x = MoveDirectionX::LEFT; + } else if (stickX >= 0.5 || IsControllerButtonPressed(ControllerButton::BUTTON_DPAD_RIGHT)) { + result.x = MoveDirectionX::RIGHT; + } + + return result; +} + +} // namespace dvl diff --git a/SourceX/controls/game_controls.h b/SourceX/controls/game_controls.h new file mode 100644 index 000000000..8a91422dd --- /dev/null +++ b/SourceX/controls/game_controls.h @@ -0,0 +1,79 @@ +#pragma once + +#include "devilution.h" + +namespace dvl { + +enum class GameActionType { + NONE = 0, + USE_HEALTH_POTION, + USE_MANA_POTION, + PRIMARY_ACTION, // Talk to towners, click on inv items, attack, etc. + SECONDARY_ACTION, // Open chests, doors, pickup items. + TOGGLE_INVENTORY, + TOGGLE_CHARACTER_INFO, + TOGGLE_QUICK_SPELL_MENU, + SEND_KEY, + SEND_MOUSE_LEFT_CLICK +}; + +struct GameActionSendKey { + DWORD vk_code; + bool up; +}; + +struct GameActionSendMouseLeftClick { + bool up; +}; + +struct GameAction { + GameActionType type; + + GameAction() + : type(GameActionType::NONE) + { + } + + explicit GameAction(GameActionType type) + : type(type) + { + } + + GameAction(GameActionSendKey send_key) + : type(GameActionType::SEND_KEY) + , send_key(send_key) + { + } + + GameAction(GameActionSendMouseLeftClick send_mouse_left_click) + : type(GameActionType::SEND_MOUSE_LEFT_CLICK) + , send_mouse_left_click(send_mouse_left_click) + { + } + + union { + GameActionSendKey send_key; + GameActionSendMouseLeftClick send_mouse_left_click; + }; +}; + +bool GetGameAction(const SDL_Event &event, GameAction *action); +bool ShouldSkipMovie(const SDL_Event &event); + +enum class MoveDirectionX { + NONE = 0, + LEFT, + RIGHT +}; +enum class MoveDirectionY { + NONE = 0, + UP, + DOWN +}; +struct MoveDirection { + MoveDirectionX x; + MoveDirectionY y; +}; +MoveDirection GetMoveDirection(); + +} // namespace dvl diff --git a/SourceX/controls/menu_controls.cpp b/SourceX/controls/menu_controls.cpp new file mode 100644 index 000000000..4ac0424f5 --- /dev/null +++ b/SourceX/controls/menu_controls.cpp @@ -0,0 +1,74 @@ +#include "controls/menu_controls.h" + +#include "controls/controller.h" + +namespace dvl { + +MenuAction GetMenuAction(const SDL_Event &event) +{ + const ControllerButtonEvent ctrl_event = ToControllerButtonEvent(event); + if (!ctrl_event.up) { + switch (ctrl_event.button) { + case ControllerButton::BUTTON_B: // Right button + case ControllerButton::BUTTON_START: + return MenuAction::SELECT; + case ControllerButton::BUTTON_BACK: + case ControllerButton::BUTTON_A: // Bottom button + return MenuAction::BACK; + case ControllerButton::BUTTON_X: // Left button + return MenuAction::DELETE; + case ControllerButton::BUTTON_DPAD_UP: + return MenuAction::UP; + case ControllerButton::BUTTON_DPAD_DOWN: + return MenuAction::DOWN; + case ControllerButton::BUTTON_DPAD_LEFT: + return MenuAction::LEFT; + case ControllerButton::BUTTON_DPAD_RIGHT: + return MenuAction::RIGHT; + case ControllerButton::BUTTON_LEFTSHOULDER: + return MenuAction::PAGE_UP; + case ControllerButton::BUTTON_RIGHTSHOULDER: + return MenuAction::PAGE_DOWN; + default: + break; + } + } + +#if HAS_KBCTRL == 0 + if (event.type == SDL_KEYDOWN) { + switch (event.key.keysym.sym) { + case SDLK_UP: + return MenuAction::UP; + case SDLK_DOWN: + return MenuAction::DOWN; + case SDLK_TAB: + if (SDL_GetModState() & KMOD_SHIFT) + return MenuAction::UP; + else + return MenuAction::DOWN; + case SDLK_PAGEUP: + return MenuAction::PAGE_UP; + case SDLK_PAGEDOWN: + return MenuAction::PAGE_DOWN; + case SDLK_RETURN: + case SDLK_KP_ENTER: + case SDLK_SPACE: + return MenuAction::SELECT; + case SDLK_DELETE: + return MenuAction::DELETE; + case SDLK_LEFT: + return MenuAction::LEFT; + case SDLK_RIGHT: + return MenuAction::RIGHT; + case SDLK_ESCAPE: + return MenuAction::BACK; + default: + break; + } + } +#endif + + return MenuAction::NONE; +} // namespace dvl + +} // namespace dvl diff --git a/SourceX/controls/menu_controls.h b/SourceX/controls/menu_controls.h new file mode 100644 index 000000000..b878857e5 --- /dev/null +++ b/SourceX/controls/menu_controls.h @@ -0,0 +1,24 @@ +#pragma once + +#include "devilution.h" + +namespace dvl { + +enum class MenuAction { + NONE = 0, + SELECT, + BACK, + DELETE, + + UP, + DOWN, + LEFT, + RIGHT, + + PAGE_UP, + PAGE_DOWN, +}; + +MenuAction GetMenuAction(const SDL_Event &event); + +} // namespace dvl diff --git a/SourceX/controls/plrctrls.cpp b/SourceX/controls/plrctrls.cpp index a34cfd72a..042b387b8 100644 --- a/SourceX/controls/plrctrls.cpp +++ b/SourceX/controls/plrctrls.cpp @@ -1,23 +1,36 @@ -#include "diablo.h" -#include "../3rdParty/Storm/Source/storm.h" +#include "controls/plrctrls.h" -DEVILUTION_BEGIN_NAMESPACE +#include -extern float leftStickX; -extern float leftStickY; +#include "controls/controller_motion.h" +#include "controls/game_controls.h" -// JAKE: My functions for movement and interaction via keyboard/controller -bool newCurHidden = false; -static DWORD attacktick; -static DWORD invmove; -int slot = SLOTXY_INV_FIRST; -int spbslot = 0; +// Based on the Nintendo Switch port by @lantus, @erfg12, @rsn8887. + +namespace dvl { + +bool sgbControllerActive = false; coords speedspellscoords[50]; int speedspellcount = 0; + +// Native game menu, controlled by simulating a keyboard. +bool InGameMenu() +{ + return stextflag > 0 || questlog || helpflag || talkflag || qtextflag || sgpCurrentMenu; +} + +namespace { + +DWORD invmove = 0; int hsr[3] = { 0, 0, 0 }; // hot spell row counts -DWORD talkwait; -DWORD talktick; -DWORD castwait; +int slot = SLOTXY_INV_FIRST; +int spbslot = 0; + +// Menu controlled by simulating a mouse. +bool InControlledMenu() +{ + return invflag || spselflag || chrflag; +} // 0 = not near, >0 = distance related player 1 coordinates coords checkNearbyObjs(int x, int y, int diff) @@ -49,7 +62,7 @@ void checkItemsNearby(bool interact) return; // item nearby, don't find objects } } - if (newCurHidden) + if (sgbControllerActive) pcursitem = -1; //sprintf(tempstr, "SCANNING FOR OBJECTS"); //NetSendCmdString(1 << myplr, tempstr); @@ -62,12 +75,16 @@ void checkItemsNearby(bool interact) return; } } - if (newCurHidden) + if (sgbControllerActive) pcursobj = -1; } void checkTownersNearby(bool interact) { + if (pcursitem != -1) + // Items take priority over towners because the player can move + // items but not towners. + return; for (int i = 0; i < 16; i++) { if (checkNearbyObjs(towner[i]._tx, towner[i]._ty, 2).x != -1) { if (towner[i]._ttype == -1) @@ -85,7 +102,8 @@ bool checkMonstersNearby(bool attack) { int closest = 0; // monster ID who is closest coords objDistLast = { 99, 99 }; // previous obj distance - for (int i = 0; i < MAXMONSTERS; i++) { + // The first MAX_PLRS monsters are reserved for players' golems. + for (int i = MAX_PLRS; i < MAXMONSTERS; i++) { int d_monster = dMonster[monster[i]._mx][monster[i]._my]; if (monster[i]._mFlags & MFLAG_HIDDEN || monster[i]._mhitpoints <= 0) // monster is hiding or dead, skip continue; @@ -110,6 +128,7 @@ bool checkMonstersNearby(bool attack) return false; } if (attack) { + static DWORD attacktick = 0; DWORD ticks = GetTickCount(); if (ticks - attacktick > 100) { // prevent accidental double attacks attacktick = ticks; @@ -131,8 +150,7 @@ void HideCursor() SetCursorPos(320, 180); MouseX = 320; MouseY = 180; - SetCursor_(CURSOR_NONE); - newCurHidden = true; + sgbControllerActive = true; } void attrIncBtnSnap(int key) @@ -152,28 +170,27 @@ void attrIncBtnSnap(int key) // first, find our cursor location int slot = 0; for (int i = 0; i < 4; i++) { - // 0 = x, 1 = y, 2 = width, 3 = height - if (MouseX >= attribute_inc_rects2[i][0] - && MouseX <= attribute_inc_rects2[i][0] + attribute_inc_rects2[i][2] - && MouseY >= attribute_inc_rects2[i][1] - && MouseY <= attribute_inc_rects2[i][3] + attribute_inc_rects2[i][1]) { + if (MouseX >= ChrBtnsRect[i].x + && MouseX <= ChrBtnsRect[i].x + ChrBtnsRect[i].w + && MouseY >= ChrBtnsRect[i].y + && MouseY <= ChrBtnsRect[i].h + ChrBtnsRect[i].y) { slot = i; break; } } // set future location up or down - if (key == VK_UP) { + if (key == DVL_VK_UP) { if (slot > 0) - slot--; - } else if (key == VK_DOWN) { + --slot; + } else if (key == DVL_VK_DOWN) { if (slot < 3) - slot++; + ++slot; } // move cursor to our new location - int x = attribute_inc_rects2[slot][0] + (attribute_inc_rects2[slot][2] / 2); - int y = attribute_inc_rects2[slot][1] + (attribute_inc_rects2[slot][3] / 2); + int x = ChrBtnsRect[slot].x + (ChrBtnsRect[slot].w / 2); + int y = ChrBtnsRect[slot].y + (ChrBtnsRect[slot].h / 2); SetCursorPos(x, y); MouseX = x; MouseY = y; @@ -328,17 +345,14 @@ void invMove(int key) if (pcurs > 1) { // [3] Keep item in the same slot, don't jump it up if (x != MouseX) // without this, the cursor keeps moving -10 - { - SetCursorPos((x - 10), (y - 10)); - MouseX = x - 10; - MouseY = y - 10; - } + { + SetCursorPos(x - 10, y - 10); + } } else { SetCursorPos(x, y); - MouseX = x; - MouseY = y; } } + // check if hot spell at X Y exists bool HSExists(int x, int y) { @@ -352,12 +366,12 @@ bool HSExists(int x, int y) void hotSpellMove(int key) { - int x = 0; - int y = 0; if (!spselflag) return; + int x = 0; + int y = 0; - if (pcurs > 0) + if (!sgbControllerActive) HideCursor(); DWORD ticks = GetTickCount(); @@ -381,7 +395,7 @@ void hotSpellMove(int key) } } - if (key == VK_UP) { + if (key == DVL_VK_UP) { if (speedspellscoords[spbslot].y == 307 && hsr[1] > 0) { // we're in row 1, check if row 2 has spells if (HSExists(MouseX, 251)) { x = MouseX; @@ -393,7 +407,7 @@ void hotSpellMove(int key) y = 195; } } - } else if (key == VK_DOWN) { + } else if (key == DVL_VK_DOWN) { if (speedspellscoords[spbslot].y == 251) { // we're in row 2 if (HSExists(MouseX, 307)) { x = MouseX; @@ -405,13 +419,13 @@ void hotSpellMove(int key) y = 251; } } - } else if (key == VK_LEFT) { + } else if (key == DVL_VK_LEFT) { if (spbslot >= speedspellcount - 1) return; spbslot++; x = speedspellscoords[spbslot].x; y = speedspellscoords[spbslot].y; - } else if (key == VK_RIGHT) { + } else if (key == DVL_VK_RIGHT) { if (spbslot <= 0) return; spbslot--; @@ -425,11 +439,10 @@ void hotSpellMove(int key) MouseY = y; } } -// walk in the direction specified -void walkInDir(int dir) +void walkInDir(MoveDirection dir) { - if (invflag || spselflag || chrflag || questlog) // don't walk if inventory, speedbook or char info windows are open + if (dir.x == MoveDirectionX::NONE && dir.y == MoveDirectionY::NONE) return; DWORD ticks = GetTickCount(); if (ticks - invmove < 370) { @@ -439,148 +452,235 @@ void walkInDir(int dir) ClrPlrPath(myplr); // clear nodes plr[myplr].destAction = ACTION_NONE; // stop attacking, etc. HideCursor(); - plr[myplr].walkpath[0] = dir; + static const _walk_path kMoveToWalkDir[3][3] = { + // NONE UP DOWN + { WALK_NONE, WALK_N, WALK_S }, // NONE + { WALK_W, WALK_NW, WALK_SW }, // LEFT + { WALK_E, WALK_NE, WALK_SE }, // RIGHT + }; + plr[myplr].walkpath[0] = kMoveToWalkDir[static_cast(dir.x)][static_cast(dir.y)]; } -static DWORD menuopenslow; -void useBeltPotion(bool mana) + +void menuMoveX(MoveDirectionX dir) { - DWORD ticks = GetTickCount(); - int invNum = 0; - if (ticks - menuopenslow < 300) { + if (dir == MoveDirectionX::NONE) + return; + invMove(dir == MoveDirectionX::LEFT ? WALK_W : WALK_E); + hotSpellMove(dir == MoveDirectionX::LEFT ? DVL_VK_LEFT : DVL_VK_RIGHT); +} + +void menuMoveY(MoveDirectionY dir) +{ + if (dir == MoveDirectionY::NONE) return; + invMove(dir == MoveDirectionY::UP ? WALK_N : WALK_S); + const auto key = dir == MoveDirectionY::UP ? DVL_VK_UP : DVL_VK_DOWN; + hotSpellMove(key); + attrIncBtnSnap(key); +} + +void movement() +{ + if (InGameMenu()) + return; + + MoveDirection move_dir = GetMoveDirection(); + if (InControlledMenu()) { + menuMoveX(move_dir.x); + menuMoveY(move_dir.y); + } else { + walkInDir(move_dir); } - menuopenslow = ticks; - for (int i = 0; i < MAXBELTITEMS; i++) { - if ((AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId == IMISC_HEAL && mana == false) || (AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId == IMISC_FULLHEAL && mana == false) || (AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId == IMISC_MANA && mana == true) || (AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId == IMISC_FULLMANA && mana == true) || (AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId == IMISC_REJUV && AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId == IMISC_FULLREJUV)) { - if (plr[myplr].SpdList[i]._itype > -1) { - invNum = i + INVITEM_BELT_FIRST; - UseInvItem(myplr, invNum); - break; - } + + if (GetAsyncKeyState(DVL_MK_LBUTTON) & 0x8000) { + if (sgbControllerActive) { // show cursor first, before clicking + sgbControllerActive = false; + } else if (spselflag) { + SetSpell(); + } else { + LeftMouseCmd(false); } } } -void movements(int key) { - if (key == VK_UP) { - invMove(WALK_N); - hotSpellMove(VK_UP); - attrIncBtnSnap(VK_UP); - walkInDir(WALK_N); - } else if (key == VK_RIGHT) { - invMove(WALK_E); - hotSpellMove(VK_RIGHT); - walkInDir(WALK_E); - } else if (key == VK_DOWN) { - invMove(WALK_S); - hotSpellMove(VK_DOWN); - attrIncBtnSnap(VK_DOWN); - walkInDir(WALK_S); - } else if (key == VK_LEFT) { - invMove(WALK_W); - hotSpellMove(VK_LEFT); - walkInDir(WALK_W); +// Move map or mouse no more than 60 times per second. +// This is called from both the game loop and the event loop for responsiveness. +void HandleRightStickMotionAt60Fps() +{ + static std::int64_t currentTime = 0; + static std::int64_t lastTime = 0; + currentTime = SDL_GetTicks(); + + if (currentTime - lastTime > 15) { + HandleRightStickMotion(); + lastTime = currentTime; } } -void charMovement() { - if (stextflag > 0 || questlog || helpflag || talkflag || qtextflag) - return; +struct RightStickAccumulator { + void start(int *x, int *y) + { + hiresDX += rightStickX * kGranularity; + hiresDY += rightStickY * kGranularity; + *x += hiresDX / slowdown; + *y += -hiresDY / slowdown; + } - if (!invflag && !spselflag && !chrflag) { - if (GetAsyncKeyState(VK_RIGHT) & 0x8000 && GetAsyncKeyState(VK_DOWN) & 0x8000 || GetAsyncKeyState(0x44) & 0x8000 && GetAsyncKeyState(0x53) & 0x8000 || leftStickY <= -0.5 && leftStickX >= 0.5) { - walkInDir(WALK_SE); - } else if (GetAsyncKeyState(VK_RIGHT) & 0x8000 && GetAsyncKeyState(VK_UP) & 0x8000 || GetAsyncKeyState(0x57) & 0x8000 && GetAsyncKeyState(0x44) & 0x8000 || leftStickY >= 0.5 && leftStickX >= 0.5) { - walkInDir(WALK_NE); - } else if (GetAsyncKeyState(VK_LEFT) & 0x8000 && GetAsyncKeyState(VK_DOWN) & 0x8000 || GetAsyncKeyState(0x41) & 0x8000 && GetAsyncKeyState(0x53) & 0x8000 || leftStickY <= -0.5 && leftStickX <= -0.5) { - walkInDir(WALK_SW); - } else if (GetAsyncKeyState(VK_LEFT) & 0x8000 && GetAsyncKeyState(VK_UP) & 0x8000 || GetAsyncKeyState(0x57) & 0x8000 && GetAsyncKeyState(0x41) & 0x8000 || leftStickY >= 0.40 && leftStickX <= -0.5) { - walkInDir(WALK_NW); - } + void finish() + { + // keep track of remainder for sub-pixel motion + hiresDX %= slowdown; + hiresDY %= slowdown; } - if (GetAsyncKeyState(VK_UP) & 0x8000 || GetAsyncKeyState(0x57) & 0x8000 || leftStickY >= 0.5) { - movements(VK_UP); - } else if (GetAsyncKeyState(VK_RIGHT) & 0x8000 || GetAsyncKeyState(0x44) & 0x8000 || leftStickX >= 0.5) { - movements(VK_RIGHT); - } else if (GetAsyncKeyState(VK_DOWN) & 0x8000 || GetAsyncKeyState(0x53) & 0x8000 || leftStickY <= -0.5) { - movements(VK_DOWN); - } else if (GetAsyncKeyState(VK_LEFT) & 0x8000 || GetAsyncKeyState(0x41) & 0x8000 || leftStickX <= -0.5) { - movements(VK_LEFT); + static const int kGranularity = (1 << 15) - 1; + int slowdown; // < kGranularity + int hiresDX; + int hiresDY; +}; + +} // namespace + +void HandleRightStickMotion() +{ + // deadzone is handled in ScaleJoystickAxes() already + if (rightStickX == 0 && rightStickY == 0) + return; + + if (automapflag) { // move map + static RightStickAccumulator acc = { /*slowdown=*/(1 << 14) + (1 << 13), 0, 0 }; + int dx = 0, dy = 0; + acc.start(&dx, &dy); + if (dy > 1) + AutomapUp(); + else if (dy < -1) + AutomapDown(); + else if (dx < -1) + AutomapRight(); + else if (dx > 1) + AutomapLeft(); + acc.finish(); + } else { // move cursor + if (sgbControllerActive) { + sgbControllerActive = false; + } + static RightStickAccumulator acc = { /*slowdown=*/(1 << 13) + (1 << 12), 0, 0 }; + int x = MouseX; + int y = MouseY; + acc.start(&x, &y); + if (x < 0) + x = 0; + if (y < 0) + y = 0; + SetCursorPos(x, y); + acc.finish(); } +} - if (GetAsyncKeyState(MK_LBUTTON) & 0x8000) { - if (newCurHidden) { // show cursor first, before clicking - SetCursor_(CURSOR_HAND); - newCurHidden = false; - } else if (spselflag) { - SetSpell(); +void plrctrls_game_logic() +{ + // check for monsters first, then items, then towners. + if (sgbControllerActive) { // cursor should be missing + if (!checkMonstersNearby(false)) { + pcursmonst = -1; + checkItemsNearby(false); + checkTownersNearby(false); } else { - LeftMouseCmd(false); + pcursitem = -1; } } + movement(); + HandleRightStickMotionAt60Fps(); } -void keyboardExpansion(int vikey) +void plrctrls_event_loop() { - static DWORD opentimer; - static DWORD clickinvtimer; - static DWORD statuptimer; - DWORD ticks = GetTickCount(); + HandleRightStickMotionAt60Fps(); +} - if (stextflag > 0 || questlog || helpflag || talkflag || qtextflag) +void useBeltPotion(bool mana) +{ + static DWORD menuopenslow = 0; + DWORD ticks = GetTickCount(); + int invNum = 0; + if (ticks - menuopenslow < 300) { return; - if (vikey == VK_SPACE) { // similar to X button on PS1 ccontroller. Talk to towners, click on inv items, attack. - if (invflag) { // inventory is open - if (ticks - clickinvtimer >= 300) { - clickinvtimer = ticks; - if (pcurs == CURSOR_IDENTIFY) - CheckIdentify(myplr, pcursinvitem); - else if (pcurs == CURSOR_REPAIR) - DoRepair(myplr, pcursinvitem); - else - CheckInvItem(); + } + menuopenslow = ticks; + for (int i = 0; i < MAXBELTITEMS; i++) { + const auto id = AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId; + if ((!mana && (id == IMISC_HEAL || id == IMISC_FULLHEAL)) || (mana && (id == IMISC_MANA || id == IMISC_FULLMANA)) || id == IMISC_REJUV || id == IMISC_FULLREJUV) { + if (plr[myplr].SpdList[i]._itype > -1) { + invNum = i + INVITEM_BELT_FIRST; + UseInvItem(myplr, invNum); + break; } - } else if (spselflag) { - SetSpell(); - } else if (chrflag) { - if (ticks - statuptimer >= 400) { - statuptimer = ticks; - if (!chrbtnactive && plr[myplr]._pStatPts) { - CheckChrBtns(); - for (int i = 0; i < 4; i++) { - if (MouseX >= attribute_inc_rects2[i][0] - && MouseX <= attribute_inc_rects2[i][0] + attribute_inc_rects2[i][2] - && MouseY >= attribute_inc_rects2[i][1] - && MouseY <= attribute_inc_rects2[i][3] + attribute_inc_rects2[i][1]) { - chrbtn[i] = 1; - chrbtnactive = TRUE; - ReleaseChrBtns(); - } + } + } +} + +void performPrimaryAction() +{ + const DWORD ticks = GetTickCount(); + if (invflag) { // inventory is open + static DWORD clickinvtimer; + if (ticks - clickinvtimer >= 300) { + clickinvtimer = ticks; + if (pcurs == CURSOR_IDENTIFY) + CheckIdentify(myplr, pcursinvitem); + else if (pcurs == CURSOR_REPAIR) + DoRepair(myplr, pcursinvitem); + else + CheckInvItem(); + } + } else if (spselflag) { + SetSpell(); + } else if (chrflag) { + static DWORD statuptimer; + if (ticks - statuptimer >= 400) { + statuptimer = ticks; + if (!chrbtnactive && plr[myplr]._pStatPts) { + CheckChrBtns(); + for (int i = 0; i < 4; i++) { + if (MouseX >= ChrBtnsRect[i].x + && MouseX <= ChrBtnsRect[i].x + ChrBtnsRect[i].w + && MouseY >= ChrBtnsRect[i].y + && MouseY <= ChrBtnsRect[i].h + ChrBtnsRect[i].y) { + chrbtn[i] = 1; + chrbtnactive = true; + ReleaseChrBtns(); } } - if (plr[myplr]._pStatPts == 0) - HideCursor(); - } - } else { - HideCursor(); - talktick = GetTickCount(); // this is shared with STextESC, do NOT duplicate or use anywhere else - if (!checkMonstersNearby(true)) { - if (talktick - talkwait > 600) { // prevent re-entering talk after finished - talkwait = talktick; - checkTownersNearby(true); - } } + if (plr[myplr]._pStatPts == 0) + HideCursor(); } - } else if (vikey == VK_RETURN) { // similar to [] button on PS1 controller. Open chests, doors, pickup items - if (!invflag) { - HideCursor(); - if (ticks - opentimer > 500) { - opentimer = ticks; - checkItemsNearby(true); + } else { + static DWORD talkwait; + if (stextflag) + talkwait = GetTickCount(); // Wait before we re-initiate talking + HideCursor(); + DWORD talktick = GetTickCount(); + if (!checkMonstersNearby(true)) { + if (talktick - talkwait > 600) { // prevent re-entering talk after finished + talkwait = talktick; + checkTownersNearby(true); } } } } -DEVILUTION_END_NAMESPACE +void performSecondaryAction() +{ + if (invflag) + return; + static DWORD opentimer = 0; + HideCursor(); + const DWORD ticks = GetTickCount(); + if (ticks - opentimer > 500) { + opentimer = ticks; + checkItemsNearby(true); + } +} + +} // namespace dvl diff --git a/SourceX/controls/plrctrls.h b/SourceX/controls/plrctrls.h index b30e09b19..924ca44ff 100644 --- a/SourceX/controls/plrctrls.h +++ b/SourceX/controls/plrctrls.h @@ -1,29 +1,35 @@ #pragma once +// Controller actions implementation + +#include "devilution.h" + +namespace dvl { + +// Run on every game logic iteration. +void plrctrls_game_logic(); + +// Runs before the start of event loop drain, even if there are no events. +void plrctrls_event_loop(); + +// Moves the map if active, the cursor otherwise. +void HandleRightStickMotion(); + +// Whether we're in a dialog menu that the game handles natively with keyboard controls. +bool InGameMenu(); -void checkTownersNearby(bool interact); -void checkItemsNearby(bool interact); -void keyboardExpansion(int vikey); -void charMovement(); -void movements(int key); -bool checkMonstersNearby(bool attack); -extern bool newCurHidden; -void invMove(int key); -void HideCursor(); void useBeltPotion(bool mana); + +// Talk to towners, click on inv items, attack, etc. +void performPrimaryAction(); + +// Open chests, doors, pickup items. +void performSecondaryAction(); + typedef struct coords { int x; int y; } coords; extern coords speedspellscoords[50]; extern int speedspellcount; -extern const InvXY InvRect[73]; // wasn't made public, so I'll add this here from inv.cpp - -extern DWORD talkwait; -extern DWORD talktick; -extern DWORD castwait; -#define INV_TOP 240; -#define INV_LEFT 350; -#define INV_HEIGHT 320; -#define DISPLAY_HEIGHT 360; -#define DISPLAY_WIDTH 640; +} // namespace dvl diff --git a/SourceX/miniwin/misc.cpp b/SourceX/miniwin/misc.cpp index 415f40d33..e00066ecb 100644 --- a/SourceX/miniwin/misc.cpp +++ b/SourceX/miniwin/misc.cpp @@ -4,6 +4,7 @@ #include #include +#include "controls/controller.h" #include "DiabloUI/diabloui.h" #include "DiabloUI/dialogs.h" @@ -128,6 +129,8 @@ bool SpawnWindow(LPCSTR lpWindowName, int nWidth, int nHeight) SDL_EnableUNICODE(1); #endif + InitController(); + int upscale = 1; DvlIntSetting("upscale", &upscale); DvlIntSetting("fullscreen", (int *)&fullscreen); diff --git a/SourceX/miniwin/misc_dx.cpp b/SourceX/miniwin/misc_dx.cpp index 82c9fe940..334d5cc3c 100644 --- a/SourceX/miniwin/misc_dx.cpp +++ b/SourceX/miniwin/misc_dx.cpp @@ -24,6 +24,8 @@ WINBOOL SetCursorPos(int X, int Y) #endif SDL_WarpMouseInWindow(window, X, Y); + MouseX = X; + MouseY = Y; return true; } diff --git a/SourceX/miniwin/misc_msg.cpp b/SourceX/miniwin/misc_msg.cpp index 4c7e8d670..b3fdb925e 100644 --- a/SourceX/miniwin/misc_msg.cpp +++ b/SourceX/miniwin/misc_msg.cpp @@ -1,8 +1,12 @@ +#include #include #include #include "devilution.h" #include "stubs.h" +#include "controls/controller_motion.h" +#include "controls/game_controls.h" +#include "controls/plrctrls.h" /** @file * * @@ -178,19 +182,115 @@ static int translate_sdl_key(SDL_Keysym key) } } -static WPARAM keystate_for_mouse(WPARAM ret) +namespace { + +WPARAM keystate_for_mouse(WPARAM ret) { ret |= (SDL_GetModState() & KMOD_SHIFT) ? DVL_MK_SHIFT : 0; // XXX: other DVL_MK_* codes not implemented return ret; } -static WINBOOL false_avail() +WINBOOL false_avail() { DUMMY_PRINT("return %s although event available", "false"); return false; } +void SetMouseLMBMessage(const SDL_Event &event, LPMSG lpMsg) +{ + switch (event.type) { +#ifndef USE_SDL1 + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + lpMsg->message = event.type == SDL_CONTROLLERBUTTONUP ? DVL_WM_LBUTTONUP : DVL_WM_LBUTTONDOWN; + lpMsg->lParam = (MouseY << 16) | (MouseX & 0xFFFF); + break; +#endif + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + lpMsg->message = event.type == SDL_MOUSEBUTTONUP ? DVL_WM_LBUTTONUP : DVL_WM_LBUTTONDOWN; + lpMsg->lParam = (event.button.y << 16) | (event.button.x & 0xFFFF); + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + lpMsg->message = event.type == SDL_KEYUP ? DVL_WM_LBUTTONUP : DVL_WM_LBUTTONDOWN; + lpMsg->lParam = (MouseY << 16) | (MouseX & 0xFFFF); + break; + default: + lpMsg->message = event.type == DVL_WM_LBUTTONUP; + lpMsg->lParam = (MouseY << 16) | (MouseX & 0xFFFF); + break; + } + if (lpMsg->message == DVL_WM_LBUTTONUP || lpMsg->message == DVL_WM_LBUTTONDOWN) + lpMsg->wParam = keystate_for_mouse(lpMsg->message == DVL_WM_LBUTTONUP ? 0 : DVL_MK_LBUTTON); +} + +void ShowCursor() +{ + if (sgbControllerActive) { + sgbControllerActive = false; + } +} + +void HideCursorIfNotNeeded(bool newspselflag = spselflag) { + if (!chrflag && !invflag && !newspselflag) { + sgbControllerActive = true; + } +} + +// Moves the mouse to the first inventory slot. +bool FocusOnInventory() +{ + if (!invflag) + return false; + ShowCursor(); + SetCursorPos(InvRect[25].X + (INV_SLOT_SIZE_PX / 2), InvRect[25].Y - (INV_SLOT_SIZE_PX / 2)); + return true; +} + +// Moves the mouse to the first attribute "+" button. +bool FocusOnCharInfo() +{ + if (!chrflag || plr[myplr]._pStatPts == 0) + return false; + + // Find the first incrementable stat. + int pc = plr[myplr]._pClass; + int stat = -1; + for (int i = 4; i >= 0; --i) { + switch (i) { + case ATTRIB_STR: + if (plr[myplr]._pBaseStr >= MaxStats[pc][ATTRIB_STR]) + continue; + break; + case ATTRIB_MAG: + if (plr[myplr]._pBaseMag >= MaxStats[pc][ATTRIB_MAG]) + continue; + break; + case ATTRIB_DEX: + if (plr[myplr]._pBaseDex >= MaxStats[pc][ATTRIB_DEX]) + continue; + break; + case ATTRIB_VIT: + if (plr[myplr]._pBaseVit >= MaxStats[pc][ATTRIB_VIT]) + continue; + break; + default: + continue; + } + stat = i; + } + if (stat == -1) + return false; + ShowCursor(); + const auto &rect = ChrBtnsRect[stat]; + SetCursorPos(rect.x + (rect.w / 2), rect.y + (rect.h / 2)); + return true; +} + +} // namespace + WINBOOL PeekMessageA(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) { if (wMsgFilterMin != 0) @@ -201,6 +301,8 @@ WINBOOL PeekMessageA(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilter UNIMPLEMENTED(); if (wRemoveMsg == DVL_PM_NOREMOVE) { + plrctrls_event_loop(); + // This does not actually fill out lpMsg, but this is ok // since the engine never uses it in this case return !message_queue.empty() || SDL_PollEvent(NULL); @@ -221,9 +323,81 @@ WINBOOL PeekMessageA(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilter } lpMsg->hwnd = hWnd; + lpMsg->message = 0; lpMsg->lParam = 0; lpMsg->wParam = 0; + if (e.type == SDL_QUIT) { + lpMsg->message = DVL_WM_QUIT; + return true; + } + + if (movie_playing) { + if (ShouldSkipMovie(e)) + SetMouseLMBMessage(e, lpMsg); + return true; + } + + if (ProcessControllerMotion(e)) { + ScaleJoysticks(); + HandleRightStickMotion(); + return true; + } + + GameAction action; + if (GetGameAction(e, &action)) { + switch (action.type) { + case GameActionType::NONE: + break; + case GameActionType::USE_HEALTH_POTION: + useBeltPotion(/*mana=*/false); + break; + case GameActionType::USE_MANA_POTION: + useBeltPotion(/*mana=*/true); + break; + case GameActionType::PRIMARY_ACTION: + performPrimaryAction(); + break; + case GameActionType::SECONDARY_ACTION: + performSecondaryAction(); + break; + case GameActionType::TOGGLE_QUICK_SPELL_MENU: + ShowCursor(); + lpMsg->message = DVL_WM_KEYDOWN; + lpMsg->wParam = 'S'; + // We expect the flag value to change after this. + HideCursorIfNotNeeded(!spselflag); + return true; + case GameActionType::TOGGLE_CHARACTER_INFO: + questlog = false; + chrflag = !chrflag; + if (chrflag) + FocusOnCharInfo(); + else + FocusOnInventory(); + HideCursorIfNotNeeded(); + break; + case GameActionType::TOGGLE_INVENTORY: + sbookflag = false; + invflag = !invflag; + if (invflag) + FocusOnInventory(); + else + FocusOnCharInfo(); + HideCursorIfNotNeeded(); + break; + case GameActionType::SEND_KEY: + lpMsg->message = action.send_key.up ? DVL_WM_KEYUP : DVL_WM_KEYDOWN; + lpMsg->wParam = action.send_key.vk_code; + return true; + case GameActionType::SEND_MOUSE_LEFT_CLICK: + ShowCursor(); + SetMouseLMBMessage(e, lpMsg); + return true; + } + return true; + } + switch (e.type) { case SDL_QUIT: lpMsg->message = DVL_WM_QUIT; @@ -374,43 +548,27 @@ WINBOOL TranslateMessage(const MSG *lpMsg) SHORT GetAsyncKeyState(int vKey) { -#ifndef USE_SDL1 - const Uint8 *state = SDL_GetKeyboardState(nullptr); + if (vKey == DVL_MK_LBUTTON) + return SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_LEFT); + if (vKey == DVL_MK_RBUTTON) + return SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_RIGHT); + const Uint8 *state = SDLC_GetKeyboardState(); switch (vKey) { case DVL_VK_SHIFT: - return state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_LEFTSHIFT] || state[SDLC_KEYSTATE_RIGHTSHIFT] ? 0x8000 : 0; case DVL_VK_MENU: - return state[SDL_SCANCODE_MENU] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_MENU] ? 0x8000 : 0; case DVL_VK_LEFT: - return state[SDL_SCANCODE_LEFT] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_LEFT] ? 0x8000 : 0; case DVL_VK_UP: - return state[SDL_SCANCODE_UP] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_UP] ? 0x8000 : 0; case DVL_VK_RIGHT: - return state[SDL_SCANCODE_RIGHT] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_RIGHT] ? 0x8000 : 0; case DVL_VK_DOWN: - return state[SDL_SCANCODE_DOWN] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_DOWN] ? 0x8000 : 0; default: return 0; } -#else - const Uint8 *state = SDL_GetKeyState(nullptr); - switch (vKey) { - case DVL_VK_SHIFT: - return state[SDLK_LSHIFT] || state[SDLK_RSHIFT] ? 0x8000 : 0; - case DVL_VK_MENU: - return state[SDLK_MENU] ? 0x8000 : 0; - case DVL_VK_LEFT: - return state[SDLK_LEFT] ? 0x8000 : 0; - case DVL_VK_UP: - return state[SDLK_UP] ? 0x8000 : 0; - case DVL_VK_RIGHT: - return state[SDLK_RIGHT] ? 0x8000 : 0; - case DVL_VK_DOWN: - return state[SDLK_DOWN] ? 0x8000 : 0; - default: - return 0; - } -#endif } LRESULT DispatchMessageA(const MSG *lpMsg) @@ -438,4 +596,4 @@ WINBOOL PostMessageA(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) return true; } -} +} // namespace dvl