diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e4d786fc05..dfd09d96f8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -206,6 +206,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 @@ -283,6 +291,69 @@ target_compile_definitions(devilution PUBLIC "$<$:FASTER>") target_compile_definitions(devilutionx PRIVATE ASIO_STANDALONE) +# Defines without value +foreach( + def_name + SPAWN + NONET + DINGUX + RETROFW +) +if(${def_name}) + list(APPEND def_list ${def_name}) +endif() +endforeach(def_name) + +# Defines with value +foreach( + def_name + HAS_KBCTRL + KBCTRL_BUTTON_DPAD_LEFT + KBCTRL_BUTTON_DPAD_RIGHT + KBCTRL_BUTTON_DPAD_UP + KBCTRL_BUTTON_DPAD_DOWN + KBCTRL_BUTTON_B + KBCTRL_BUTTON_A + KBCTRL_BUTTON_Y + KBCTRL_BUTTON_X + KBCTRL_BUTTON_LEFTSTICK + KBCTRL_BUTTON_RIGHTSTICK + KBCTRL_BUTTON_RIGHTSHOULDER + KBCTRL_BUTTON_LEFTSHOULDER + KBCTRL_BUTTON_START + KBCTRL_BUTTON_BACK + KBCTRL_MODIFIER_KEY + JOY_AXIS_LEFTX + JOY_AXIS_LEFTY + JOY_AXIS_RIGHTX + JOY_AXIS_RIGHTY + JOY_HAT_DPAD_UP_HAT + JOY_HAT_DPAD_UP + JOY_HAT_DPAD_DOWN_HAT + JOY_HAT_DPAD_DOWN + JOY_HAT_DPAD_LEFT_HAT + JOY_HAT_DPAD_LEFT + JOY_HAT_DPAD_RIGHT_HAT + JOY_HAT_DPAD_RIGHT + JOY_BUTTON_DPAD_LEFT + JOY_BUTTON_DPAD_RIGHT + JOY_BUTTON_DPAD_UP + JOY_BUTTON_DPAD_DOWN + JOY_BUTTON_B + JOY_BUTTON_A + JOY_BUTTON_Y + JOY_BUTTON_X + JOY_BUTTON_LEFTSTICK + JOY_BUTTON_RIGHTSTICK + JOY_BUTTON_RIGHTSHOULDER + JOY_BUTTON_LEFTSHOULDER + JOY_BUTTON_START + JOY_BUTTON_BACK +) + if(DEFINED ${def_name}) + list(APPEND def_list ${def_name}=${${def_name}}) + endif() +endforeach(def_name) foreach(target devilution devilutionx) if(USE_SDL1) @@ -296,14 +367,6 @@ foreach(target devilution devilutionx) SDL2::SDL2_mixer) endif() - if(NONET) - target_compile_definitions(${target} PRIVATE NONET) - endif() - - if(SPAWN) - target_compile_definitions(${target} PRIVATE SPAWN) - endif() - if(ASAN) target_compile_options(${target} PUBLIC -fsanitize=address -fsanitize-recover=address) target_link_libraries(${target} PUBLIC -fsanitize=address -fsanitize-recover=address) @@ -314,13 +377,8 @@ foreach(target devilution devilutionx) target_link_libraries(${target} PUBLIC -fsanitize=undefined) endif() - if(DINGUX) - target_compile_definitions(${target} PRIVATE DINGUX) - endif() - if(RETROFW) - target_compile_definitions(${target} PRIVATE RETROFW) - endif() -endforeach(target devilution devilutionx) + target_compile_definitions(${target} PRIVATE ${def_list}) +endforeach(target) if(DIST AND CMAKE_CXX_COMPILER_ID MATCHES "GNU") target_link_libraries(devilutionx PUBLIC -static-libgcc -static-libstdc++) diff --git a/Packaging/OpenDingux/build-retrofw.sh b/Packaging/OpenDingux/build-retrofw.sh index cc8d9f94812..7036e8a476d 100755 --- a/Packaging/OpenDingux/build-retrofw.sh +++ b/Packaging/OpenDingux/build-retrofw.sh @@ -1,2 +1,4 @@ #!/usr/bin/env bash -./build.sh retrofw + +declare -r DIR="$(dirname "${BASH_SOURCE[0]}")" +"${DIR}/build.sh" retrofw diff --git a/Packaging/OpenDingux/build-rg350.sh b/Packaging/OpenDingux/build-rg350.sh index bd5cc8eac06..fa6c99751ba 100755 --- a/Packaging/OpenDingux/build-rg350.sh +++ b/Packaging/OpenDingux/build-rg350.sh @@ -1,2 +1,4 @@ #!/usr/bin/env bash -./build.sh rg350 + +declare -r DIR="$(dirname "${BASH_SOURCE[0]}")" +"${DIR}/build.sh" rg350 diff --git a/Packaging/OpenDingux/build-rs90.sh b/Packaging/OpenDingux/build-rs90.sh index b288649bd08..89e27b3b593 100755 --- a/Packaging/OpenDingux/build-rs90.sh +++ b/Packaging/OpenDingux/build-rs90.sh @@ -1,2 +1,4 @@ #!/usr/bin/env bash -./build.sh rs90 + +declare -r DIR="$(dirname "${BASH_SOURCE[0]}")" +"${DIR}/build.sh" rs90 diff --git a/Packaging/OpenDingux/build.sh b/Packaging/OpenDingux/build.sh index 0f682b60662..0021bfc004a 100755 --- a/Packaging/OpenDingux/build.sh +++ b/Packaging/OpenDingux/build.sh @@ -33,8 +33,6 @@ fi BUILDROOT="${BUILDROOT:-$HOME/buildroot-${TARGET}-devilutionx}" -set -x - main() { set -x prepare_buildroot @@ -64,7 +62,9 @@ prepare_buildroot() { make_buildroot() { cd "$BUILDROOT" if [[ "$TARGET" != "rg350" ]]; then - echo 'LIBSODIUM_CONF_OPTS += --enable-static' >> package/libsodium/libsodium.mk + if ! grep '--enable-static' package/libsodium/libsodium.mk; then + echo 'LIBSODIUM_CONF_OPTS += --enable-static' >> package/libsodium/libsodium.mk + fi fi make ${TARGET}_devilutionx_defconfig if [[ "$TARGET" == "rg350" ]]; then @@ -79,14 +79,33 @@ build() { mkdir -p ../../build cd ../../build rm -f CMakeCache.txt + + local -a defs=(-DDINGUX=ON -DBINARY_RELEASE=ON) if [[ "$TARGET" == "rg350" ]]; then - TARGET_DEFINES="-DNONET=ON" + defs+=(-DNONET=ON) elif [[ "$TARGET" == "rs90" ]]; then - TARGET_DEFINES="-DUSE_SDL1=ON" + defs+=(-DUSE_SDL1=ON) else - TARGET_DEFINES="-DRETROFW=ON -DUSE_SDL1=ON" + defs+=( + -DUSE_SDL1=ON + -DRETROFW=ON + -DHAS_KBCTRL=1 + -DKBCTRL_BUTTON_DPAD_LEFT=SDLK_LEFT + -DKBCTRL_BUTTON_DPAD_RIGHT=SDLK_RIGHT + -DKBCTRL_BUTTON_DPAD_UP=SDLK_UP + -DKBCTRL_BUTTON_DPAD_DOWN=SDLK_DOWN + -DKBCTRL_BUTTON_B=SDLK_LCTRL + -DKBCTRL_BUTTON_A=SDLK_LALT + -DKBCTRL_BUTTON_Y=SDLK_SPACE + -DKBCTRL_BUTTON_X=SDLK_LSHIFT + -DKBCTRL_BUTTON_RIGHTSHOULDER=SDLK_BACKSPACE + -DKBCTRL_BUTTON_LEFTSHOULDER=SDLK_TAB + -DKBCTRL_BUTTON_START=SDLK_RETURN + -DKBCTRL_BUTTON_BACK=SDLK_ESCAPE + -DKBCTRL_MODIFIER_KEY=SDLK_END + ) fi - cmake .. -DDINGUX=ON -DBINARY_RELEASE=ON ${TARGET_DEFINES} \ + cmake .. ${defs[@]} \ -DCMAKE_TOOLCHAIN_FILE="$BUILDROOT/output/host/usr/share/buildroot/toolchainfile.cmake" make -j $(nproc) cd - diff --git a/README.md b/README.md index 2c4ae08ddf3..38138d056c6 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,31 @@ Note that some features are not yet supported in SDL v1, notably upscaling, and All games are encrypted and password protected. +# Controller support + +DevilutionX supports gamepad controls. + +Default controller mappings (A/B/X/Y as in Nintendo layout, so the rightmost button is attack): + +- Left analog / DPad: move hero +- Right analog: simulate mouse +- A: attack nearby enemies, talk to towns people and merchants, pickup & drop items in inventory, OK while in main menu +- B: Select spell, cancel while in main menu +- X: pickup gold, potions & equipment from ground, open chests and doors that are nearby, use item when in inventory (useful to read books etc.) +- Y: cast spell, go to previous screen when talking to people and in shops, delete character while in main menu +- R1: inventory +- L1: character +- R2: drink mana potion +- L2: drink health potion +- Left analog click: quest log +- Right analog click: left mouse click +- Select: automap +- Start: game Menu, skip intro + +For now, they can be re-mapped by changing `SourceX/controls` or by setting the `SDL_GAMECONTROLLERCONFIG` environment +variable (see +[SDL_GameControllerDB](https://github.com/gabomdq/SDL_GameControllerDB)). + # Contributing [Guidelines](docs/CONTRIBUTING.md) diff --git a/Source/cursor.cpp b/Source/cursor.cpp index 92af07765c0..28a7cb8a88f 100644 --- a/Source/cursor.cpp +++ b/Source/cursor.cpp @@ -170,6 +170,10 @@ void CheckRportal() } } +// Controller support: Actions to run after updating the cursor state. +// Defined in SourceX/controls/plctrls.cpp. +extern void plrctrls_after_check_curs_move(); + void CheckCursMove() { int i, sx, sy, mx, my, tx, ty, px, py, xx, yy, mi; @@ -600,6 +604,8 @@ void CheckCursMove() if (pcursmonst != -1 && monster[pcursmonst]._mFlags & MFLAG_GOLEM) { pcursmonst = -1; } + + plrctrls_after_check_curs_move(); } DEVILUTION_END_NAMESPACE diff --git a/Source/diablo.cpp b/Source/diablo.cpp index addafeeddeb..ee71ec4c716 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1675,6 +1675,10 @@ void game_loop(BOOL bStartup) } } + +// Controller support: +extern void plrctrls_after_game_logic(); + void game_logic() { if (PauseMode == 2) { @@ -1720,6 +1724,8 @@ void game_logic() CheckQuests(); force_redraw |= 1; pfile_update(FALSE); + + plrctrls_after_game_logic(); } void timeout_cursor(BOOL bTimeout) diff --git a/Source/inv.h b/Source/inv.h index 0ef9c39cacf..77dc3509ba6 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 522adf6971d..a333882125c 100644 --- a/Source/scrollrt.cpp +++ b/Source/scrollrt.cpp @@ -106,6 +106,10 @@ static void scrollrt_draw_cursor_item() return; } + if (sgbControllerActive && !invflag && (!chrflag || plr[myplr]._pStatPts == 0)) { + return; + } + mx = MouseX - 1; if (mx < 0) { mx = 0; diff --git a/Source/scrollrt.h b/Source/scrollrt.h index 600af6555e7..e3fd649ce2e 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 c24a00e9bd6..938964d8a21 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 69941d78210..39fdee029d4 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 78ada696662..82dc9c731f1 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 0f298431330..811e1cce031 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: { @@ -286,12 +283,13 @@ 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; + if (event->type == SDL_MOUSEBUTTONDOWN || event->type == SDL_MOUSEBUTTONUP) { + // In SDL2 mouse events already use logical coordinates. +#ifdef USE_SDL1 + OutputToLogical(&event->button.x, &event->button.y); +#endif + if (UiItemMouseEvents(event, gUiItems, gUiItemCnt)) + return true; } return false; @@ -786,22 +784,11 @@ bool UiItemMouseEvents(SDL_Event *event, UiItem *items, std::size_t size) void DrawMouse() { - SDL_GetMouseState(&MouseX, &MouseY); - -#ifndef USE_SDL1 - if (renderer) { - float scaleX; - SDL_RenderGetScale(renderer, &scaleX, NULL); - MouseX /= scaleX; - MouseY /= scaleX; - - SDL_Rect view; - SDL_RenderGetViewport(renderer, &view); - MouseX -= view.x; - MouseY -= view.y; - } -#endif + if (sgbControllerActive) + return; + SDL_GetMouseState(&MouseX, &MouseY); + OutputToLogical(&MouseX, &MouseY); DrawArt(MouseX, MouseY, &ArtCursor); } diff --git a/SourceX/DiabloUI/dialogs.cpp b/SourceX/DiabloUI/dialogs.cpp index a5b67620c5a..7cc5a76e7f9 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 665e279bcad..6bdb43b648f 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 00000000000..8689f41c3c0 --- /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 00000000000..2dc7b5e6d78 --- /dev/null +++ b/SourceX/controls/controller.cpp @@ -0,0 +1,62 @@ +#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_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 00000000000..4a46d021c20 --- /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 00000000000..4f5a136fa89 --- /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 00000000000..731c3510850 --- /dev/null +++ b/SourceX/controls/controller_motion.cpp @@ -0,0 +1,94 @@ +#include "controls/controller_motion.h" + +#include "controls/devices/game_controller.h" +#include "controls/devices/joystick.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 + if (ProcessJoystickAxisMotion(event)) + return true; + return false; +} + +} // namespace dvl diff --git a/SourceX/controls/controller_motion.h b/SourceX/controls/controller_motion.h new file mode 100644 index 00000000000..e2dfca1a48c --- /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 00000000000..bf5bee162a7 --- /dev/null +++ b/SourceX/controls/devices/game_controller.cpp @@ -0,0 +1,173 @@ +#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; +static bool sgbTriggerLeftDown = false; +static bool sgbTriggerRightDown = false; + +ControllerButton GameControllerToControllerButton(const SDL_Event &event) +{ + switch (event.type) { + case SDL_CONTROLLERAXISMOTION: + switch (event.caxis.axis) { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + if (event.caxis.value < 8192) { // 25% pressed + sgbTriggerLeftDown = false; + } + if (event.caxis.value > 16384 && !sgbTriggerLeftDown) { // 50% pressed + sgbTriggerLeftDown = true; + return ControllerButton::AXIS_TRIGGERLEFT; + } + return ControllerButton::NONE; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + if (event.caxis.value < 8192) { // 25% pressed + sgbTriggerRightDown = false; + } + if (event.caxis.value > 16384 && !sgbTriggerRightDown) { // 50% pressed + sgbTriggerRightDown = true; + return ControllerButton::AXIS_TRIGGERRIGHT; + } + return ControllerButton::NONE; + } + 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 00000000000..864ebc9c55e --- /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 00000000000..26403c33c71 --- /dev/null +++ b/SourceX/controls/devices/joystick.cpp @@ -0,0 +1,281 @@ + +#include "controls/devices/joystick.h" + +#include "controls/controller_motion.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; + case SDL_JOYHATMOTION: +#if defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP) + if (event.jhat.hat == JOY_HAT_DPAD_UP_HAT && (event.jhat.value & JOY_HAT_DPAD_UP) != 0) + return ControllerButton::BUTTON_DPAD_UP; +#endif +#if defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN) + if (event.jhat.hat == JOY_HAT_DPAD_DOWN_HAT && (event.jhat.value & JOY_HAT_DPAD_DOWN) != 0) + return ControllerButton::BUTTON_DPAD_DOWN; +#endif +#if defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT) + if (event.jhat.hat == JOY_HAT_DPAD_LEFT_HAT && (event.jhat.value & JOY_HAT_DPAD_LEFT) != 0) + return ControllerButton::BUTTON_DPAD_LEFT; +#endif +#if defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT) + if (event.jhat.hat == JOY_HAT_DPAD_RIGHT_HAT && (event.jhat.value & JOY_HAT_DPAD_RIGHT) != 0) + return ControllerButton::BUTTON_DPAD_RIGHT; +#endif + break; + default: + break; + } + return ControllerButton::NONE; +} + +namespace { + +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 IsJoystickHatButtonPressed(ControllerButton button) +{ + switch (button) { +#if defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP) + case ControllerButton::BUTTON_DPAD_UP: + return (SDL_JoystickGetHat(CurrentJoystick(), JOY_HAT_DPAD_UP_HAT) & JOY_HAT_DPAD_UP) != 0; +#endif +#if defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN) + case ControllerButton::BUTTON_DPAD_DOWN: + return (SDL_JoystickGetHat(CurrentJoystick(), JOY_HAT_DPAD_DOWN_HAT) & JOY_HAT_DPAD_DOWN) != 0; +#endif +#if defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT) + case ControllerButton::BUTTON_DPAD_LEFT: + return (SDL_JoystickGetHat(CurrentJoystick(), JOY_HAT_DPAD_LEFT_HAT) & JOY_HAT_DPAD_LEFT) != 0; +#endif +#if defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT) + case ControllerButton::BUTTON_DPAD_RIGHT: + return (SDL_JoystickGetHat(CurrentJoystick(), JOY_HAT_DPAD_RIGHT_HAT) & JOY_HAT_DPAD_RIGHT) != 0; +#endif + default: + return false; + } +} + +} // namespace + +bool IsJoystickButtonPressed(ControllerButton button) +{ + if (CurrentJoystick() == nullptr) + return false; + if (IsJoystickHatButtonPressed(button)) + return true; + const int joy_button = JoyButtonToControllerButton(button); + return joy_button != -1 && SDL_JoystickGetButton(CurrentJoystick(), joy_button); +} + +bool ProcessJoystickAxisMotion(const SDL_Event &event) +{ + if (event.type != SDL_JOYAXISMOTION) + return false; + switch (event.jaxis.axis) { +#ifdef JOY_AXIS_LEFTX + case JOY_AXIS_LEFTX: + leftStickXUnscaled = event.jaxis.value; + leftStickNeedsScaling = true; + break; +#endif +#ifdef JOY_AXIS_LEFTY + case JOY_AXIS_LEFTY: + leftStickYUnscaled = -event.jaxis.value; + leftStickNeedsScaling = true; + break; +#endif +#ifdef JOY_AXIS_RIGHTX + case JOY_AXIS_RIGHTX: + rightStickXUnscaled = event.jaxis.value; + rightStickNeedsScaling = true; + break; +#endif +#ifdef JOY_AXIS_RIGHTY + case JOY_AXIS_RIGHTY: + rightStickYUnscaled = -event.jaxis.value; + rightStickNeedsScaling = true; + break; +#endif + default: + return false; + } + return true; +} + +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 00000000000..14806a57f94 --- /dev/null +++ b/SourceX/controls/devices/joystick.h @@ -0,0 +1,21 @@ +#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); + +bool ProcessJoystickAxisMotion(const SDL_Event &event); + +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 00000000000..299c4cea770 --- /dev/null +++ b/SourceX/controls/devices/kbcontroller.cpp @@ -0,0 +1,204 @@ +#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_GetKeyState()[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; + } + 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 00000000000..663979916f4 --- /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 00000000000..3e1573b928b --- /dev/null +++ b/SourceX/controls/game_controls.cpp @@ -0,0 +1,165 @@ +#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_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 (invflag) + *action = GameActionSendMouseClick { GameActionSendMouseClick::RIGHT, ctrl_event.up }; + else if (!ctrl_event.up) + *action = GameAction(GameActionType::SECONDARY_ACTION); + return true; + case ControllerButton::BUTTON_X: // Left button + if (InGameMenu()) + break; // Map to keyboard key + if (!ctrl_event.up) + *action = GameAction(GameActionType::CAST_SPELL); + 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 = GameActionSendMouseClick { GameActionSendMouseClick::LEFT, 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; + } + if (event.type == SDL_CONTROLLERAXISMOTION) { + return true; // Ignore releasing the trigger buttons + } +#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 00000000000..b544ca889c1 --- /dev/null +++ b/SourceX/controls/game_controls.h @@ -0,0 +1,85 @@ +#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. + CAST_SPELL, + TOGGLE_INVENTORY, + TOGGLE_CHARACTER_INFO, + TOGGLE_QUICK_SPELL_MENU, + SEND_KEY, + SEND_MOUSE_CLICK, +}; + +struct GameActionSendKey { + DWORD vk_code; + bool up; +}; + +struct GameActionSendMouseClick { + enum Button { + LEFT = 0, + RIGHT, + }; + Button button; + 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(GameActionSendMouseClick send_mouse_click) + : type(GameActionType::SEND_MOUSE_CLICK) + , send_mouse_click(send_mouse_click) + { + } + + union { + GameActionSendKey send_key; + GameActionSendMouseClick send_mouse_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 00000000000..841fcfd83d2 --- /dev/null +++ b/SourceX/controls/menu_controls.cpp @@ -0,0 +1,75 @@ +#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) { + sgbControllerActive = true; + 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 00000000000..b878857e5f0 --- /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 new file mode 100644 index 00000000000..12089867853 --- /dev/null +++ b/SourceX/controls/plrctrls.cpp @@ -0,0 +1,628 @@ +#include "controls/plrctrls.h" + +#include + +#include "controls/controller_motion.h" +#include "controls/game_controls.h" + +// Based on the Nintendo Switch port by @lantus, @erfg12, @rsn8887. + +namespace dvl { + +#define INV_TOP 240; +#define INV_LEFT RIGHT_PANEL_X; +#define INV_HEIGHT 320; + +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 +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) +{ + int diff_x = abs(plr[myplr]._px - x); + int diff_y = abs(plr[myplr]._py - y); + + if (diff_x <= diff && diff_y <= diff) { + coords cm = { diff_x, diff_y }; + //sprintf(tempstr, "N-DIFF X:%i Y:%i", diff_x, diff_y); + //NetSendCmdString(1 << myplr, tempstr); + return cm; + } + return { -1, -1 }; +} + +void CheckItemsNearby() +{ + for (int i = 0; i < MAXITEMS; i++) { + if (CheckNearbyObjs(item[i]._ix, item[i]._iy, 1).x != -1 && item[i]._iSelFlag > 0 && item[i]._itype > -1) { + pcursitem = i; + if (dItem[item[i]._ix][item[i]._iy] <= 0) + continue; + return; // item nearby, don't find objects + } + } + pcursitem = -1; + + for (int i = 0; i < MAXOBJECTS; i++) { + if (CheckNearbyObjs(object[i]._ox, object[i]._oy, 1).x != -1 && object[i]._oSelFlag > 0 && object[i]._otype > -1 && currlevel) { // make sure we're in the dungeon to scan for objs + pcursobj = i; + return; + } + } + pcursobj = -1; +} + +void CheckTownersNearby() +{ + 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) + continue; + pcursmonst = i; + break; + } + } +} + +void CheckMonstersNearby() +{ + pcursmonst = -1; + coords objDistLast = { 99, 99 }; // previous obj distance + // The first MAX_PLRS monsters are reserved for players' golems. + for (int i = MAX_PLRS; i < MAXMONSTERS; i++) { + const auto &monst = monster[i]; + const int mx = monst._mx; + const int my = monst._my; + if (dMonster[mx][my] == 0 || (monst._mFlags & MFLAG_HIDDEN) || // hidden + monst._mhitpoints <= 0 || // dead + !((dFlags[mx][my] & BFLAG_LIT) || plr[myplr]._pInfraFlag)) // not visable + continue; + const char mSelFlag = monst.MData->mSelFlag; + if (mSelFlag & 1 || mSelFlag & 2 || mSelFlag & 3 || mSelFlag & 4) { // is monster selectable + coords objDist = CheckNearbyObjs(mx, my, 6); + if (objDist.x > -1 && objDist.x <= objDistLast.x && objDist.y <= objDistLast.y) { + pcursmonst = i; + objDistLast = objDist; + } + } + } +} + +void Interact() +{ + if (leveltype == DTYPE_TOWN && pcursmonst != -1) { + NetSendCmdLocParam1(true, CMD_TALKXY, towner[pcursmonst]._tx, towner[pcursmonst]._ty, pcursmonst); + } else if (pcursmonst != -1) { + if (plr[myplr]._pwtype != WT_RANGED || CanTalkToMonst(pcursmonst)) { + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else { + NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); + } + } else if (pcursplr != -1 && !FriendlyMode) { + NetSendCmdParam1(true, plr[myplr]._pwtype == WT_RANGED ? CMD_RATTACKPID : CMD_ATTACKPID, pcursplr); + } +} + +void AttrIncBtnSnap(int key) +{ + if (invflag || spselflag || !chrflag) + return; + + if (chrbtnactive && !plr[myplr]._pStatPts) + return; + + DWORD ticks = GetTickCount(); + if (ticks - invmove < 100) { + return; + } + invmove = ticks; + + // first, find our cursor location + int slot = 0; + 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) { + slot = i; + break; + } + } + + // set future location up or down + if (key == DVL_VK_UP) { + if (slot > 0) + --slot; + } else if (key == DVL_VK_DOWN) { + if (slot < 3) + ++slot; + } + + // move cursor to our new location + int x = ChrBtnsRect[slot].x + (ChrBtnsRect[slot].w / 2); + int y = ChrBtnsRect[slot].y + (ChrBtnsRect[slot].h / 2); + SetCursorPos(x, y); +} + +// move the cursor around in our inventory +// if mouse coords are at SLOTXY_CHEST_LAST, consider this center of equipment +// small inventory squares are 29x29 (roughly) +void InvMove(MoveDirection dir) +{ + if (!invflag) + return; + + DWORD ticks = GetTickCount(); + if (ticks - invmove < 100) { + return; + } + invmove = ticks; + int x = MouseX; + int y = MouseY; + + // check which inventory rectangle the mouse is in, if any + for (int r = 0; (DWORD)r < NUM_XY_SLOTS; r++) { + if (x >= InvRect[r].X && x < InvRect[r].X + (INV_SLOT_SIZE_PX + 1) && y >= InvRect[r].Y - (INV_SLOT_SIZE_PX + 1) && y < InvRect[r].Y) { + slot = r; + break; + } + } + + if (slot < 0) + slot = 0; + if (slot > SLOTXY_BELT_LAST) + slot = SLOTXY_BELT_LAST; + + // when item is on cursor, this is the real cursor XY + if (dir.x == MoveDirectionX::LEFT) { + if (slot >= SLOTXY_HAND_RIGHT_FIRST && slot <= SLOTXY_HAND_RIGHT_LAST) { + x = InvRect[SLOTXY_CHEST_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_CHEST_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_CHEST_FIRST && slot <= SLOTXY_CHEST_LAST) { + x = InvRect[SLOTXY_HAND_LEFT_FIRST + 2].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HAND_LEFT_FIRST + 2].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_AMULET) { + x = InvRect[SLOTXY_HEAD_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HEAD_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_RING_RIGHT) { + x = InvRect[SLOTXY_RING_LEFT].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_RING_LEFT].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_BELT_FIRST) { + // do nothing + } else if (slot == SLOTXY_RING_LEFT) { // left ring + // do nothing + } else if (slot >= SLOTXY_HAND_LEFT_FIRST && slot <= SLOTXY_HAND_LEFT_LAST) { // left hand + // do nothing + } else if (slot >= SLOTXY_HEAD_FIRST && slot <= SLOTXY_HEAD_LAST) { // head + // do nothing + } else if (slot > SLOTXY_INV_FIRST) { // general inventory + if (slot != SLOTXY_INV_FIRST && slot != 35 && slot != 45 && slot != 55) { // left bounds + slot -= 1; + x = InvRect[slot].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[slot].Y - (INV_SLOT_SIZE_PX / 2); + } + } + } else if (dir.x == MoveDirectionX::RIGHT) { + if (slot == SLOTXY_RING_LEFT) { + x = InvRect[SLOTXY_RING_RIGHT].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_RING_RIGHT].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HAND_LEFT_FIRST && slot <= SLOTXY_HAND_LEFT_LAST) { + x = InvRect[SLOTXY_CHEST_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_CHEST_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_CHEST_FIRST && slot <= SLOTXY_CHEST_LAST) { + x = InvRect[SLOTXY_HAND_RIGHT_FIRST + 2].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HAND_RIGHT_FIRST + 2].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HEAD_FIRST && slot <= SLOTXY_HEAD_LAST) { // head to amulet + x = InvRect[SLOTXY_AMULET].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_AMULET].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HAND_RIGHT_FIRST && slot <= SLOTXY_HAND_RIGHT_LAST) { // right hand + // do nothing + } else if (slot == SLOTXY_AMULET) { + // do nothing + } else if (slot == SLOTXY_RING_RIGHT) { + // do nothing + } else if (slot < SLOTXY_BELT_LAST && slot >= SLOTXY_INV_FIRST) { // general inventory + if (slot != 34 && slot != 44 && slot != 54 && slot != SLOTXY_INV_LAST) { // right bounds + slot += 1; + x = InvRect[slot].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[slot].Y - (INV_SLOT_SIZE_PX / 2); + } + } + } + if (dir.y == MoveDirectionY::UP) { + if (slot > 24 && slot <= 27) { // first 3 general slots + x = InvRect[SLOTXY_RING_LEFT].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_RING_LEFT].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= 28 && slot <= 32) { // middle 4 general slots + x = InvRect[SLOTXY_CHEST_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_CHEST_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= 33 && slot < 35) { // last 3 general slots + x = InvRect[SLOTXY_RING_RIGHT].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_RING_RIGHT].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_CHEST_FIRST && slot <= SLOTXY_CHEST_LAST) { // chest to head + x = InvRect[SLOTXY_HEAD_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HEAD_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_RING_LEFT) { // left ring to left hand + x = InvRect[SLOTXY_HAND_LEFT_FIRST + 2].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HAND_LEFT_FIRST + 2].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_RING_RIGHT) { // right ring to right hand + x = InvRect[SLOTXY_HAND_RIGHT_FIRST + 2].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HAND_RIGHT_FIRST + 2].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HAND_RIGHT_FIRST && slot <= SLOTXY_HAND_RIGHT_LAST) { // right hand to amulet + x = InvRect[SLOTXY_AMULET].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_AMULET].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HEAD_FIRST && slot <= SLOTXY_HEAD_LAST) { + // do nothing + } else if (slot >= SLOTXY_HAND_LEFT_FIRST && slot <= SLOTXY_HAND_LEFT_LAST) { // left hand to head + x = InvRect[SLOTXY_HEAD_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HEAD_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_AMULET) { + // do nothing + } else if (slot >= (SLOTXY_INV_FIRST + 10)) { // general inventory + slot -= 10; + x = InvRect[slot].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[slot].Y - (INV_SLOT_SIZE_PX / 2); + } + } else if (dir.y == MoveDirectionY::DOWN) { + if (slot >= SLOTXY_HEAD_FIRST && slot <= SLOTXY_HEAD_LAST) { + x = InvRect[SLOTXY_CHEST_FIRST].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_CHEST_FIRST].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_CHEST_FIRST && slot <= SLOTXY_CHEST_LAST) { + x = InvRect[30].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[30].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HAND_LEFT_FIRST && slot <= SLOTXY_HAND_LEFT_LAST) { + x = InvRect[SLOTXY_RING_LEFT].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_RING_LEFT].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_RING_LEFT) { + x = InvRect[26].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[26].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_RING_RIGHT) { + x = InvRect[34].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[34].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot == SLOTXY_AMULET) { + x = InvRect[SLOTXY_HAND_RIGHT_FIRST + 2].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_HAND_RIGHT_FIRST + 2].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot >= SLOTXY_HAND_RIGHT_FIRST && slot <= SLOTXY_HAND_RIGHT_LAST) { + x = InvRect[SLOTXY_RING_RIGHT].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[SLOTXY_RING_RIGHT].Y - (INV_SLOT_SIZE_PX / 2); + } else if (slot < (SLOTXY_BELT_LAST - 10)) { // general inventory + slot += 10; + x = InvRect[slot].X + (INV_SLOT_SIZE_PX / 2); + y = InvRect[slot].Y - (INV_SLOT_SIZE_PX / 2); + } + } + + if (x == MouseX && y == MouseY) { + return; // Avoid wobeling when scalled + } + + 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 + x -= 10; + y -= 10; + } + } + SetCursorPos(x, y); +} + +// check if hot spell at X Y exists +bool HSExists(int x, int y) +{ + for (int r = 0; r < speedspellcount; r++) { // speedbook cells are 56x56 + if (MouseX >= speedspellscoords[r].x - 28 && MouseX < speedspellscoords[r].x + (28) && MouseY >= speedspellscoords[r].y - (28) && MouseY < speedspellscoords[r].y + 28) { + return true; + } + } + return false; +} + +void HotSpellMove(int key) +{ + if (!spselflag) + return; + int x = 0; + int y = 0; + + DWORD ticks = GetTickCount(); + if (ticks - invmove < 100) { + return; + } + invmove = ticks; + + for (int r = 0; r < speedspellcount; r++) { // speedbook cells are 56x56 + // our 3 rows by y axis + if (speedspellscoords[r].y == 307) + hsr[0]++; + if (speedspellscoords[r].y == 251) + hsr[1]++; + if (speedspellscoords[r].y == 195) + hsr[2]++; + if (MouseX >= speedspellscoords[r].x - 28 && MouseX < speedspellscoords[r].x + (28) && MouseY >= speedspellscoords[r].y - (28) && MouseY < speedspellscoords[r].y + 28) { + spbslot = r; + //sprintf(tempstr, "IN HOT SPELL CELL NUM:%i", r); + //NetSendCmdString(1 << myplr, tempstr); + } + } + + 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; + y = 251; + } + } else if (speedspellscoords[spbslot].y == 251 && hsr[2] > 0) { // we're in row 2, check if row 3 has spells + if (HSExists(MouseX, 195)) { + x = MouseX; + y = 195; + } + } + } else if (key == DVL_VK_DOWN) { + if (speedspellscoords[spbslot].y == 251) { // we're in row 2 + if (HSExists(MouseX, 307)) { + x = MouseX; + y = 307; + } + } else if (speedspellscoords[spbslot].y == 195) { // we're in row 3 + if (HSExists(MouseX, 251)) { + x = MouseX; + y = 251; + } + } + } else if (key == DVL_VK_LEFT) { + if (spbslot >= speedspellcount - 1) + return; + spbslot++; + x = speedspellscoords[spbslot].x; + y = speedspellscoords[spbslot].y; + } else if (key == DVL_VK_RIGHT) { + if (spbslot <= 0) + return; + spbslot--; + x = speedspellscoords[spbslot].x; + y = speedspellscoords[spbslot].y; + } + + if (x > 0 && y > 0) { + SetCursorPos(x, y); + } +} + +void WalkInDir(MoveDirection dir) +{ + if (dir.x == MoveDirectionX::NONE && dir.y == MoveDirectionY::NONE) { + if (sgbControllerActive) + plr[myplr].walkpath[0] = WALK_NONE; + return; + } + + if (plr[myplr].walkpath[0] != WALK_NONE) { + return; + } + + ClrPlrPath(myplr); // clear nodes + plr[myplr].destAction = ACTION_NONE; // stop attacking, etc. + 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 const direction kFaceDir[3][3] = { + // NONE UP DOWN + { DIR_OMNI, DIR_N, DIR_S }, // NONE + { DIR_W, DIR_NW, DIR_SW }, // LEFT + { DIR_E, DIR_NE, DIR_SE }, // RIGHT + }; + plr[myplr]._pdir = kFaceDir[static_cast(dir.x)][static_cast(dir.y)]; +} + +void MenuMoveX(MoveDirectionX dir) +{ + if (dir == MoveDirectionX::NONE) + return; + HotSpellMove(dir == MoveDirectionX::LEFT ? DVL_VK_LEFT : DVL_VK_RIGHT); +} + +void MenuMoveY(MoveDirectionY dir) +{ + if (dir == MoveDirectionY::NONE) + return; + 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 (move_dir.x != MoveDirectionX::NONE || move_dir.y != MoveDirectionY::NONE) { + sgbControllerActive = true; + } + if (InControlledMenu()) { + MenuMoveX(move_dir.x); + MenuMoveY(move_dir.y); + InvMove(move_dir); + } else { + WalkInDir(move_dir); + } +} + +struct RightStickAccumulator { + void start(int *x, int *y) + { + hiresDX += rightStickX * kGranularity; + hiresDY += rightStickY * kGranularity; + *x += hiresDX / slowdown; + *y += -hiresDY / slowdown; + } + + void finish() + { + // keep track of remainder for sub-pixel motion + hiresDX %= slowdown; + hiresDY %= slowdown; + } + + 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(); + } +} + +void plrctrls_after_check_curs_move() +{ + HandleRightStickMotion(); + + // check for monsters first, then items, then towners. + if (sgbControllerActive) { // cursor should be missing + CheckMonstersNearby(); + pcursitem = -1; + if (pcursmonst == -1) { + CheckItemsNearby(); + CheckTownersNearby(); + } + } +} + +void plrctrls_after_game_logic() +{ + Movement(); +} + +void UseBeltItem(int type) +{ + for (int i = 0; i < MAXBELTITEMS; i++) { + const auto id = AllItemsList[plr[myplr].SpdList[i].IDidx].iMiscId; + const auto spellId = AllItemsList[plr[myplr].SpdList[i].IDidx].iSpell; + if ((type == BLT_HEALING && (id == IMISC_HEAL || id == IMISC_FULLHEAL || (id == IMISC_SCROLL && spellId == SPL_HEAL))) + || (type == BLT_MANA && (id == IMISC_MANA || id == IMISC_FULLMANA)) + || id == IMISC_REJUV || id == IMISC_FULLREJUV) { + if (plr[myplr].SpdList[i]._itype > -1) { + UseInvItem(myplr, INVITEM_BELT_FIRST + i); + break; + } + } + } +} + +void PerformPrimaryAction() +{ + if (invflag) { // inventory is open + if (pcurs == CURSOR_IDENTIFY) + CheckIdentify(myplr, pcursinvitem); + else if (pcurs == CURSOR_REPAIR) + DoRepair(myplr, pcursinvitem); + else if (pcurs == CURSOR_RECHARGE) + DoRecharge(myplr, pcursinvitem); + else + CheckInvItem(); + return; + } + + if (spselflag) { + SetSpell(); + return; + } + + if (chrflag && !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(); + } + } + return; + } + + if (pcursmonst == -1) { + CheckTownersNearby(); + } + Interact(); +} + +void PerformSecondaryAction() +{ + if (invflag) + return; + + CheckItemsNearby(); + + if (pcursitem != -1 && pcurs == CURSOR_HAND) { + NetSendCmdLocParam1(pcurs, CMD_GOTOAGETITEM, item[pcursitem]._ix, item[pcursitem]._iy, pcursitem); + } else if (pcursobj != -1) { + NetSendCmdLocParam1(true, pcurs == CURSOR_DISARM ? CMD_DISARMXY : CMD_OPOBJXY, object[pcursobj]._ox, object[pcursobj]._oy, pcursobj); + } +} + +} // namespace dvl diff --git a/SourceX/controls/plrctrls.h b/SourceX/controls/plrctrls.h new file mode 100644 index 00000000000..ad58b8eae3c --- /dev/null +++ b/SourceX/controls/plrctrls.h @@ -0,0 +1,42 @@ +#pragma once +// Controller actions implementation + +#include "devilution.h" + +namespace dvl { + +typedef enum belt_item_type { + BLT_HEALING, + BLT_MANA, +}; + +// Run after every game logic iteration. +// Handles player and menu movement. +void plrctrls_after_game_logic(); + +// Runs at the end of CheckCursMove() +// Handles item, object, and monster auto-aim. +void plrctrls_after_check_curs_move(); + +// 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 UseBeltItem(int type); + +// 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; + +} // namespace dvl diff --git a/SourceX/dx.cpp b/SourceX/dx.cpp index 7b912c039bf..bb7437d8c40 100644 --- a/SourceX/dx.cpp +++ b/SourceX/dx.cpp @@ -186,12 +186,21 @@ void BltFast(DWORD dwX, DWORD dwY, LPRECT lpSrcRect) static_cast(dwY), w, h }; - - // Convert from 8-bit to 32-bit - if (SDL_BlitSurface(pal_surface, &src_rect, GetOutputSurface(), &dst_rect) <= -1) { - ErrSdl(); + if (OutputRequiresScaling()) { + ScaleOutputRect(&dst_rect); + // Convert from 8-bit to 32-bit + SDL_Surface *tmp = SDL_ConvertSurface(pal_surface, GetOutputSurface()->format, 0); + if (SDL_BlitScaled(tmp, &src_rect, GetOutputSurface(), &dst_rect) <= -1) { + SDL_FreeSurface(tmp); + ErrSdl(); + } + SDL_FreeSurface(tmp); + } else { + // Convert from 8-bit to 32-bit + if (SDL_BlitSurface(pal_surface, &src_rect, GetOutputSurface(), &dst_rect) <= -1) { + ErrSdl(); + } } - bufferUpdated = true; } diff --git a/SourceX/miniwin/ddraw.cpp b/SourceX/miniwin/ddraw.cpp index fc9a3774ec0..f8fbfd65b16 100644 --- a/SourceX/miniwin/ddraw.cpp +++ b/SourceX/miniwin/ddraw.cpp @@ -4,7 +4,8 @@ namespace dvl { extern SDL_Surface *renderer_texture_surface; // defined in dx.cpp -SDL_Surface *GetOutputSurface() { +SDL_Surface *GetOutputSurface() +{ #ifdef USE_SDL1 return SDL_GetVideoSurface(); #else @@ -14,4 +15,24 @@ SDL_Surface *GetOutputSurface() { #endif } +bool OutputRequiresScaling() +{ +#ifdef USE_SDL1 + return SCREEN_WIDTH != GetOutputSurface()->w; +#else // SDL2, scaling handled by renderer. + return false; +#endif +} + +void ScaleOutputRect(SDL_Rect *rect) +{ + if (!OutputRequiresScaling()) + return; + const auto *surface = GetOutputSurface(); + rect->x = rect->x * surface->w / SCREEN_WIDTH; + rect->y = rect->y * surface->h / SCREEN_HEIGHT; + rect->w = rect->w * surface->w / SCREEN_WIDTH; + rect->h = rect->h * surface->h / SCREEN_HEIGHT; +} + } // namespace dvl diff --git a/SourceX/miniwin/ddraw.h b/SourceX/miniwin/ddraw.h index fb335cda7a2..e2a9723623f 100644 --- a/SourceX/miniwin/ddraw.h +++ b/SourceX/miniwin/ddraw.h @@ -1,5 +1,6 @@ #include "devilution.h" #include +#include namespace dvl { @@ -18,4 +19,64 @@ extern bool bufferUpdated; // SDL2, upscale: Renderer texture surface. SDL_Surface *GetOutputSurface(); +// Whether the output surface requires software scaling. +// Always returns false on SDL2. +bool OutputRequiresScaling(); + +// Scales rect if necessary. +void ScaleOutputRect(SDL_Rect *rect); + +// Convert from output coordinates to logical (resolution-independent) coordinates. +template < + typename T, + typename = typename std::enable_if::value, T>::type> +void OutputToLogical(T *x, T *y) +{ +#ifndef USE_SDL1 + if (!renderer) + return; + float scaleX; + SDL_RenderGetScale(renderer, &scaleX, NULL); + *x /= scaleX; + *y /= scaleX; + + SDL_Rect view; + SDL_RenderGetViewport(renderer, &view); + *x -= view.x; + *y -= view.y; +#else + if (!OutputRequiresScaling()) + return; + const auto *surface = GetOutputSurface(); + *x = *x * SCREEN_WIDTH / surface->w; + *y = *y * SCREEN_HEIGHT / surface->h; +#endif +} + +template < + typename T, + typename = typename std::enable_if::value, T>::type> +void LogicalToOutput(T *x, T *y) +{ +#ifndef USE_SDL1 + if (!renderer) + return; + float scaleX; + SDL_RenderGetScale(renderer, &scaleX, NULL); + *x /= scaleX; + *y /= scaleX; + + SDL_Rect view; + SDL_RenderGetViewport(renderer, &view); + *x -= view.x; + *y -= view.y; +#else + if (!OutputRequiresScaling()) + return; + const auto *surface = GetOutputSurface(); + *x = *x * surface->w / SCREEN_WIDTH; + *y = *y * surface->h / SCREEN_HEIGHT; +#endif +} + } // namespace dvl diff --git a/SourceX/miniwin/misc.cpp b/SourceX/miniwin/misc.cpp index 415f40d33c0..e3a77452a53 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" @@ -12,6 +13,10 @@ #define strncasecmp _strnicmp #endif +#if defined(USE_SDL1) && defined(RETROFW) +#include +#endif + namespace dvl { DWORD last_error; @@ -128,6 +133,8 @@ bool SpawnWindow(LPCSTR lpWindowName, int nWidth, int nHeight) SDL_EnableUNICODE(1); #endif + InitController(); + int upscale = 1; DvlIntSetting("upscale", &upscale); DvlIntSetting("fullscreen", (int *)&fullscreen); @@ -140,8 +147,19 @@ bool SpawnWindow(LPCSTR lpWindowName, int nWidth, int nHeight) if (fullscreen) flags |= SDL_FULLSCREEN; SDL_WM_SetCaption(lpWindowName, WINDOW_ICON_NAME); +#ifndef RETROFW SDL_SetVideoMode(nWidth, nHeight, /*bpp=*/0, flags); +#else // RETROFW + // JZ4760 IPU scaler (e.g. on RG-300 v2/3) - automatic high-quality scaling. + if (access("/proc/jz/ipu_ratio", F_OK) == 0) { + SDL_SetVideoMode(nWidth, nHeight, /*bpp=*/0, flags); + } else { + // Other RetroFW devices have 320x480 screens with non-square pixels. + SDL_SetVideoMode(320, 480, /*bpp=*/0, flags); + } +#endif window = SDL_GetVideoSurface(); + SDL_Log("Output surface: %dx%d sw-scaling=%d bpp=%d", window->w, window->h, OutputRequiresScaling(), window->format->BitsPerPixel); if (grabInput) SDL_WM_GrabInput(SDL_GRAB_ON); atexit(SDL_VideoQuit); // Without this video mode is not restored after fullscreen. diff --git a/SourceX/miniwin/misc_dx.cpp b/SourceX/miniwin/misc_dx.cpp index 82c9fe940f7..866b919820f 100644 --- a/SourceX/miniwin/misc_dx.cpp +++ b/SourceX/miniwin/misc_dx.cpp @@ -8,22 +8,11 @@ namespace dvl { WINBOOL SetCursorPos(int X, int Y) { assert(window); - -#ifndef USE_SDL1 - if (renderer) { - SDL_Rect view; - SDL_RenderGetViewport(renderer, &view); - X += view.x; - Y += view.y; - - float scaleX; - SDL_RenderGetScale(renderer, &scaleX, NULL); - X *= scaleX; - Y *= scaleX; - } -#endif - - SDL_WarpMouseInWindow(window, X, Y); + int outX = X, outY = Y; + LogicalToOutput(&outX, &outY); + SDL_WarpMouseInWindow(window, outX, outY); + MouseX = X; + MouseY = Y; return true; } diff --git a/SourceX/miniwin/misc_msg.cpp b/SourceX/miniwin/misc_msg.cpp index 4c7e8d67050..7fbba4136c7 100644 --- a/SourceX/miniwin/misc_msg.cpp +++ b/SourceX/miniwin/misc_msg.cpp @@ -1,8 +1,13 @@ +#include #include #include #include "devilution.h" #include "stubs.h" +#include "controls/controller_motion.h" +#include "controls/game_controls.h" +#include "controls/plrctrls.h" +#include "miniwin/ddraw.h" /** @file * * @@ -178,19 +183,160 @@ 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_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + lpMsg->message = event.type == SDL_JOYBUTTONUP ? DVL_WM_LBUTTONUP : DVL_WM_LBUTTONDOWN; + lpMsg->lParam = (MouseY << 16) | (MouseX & 0xFFFF); + break; + 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: + UNIMPLEMENTED(); + } + 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); +} + +// Moves the mouse to the first inventory slot. +void FocusOnInventory() +{ + if (!invflag) + return; + SetCursorPos(InvRect[25].X + (INV_SLOT_SIZE_PX / 2), InvRect[25].Y - (INV_SLOT_SIZE_PX / 2)); +} + +// Moves the mouse to the first attribute "+" button. +void FocusOnCharInfo() +{ + if (!chrflag || plr[myplr]._pStatPts == 0) + return; + + // 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; + const auto &rect = ChrBtnsRect[stat]; + SetCursorPos(rect.x + (rect.w / 2), rect.y + (rect.h / 2)); +} + +void StoreSpellCoords() +{ + constexpr int START_X = 20; + constexpr int END_X = 636; + constexpr int END_Y = 495; + constexpr int BOX_SIZE = 56; + speedspellcount = 0; + int xo = END_X, yo = END_Y; + for (int i = 0; i < 4; i++) { + std::uint64_t spells; + switch (i) { + case RSPLTYPE_SKILL: + spells = plr[myplr]._pAblSpells; + break; + case RSPLTYPE_SPELL: + spells = plr[myplr]._pMemSpells; + break; + case RSPLTYPE_SCROLL: + spells = plr[myplr]._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + spells = plr[myplr]._pISpells; + break; + default: + continue; + } + std::uint64_t spell = 1; + for (int j = 1; j < MAX_SPELLS; j++) { + if ((spell & spells)) { + speedspellscoords[speedspellcount] = { xo - 36, yo - 188 }; + ++speedspellcount; + xo -= BOX_SIZE; + if (xo == START_X) { + xo = END_X; + yo -= BOX_SIZE; + } + } + spell <<= 1; + } + if (spells && xo != END_X) + xo -= BOX_SIZE; + if (xo == START_X) { + xo = END_X; + yo -= BOX_SIZE; + } + } +} + +} // namespace + +/** + * @brief Clean the inventory related cursor states. + */ +void BlurInventory() +{ + if (pcurs >= CURSOR_FIRSTITEM) // drop item to allow us to pick up other items + DropItemBeforeTrig(); + if (pcurs == CURSOR_REPAIR || pcurs == CURSOR_RECHARGE) + SetCursor_(CURSOR_HAND); +} + WINBOOL PeekMessageA(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) { if (wMsgFilterMin != 0) @@ -221,9 +367,118 @@ 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; + } + +#ifdef USE_SDL1 + if (e.type == SDL_MOUSEMOTION) { + OutputToLogical(&e.motion.x, &e.motion.y); + } else if (e.type == SDL_MOUSEBUTTONDOWN || e.type == SDL_MOUSEBUTTONUP) { + OutputToLogical(&e.button.x, &e.button.y); + } +#endif + + if (movie_playing) { + if (ShouldSkipMovie(e)) + SetMouseLMBMessage(e, lpMsg); + return true; + } + + if (ProcessControllerMotion(e)) { + ScaleJoysticks(); + HandleRightStickMotion(); + return true; + } + + GameAction action; + if (GetGameAction(e, &action)) { + if (action.type != GameActionType::NONE) { + sgbControllerActive = true; + } + + switch (action.type) { + case GameActionType::NONE: + break; + case GameActionType::USE_HEALTH_POTION: + UseBeltItem(BLT_HEALING); + break; + case GameActionType::USE_MANA_POTION: + UseBeltItem(BLT_MANA); + break; + case GameActionType::PRIMARY_ACTION: + PerformPrimaryAction(); + break; + case GameActionType::SECONDARY_ACTION: + PerformSecondaryAction(); + break; + case GameActionType::CAST_SPELL: + if (pcurs >= CURSOR_FIRSTITEM && invflag) + // Drop item so that it does not get destroyed. + DropItemBeforeTrig(); + if (!invflag && !talkflag) + // Cast the spell. + RightMouseDown(); + // Close active menu if any / stop walking. + PressEscKey(); + break; + case GameActionType::TOGGLE_QUICK_SPELL_MENU: + lpMsg->message = DVL_WM_KEYDOWN; + lpMsg->wParam = 'S'; + chrflag = false; + questlog = false; + invflag = false; + sbookflag = false; + StoreSpellCoords(); + break; + case GameActionType::TOGGLE_CHARACTER_INFO: + chrflag = !chrflag; + if (chrflag) { + questlog = false; + invflag = false; + spselflag = false; + FocusOnCharInfo(); + } + break; + case GameActionType::TOGGLE_INVENTORY: + invflag = !invflag; + if (invflag) { + chrflag = false; + questlog = false; + sbookflag = false; + spselflag = false; + FocusOnInventory(); + } else { + BlurInventory(); + } + 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_CLICK: + sgbControllerActive = false; + switch (action.send_mouse_click.button) { + case GameActionSendMouseClick::LEFT: + lpMsg->message = action.send_mouse_click.up ? DVL_WM_LBUTTONUP : DVL_WM_LBUTTONDOWN; + break; + case GameActionSendMouseClick::RIGHT: + lpMsg->message = action.send_mouse_click.up ? DVL_WM_RBUTTONUP : DVL_WM_RBUTTONDOWN; + break; + } + lpMsg->lParam = (MouseY << 16) | (MouseX & 0xFFFF); + break; + } + return true; + } else { + sgbControllerActive = false; + } + switch (e.type) { case SDL_QUIT: lpMsg->message = DVL_WM_QUIT; @@ -374,43 +629,27 @@ WINBOOL TranslateMessage(const MSG *lpMsg) SHORT GetAsyncKeyState(int vKey) { -#ifndef USE_SDL1 - const Uint8 *state = SDL_GetKeyboardState(nullptr); - switch (vKey) { - case DVL_VK_SHIFT: - return state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT] ? 0x8000 : 0; - case DVL_VK_MENU: - return state[SDL_SCANCODE_MENU] ? 0x8000 : 0; - case DVL_VK_LEFT: - return state[SDL_SCANCODE_LEFT] ? 0x8000 : 0; - case DVL_VK_UP: - return state[SDL_SCANCODE_UP] ? 0x8000 : 0; - case DVL_VK_RIGHT: - return state[SDL_SCANCODE_RIGHT] ? 0x8000 : 0; - case DVL_VK_DOWN: - return state[SDL_SCANCODE_DOWN] ? 0x8000 : 0; - default: - return 0; - } -#else - const Uint8 *state = SDL_GetKeyState(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_GetKeyState(); switch (vKey) { case DVL_VK_SHIFT: - return state[SDLK_LSHIFT] || state[SDLK_RSHIFT] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_LEFTSHIFT] || state[SDLC_KEYSTATE_RIGHTSHIFT] ? 0x8000 : 0; case DVL_VK_MENU: - return state[SDLK_MENU] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_MENU] ? 0x8000 : 0; case DVL_VK_LEFT: - return state[SDLK_LEFT] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_LEFT] ? 0x8000 : 0; case DVL_VK_UP: - return state[SDLK_UP] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_UP] ? 0x8000 : 0; case DVL_VK_RIGHT: - return state[SDLK_RIGHT] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_RIGHT] ? 0x8000 : 0; case DVL_VK_DOWN: - return state[SDLK_DOWN] ? 0x8000 : 0; + return state[SDLC_KEYSTATE_DOWN] ? 0x8000 : 0; default: return 0; } -#endif } LRESULT DispatchMessageA(const MSG *lpMsg) @@ -438,4 +677,4 @@ WINBOOL PostMessageA(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) return true; } -} +} // namespace dvl diff --git a/SourceX/storm/storm.cpp b/SourceX/storm/storm.cpp index 4099a2a0800..94a0c7b1bf3 100644 --- a/SourceX/storm/storm.cpp +++ b/SourceX/storm/storm.cpp @@ -721,6 +721,7 @@ BOOL SVidPlayContinue(void) Uint32 format = SDL_GetWindowPixelFormat(window); SDL_Surface *tmp = SDL_ConvertSurfaceFormat(SVidSurface, format, 0); #endif + ScaleOutputRect(&pal_surface_offset); if (SDL_BlitScaled(tmp, NULL, GetOutputSurface(), &pal_surface_offset) <= -1) { SDL_Log(SDL_GetError()); return false;