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