Browse Source

Merge pull request #3863 from vcmi/joystick_support

[1.5.0] Joystick support
Ivan Savenko 1 year ago
parent
commit
6c86d0ffe1

+ 2 - 0
client/CMakeLists.txt

@@ -34,6 +34,7 @@ set(client_SRCS
 	eventsSDL/InputSourceMouse.cpp
 	eventsSDL/InputSourceText.cpp
 	eventsSDL/InputSourceTouch.cpp
+	eventsSDL/InputSourceGameController.cpp
 
 	gui/CGuiHandler.cpp
 	gui/CIntObject.cpp
@@ -212,6 +213,7 @@ set(client_HEADERS
 	eventsSDL/InputSourceMouse.h
 	eventsSDL/InputSourceText.h
 	eventsSDL/InputSourceTouch.h
+	eventsSDL/InputSourceGameController.h
 
 	gui/CGuiHandler.h
 	gui/CIntObject.h

+ 31 - 6
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;
 		}
 	}
@@ -159,15 +169,15 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 	else if(ev.type == SDL_WINDOWEVENT)
 	{
 		switch (ev.window.event) {
-		case SDL_WINDOWEVENT_RESTORED:
+			case SDL_WINDOWEVENT_RESTORED:
 #ifndef VCMI_IOS
 			{
 				boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 				GH.onScreenResize(false);
 			}
 #endif
-			break;
-		case SDL_WINDOWEVENT_FOCUS_GAINED:
+				break;
+			case SDL_WINDOWEVENT_FOCUS_GAINED:
 			{
 				boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 				if(settings["general"]["audioMuteFocus"].Bool()) {
@@ -175,8 +185,8 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 					CCS->soundh->setVolume(settings["general"]["sound"].Integer());
 				}
 			}
-			break;
-		case SDL_WINDOWEVENT_FOCUS_LOST:
+				break;
+			case SDL_WINDOWEVENT_FOCUS_LOST:
 			{
 				boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 				if(settings["general"]["audioMuteFocus"].Bool()) {
@@ -184,7 +194,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 					CCS->soundh->setVolume(0);
 				}
 			}
-			break;
+				break;
 		}
 		return;
 	}
@@ -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)

+ 2 - 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();

+ 311 - 0
client/eventsSDL/InputSourceGameController.cpp

@@ -0,0 +1,311 @@
+/*
+* 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/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/EventDispatcher.h"
+#include "../gui/ShortcutHandler.h"
+
+void InputSourceGameController::gameControllerDeleter(SDL_GameController * gameController)
+{
+	if(gameController)
+		SDL_GameControllerClose(gameController);
+}
+
+InputSourceGameController::InputSourceGameController():
+	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.try_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 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::dispatchAxisShortcuts(const std::vector<EShortcut> & shortcutsVector, SDL_GameControllerAxis axisID, int axisValue)
+{
+	if(axisValue >= TRIGGER_PRESS_THRESHOLD)
+	{
+		if(!pressedAxes.count(axisID))
+		{
+			GH.events().dispatchShortcutPressed(shortcutsVector);
+			pressedAxes.insert(axisID);
+		}
+	}
+	else
+	{
+		if(pressedAxes.count(axisID))
+		{
+			GH.events().dispatchShortcutReleased(shortcutsVector);
+			pressedAxes.erase(axisID);
+		}
+	}
+}
+
+void InputSourceGameController::handleEventAxisMotion(const SDL_ControllerAxisEvent & axis)
+{
+	tryToConvertCursor();
+
+	SDL_GameControllerAxis axisID = static_cast<SDL_GameControllerAxis>(axis.axis);
+	std::string axisName = SDL_GameControllerGetStringForAxis(axisID);
+
+	auto axisActions = GH.shortcuts().translateJoystickAxis(axisName);
+	auto buttonActions = GH.shortcuts().translateJoystickButton(axisName);
+
+	for(const auto & action : axisActions)
+	{
+		switch(action)
+		{
+			case EShortcut::MOUSE_CURSOR_X:
+				cursorAxisValueX = getRealAxisValue(axis.value);
+				break;
+			case EShortcut::MOUSE_CURSOR_Y:
+				cursorAxisValueY = getRealAxisValue(axis.value);
+				break;
+			case EShortcut::MOUSE_SWIPE_X:
+				scrollAxisValueX = getRealAxisValue(axis.value);
+				break;
+			case EShortcut::MOUSE_SWIPE_Y:
+				scrollAxisValueY = getRealAxisValue(axis.value);
+				break;
+		}
+	}
+
+	dispatchAxisShortcuts(buttonActions, axisID, axis.value);
+}
+
+void InputSourceGameController::tryToConvertCursor()
+{
+	assert(CCS);
+	assert(CCS->curh);
+	if(CCS->curh->getShowType() == Cursor::ShowType::HARDWARE)
+	{
+		const Point & cursorPosition = GH.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)
+{
+	std::string buttonName = SDL_GameControllerGetStringForButton(static_cast<SDL_GameControllerButton>(button.button));
+	const auto & shortcutsVector = GH.shortcuts().translateJoystickButton(buttonName);
+	GH.events().dispatchShortcutPressed(shortcutsVector);
+}
+
+void InputSourceGameController::handleEventButtonUp(const SDL_ControllerButtonEvent & button)
+{
+	std::string buttonName = SDL_GameControllerGetStringForButton(static_cast<SDL_GameControllerButton>(button.button));
+	const auto & shortcutsVector = GH.shortcuts().translateJoystickButton(buttonName);
+	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()
+{
+	std::chrono::steady_clock::time_point nowMs = std::chrono::steady_clock::now();
+
+	if(lastCheckTime == std::chrono::steady_clock::time_point())
+	{
+		lastCheckTime = nowMs;
+		return;
+	}
+
+	int32_t deltaTime = std::chrono::duration_cast<std::chrono::milliseconds>(nowMs - lastCheckTime).count();
+	handleCursorUpdate(deltaTime);
+	handleScrollUpdate(deltaTime);
+	lastCheckTime = nowMs;
+}
+
+void InputSourceGameController::handleCursorUpdate(int32_t deltaTimeMs)
+{
+	float deltaTimeSeconds = static_cast<float>(deltaTimeMs) / 1000;
+
+	if(cursorAxisValueX == 0)
+		cursorPlanDisX = 0;
+	else
+		cursorPlanDisX += deltaTimeSeconds * AXIS_MOVE_SPEED * cursorAxisValueX / AXIS_MAX_ZOOM;
+
+	if(cursorAxisValueY == 0)
+		cursorPlanDisY = 0;
+	else
+		cursorPlanDisY += deltaTimeSeconds * AXIS_MOVE_SPEED * cursorAxisValueY / AXIS_MAX_ZOOM;
+
+	int moveDisX = getMoveDis(cursorPlanDisX);
+	int moveDisY = getMoveDis(cursorPlanDisY);
+	cursorPlanDisX -= moveDisX;
+	cursorPlanDisY -= moveDisY;
+	doCursorMove(moveDisX, moveDisY);
+}
+
+void InputSourceGameController::handleScrollUpdate(int32_t deltaTimeMs)
+{
+	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;
+	}
+	float deltaTimeSeconds = static_cast<float>(deltaTimeMs) / 1000;
+	scrollPlanDisX += deltaTimeSeconds * AXIS_MOVE_SPEED * scrollAxisValueX / AXIS_MAX_ZOOM;
+	scrollPlanDisY += deltaTimeSeconds * AXIS_MOVE_SPEED * scrollAxisValueY / AXIS_MAX_ZOOM;
+	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;
+}

+ 68 - 0
client/eventsSDL/InputSourceGameController.h

@@ -0,0 +1,68 @@
+/*
+* 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_events.h>
+#include <SDL_gamecontroller.h>
+
+#include "../../lib/Point.h"
+#include "../gui/Shortcut.h"
+
+constexpr int AXIS_DEAD_ZOOM = 6000;
+constexpr int AXIS_MAX_ZOOM = 32000;
+constexpr int AXIS_MOVE_SPEED = 500;
+constexpr 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;
+	std::set<SDL_GameControllerAxis> pressedAxes;
+
+	std::chrono::steady_clock::time_point 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 dispatchAxisShortcuts(const std::vector<EShortcut> & shortcutsVector, SDL_GameControllerAxis axisID, int axisValue);
+	void tryToConvertCursor();
+	void doCursorMove(int deltaX, int deltaY);
+	int getMoveDis(float planDis);
+	void handleCursorUpdate(int32_t deltaTimeMs);
+	void handleScrollUpdate(int32_t deltaTimeMs);
+	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();
+};

+ 1 - 1
client/gui/CGuiHandler.cpp

@@ -76,8 +76,8 @@ void CGuiHandler::init()
 	windowHandlerInstance = std::make_unique<WindowHandler>();
 	screenHandlerInstance = std::make_unique<ScreenHandler>();
 	renderHandlerInstance = std::make_unique<RenderHandler>();
-	inputHandlerInstance = std::make_unique<InputHandler>(); // Must be after windowHandlerInstance
 	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
+	inputHandlerInstance = std::make_unique<InputHandler>(); // Must be after windowHandlerInstance and shortcutsHandlerInstance
 	framerateManagerInstance = std::make_unique<FramerateManager>(settings["video"]["targetfps"].Integer());
 }
 

+ 25 - 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,27 @@ void CursorHandler::show()
 	cursor->setVisible(true);
 }
 
+Cursor::ShowType CursorHandler::getShowType() const
+{
+	return showType;
+}
+
+void CursorHandler::changeCursor(Cursor::ShowType newShowType)
+{
+	if(newShowType == showType)
+		return;
+
+	switch(newShowType)
+	{
+		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;
+	}
+}

+ 9 - 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,7 @@ public:
 
 	/// change cursor's positions to (x, y)
 	void cursorMove(const int & x, const int & y);
+
+	Cursor::ShowType getShowType() const;
+	void changeCursor(Cursor::ShowType showType);
 };

+ 13 - 0
client/gui/EventDispatcher.cpp

@@ -15,6 +15,7 @@
 #include "CGuiHandler.h"
 #include "MouseButton.h"
 #include "WindowHandler.h"
+#include "gui/Shortcut.h"
 
 #include "../../lib/Rect.h"
 
@@ -74,6 +75,12 @@ void EventDispatcher::dispatchShortcutPressed(const std::vector<EShortcut> & sho
 {
 	bool keysCaptured = false;
 
+	if (vstd::contains(shortcutsVector, EShortcut::MOUSE_LEFT))
+		dispatchMouseLeftButtonPressed(GH.getCursorPosition(), 0);
+
+	if (vstd::contains(shortcutsVector, EShortcut::MOUSE_RIGHT))
+		dispatchShowPopup(GH.getCursorPosition(), 0);
+
 	for(auto & i : keyinterested)
 		for(EShortcut shortcut : shortcutsVector)
 			if(i->captureThisKey(shortcut))
@@ -97,6 +104,12 @@ void EventDispatcher::dispatchShortcutReleased(const std::vector<EShortcut> & sh
 {
 	bool keysCaptured = false;
 
+	if (vstd::contains(shortcutsVector, EShortcut::MOUSE_LEFT))
+		dispatchMouseLeftButtonReleased(GH.getCursorPosition(), 0);
+
+	if (vstd::contains(shortcutsVector, EShortcut::MOUSE_RIGHT))
+		dispatchClosePopup(GH.getCursorPosition());
+
 	for(auto & i : keyinterested)
 		for(EShortcut shortcut : shortcutsVector)
 			if(i->captureThisKey(shortcut))

+ 8 - 0
client/gui/Shortcut.h

@@ -13,6 +13,14 @@ enum class EShortcut
 {
 	NONE,
 
+	// preudo-shortcuts that trigger mouse events
+	MOUSE_LEFT,
+	MOUSE_RIGHT,
+	MOUSE_CURSOR_X,
+	MOUSE_CURSOR_Y,
+	MOUSE_SWIPE_X,
+	MOUSE_SWIPE_Y,
+
 	// Global hotkeys that are available in multiple dialogs
 	GLOBAL_ACCEPT,     // Return - Accept query
 	GLOBAL_CANCEL,     // Escape - Cancel query

+ 38 - 5
client/gui/ShortcutHandler.cpp

@@ -19,7 +19,16 @@ ShortcutHandler::ShortcutHandler()
 {
 	const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig");
 
-	for (auto const & entry : config["keyboard"].Struct())
+	mappedKeyboardShortcuts = loadShortcuts(config["keyboard"]);
+	mappedJoystickShortcuts = loadShortcuts(config["joystickButtons"]);
+	mappedJoystickAxes = loadShortcuts(config["joystickAxes"]);
+}
+
+std::multimap<std::string, EShortcut> ShortcutHandler::loadShortcuts(const JsonNode & data) const
+{
+	std::multimap<std::string, EShortcut> result;
+
+	for (auto const & entry : data.Struct())
 	{
 		std::string shortcutName = entry.first;
 		EShortcut shortcutID = findShortcut(shortcutName);
@@ -32,20 +41,22 @@ ShortcutHandler::ShortcutHandler()
 
 		if (entry.second.isString())
 		{
-			mappedShortcuts.emplace(entry.second.String(), shortcutID);
+			result.emplace(entry.second.String(), shortcutID);
 		}
 
 		if (entry.second.isVector())
 		{
 			for (auto const & entryVector : entry.second.Vector())
-				mappedShortcuts.emplace(entryVector.String(), shortcutID);
+				result.emplace(entryVector.String(), shortcutID);
 		}
 	}
+
+	return result;
 }
 
-std::vector<EShortcut> ShortcutHandler::translateKeycode(const std::string & key) const
+std::vector<EShortcut> ShortcutHandler::translateShortcut(const std::multimap<std::string, EShortcut> & options, const std::string & key) const
 {
-	auto range = mappedShortcuts.equal_range(key);
+	auto range = options.equal_range(key);
 
 	// FIXME: some code expects calls to keyPressed / captureThisKey even without defined hotkeys
 	if (range.first == range.second)
@@ -59,9 +70,30 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(const std::string & key
 	return result;
 }
 
+std::vector<EShortcut> ShortcutHandler::translateKeycode(const std::string & key) const
+{
+	return translateShortcut(mappedKeyboardShortcuts, key);
+}
+
+std::vector<EShortcut> ShortcutHandler::translateJoystickButton(const std::string & key) const
+{
+	return translateShortcut(mappedJoystickShortcuts, key);
+}
+
+std::vector<EShortcut> ShortcutHandler::translateJoystickAxis(const std::string & key) const
+{
+	return translateShortcut(mappedJoystickAxes, key);
+}
+
 EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 {
 	static const std::map<std::string, EShortcut> shortcutNames = {
+		{"mouseClickLeft",           EShortcut::MOUSE_LEFT                },
+		{"mouseClickRight",          EShortcut::MOUSE_RIGHT               },
+		{"mouseCursorX",             EShortcut::MOUSE_CURSOR_X,           },
+		{"mouseCursorY",             EShortcut::MOUSE_CURSOR_Y,           },
+		{"mouseSwipeX",              EShortcut::MOUSE_SWIPE_X,            },
+		{"mouseSwipeY",              EShortcut::MOUSE_SWIPE_Y,            },
 		{"globalAccept",             EShortcut::GLOBAL_ACCEPT             },
 		{"globalCancel",             EShortcut::GLOBAL_CANCEL             },
 		{"globalReturn",             EShortcut::GLOBAL_RETURN             },
@@ -153,6 +185,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          },

+ 15 - 1
client/gui/ShortcutHandler.h

@@ -12,15 +12,29 @@
 
 enum class EShortcut;
 
+VCMI_LIB_NAMESPACE_BEGIN
+class JsonNode;
+VCMI_LIB_NAMESPACE_END
+
 class ShortcutHandler
 {
-	std::multimap<std::string, EShortcut> mappedShortcuts;
+	std::multimap<std::string, EShortcut> mappedKeyboardShortcuts;
+	std::multimap<std::string, EShortcut> mappedJoystickShortcuts;
+	std::multimap<std::string, EShortcut> mappedJoystickAxes;
+
+	std::multimap<std::string, EShortcut> loadShortcuts(const JsonNode & data) const;
+	std::vector<EShortcut> translateShortcut(const std::multimap<std::string, EShortcut> & options, const std::string & key) const;
+
 public:
 	ShortcutHandler();
 
 	/// returns list of shortcuts assigned to provided SDL keycode
 	std::vector<EShortcut> translateKeycode(const std::string & key) const;
 
+	std::vector<EShortcut> translateJoystickButton(const std::string & key) const;
+
+	std::vector<EShortcut> translateJoystickAxis(const std::string & key) const;
+
 	/// attempts to find shortcut by its unique identifier. Returns EShortcut::NONE on failure
 	EShortcut findShortcut(const std::string & identifier ) const;
 };

+ 1 - 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"

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

+ 69 - 0
config/shortcutsConfig.json

@@ -110,6 +110,7 @@
 		"battleConsoleDown":        "Down",
 		"battleTacticsNext":        "Space",
 		"battleTacticsEnd":         [ "Return", "Keypad Enter"],
+		"battleToggleHeroesStats":  [],
 		"battleSelectAction":       "S",
 		"townOpenTavern":           "T",
 		"townSwapArmies":           "Space",
@@ -136,5 +137,73 @@
 		"heroCostume7":             "7",
 		"heroCostume8":             "8",
 		"heroCostume9":             "9"
+	},
+	
+	"joystickAxes":
+	{
+		"mouseCursorX" : "leftx",
+		"mouseCursorY" : "lefty",
+		"mouseSwipeX" : "rightx",
+		"mouseSwipeY" : "righty"
+	},
+	
+	"joystickButtons": {
+		"globalAccept" : "a",
+		"globalCancel" : "b",
+		"globalReturn" : [ "a", "b" ],
+		
+		"lobbyBeginStandardGame" : "a",
+		"lobbyBeginCampaign" : "a",
+		"lobbyLoadGame" : "a",
+		"lobbySaveGame" : "a",
+		"adventureViewSelected" : "a",
+		"adventureExitWorldView" : [ "a", "b" ],
+		"battleTacticsEnd" : "a",
+		
+		"mouseClickLeft": "x",
+		"mouseClickRight": "y",
+
+		"adventureNextHero" : "leftshoulder",
+		"battleDefend" : "leftshoulder",
+
+		"adventureNextTown" : "rightshoulder",
+		"battleWait" : "rightshoulder",
+
+		"adventureVisitObject" : "lefttrigger",
+		"battleTacticsNext" : "lefttrigger",
+		"battleUseCreatureSpell" : "lefttrigger",
+
+		"adventureCastSpell" : "righttrigger",
+		"battleCastSpell" : "righttrigger",
+		
+		"gameEndTurn" : "back",
+		"battleAutocombatEnd" : "back",
+		
+		"globalOptions" : "start",
+		"adventureGameOptions" : "start",
+		
+		"moveUp" : "dpup",
+		"adventureViewWorld" : "dpup",
+		"recruitmentUpgrade" : "dpup",
+		"recruitmentUpgradeAll" : "dpup",
+		"battleConsoleUp" : "dpup",
+		"recruitmentMax" : "dpup",
+		
+		"moveDown" : "dpdown",
+		"adventureKingdomOverview" : "dpdown",
+		"battleConsoleDown" : "dpdown",
+		"recruitmentMin" : "dpdown",
+		
+		"moveLeft" : "dpleft",
+		"adventureViewScenario" : "dpleft",
+
+		"moveRight" : "dpright",
+		"adventureThievesGuild" : "dpright",
+		
+		"adventureToggleMapLevel" : "leftstick",
+		"battleToggleHeroesStats" : "leftstick",
+		
+		"adventureToggleGrid" : "rightstick",
+		"battleToggleQueue" : "rightstick",
 	}
 }