diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index f30f77d9f..ba2ed5bf1 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/GameControllerConfig.cpp gui/CGuiHandler.cpp gui/CIntObject.cpp @@ -212,6 +214,8 @@ set(client_HEADERS eventsSDL/InputSourceMouse.h eventsSDL/InputSourceText.h eventsSDL/InputSourceTouch.h + eventsSDL/InputSourceGameController.h + eventsSDL/GameControllerConfig.h gui/CGuiHandler.h gui/CIntObject.h diff --git a/client/eventsSDL/GameControllerConfig.cpp b/client/eventsSDL/GameControllerConfig.cpp new file mode 100644 index 000000000..d323d776b --- /dev/null +++ b/client/eventsSDL/GameControllerConfig.cpp @@ -0,0 +1,212 @@ +/* +* GameControllerConfig.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 + +#include "StdInc.h" +#include "GameControllerConfig.h" +#include "../gui/CGuiHandler.h" +#include "../gui/ShortcutHandler.h" + + +GameControllerConfig::GameControllerConfig(): leftAxisType(AxisType::NONE), rightAxisType(AxisType::NONE) +{ + load(); +} + +void GameControllerConfig::load() +{ + const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig"); + for(auto const & entry : config["joystick"].Struct()) + { + std::string configName = entry.first; + if(configName == "leftaxis") + leftAxisType = parseAxis(entry.first, entry.second); + else if (configName == "rightaxis") + rightAxisType = parseAxis(entry.first, entry.second); + else if (configName == "lefttrigger" || configName == "righttrigger") + parseTrigger(entry.first, entry.second); + else + parseButton(entry.first, entry.second); + } +} + +AxisType GameControllerConfig::parseAxis(const std::string & key, const JsonNode & value) +{ + if(!value.isString()) + { + logGlobal->error("The value of joystick config key %s should be a string!", key); + return AxisType::NONE; + } + + std::string featureName = value.String(); + if(featureName == "cursorMotion") + return AxisType::CURSOR_MOTION; + else if(featureName == "mapScroll") + return AxisType::MAP_SCROLL; + else if(featureName != "") + logGlobal->error("Unknown value %s of joystick config key %s!", featureName, key); + return AxisType::NONE; +} + +void GameControllerConfig::parseTrigger(const std::string & key, const JsonNode & value) +{ + std::vector operations = getOperations(key, value); + SDL_GameControllerAxis triggerAxis = key == "lefttrigger" ? + SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT; + std::vector shortcuts; + for(const auto & operation : operations) + { + if(operation == "mouseLeftClick") + { + leftClickTriggerSet.insert(triggerAxis); + } + else if(operation == "mouseRightClick") + { + rightClickTriggerSet.insert(triggerAxis); + } + else + { + EShortcut shortcut = GH.shortcuts().findShortcut(operation); + if(shortcut == EShortcut::NONE) + logGlobal->error("Shortcut %s in joystick config key %s is invalid.", operation, key); + else + shortcuts.push_back(shortcut); + } + } + + if(!shortcuts.empty()) + triggerShortcutsMap.emplace(triggerAxis, std::move(shortcuts)); +} + +void GameControllerConfig::parseButton(const std::string & key, const JsonNode & value) +{ + std::vector operations = getOperations(key, value); + SDL_GameControllerButton button = SDL_GameControllerGetButtonFromString(key.c_str()); + if(button == SDL_CONTROLLER_BUTTON_INVALID) + { + logGlobal->error("Joystick config key %s is invalid.", key); + return; + } + + std::vector shortcuts; + for(const auto & operation : operations) + { + if(operation == "mouseLeftClick") + { + leftClickButtonSet.insert(button); + } + else if(operation == "mouseRightClick") + { + rightClickButtonSet.insert(button); + } + else + { + EShortcut shortcut = GH.shortcuts().findShortcut(operation); + if(shortcut == EShortcut::NONE) + logGlobal->error("Shortcut %s in joystick config key %s is invalid.", operation, key); + else + shortcuts.push_back(shortcut); + } + } + + if(!shortcuts.empty()) + buttonShortcutsMap.emplace(button, std::move(shortcuts)); +} + +const AxisType & GameControllerConfig::getLeftAxisType() +{ + return leftAxisType; +} + +const AxisType & GameControllerConfig::getRightAxisType() +{ + return rightAxisType; +} + +std::vector GameControllerConfig::getOperations(const std::string & key, const JsonNode & value) +{ + std::vector operations; + if(value.isString()) + { + operations.push_back(value.String()); + } + else if(value.isVector()) + { + for(auto const & entryVector : value.Vector()) + { + if(!entryVector.isString()) + logGlobal->error("The vector of joystick config key %s can not contain non-string element.", key); + else + operations.push_back(entryVector.String()); + } + } + else + { + logGlobal->error("The value of joystick config key %s should be string or string vector.", key); + } + return operations; +} + +bool GameControllerConfig::isLeftClickButton(int buttonValue) +{ + SDL_GameControllerButton button = static_cast(buttonValue); + return leftClickButtonSet.find(button) != leftClickButtonSet.end(); +} + +bool GameControllerConfig::isRightClickButton(int buttonValue) +{ + SDL_GameControllerButton button = static_cast(buttonValue); + return rightClickButtonSet.find(button) != rightClickButtonSet.end(); +} + +bool GameControllerConfig::isShortcutsButton(int buttonValue) +{ + SDL_GameControllerButton button = static_cast(buttonValue); + return buttonShortcutsMap.find(button) != buttonShortcutsMap.end(); +} + +const std::vector & GameControllerConfig::getButtonShortcuts(int buttonValue) +{ + SDL_GameControllerButton button = static_cast(buttonValue); + auto it = buttonShortcutsMap.find(button); + if(it != buttonShortcutsMap.end()) + return it->second; + static std::vector emptyVec; + return emptyVec; +} + +bool GameControllerConfig::isLeftClickTrigger(int axisValue) +{ + SDL_GameControllerAxis axis = static_cast(axisValue); + return leftClickTriggerSet.find(axis) != leftClickTriggerSet.end(); +} + +bool GameControllerConfig::isRightClickTrigger(int axisValue) +{ + SDL_GameControllerAxis axis = static_cast(axisValue); + return rightClickTriggerSet.find(axis) != rightClickTriggerSet.end(); +} + +bool GameControllerConfig::isShortcutsTrigger(int axisValue) +{ + SDL_GameControllerAxis axis = static_cast(axisValue); + return triggerShortcutsMap.find(axis) != triggerShortcutsMap.end(); +} + +const std::vector & GameControllerConfig::getTriggerShortcuts(int axisValue) +{ + SDL_GameControllerAxis axis = static_cast(axisValue); + auto it = triggerShortcutsMap.find(axis); + if(it != triggerShortcutsMap.end()) + return it->second; + static std::vector emptyVec; + return emptyVec; +} + diff --git a/client/eventsSDL/GameControllerConfig.h b/client/eventsSDL/GameControllerConfig.h new file mode 100644 index 000000000..13fdc1637 --- /dev/null +++ b/client/eventsSDL/GameControllerConfig.h @@ -0,0 +1,59 @@ +/* +* GameControllerConfig.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 "../gui/Shortcut.h" +#include "../../lib/json/JsonUtils.h" + +enum AxisType +{ + CURSOR_MOTION, + MAP_SCROLL, + NONE +}; + +class GameControllerConfig { + using ButtonShortcutsMap = std::map >; + using TriggerShortcutsMap = std::map >; + ButtonShortcutsMap buttonShortcutsMap; + TriggerShortcutsMap triggerShortcutsMap; + std::set leftClickButtonSet; + std::set rightClickButtonSet; + std::set leftClickTriggerSet; + std::set rightClickTriggerSet; + AxisType leftAxisType; + AxisType rightAxisType; + + void load(); + std::vector getOperations(const std::string & key, const JsonNode & value); + AxisType parseAxis(const std::string & key, const JsonNode & value); + void parseTrigger(const std::string & key, const JsonNode & value); + void parseButton(const std::string & key, const JsonNode & value); + +public: + GameControllerConfig(); + ~GameControllerConfig() = default; + + const AxisType & getLeftAxisType(); + const AxisType & getRightAxisType(); + + bool isLeftClickButton(int buttonValue); + bool isRightClickButton(int buttonValue); + bool isShortcutsButton(int buttonValue); + const std::vector & getButtonShortcuts(int buttonValue); + + bool isLeftClickTrigger(int axisValue); + bool isRightClickTrigger(int axisValue); + bool isShortcutsTrigger(int axisValue); + const std::vector & getTriggerShortcuts(int axisValue); +}; diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index ae4494db4..ccb4cabb6 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) diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index 0584de677..b1fbf312e 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,5 @@ public: bool isKeyboardAltDown() const; bool isKeyboardCtrlDown() const; bool isKeyboardShiftDown() const; + }; diff --git a/client/eventsSDL/InputSourceGameController.cpp b/client/eventsSDL/InputSourceGameController.cpp new file mode 100644 index 000000000..2f6a5991c --- /dev/null +++ b/client/eventsSDL/InputSourceGameController.cpp @@ -0,0 +1,354 @@ +/* +* 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 "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), + cursorAxisValueX(0), + cursorAxisValueY(0), + cursorPlanDisX(0.0), + cursorPlanDisY(0.0), + scrollAxisMoved(false), + scrollStart(Point(0,0)), + scrollCurrent(Point(0,0)), + scrollAxisValueX(0), + scrollAxisValueY(0), + scrollPlanDisX(0.0), + scrollPlanDisY(0.0) +{ + tryOpenAllGameControllers(); +} + +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::dispatchTriggerLeftClick(int axisValue) +{ + const Point & position = GH.input().getCursorPosition(); + if(axisValue >= TRIGGER_PRESS_THRESHOLD) + GH.events().dispatchMouseLeftButtonPressed(position, 0); + else + GH.events().dispatchMouseLeftButtonReleased(position, 0); +} + +void InputSourceGameController::dispatchTriggerRightClick(int axisValue) +{ + const Point & position = GH.input().getCursorPosition(); + if(axisValue >= TRIGGER_PRESS_THRESHOLD) + GH.events().dispatchShowPopup(position, 0); + else + GH.events().dispatchClosePopup(position); +} + +void InputSourceGameController::handleEventAxisMotion(const SDL_ControllerAxisEvent & axis) +{ + tryToConvertCursor(); + if(axis.axis == SDL_CONTROLLER_AXIS_LEFTX) + { + if(config.getLeftAxisType() == AxisType::CURSOR_MOTION) + cursorAxisValueX = getRealAxisValue(axis.value); + else if(config.getLeftAxisType() == AxisType::MAP_SCROLL) + scrollAxisValueX = getRealAxisValue(axis.value); + } + else if(axis.axis == SDL_CONTROLLER_AXIS_LEFTY) + { + if(config.getLeftAxisType() == AxisType::CURSOR_MOTION) + cursorAxisValueY = getRealAxisValue(axis.value); + else if(config.getLeftAxisType() == AxisType::MAP_SCROLL) + scrollAxisValueY = getRealAxisValue(axis.value); + } + if(axis.axis == SDL_CONTROLLER_AXIS_RIGHTX) + { + if(config.getRightAxisType() == AxisType::CURSOR_MOTION) + cursorAxisValueX = getRealAxisValue(axis.value); + else if(config.getRightAxisType() == AxisType::MAP_SCROLL) + scrollAxisValueX = getRealAxisValue(axis.value); + } + else if(axis.axis == SDL_CONTROLLER_AXIS_RIGHTY) + { + if(config.getRightAxisType() == AxisType::CURSOR_MOTION) + cursorAxisValueY = getRealAxisValue(axis.value); + else if(config.getRightAxisType() == AxisType::MAP_SCROLL) + scrollAxisValueY = getRealAxisValue(axis.value); + } + else if(config.isLeftClickTrigger(axis.axis)) + { + dispatchTriggerLeftClick(axis.value); + } + else if(config.isRightClickTrigger(axis.axis)) + { + dispatchTriggerRightClick(axis.value); + } + else if(config.isShortcutsTrigger(axis.axis)) + { + const auto & shortcutsVector = config.getTriggerShortcuts(axis.axis); + dispatchTriggerShortcuts(shortcutsVector, axis.value); + } +} + +void InputSourceGameController::tryToConvertCursor() +{ + if(CCS && CCS->curh && CCS->curh->getShowType() == Cursor::ShowType::HARDWARE) + { + const Point & cursorPosition = CCS->curh->getCursorPosition(); + CCS->curh->ChangeCursor(Cursor::ShowType::SOFTWARE); + CCS->curh->cursorMove(cursorPosition.x, cursorPosition.y); + GH.input().setCursorPosition(cursorPosition); + } +} + +void InputSourceGameController::handleEventButtonDown(const SDL_ControllerButtonEvent & button) +{ + const Point & position = GH.input().getCursorPosition(); + + if(config.isLeftClickButton(button.button)) + { + GH.events().dispatchMouseLeftButtonPressed(position, 0); + } + + if(config.isRightClickButton(button.button)) + { + GH.events().dispatchShowPopup(position, 0); + } + + if(config.isShortcutsButton(button.button)) + { + const auto & shortcutsVector = config.getButtonShortcuts(button.button); + GH.events().dispatchShortcutPressed(shortcutsVector); + } +} + +void InputSourceGameController::handleEventButtonUp(const SDL_ControllerButtonEvent & button) +{ + const Point & position = GH.input().getCursorPosition(); + + if(config.isLeftClickButton(button.button)) + { + GH.events().dispatchMouseLeftButtonReleased(position, 0); + } + if(config.isRightClickButton(button.button)) + { + GH.events().dispatchClosePopup(position); + } + if(config.isShortcutsButton(button.button)) + { + const auto & shortcutsVector = config.getButtonShortcuts(button.button); + 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; + handleCursorUpdate(deltaTime); + handleScrollUpdate(deltaTime); + lastCheckTime = nowMs; +} + +void InputSourceGameController::handleCursorUpdate(long long deltaTime) +{ + if(cursorAxisValueX == 0) + cursorPlanDisX = 0; + else + cursorPlanDisX += ((float)deltaTime / 1000) * ((float)cursorAxisValueX / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED; + + if(cursorAxisValueY == 0) + cursorPlanDisY = 0; + else + cursorPlanDisY += ((float)deltaTime / 1000) * ((float)cursorAxisValueY / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED; + + int moveDisX = getMoveDis(cursorPlanDisX); + int moveDisY = getMoveDis(cursorPlanDisY); + cursorPlanDisX -= moveDisX; + cursorPlanDisY -= moveDisY; + doCursorMove(moveDisX, moveDisY); +} + +void InputSourceGameController::handleScrollUpdate(long long deltaTime) +{ + if(!scrollAxisMoved && isScrollAxisReleased()) + { + return; + } + else if(!scrollAxisMoved && !isScrollAxisReleased()) + { + scrollAxisMoved = true; + scrollCurrent = scrollStart = GH.input().getCursorPosition(); + GH.events().dispatchGesturePanningStarted(scrollStart); + } + else if(scrollAxisMoved && isScrollAxisReleased()) + { + GH.events().dispatchGesturePanningEnded(scrollStart, scrollCurrent); + scrollAxisMoved = false; + scrollPlanDisX = scrollPlanDisY = 0; + return; + } + scrollPlanDisX += ((float)deltaTime / 1000) * ((float)scrollAxisValueX / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED; + scrollPlanDisY += ((float)deltaTime / 1000) * ((float)scrollAxisValueY / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED; + int moveDisX = getMoveDis(scrollPlanDisX); + int moveDisY = getMoveDis(scrollPlanDisY); + if(moveDisX != 0 || moveDisY != 0) + { + scrollPlanDisX -= moveDisX; + scrollPlanDisY -= moveDisY; + scrollCurrent.x += moveDisX; + scrollCurrent.y += moveDisY; + Point distance(moveDisX, moveDisY); + GH.events().dispatchGesturePanning(scrollStart, scrollCurrent, distance); + } +} + +bool InputSourceGameController::isScrollAxisReleased() +{ + return scrollAxisValueX == 0 && scrollAxisValueY == 0; +} diff --git a/client/eventsSDL/InputSourceGameController.h b/client/eventsSDL/InputSourceGameController.h new file mode 100644 index 000000000..18ad372ce --- /dev/null +++ b/client/eventsSDL/InputSourceGameController.h @@ -0,0 +1,72 @@ +/* +* 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 "GameControllerConfig.h" +#include "../gui/Shortcut.h" +#include "../../lib/Point.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; + GameControllerConfig config; + long long lastCheckTime; + int cursorAxisValueX; + int cursorAxisValueY; + float cursorPlanDisX; + float cursorPlanDisY; + + bool scrollAxisMoved; + Point scrollStart; + Point scrollCurrent; + int scrollAxisValueX; + int scrollAxisValueY; + float scrollPlanDisX; + float scrollPlanDisY; + + void openGameController(int index); + int getJoystickIndex(SDL_GameController * controller); + int getRealAxisValue(int value); + void dispatchTriggerShortcuts(const std::vector & shortcutsVector, int axisValue); + void dispatchTriggerLeftClick(int axisValue); + void dispatchTriggerRightClick(int axisValue); + void tryToConvertCursor(); + void doCursorMove(int deltaX, int deltaY); + int getMoveDis(float planDis); + void handleCursorUpdate(long long deltaTime); + void handleScrollUpdate(long long deltaTime); + bool isScrollAxisReleased(); + +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/CursorHandler.cpp b/client/gui/CursorHandler.cpp index a9fa812e2..2d529e2cf 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -57,6 +57,7 @@ CursorHandler::CursorHandler() cursor->preload(); set(Cursor::Map::POINTER); + showType = dynamic_cast(cursor.get()) ? Cursor::ShowType::SOFTWARE : Cursor::ShowType::HARDWARE; } CursorHandler::~CursorHandler() = default; @@ -290,3 +291,32 @@ void CursorHandler::show() cursor->setVisible(true); } +Cursor::ShowType CursorHandler::getShowType() +{ + return showType; +} + +void CursorHandler::ChangeCursor(Cursor::ShowType showType) +{ + if(this->showType == showType) + return; + + switch(showType) + { + case Cursor::ShowType::SOFTWARE: + cursor.reset(new CursorSoftware()); + showType = Cursor::ShowType::SOFTWARE; + cursor->setImage(getCurrentImage(), getPivotOffset()); + break; + case Cursor::ShowType::HARDWARE: + cursor.reset(new CursorHardware()); + showType = Cursor::ShowType::HARDWARE; + cursor->setImage(getCurrentImage(), getPivotOffset()); + break; + } +} + +const Point & CursorHandler::getCursorPosition() +{ + return cursor->getCursorPosition(); +} diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index b1f3d4a27..2ff7790c1 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -25,6 +25,11 @@ namespace Cursor SPELLBOOK // animated cursor for spellcasting }; + enum class ShowType { + SOFTWARE, + HARDWARE + }; + enum class Default { POINTER = 0, //ARROW_COPY = 1, // probably unused @@ -120,6 +125,7 @@ class CursorHandler final /// Current cursor Cursor::Type type; + Cursor::ShowType showType; size_t frame; float frameTime; Point pos; @@ -179,4 +185,8 @@ public: /// change cursor's positions to (x, y) void cursorMove(const int & x, const int & y); + + Cursor::ShowType getShowType(); + void ChangeCursor(Cursor::ShowType showType); + const Point & getCursorPosition(); }; diff --git a/client/gui/ShortcutHandler.cpp b/client/gui/ShortcutHandler.cpp index 3b17f4ae6..cf9825916 100644 --- a/client/gui/ShortcutHandler.cpp +++ b/client/gui/ShortcutHandler.cpp @@ -153,6 +153,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"adventureZoomIn", EShortcut::ADVENTURE_ZOOM_IN }, {"adventureZoomOut", EShortcut::ADVENTURE_ZOOM_OUT }, {"adventureZoomReset", EShortcut::ADVENTURE_ZOOM_RESET }, + {"battleToggleHeroesStats", EShortcut::BATTLE_TOGGLE_HEROES_STATS}, {"battleToggleQueue", EShortcut::BATTLE_TOGGLE_QUEUE }, {"battleUseCreatureSpell", EShortcut::BATTLE_USE_CREATURE_SPELL }, {"battleSurrender", EShortcut::BATTLE_SURRENDER }, diff --git a/client/render/ICursor.h b/client/render/ICursor.h index af9e2f1c6..8076c984e 100644 --- a/client/render/ICursor.h +++ b/client/render/ICursor.h @@ -24,5 +24,6 @@ public: virtual void setCursorPosition( const Point & newPos ) = 0; virtual void render() = 0; virtual void setVisible( bool on) = 0; + virtual const Point & getCursorPosition() = 0; }; diff --git a/client/render/IScreenHandler.h b/client/render/IScreenHandler.h index 49e5cd95e..3c2d99357 100644 --- a/client/render/IScreenHandler.h +++ b/client/render/IScreenHandler.h @@ -43,4 +43,7 @@ public: /// Window has focus virtual bool hasFocus() = 0; + + /// Get the scale value of screen + virtual void getRenderScale(float & scaleX, float & scaleY) = 0; }; diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index aed082adc..8ace8b458 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -12,6 +12,7 @@ #include "CursorHardware.h" #include "../gui/CGuiHandler.h" +#include "../renderSDL/ScreenHandler.h" #include "../render/Colors.h" #include "../render/IImage.h" #include "SDL_Extensions.h" @@ -66,6 +67,17 @@ void CursorHardware::setImage(std::shared_ptr image, const Point & pivot }); } +const Point & CursorHardware::getCursorPosition() +{ + int mouseX, mouseY; + SDL_GetMouseState(&mouseX, &mouseY); + float scaleX, scaleY; + GH.screenHandler().getRenderScale(scaleX, scaleY); + pos.x = int(mouseX / scaleX); + pos.y = int(mouseY / scaleY); + return pos; +} + void CursorHardware::setCursorPosition( const Point & newPos ) { //no-op diff --git a/client/renderSDL/CursorHardware.h b/client/renderSDL/CursorHardware.h index c4e311778..77ae179a4 100644 --- a/client/renderSDL/CursorHardware.h +++ b/client/renderSDL/CursorHardware.h @@ -23,6 +23,7 @@ class CursorHardware : public ICursor std::shared_ptr cursorImage; SDL_Cursor * cursor; + Point pos; public: CursorHardware(); @@ -32,5 +33,6 @@ public: void setCursorPosition( const Point & newPos ) override; void render() override; void setVisible( bool on) override; + const Point & getCursorPosition() override; }; diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index 78b9e1250..3647b1dda 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -81,6 +81,11 @@ void CursorSoftware::setVisible(bool on) visible = on; } +const Point & CursorSoftware::getCursorPosition() +{ + return pos; +} + CursorSoftware::CursorSoftware(): cursorTexture(nullptr), cursorSurface(nullptr), diff --git a/client/renderSDL/CursorSoftware.h b/client/renderSDL/CursorSoftware.h index 44080e3e2..7af503d33 100644 --- a/client/renderSDL/CursorSoftware.h +++ b/client/renderSDL/CursorSoftware.h @@ -40,5 +40,6 @@ public: void setCursorPosition( const Point & newPos ) override; void render() override; void setVisible( bool on) override; + const Point & getCursorPosition() override; }; diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index f08f80c4d..fa77f8f43 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -172,7 +172,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); @@ -582,3 +582,8 @@ bool ScreenHandler::hasFocus() ui32 flags = SDL_GetWindowFlags(mainWindow); return flags & SDL_WINDOW_INPUT_FOCUS; } + +void ScreenHandler::getRenderScale(float & scaleX, float & scaleY) +{ + SDL_RenderGetScale(mainRenderer, &scaleX, &scaleY); +} \ No newline at end of file diff --git a/client/renderSDL/ScreenHandler.h b/client/renderSDL/ScreenHandler.h index fb3d6a334..ab0ff8894 100644 --- a/client/renderSDL/ScreenHandler.h +++ b/client/renderSDL/ScreenHandler.h @@ -89,6 +89,9 @@ public: /// Window has focus bool hasFocus() final; + /// Get the scale value of screen + void getRenderScale(float & scaleX, float & scaleY); + std::vector getSupportedResolutions() const final; std::vector getSupportedResolutions(int displayIndex) const; std::tuple getSupportedScalingRange() const final; diff --git a/config/shortcutsConfig.json b/config/shortcutsConfig.json index 2954bcf56..205d7b495 100644 --- a/config/shortcutsConfig.json +++ b/config/shortcutsConfig.json @@ -136,5 +136,27 @@ "heroCostume7": "7", "heroCostume8": "8", "heroCostume9": "9" + }, + "joystick": { + "leftaxis": "cursorMotion", + "rightaxis": "mapScroll", + "a": ["globalAccept", "globalReturn", "lobbyBeginStandardGame", "lobbyBeginCampaign", "lobbyLoadGame", + "lobbySaveGame", "adventureViewSelected", "adventureExitWorldView", "battleTacticsEnd"], + "b": ["globalCancel", "globalReturn", "adventureExitWorldView"], + "x": "mouseLeftClick", + "y": "mouseRightClick", + "leftshoulder": ["adventureNextHero", "battleDefend"], + "rightshoulder": ["adventureNextTown", "battleWait"], + "lefttrigger": ["adventureVisitObject", "battleTacticsNext", "battleUseCreatureSpell"], + "righttrigger": ["adventureCastSpell", "battleCastSpell"], + "back": ["gameEndTurn", "battleAutocombatEnd"], + "start": ["globalOptions", "adventureGameOptions"], + "dpup": ["moveUp", "adventureViewWorld", "recruitmentUpgrade", "recruitmentUpgradeAll", + "battleConsoleUp", "recruitmentMax"], + "dpdown": ["moveDown", "adventureKingdomOverview", "battleConsoleDown","recruitmentMin"], + "dpleft": ["moveLeft", "adventureViewScenario"], + "dpright": ["moveRight", "adventureThievesGuild"], + "leftstick" : ["adventureToggleMapLevel", "battleToggleHeroesStats"], + "rightstick": ["adventureToggleGrid", "battleToggleQueue"] } }