Browse Source

Merge pull request #3762 from kdmcser/joystick_support

joystick support for VCMI
Ivan Savenko 1 year ago
parent
commit
62bb9fe4b7

+ 4 - 0
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

+ 212 - 0
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 <SDL.h>
+
+#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<std::string> operations = getOperations(key, value);
+    SDL_GameControllerAxis triggerAxis = key == "lefttrigger" ?
+            SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
+    std::vector<EShortcut> 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<std::string> 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<EShortcut> 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<std::string> GameControllerConfig::getOperations(const std::string & key, const JsonNode & value)
+{
+    std::vector<std::string> 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<SDL_GameControllerButton>(buttonValue);
+    return leftClickButtonSet.find(button) != leftClickButtonSet.end();
+}
+
+bool GameControllerConfig::isRightClickButton(int buttonValue)
+{
+    SDL_GameControllerButton button = static_cast<SDL_GameControllerButton>(buttonValue);
+    return rightClickButtonSet.find(button) != rightClickButtonSet.end();
+}
+
+bool GameControllerConfig::isShortcutsButton(int buttonValue)
+{
+    SDL_GameControllerButton button = static_cast<SDL_GameControllerButton>(buttonValue);
+    return buttonShortcutsMap.find(button) != buttonShortcutsMap.end();
+}
+
+const std::vector<EShortcut> & GameControllerConfig::getButtonShortcuts(int buttonValue)
+{
+    SDL_GameControllerButton button = static_cast<SDL_GameControllerButton>(buttonValue);
+    auto it = buttonShortcutsMap.find(button);
+    if(it != buttonShortcutsMap.end())
+        return it->second;
+    static std::vector<EShortcut> emptyVec;
+    return emptyVec;
+}
+
+bool GameControllerConfig::isLeftClickTrigger(int axisValue)
+{
+    SDL_GameControllerAxis axis = static_cast<SDL_GameControllerAxis>(axisValue);
+    return leftClickTriggerSet.find(axis) != leftClickTriggerSet.end();
+}
+
+bool GameControllerConfig::isRightClickTrigger(int axisValue)
+{
+    SDL_GameControllerAxis axis = static_cast<SDL_GameControllerAxis>(axisValue);
+    return rightClickTriggerSet.find(axis) != rightClickTriggerSet.end();
+}
+
+bool GameControllerConfig::isShortcutsTrigger(int axisValue)
+{
+    SDL_GameControllerAxis axis = static_cast<SDL_GameControllerAxis>(axisValue);
+    return triggerShortcutsMap.find(axis) != triggerShortcutsMap.end();
+}
+
+const std::vector<EShortcut> & GameControllerConfig::getTriggerShortcuts(int axisValue)
+{
+    SDL_GameControllerAxis axis = static_cast<SDL_GameControllerAxis>(axisValue);
+    auto it = triggerShortcutsMap.find(axis);
+    if(it != triggerShortcutsMap.end())
+        return it->second;
+    static std::vector<EShortcut> emptyVec;
+    return emptyVec;
+}
+

+ 59 - 0
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 <SDL.h>
+
+#include "../gui/Shortcut.h"
+#include "../../lib/json/JsonUtils.h"
+
+enum AxisType
+{
+    CURSOR_MOTION,
+    MAP_SCROLL,
+    NONE
+};
+
+class GameControllerConfig {
+    using ButtonShortcutsMap = std::map<SDL_GameControllerButton, std::vector<EShortcut> >;
+    using TriggerShortcutsMap = std::map<SDL_GameControllerAxis, std::vector<EShortcut> >;
+    ButtonShortcutsMap buttonShortcutsMap;
+    TriggerShortcutsMap triggerShortcutsMap;
+    std::set<SDL_GameControllerButton> leftClickButtonSet;
+    std::set<SDL_GameControllerButton> rightClickButtonSet;
+    std::set<SDL_GameControllerAxis> leftClickTriggerSet;
+    std::set<SDL_GameControllerAxis> rightClickTriggerSet;
+    AxisType leftAxisType;
+    AxisType rightAxisType;
+
+    void load();
+    std::vector<std::string> 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<EShortcut> & getButtonShortcuts(int buttonValue);
+
+    bool isLeftClickTrigger(int axisValue);
+    bool isRightClickTrigger(int axisValue);
+    bool isShortcutsTrigger(int axisValue);
+    const std::vector<EShortcut> & getTriggerShortcuts(int axisValue);
+};

+ 25 - 0
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<InputSourceKeyboard>())
 	, fingerHandler(std::make_unique<InputSourceTouch>())
 	, textHandler(std::make_unique<InputSourceText>())
+    , gameControllerHandler(std::make_unique<InputSourceGameController>())
 {
 }
 
@@ -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)

+ 3 - 0
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<InputSourceKeyboard> keyboardHandler;
 	std::unique_ptr<InputSourceTouch> fingerHandler;
 	std::unique_ptr<InputSourceText> textHandler;
+    std::unique_ptr<InputSourceGameController> gameControllerHandler;
 
 public:
 	InputHandler();
@@ -84,4 +86,5 @@ public:
 	bool isKeyboardAltDown() const;
 	bool isKeyboardCtrlDown() const;
 	bool isKeyboardShiftDown() const;
+
 };

+ 354 - 0
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<EShortcut> & 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<std::chrono::milliseconds>(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;
+}

+ 72 - 0
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 <SDL.h>
+
+#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<SDL_GameController, decltype(&gameControllerDeleter)>;
+
+    std::map<int, GameControllerPtr> 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<EShortcut> & 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();
+};

+ 30 - 0
client/gui/CursorHandler.cpp

@@ -57,6 +57,7 @@ CursorHandler::CursorHandler()
 		cursor->preload();
 
 	set(Cursor::Map::POINTER);
+    showType = dynamic_cast<CursorSoftware *>(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();
+}

+ 10 - 0
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();
 };

+ 1 - 0
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          },

+ 1 - 0
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;
 };
 

+ 3 - 0
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;
 };

+ 12 - 0
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<IImage> 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

+ 2 - 0
client/renderSDL/CursorHardware.h

@@ -23,6 +23,7 @@ class CursorHardware : public ICursor
 	std::shared_ptr<IImage> 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;
 };
 

+ 5 - 0
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),

+ 1 - 0
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;
 };
 

+ 6 - 1
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);
+}

+ 3 - 0
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<Point> getSupportedResolutions() const final;
 	std::vector<Point> getSupportedResolutions(int displayIndex) const;
 	std::tuple<int, int> getSupportedScalingRange() const final;

+ 22 - 0
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"]
 	}
 }