diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 13bcb61c9..0259c30e7 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -34,6 +34,8 @@ set(client_SRCS eventsSDL/InputSourceMouse.cpp eventsSDL/InputSourceText.cpp eventsSDL/InputSourceTouch.cpp + eventsSDL/InputSourceGameController.cpp + eventsSDL/GameControllerShortcuts.cpp gui/CGuiHandler.cpp gui/CIntObject.cpp @@ -207,6 +209,8 @@ set(client_HEADERS eventsSDL/InputSourceMouse.h eventsSDL/InputSourceText.h eventsSDL/InputSourceTouch.h + eventsSDL/InputSourceGameController.h + eventsSDL/GameControllerShortcuts.h gui/CGuiHandler.h gui/CIntObject.h diff --git a/client/eventsSDL/GameControllerShortcuts.cpp b/client/eventsSDL/GameControllerShortcuts.cpp new file mode 100644 index 000000000..3663d3fc7 --- /dev/null +++ b/client/eventsSDL/GameControllerShortcuts.cpp @@ -0,0 +1,69 @@ +/* +* GameControllerShortcuts.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#include "StdInc.h" +#include "GameControllerShortcuts.h" +#include "../gui/CGuiHandler.h" +#include "../gui/ShortcutHandler.h" + + +std::vector getButtonBShortcuts() +{ + auto shortcuts = GH.shortcuts().translateKeycode(SDLK_ESCAPE); + // avoid someday ADVENTURE_EXIT_WORLD_VIEW be add in SDLK_ESCAPE shortcuts + if(std::find(shortcuts.begin(), shortcuts.end(), EShortcut::ADVENTURE_EXIT_WORLD_VIEW) == shortcuts.end()) + shortcuts.push_back(EShortcut::ADVENTURE_EXIT_WORLD_VIEW); + return shortcuts; +} + +std::vector getButtonXShortcuts() +{ + auto shortcuts = GH.shortcuts().translateKeycode(SDLK_RETURN); + // avoid someday ADVENTURE_EXIT_WORLD_VIEW be add in SDLK_RETURN shortcuts + if(std::find(shortcuts.begin(), shortcuts.end(), EShortcut::ADVENTURE_EXIT_WORLD_VIEW) == shortcuts.end()) + shortcuts.push_back(EShortcut::ADVENTURE_EXIT_WORLD_VIEW); + return shortcuts; +} + +const ButtonShortcutsMap & getButtonShortcutsMap() { + static const ButtonShortcutsMap buttonShortcutsMap = + { + // SDL_CONTROLLER_BUTTON_A for mouse left click + {SDL_CONTROLLER_BUTTON_B, getButtonBShortcuts()}, + {SDL_CONTROLLER_BUTTON_X, getButtonXShortcuts()}, + // SDL_CONTROLLER_BUTTON_Y for mouse right click + {SDL_CONTROLLER_BUTTON_LEFTSHOULDER, {EShortcut::ADVENTURE_NEXT_HERO, EShortcut::BATTLE_DEFEND}}, + {SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, {EShortcut::ADVENTURE_NEXT_TOWN, EShortcut::BATTLE_WAIT}}, + {SDL_CONTROLLER_BUTTON_BACK, {EShortcut::GAME_END_TURN, EShortcut::BATTLE_END_WITH_AUTOCOMBAT}}, + {SDL_CONTROLLER_BUTTON_START, {EShortcut::GLOBAL_OPTIONS, EShortcut::ADVENTURE_GAME_OPTIONS}}, + {SDL_CONTROLLER_BUTTON_DPAD_UP, {EShortcut::MOVE_UP, EShortcut::ADVENTURE_VIEW_WORLD, + EShortcut::RECRUITMENT_UPGRADE, + EShortcut::RECRUITMENT_UPGRADE_ALL, + EShortcut::BATTLE_CONSOLE_UP, EShortcut::RECRUITMENT_MAX}}, + {SDL_CONTROLLER_BUTTON_DPAD_DOWN, {EShortcut::MOVE_DOWN, EShortcut::ADVENTURE_KINGDOM_OVERVIEW, + EShortcut::BATTLE_CONSOLE_DOWN, EShortcut::RECRUITMENT_MIN}}, + {SDL_CONTROLLER_BUTTON_DPAD_LEFT, {EShortcut::MOVE_LEFT, EShortcut::ADVENTURE_VIEW_SCENARIO}}, + {SDL_CONTROLLER_BUTTON_DPAD_RIGHT, {EShortcut::MOVE_RIGHT, EShortcut::ADVENTURE_THIEVES_GUILD}}, + {SDL_CONTROLLER_BUTTON_LEFTSTICK, {EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL, + EShortcut::BATTLE_TOGGLE_HEROES_STATS}}, + {SDL_CONTROLLER_BUTTON_RIGHTSTICK, {EShortcut::ADVENTURE_TOGGLE_GRID, EShortcut::BATTLE_TOGGLE_QUEUE}} + }; + return buttonShortcutsMap; +} + +const TriggerShortcutsMap & getTriggerShortcutsMap() +{ + static const TriggerShortcutsMap triggerShortcutsMap = { + {SDL_CONTROLLER_AXIS_TRIGGERLEFT, {EShortcut::ADVENTURE_VISIT_OBJECT, EShortcut::BATTLE_TACTICS_NEXT, + EShortcut::BATTLE_USE_CREATURE_SPELL}}, + {SDL_CONTROLLER_AXIS_TRIGGERRIGHT, {EShortcut::ADVENTURE_CAST_SPELL, EShortcut::BATTLE_CAST_SPELL}} + }; + return triggerShortcutsMap; +} \ No newline at end of file diff --git a/client/eventsSDL/GameControllerShortcuts.h b/client/eventsSDL/GameControllerShortcuts.h new file mode 100644 index 000000000..347e79f2a --- /dev/null +++ b/client/eventsSDL/GameControllerShortcuts.h @@ -0,0 +1,21 @@ +/* +* GameControllerShortcuts.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#pragma once + +#include +#include +#include "../gui/Shortcut.h" + +using ButtonShortcutsMap = std::map >; +using TriggerShortcutsMap = std::map >; + +const ButtonShortcutsMap & getButtonShortcutsMap(); +const TriggerShortcutsMap & getTriggerShortcutsMap(); \ No newline at end of file diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index ae4494db4..58d857df7 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -16,6 +16,7 @@ #include "InputSourceKeyboard.h" #include "InputSourceTouch.h" #include "InputSourceText.h" +#include "InputSourceGameController.h" #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" @@ -36,6 +37,7 @@ InputHandler::InputHandler() , keyboardHandler(std::make_unique()) , fingerHandler(std::make_unique()) , textHandler(std::make_unique()) + , gameControllerHandler(std::make_unique()) { } @@ -69,6 +71,12 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) return fingerHandler->handleEventFingerDown(current.tfinger); case SDL_FINGERUP: return fingerHandler->handleEventFingerUp(current.tfinger); + case SDL_CONTROLLERAXISMOTION: + return gameControllerHandler->handleEventAxisMotion(current.caxis); + case SDL_CONTROLLERBUTTONDOWN: + return gameControllerHandler->handleEventButtonDown(current.cbutton); + case SDL_CONTROLLERBUTTONUP: + return gameControllerHandler->handleEventButtonUp(current.cbutton); } } @@ -88,6 +96,7 @@ void InputHandler::processEvents() for(const auto & currentEvent : eventsToProcess) handleCurrentEvent(currentEvent); + gameControllerHandler->handleUpdate(); fingerHandler->handleUpdate(); } @@ -103,6 +112,7 @@ bool InputHandler::ignoreEventsUntilInput() case SDL_MOUSEBUTTONDOWN: case SDL_FINGERDOWN: case SDL_KEYDOWN: + case SDL_CONTROLLERBUTTONDOWN: inputFound = true; } } @@ -196,6 +206,21 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) NotificationHandler::handleSdlEvent(ev); } } + else if(ev.type == SDL_CONTROLLERDEVICEADDED) + { + gameControllerHandler->handleEventDeviceAdded(ev.cdevice); + return; + } + else if(ev.type == SDL_CONTROLLERDEVICEREMOVED) + { + gameControllerHandler->handleEventDeviceRemoved(ev.cdevice); + return; + } + else if(ev.type == SDL_CONTROLLERDEVICEREMAPPED) + { + gameControllerHandler->handleEventDeviceRemapped(ev.cdevice); + return; + } //preprocessing if(ev.type == SDL_MOUSEMOTION) @@ -324,3 +349,8 @@ const Point & InputHandler::getCursorPosition() const { return cursorPosition; } + +void InputHandler::tryOpenGameController() +{ + gameControllerHandler->tryOpenAllGameControllers(); +} \ No newline at end of file diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index 0584de677..483363440 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -21,6 +21,7 @@ class InputSourceMouse; class InputSourceKeyboard; class InputSourceTouch; class InputSourceText; +class InputSourceGameController; class InputHandler { @@ -39,6 +40,7 @@ class InputHandler std::unique_ptr keyboardHandler; std::unique_ptr fingerHandler; std::unique_ptr textHandler; + std::unique_ptr gameControllerHandler; public: InputHandler(); @@ -84,4 +86,7 @@ public: bool isKeyboardAltDown() const; bool isKeyboardCtrlDown() const; bool isKeyboardShiftDown() const; + + /// If any game controller available, use it. + void tryOpenGameController(); }; diff --git a/client/eventsSDL/InputSourceGameController.cpp b/client/eventsSDL/InputSourceGameController.cpp new file mode 100644 index 000000000..3843d7366 --- /dev/null +++ b/client/eventsSDL/InputSourceGameController.cpp @@ -0,0 +1,248 @@ +/* +* InputSourceGameController.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#include "StdInc.h" +#include "InputSourceGameController.h" +#include "GameControllerShortcuts.h" +#include "InputHandler.h" + +#include "../CGameInfo.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/EventDispatcher.h" +#include "../gui/ShortcutHandler.h" + + + +void InputSourceGameController::gameControllerDeleter(SDL_GameController * gameController) +{ + if(gameController) + SDL_GameControllerClose(gameController); +} + +InputSourceGameController::InputSourceGameController(): + lastCheckTime(0), + axisValueX(0), + axisValueY(0), + planDisX(0.0), + planDisY(0.0) +{ + // SDL_init has not been called. so it is unnecessary to open joystick. +} + +void InputSourceGameController::tryOpenAllGameControllers() +{ + for(int i = 0; i < SDL_NumJoysticks(); ++i) + if(SDL_IsGameController(i)) + openGameController(i); + else + logGlobal->warn("Joystick %d is an unsupported game controller!", i); +} + + +void InputSourceGameController::openGameController(int index) +{ + SDL_GameController * controller = SDL_GameControllerOpen(index); + if(!controller) + { + logGlobal->error("Fail to open game controller %d!", index); + return; + } + GameControllerPtr controllerPtr(controller, gameControllerDeleter); + + // Need to save joystick index for event. Joystick index may not be equal to index sometimes. + int joystickIndex = getJoystickIndex(controllerPtr.get()); + if(joystickIndex < 0) + { + logGlobal->error("Fail to get joystick index of game controller %d!", index); + return; + } + + if(gameControllerMap.find(joystickIndex) != gameControllerMap.end()) + { + logGlobal->warn("Game controller with joystick index %d is already opened.", joystickIndex); + return; + } + + gameControllerMap.emplace(joystickIndex, std::move(controllerPtr)); +} + +int InputSourceGameController::getJoystickIndex(SDL_GameController * controller) +{ + SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller); + if(!joystick) + return -1; + + SDL_JoystickID instanceID = SDL_JoystickInstanceID(joystick); + if(instanceID < 0) + return -1; + return (int)instanceID; +} + +void InputSourceGameController::handleEventDeviceAdded(const SDL_ControllerDeviceEvent & device) +{ + if(gameControllerMap.find(device.which) != gameControllerMap.end()) + { + logGlobal->warn("Game controller %d is already opened.", device.which); + return; + } + openGameController(device.which); +} + +void InputSourceGameController::handleEventDeviceRemoved(const SDL_ControllerDeviceEvent & device) +{ + if(gameControllerMap.find(device.which) == gameControllerMap.end()) + { + logGlobal->warn("Game controller %d is not opened before.", device.which); + return; + } + gameControllerMap.erase(device.which); +} + +void InputSourceGameController::handleEventDeviceRemapped(const SDL_ControllerDeviceEvent & device) +{ + if(gameControllerMap.find(device.which) == gameControllerMap.end()) + { + logGlobal->warn("Game controller %d is not opened.", device.which); + return; + } + gameControllerMap.erase(device.which); + openGameController(device.which); +} + +int InputSourceGameController::getRealAxisValue(int value) +{ + if(value < AXIS_DEAD_ZOOM && value > -AXIS_DEAD_ZOOM) + return 0; + if(value > AXIS_MAX_ZOOM) + return AXIS_MAX_ZOOM; + if(value < -AXIS_MAX_ZOOM) + return -AXIS_MAX_ZOOM; + int base = value > 0 ? AXIS_DEAD_ZOOM: -AXIS_DEAD_ZOOM; + return (value - base) * AXIS_MAX_ZOOM / (AXIS_MAX_ZOOM - AXIS_DEAD_ZOOM); +} + +void InputSourceGameController::dispatchTriggerShortcuts(const std::vector & shortcutsVector, int axisValue) +{ + if(axisValue >= TRIGGER_PRESS_THRESHOLD) + GH.events().dispatchShortcutPressed(shortcutsVector); + else + GH.events().dispatchShortcutReleased(shortcutsVector); +} + +void InputSourceGameController::handleEventAxisMotion(const SDL_ControllerAxisEvent & axis) +{ + const auto & triggerShortcutsMap = getTriggerShortcutsMap(); + if(axis.axis == SDL_CONTROLLER_AXIS_LEFTX) + { + axisValueX = getRealAxisValue(axis.value); + } + else if(axis.axis == SDL_CONTROLLER_AXIS_LEFTY) + { + axisValueY = getRealAxisValue(axis.value); + } + else if(triggerShortcutsMap.find(axis.axis) != triggerShortcutsMap.end()) + { + const auto & shortcutsVector = triggerShortcutsMap.find(axis.axis)->second; + dispatchTriggerShortcuts(shortcutsVector, axis.value); + } +} + +void InputSourceGameController::handleEventButtonDown(const SDL_ControllerButtonEvent & button) +{ + const Point & position = GH.input().getCursorPosition(); + const auto & buttonShortcutsMap = getButtonShortcutsMap(); + + // TODO: define keys by user + if(button.button == SDL_CONTROLLER_BUTTON_A) + { + GH.events().dispatchMouseLeftButtonPressed(position, 0); + } + else if(button.button == SDL_CONTROLLER_BUTTON_Y) + { + GH.events().dispatchShowPopup(position, 0); + } + else if(buttonShortcutsMap.find(button.button) != buttonShortcutsMap.end()) + { + const auto & shortcutsVector = buttonShortcutsMap.find(button.button)->second; + GH.events().dispatchShortcutPressed(shortcutsVector); + } +} + +void InputSourceGameController::handleEventButtonUp(const SDL_ControllerButtonEvent & button) +{ + const Point & position = GH.input().getCursorPosition(); + const auto & buttonShortcutsMap = getButtonShortcutsMap(); + if(button.button == SDL_CONTROLLER_BUTTON_A) + { + GH.events().dispatchMouseLeftButtonReleased(position, 0); + } + else if(button.button == SDL_CONTROLLER_BUTTON_Y) + { + GH.events().dispatchClosePopup(position); + } + else if(buttonShortcutsMap.find(button.button) != buttonShortcutsMap.end()) + { + const auto & shortcutsVector = buttonShortcutsMap.find(button.button)->second; + GH.events().dispatchShortcutReleased(shortcutsVector); + } +} + +void InputSourceGameController::doCursorMove(int deltaX, int deltaY) +{ + if(deltaX == 0 && deltaY == 0) + return; + const Point & screenSize = GH.screenDimensions(); + const Point &cursorPosition = GH.getCursorPosition(); + int newX = std::min(std::max(cursorPosition.x + deltaX, 0), screenSize.x); + int newY = std::min(std::max(cursorPosition.y + deltaY, 0), screenSize.y); + Point targetPosition{newX, newY}; + GH.input().setCursorPosition(targetPosition); + if(CCS && CCS->curh) + CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y); +} + +int InputSourceGameController::getMoveDis(float planDis) +{ + if(planDis >= 0) + return std::floor(planDis); + else + return std::ceil(planDis); +} + +void InputSourceGameController::handleUpdate() +{ + auto now = std::chrono::high_resolution_clock::now(); + auto nowMs = std::chrono::duration_cast(now.time_since_epoch()).count(); + if(lastCheckTime == 0) + { + lastCheckTime = nowMs; + return; + } + + long long deltaTime = nowMs - lastCheckTime; + + if(axisValueX == 0) + planDisX = 0; + else + planDisX += ((float)deltaTime / 1000) * ((float)axisValueX / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED; + + if(axisValueY == 0) + planDisY = 0; + else + planDisY += ((float)deltaTime / 1000) * ((float)axisValueY / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED; + + int moveDisX = getMoveDis(planDisX); + int moveDisY = getMoveDis(planDisY); + planDisX -= moveDisX; + planDisY -= moveDisY; + doCursorMove(moveDisX, moveDisY); + lastCheckTime = nowMs; +} \ No newline at end of file diff --git a/client/eventsSDL/InputSourceGameController.h b/client/eventsSDL/InputSourceGameController.h new file mode 100644 index 000000000..8bc71fa00 --- /dev/null +++ b/client/eventsSDL/InputSourceGameController.h @@ -0,0 +1,57 @@ +/* +* InputSourceGameController.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#pragma once + +#include +#include + +#include "../../lib/Point.h" +#include "../gui/Shortcut.h" + + +const int AXIS_DEAD_ZOOM = 6000; +const int AXIS_MAX_ZOOM = 32000; +const int AXIS_MOVE_SPEED = 500; +const int AXIS_CURSOR_MOVE_INTERVAL = 1000; +const int TRIGGER_PRESS_THRESHOLD = 8000; + + +/// Class that handles game controller input from SDL events +class InputSourceGameController +{ + static void gameControllerDeleter(SDL_GameController * gameController); + using GameControllerPtr = std::unique_ptr; + + std::map gameControllerMap; + long long lastCheckTime; + int axisValueX; + int axisValueY; + float planDisX; + float planDisY; + + void openGameController(int index); + int getJoystickIndex(SDL_GameController * controller); + int getRealAxisValue(int value); + void dispatchTriggerShortcuts(const std::vector & shortcutsVector, int axisValue); + void doCursorMove(int deltaX, int deltaY); + int getMoveDis(float planDis); + +public: + InputSourceGameController(); + void tryOpenAllGameControllers(); + void handleEventDeviceAdded(const SDL_ControllerDeviceEvent & device); + void handleEventDeviceRemoved(const SDL_ControllerDeviceEvent & device); + void handleEventDeviceRemapped(const SDL_ControllerDeviceEvent & device); + void handleEventAxisMotion(const SDL_ControllerAxisEvent & axis); + void handleEventButtonDown(const SDL_ControllerButtonEvent & button); + void handleEventButtonUp(const SDL_ControllerButtonEvent & button); + void handleUpdate(); +}; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index d8d56eb2c..0eb96b205 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -79,6 +79,9 @@ void CGuiHandler::init() renderHandlerInstance = std::make_unique(); shortcutsHandlerInstance = std::make_unique(); framerateManagerInstance = std::make_unique(settings["video"]["targetfps"].Integer()); + + // This must be called after SDL_init(), so put after screenHandlerInstance init. + inputHandlerInstance->tryOpenGameController(); } void CGuiHandler::handleEvents() diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index f8ceb26f1..5441b666f 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -173,7 +173,7 @@ ScreenHandler::ScreenHandler() SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor"); #endif - if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO)) + if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER)) { logGlobal->error("Something was wrong: %s", SDL_GetError()); exit(-1);