From 2060fd9cbe587d7e8ffe0cecdd67925f13a56c05 Mon Sep 17 00:00:00 2001
From: est31 <MTest31@outlook.com>
Date: Fri, 27 May 2016 08:35:07 +0200
Subject: [PATCH] Initial Gamepad support

Adds initial ingame gamepad support to minetest.

Full Formspec support is not implemented yet and
can be added by a later change.
---
 builtin/settingtypes.txt           |  11 ++
 src/client/CMakeLists.txt          |   1 +
 src/client/clientlauncher.cpp      |  20 +++-
 src/client/inputhandler.h          |  13 +++
 src/client/joystick_controller.cpp | 179 +++++++++++++++++++++++++++++
 src/client/joystick_controller.h   | 163 ++++++++++++++++++++++++++
 src/client/keys.h                  |   6 +
 src/content_cao.cpp                |   4 +-
 src/defaultsettings.cpp            |   3 +
 src/game.cpp                       | 165 +++++++++++++++++---------
 src/game.h                         |   3 +
 src/guiEngine.cpp                  |   2 +
 src/guiEngine.h                    |   3 +-
 src/guiFormSpecMenu.cpp            |  42 +++++--
 src/guiFormSpecMenu.h              |  13 ++-
 src/localplayer.cpp                |  17 ++-
 src/player.h                       |  10 +-
 17 files changed, 576 insertions(+), 79 deletions(-)
 create mode 100644 src/client/joystick_controller.cpp
 create mode 100644 src/client/joystick_controller.h

diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt
index 538a04f33..b53e35a85 100644
--- a/builtin/settingtypes.txt
+++ b/builtin/settingtypes.txt
@@ -104,6 +104,17 @@ random_input (Random input) bool false
 #    Continuous forward movement (only used for testing).
 continuous_forward (Continuous forward) bool false
 
+#    Enable Joysticks
+enable_joysticks (Enable Joysticks) bool true
+
+#    The time in seconds it takes between repeated events
+#    when holding down a joystick button combination.
+repeat_joystick_button_time (Joystick button repetition invterval) float 0.17
+
+#    The sensitivity of the joystick axes for moving the
+#    ingame view frustum around.
+joystick_frustum_sensitivity (Joystick frustum sensitivity) float 170
+
 #    Key for moving the player forward.
 #    See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3
 keymap_forward (Forward key) key KEY_KEY_W
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
index a1ec37fe3..5faa186a7 100644
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -1,6 +1,7 @@
 set(client_SRCS
 	${CMAKE_CURRENT_SOURCE_DIR}/clientlauncher.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/tile.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/joystick_controller.cpp
 	PARENT_SCOPE
 )
 
diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp
index 404a16310..ee8662ed6 100644
--- a/src/client/clientlauncher.cpp
+++ b/src/client/clientlauncher.cpp
@@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "guiEngine.h"
 #include "player.h"
 #include "fontengine.h"
+#include "joystick_controller.h"
 #include "clientlauncher.h"
 
 /* mainmenumanager.h
@@ -499,7 +500,8 @@ void ClientLauncher::main_menu(MainMenuData *menudata)
 #endif
 
 	/* show main menu */
-	GUIEngine mymenu(device, guiroot, &g_menumgr, smgr, menudata, *kill);
+	GUIEngine mymenu(device, &input->joystick, guiroot,
+		&g_menumgr, smgr, menudata, *kill);
 
 	smgr->clear();	/* leave scene manager in a clean state */
 }
@@ -558,6 +560,22 @@ bool ClientLauncher::create_engine_device()
 	device = createDeviceEx(params);
 
 	if (device) {
+		if (g_settings->getBool("enable_joysticks")) {
+			irr::core::array<irr::SJoystickInfo> infos;
+			std::vector<irr::SJoystickInfo> joystick_infos;
+			// Make sure this is called maximum once per
+			// irrlicht device, otherwise it will give you
+			// multiple events for the same joystick.
+			if (device->activateJoysticks(infos)) {
+				infostream << "Joystick support enabled" << std::endl;
+				joystick_infos.reserve(infos.size());
+				for (u32 i = 0; i < infos.size(); i++) {
+					joystick_infos.push_back(infos[i]);
+				}
+			} else {
+				errorstream << "Could not activate joystick support." << std::endl;
+			}
+		}
 		porting::initIrrlicht(device);
 	}
 
diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h
index 73e507fdc..824b0da2e 100644
--- a/src/client/inputhandler.h
+++ b/src/client/inputhandler.h
@@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define INPUT_HANDLER_H
 
 #include "irrlichttypes_extrabloated.h"
+#include "joystick_controller.h"
 
 class MyEventReceiver : public IEventReceiver
 {
@@ -62,6 +63,14 @@ class MyEventReceiver : public IEventReceiver
 			return true;
 		}
 #endif
+
+		if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) {
+			/* TODO add a check like:
+			if (event.JoystickEvent != joystick_we_listen_for)
+				return false;
+			*/
+			return joystick->handleEvent(event.JoystickEvent);
+		}
 		// handle mouse events
 		if (event.EventType == irr::EET_MOUSE_INPUT_EVENT) {
 			if (noMenuActive() == false) {
@@ -172,6 +181,8 @@ class MyEventReceiver : public IEventReceiver
 
 	s32 mouse_wheel;
 
+	JoystickController *joystick;
+
 #ifdef HAVE_TOUCHSCREENGUI
 	TouchScreenGUI* m_touchscreengui;
 #endif
@@ -202,6 +213,7 @@ class RealInputHandler : public InputHandler
 		m_receiver(receiver),
 		m_mousepos(0,0)
 	{
+		m_receiver->joystick = &joystick;
 	}
 	virtual bool isKeyDown(const KeyPress &keyCode)
 	{
@@ -288,6 +300,7 @@ class RealInputHandler : public InputHandler
 
 	void clear()
 	{
+		joystick.clear();
 		m_receiver->clearInput();
 	}
 private:
diff --git a/src/client/joystick_controller.cpp b/src/client/joystick_controller.cpp
new file mode 100644
index 000000000..ef8d18ab0
--- /dev/null
+++ b/src/client/joystick_controller.cpp
@@ -0,0 +1,179 @@
+/*
+Minetest
+Copyright (C) 2016 est31, <MTest31@outlook.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "joystick_controller.h"
+#include "irrlichttypes_extrabloated.h"
+#include "keys.h"
+#include "settings.h"
+#include "gettime.h"
+
+bool JoystickButtonCmb::isTriggered(const irr::SEvent::SJoystickEvent &ev) const
+{
+	u32 buttons = ev.ButtonStates;
+
+	buttons &= filter_mask;
+	return buttons == compare_mask;
+}
+
+bool JoystickAxisCmb::isTriggered(const irr::SEvent::SJoystickEvent &ev) const
+{
+	s16 ax_val = ev.Axis[axis_to_compare];
+
+	return (ax_val * direction < 0) && (thresh * direction > ax_val * direction);
+}
+
+// spares many characters
+#define JLO_B_PB(A, B, C)    jlo.button_keys.push_back(JoystickButtonCmb(A, B, C))
+#define JLO_A_PB(A, B, C, D) jlo.axis_keys.push_back(JoystickAxisCmb(A, B, C, D))
+
+static JoystickLayout create_default_layout()
+{
+	JoystickLayout jlo;
+
+	jlo.axes_dead_border = 1024;
+
+	const JoystickAxisLayout axes[JA_COUNT] = {
+		{0, 1}, // JA_SIDEWARD_MOVE
+		{1, 1}, // JA_FORWARD_MOVE
+		{3, 1}, // JA_FRUSTUM_HORIZONTAL
+		{4, 1}, // JA_FRUSTUM_VERTICAL
+	};
+	memcpy(jlo.axes, axes, sizeof(jlo.axes));
+
+	u32 sb = 1 << 7; // START button mask
+	u32 fb = 1 << 3; // FOUR button mask
+	u32 bm = sb | fb; // Mask for Both Modifiers
+
+	// The back button means "ESC".
+	JLO_B_PB(KeyType::ESC,        1 << 6,      1 << 6);
+
+	// The start button counts as modifier as well as use key.
+	// JLO_B_PB(KeyType::USE,        sb,          sb));
+
+	// Accessible without start modifier button pressed
+	// regardless whether four is pressed or not
+	JLO_B_PB(KeyType::SNEAK,      sb | 1 << 2, 1 << 2);
+
+	// Accessible without four modifier button pressed
+	// regardless whether start is pressed or not
+	JLO_B_PB(KeyType::MOUSE_L,    fb | 1 << 4, 1 << 4);
+	JLO_B_PB(KeyType::MOUSE_R,    fb | 1 << 5, 1 << 5);
+
+	// Accessible without any modifier pressed
+	JLO_B_PB(KeyType::JUMP,       bm | 1 << 0, 1 << 0);
+	JLO_B_PB(KeyType::SPECIAL1,   bm | 1 << 1, 1 << 1);
+
+	// Accessible with start button not pressed, but four pressed
+	// TODO find usage for button 0
+	JLO_B_PB(KeyType::DROP,       bm | 1 << 1, fb | 1 << 1);
+	JLO_B_PB(KeyType::SCROLL_UP,  bm | 1 << 4, fb | 1 << 4);
+	JLO_B_PB(KeyType::SCROLL_DOWN,bm | 1 << 5, fb | 1 << 5);
+
+	// Accessible with start button and four pressed
+	// TODO find usage for buttons 0, 1 and 4, 5
+
+	// Now about the buttons simulated by the axes
+
+	// Movement buttons, important for vessels
+	JLO_A_PB(KeyType::FORWARD,  1,  1, 1024);
+	JLO_A_PB(KeyType::BACKWARD, 1, -1, 1024);
+	JLO_A_PB(KeyType::LEFT,     0,  1, 1024);
+	JLO_A_PB(KeyType::RIGHT,    0, -1, 1024);
+
+	// Scroll buttons
+	JLO_A_PB(KeyType::SCROLL_UP,   2, -1, 1024);
+	JLO_A_PB(KeyType::SCROLL_DOWN, 5, -1, 1024);
+
+	return jlo;
+}
+
+static const JoystickLayout default_layout = create_default_layout();
+
+JoystickController::JoystickController()
+{
+	m_layout = &default_layout;
+	doubling_dtime = g_settings->getFloat("repeat_joystick_button_time");
+
+	for (size_t i = 0; i < KeyType::INTERNAL_ENUM_COUNT; i++) {
+		m_past_pressed_time[i] = 0;
+	}
+	clear();
+}
+
+bool JoystickController::handleEvent(const irr::SEvent::SJoystickEvent &ev)
+{
+	m_internal_time = getTimeMs() / 1000.f;
+
+	std::bitset<KeyType::INTERNAL_ENUM_COUNT> keys_pressed;
+
+	// First generate a list of keys pressed
+
+	for (size_t i = 0; i < m_layout->button_keys.size(); i++) {
+		if (m_layout->button_keys[i].isTriggered(ev)) {
+			keys_pressed.set(m_layout->button_keys[i].key);
+		}
+	}
+
+	for (size_t i = 0; i < m_layout->axis_keys.size(); i++) {
+		if (m_layout->axis_keys[i].isTriggered(ev)) {
+			keys_pressed.set(m_layout->axis_keys[i].key);
+		}
+	}
+
+	// Then update the values
+
+	for (size_t i = 0; i < KeyType::INTERNAL_ENUM_COUNT; i++) {
+		if (keys_pressed[i]) {
+			if (!m_past_pressed_keys[i] &&
+					m_past_pressed_time[i] < m_internal_time - doubling_dtime) {
+				m_past_pressed_keys[i] = true;
+				m_past_pressed_time[i] = m_internal_time;
+			}
+		} else if (m_pressed_keys[i]) {
+			m_past_released_keys[i] = true;
+		}
+
+		m_pressed_keys[i] = keys_pressed[i];
+	}
+
+	for (size_t i = 0; i < JA_COUNT; i++) {
+		const JoystickAxisLayout &ax_la = m_layout->axes[i];
+		m_axes_vals[i] = ax_la.invert * ev.Axis[ax_la.axis_id];
+	}
+
+
+	return true;
+}
+
+void JoystickController::clear()
+{
+	m_pressed_keys.reset();
+	m_past_pressed_keys.reset();
+	m_past_released_keys.reset();
+	memset(m_axes_vals, 0, sizeof(m_axes_vals));
+}
+
+s16 JoystickController::getAxisWithoutDead(JoystickAxis axis)
+{
+	s16 v = m_axes_vals[axis];
+	if (((v > 0) && (v < m_layout->axes_dead_border)) ||
+			((v < 0) && (v > -m_layout->axes_dead_border)))
+		return 0;
+	return v;
+}
diff --git a/src/client/joystick_controller.h b/src/client/joystick_controller.h
new file mode 100644
index 000000000..ed0ee4068
--- /dev/null
+++ b/src/client/joystick_controller.h
@@ -0,0 +1,163 @@
+/*
+Minetest
+Copyright (C) 2016 est31, <MTest31@outlook.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef JOYSTICK_HEADER
+#define JOYSTICK_HEADER
+
+#include "irrlichttypes_extrabloated.h"
+#include "keys.h"
+#include <bitset>
+#include <vector>
+
+enum JoystickAxis {
+	JA_SIDEWARD_MOVE,
+	JA_FORWARD_MOVE,
+
+	JA_FRUSTUM_HORIZONTAL,
+	JA_FRUSTUM_VERTICAL,
+
+	// To know the count of enum values
+	JA_COUNT,
+};
+
+struct JoystickAxisLayout {
+	u16 axis_id;
+	// -1 if to invert, 1 if to keep it.
+	int invert;
+};
+
+
+struct JoystickCombination {
+
+	virtual bool isTriggered(const irr::SEvent::SJoystickEvent &ev) const=0;
+
+	GameKeyType key;
+};
+
+struct JoystickButtonCmb : public JoystickCombination {
+
+	JoystickButtonCmb() {}
+	JoystickButtonCmb(GameKeyType key, u32 filter_mask, u32 compare_mask) :
+		filter_mask(filter_mask),
+		compare_mask(compare_mask)
+	{
+		this->key = key;
+	}
+
+	virtual bool isTriggered(const irr::SEvent::SJoystickEvent &ev) const;
+
+	u32 filter_mask;
+	u32 compare_mask;
+};
+
+struct JoystickAxisCmb : public JoystickCombination {
+
+	JoystickAxisCmb() {}
+	JoystickAxisCmb(GameKeyType key, u16 axis_to_compare, int direction, s16 thresh) :
+		axis_to_compare(axis_to_compare),
+		direction(direction),
+		thresh(thresh)
+	{
+		this->key = key;
+	}
+
+	virtual bool isTriggered(const irr::SEvent::SJoystickEvent &ev) const;
+
+	u16 axis_to_compare;
+
+	// if -1, thresh must be smaller than the axis value in order to trigger
+	// if  1, thresh must be bigger  than the axis value in order to trigger
+	int direction;
+	s16 thresh;
+};
+
+struct JoystickLayout {
+	std::vector<JoystickButtonCmb> button_keys;
+	std::vector<JoystickAxisCmb> axis_keys;
+	JoystickAxisLayout axes[JA_COUNT];
+	s16 axes_dead_border;
+};
+
+class JoystickController {
+
+public:
+	JoystickController();
+	bool handleEvent(const irr::SEvent::SJoystickEvent &ev);
+	void clear();
+
+	bool wasKeyDown(GameKeyType b)
+	{
+		bool r = m_past_pressed_keys[b];
+		m_past_pressed_keys[b] = false;
+		return r;
+	}
+	bool getWasKeyDown(GameKeyType b)
+	{
+		return m_past_pressed_keys[b];
+	}
+	void clearWasKeyDown(GameKeyType b)
+	{
+		m_past_pressed_keys[b] = false;
+	}
+
+	bool wasKeyReleased(GameKeyType b)
+	{
+		bool r = m_past_released_keys[b];
+		m_past_released_keys[b] = false;
+		return r;
+	}
+	bool getWasKeyReleased(GameKeyType b)
+	{
+		return m_past_pressed_keys[b];
+	}
+	void clearWasKeyReleased(GameKeyType b)
+	{
+		m_past_pressed_keys[b] = false;
+	}
+
+	bool isKeyDown(GameKeyType b)
+	{
+		return m_pressed_keys[b];
+	}
+
+	s16 getAxis(JoystickAxis axis)
+	{
+		return m_axes_vals[axis];
+	}
+
+	s16 getAxisWithoutDead(JoystickAxis axis);
+
+	f32 doubling_dtime;
+
+private:
+	const JoystickLayout *m_layout;
+
+	s16 m_axes_vals[JA_COUNT];
+
+	std::bitset<KeyType::INTERNAL_ENUM_COUNT> m_pressed_keys;
+
+	f32 m_internal_time;
+
+	f32 m_past_pressed_time[KeyType::INTERNAL_ENUM_COUNT];
+
+	std::bitset<KeyType::INTERNAL_ENUM_COUNT> m_past_pressed_keys;
+	std::bitset<KeyType::INTERNAL_ENUM_COUNT> m_past_released_keys;
+};
+
+#endif
diff --git a/src/client/keys.h b/src/client/keys.h
index 08b1c4123..0921bc166 100644
--- a/src/client/keys.h
+++ b/src/client/keys.h
@@ -67,6 +67,12 @@ class KeyType {
 
 		DEBUG_STACKS,
 
+		// joystick specific keys
+		MOUSE_L,
+		MOUSE_R,
+		SCROLL_UP,
+		SCROLL_DOWN,
+
 		// Fake keycode for array size and internal checks
 		INTERNAL_ENUM_COUNT
 
diff --git a/src/content_cao.cpp b/src/content_cao.cpp
index 35ab1c508..eeb85c8a6 100644
--- a/src/content_cao.cpp
+++ b/src/content_cao.cpp
@@ -1056,7 +1056,9 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
 			PlayerControl controls = player->getPlayerControl();
 
 			bool walking = false;
-			if(controls.up || controls.down || controls.left || controls.right)
+			if (controls.up || controls.down || controls.left || controls.right ||
+					controls.forw_move_joystick_axis != 0.f ||
+					controls.sidew_move_joystick_axis != 0.f)
 				walking = true;
 
 			f32 new_speed = player->local_animation_speed;
diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp
index 632ec0df9..43a7a7c00 100644
--- a/src/defaultsettings.cpp
+++ b/src/defaultsettings.cpp
@@ -116,6 +116,9 @@ void set_default_settings(Settings *settings)
 	settings->setDefault("free_move", "false");
 	settings->setDefault("noclip", "false");
 	settings->setDefault("continuous_forward", "false");
+	settings->setDefault("enable_joysticks", "true");
+	settings->setDefault("repeat_joystick_button_time", "0.17");
+	settings->setDefault("joystick_frustum_sensitivity", "170");
 	settings->setDefault("cinematic", "false");
 	settings->setDefault("camera_smoothing", "0");
 	settings->setDefault("cinematic_camera_smoothing", "0.7");
diff --git a/src/game.cpp b/src/game.cpp
index ac1c0fe6b..ba77d299a 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "client.h"
 #include "client/tile.h"     // For TextureSource
 #include "client/keys.h"
+#include "client/joystick_controller.h"
 #include "clientmap.h"
 #include "clouds.h"
 #include "config.h"
@@ -1108,12 +1109,14 @@ bool nodePlacementPrediction(Client &client,
 static inline void create_formspec_menu(GUIFormSpecMenu **cur_formspec,
 		InventoryManager *invmgr, IGameDef *gamedef,
 		IWritableTextureSource *tsrc, IrrlichtDevice *device,
+		JoystickController *joystick,
 		IFormSource *fs_src, TextDest *txt_dest, Client *client)
 {
 
 	if (*cur_formspec == 0) {
-		*cur_formspec = new GUIFormSpecMenu(device, guiroot, -1, &g_menumgr,
-						    invmgr, gamedef, tsrc, fs_src, txt_dest, client);
+		*cur_formspec = new GUIFormSpecMenu(device, joystick,
+			guiroot, -1, &g_menumgr, invmgr, gamedef, tsrc,
+			fs_src, txt_dest, client);
 		(*cur_formspec)->doPause = false;
 
 		/*
@@ -1138,7 +1141,8 @@ static inline void create_formspec_menu(GUIFormSpecMenu **cur_formspec,
 
 static void show_deathscreen(GUIFormSpecMenu **cur_formspec,
 		InventoryManager *invmgr, IGameDef *gamedef,
-		IWritableTextureSource *tsrc, IrrlichtDevice *device, Client *client)
+		IWritableTextureSource *tsrc, IrrlichtDevice *device,
+		JoystickController *joystick, Client *client)
 {
 	std::string formspec =
 		std::string(FORMSPEC_VERSION_STRING) +
@@ -1154,14 +1158,15 @@ static void show_deathscreen(GUIFormSpecMenu **cur_formspec,
 	FormspecFormSource *fs_src = new FormspecFormSource(formspec);
 	LocalFormspecHandler *txt_dst = new LocalFormspecHandler("MT_DEATH_SCREEN", client);
 
-	create_formspec_menu(cur_formspec, invmgr, gamedef, tsrc, device,  fs_src, txt_dst, NULL);
+	create_formspec_menu(cur_formspec, invmgr, gamedef, tsrc, device,
+		joystick, fs_src, txt_dst, NULL);
 }
 
 /******************************************************************************/
 static void show_pause_menu(GUIFormSpecMenu **cur_formspec,
 		InventoryManager *invmgr, IGameDef *gamedef,
 		IWritableTextureSource *tsrc, IrrlichtDevice *device,
-		bool singleplayermode)
+		JoystickController *joystick, bool singleplayermode)
 {
 #ifdef __ANDROID__
 	std::string control_text = strgettext("Default Controls:\n"
@@ -1226,7 +1231,8 @@ static void show_pause_menu(GUIFormSpecMenu **cur_formspec,
 	FormspecFormSource *fs_src = new FormspecFormSource(os.str());
 	LocalFormspecHandler *txt_dst = new LocalFormspecHandler("MT_PAUSE_MENU");
 
-	create_formspec_menu(cur_formspec, invmgr, gamedef, tsrc, device,  fs_src, txt_dst, NULL);
+	create_formspec_menu(cur_formspec, invmgr, gamedef, tsrc, device,
+		joystick, fs_src, txt_dst, NULL);
 	std::string con("btn_continue");
 	(*cur_formspec)->setFocus(con);
 	(*cur_formspec)->doPause = true;
@@ -1574,9 +1580,10 @@ class Game {
 	void decreaseViewRange(float *statustext_time);
 	void toggleFullViewRange(float *statustext_time);
 
-	void updateCameraDirection(CameraOrientation *cam, VolatileRunFlags *flags);
+	void updateCameraDirection(CameraOrientation *cam, VolatileRunFlags *flags,
+		float dtime);
 	void updateCameraOrientation(CameraOrientation *cam,
-			const VolatileRunFlags &flags);
+		const VolatileRunFlags &flags, float dtime);
 	void updatePlayerControl(const CameraOrientation &cam);
 	void step(f32 *dtime);
 	void processClientEvents(CameraOrientation *cam, float *damage_flash);
@@ -1611,34 +1618,39 @@ class Game {
 	static void settingChangedCallback(const std::string &setting_name, void *data);
 	void readSettings();
 
-	bool getLeftClicked()
+	inline bool getLeftClicked()
 	{
-		return input->getLeftClicked();
+		return input->getLeftClicked() ||
+			input->joystick.getWasKeyDown(KeyType::MOUSE_L);
 	}
-	bool getRightClicked()
+	inline bool getRightClicked()
 	{
-		return input->getRightClicked();
+		return input->getRightClicked() ||
+			input->joystick.getWasKeyDown(KeyType::MOUSE_R);
 	}
-	bool isLeftPressed()
+	inline bool isLeftPressed()
 	{
-		return input->getLeftState();
+		return input->getLeftState() ||
+			input->joystick.isKeyDown(KeyType::MOUSE_L);
 	}
-	bool isRightPressed()
+	inline bool isRightPressed()
 	{
-		return input->getRightState();
+		return input->getRightState() ||
+			input->joystick.isKeyDown(KeyType::MOUSE_R);
 	}
-	bool getLeftReleased()
+	inline bool getLeftReleased()
 	{
-		return input->getLeftReleased();
+		return input->getLeftReleased() ||
+			input->joystick.wasKeyReleased(KeyType::MOUSE_L);
 	}
 
-	bool isKeyDown(GameKeyType k)
+	inline bool isKeyDown(GameKeyType k)
 	{
-		return input->isKeyDown(keycache.key[k]);
+		return input->isKeyDown(keycache.key[k]) || input->joystick.isKeyDown(k);
 	}
-	bool wasKeyDown(GameKeyType k)
+	inline bool wasKeyDown(GameKeyType k)
 	{
-		return input->wasKeyDown(keycache.key[k]);
+		return input->wasKeyDown(keycache.key[k]) || input->joystick.wasKeyDown(k);
 	}
 
 #ifdef __ANDROID__
@@ -1724,9 +1736,11 @@ class Game {
 	 */
 	bool m_cache_doubletap_jump;
 	bool m_cache_enable_clouds;
+	bool m_cache_enable_joysticks;
 	bool m_cache_enable_particles;
 	bool m_cache_enable_fog;
 	f32  m_cache_mouse_sensitivity;
+	f32  m_cache_joystick_frustum_sensitivity;
 	f32  m_repeat_right_click_time;
 
 #ifdef __ANDROID__
@@ -1762,12 +1776,16 @@ Game::Game() :
 		&settingChangedCallback, this);
 	g_settings->registerChangedCallback("enable_clouds",
 		&settingChangedCallback, this);
+	g_settings->registerChangedCallback("doubletap_joysticks",
+		&settingChangedCallback, this);
 	g_settings->registerChangedCallback("enable_particles",
 		&settingChangedCallback, this);
 	g_settings->registerChangedCallback("enable_fog",
 		&settingChangedCallback, this);
 	g_settings->registerChangedCallback("mouse_sensitivity",
 		&settingChangedCallback, this);
+	g_settings->registerChangedCallback("joystick_frustum_sensitivity",
+		&settingChangedCallback, this);
 	g_settings->registerChangedCallback("repeat_rightclick_time",
 		&settingChangedCallback, this);
 
@@ -1930,7 +1948,7 @@ void Game::run()
 		updateProfilers(runData, stats, draw_times, dtime);
 		processUserInput(&flags, &runData, dtime);
 		// Update camera before player movement to avoid camera lag of one frame
-		updateCameraDirection(&cam_view_target, &flags);
+		updateCameraDirection(&cam_view_target, &flags, dtime);
 		float cam_smoothing = 0;
 		if (g_settings->getBool("cinematic"))
 			cam_smoothing = 1 - g_settings->getFloat("cinematic_camera_smoothing");
@@ -2727,7 +2745,8 @@ void Game::processKeyInput(VolatileRunFlags *flags,
 	} else if (wasKeyDown(KeyType::ESC) || input->wasKeyDown(CancelKey)) {
 		if (!gui_chat_console->isOpenInhibited()) {
 			show_pause_menu(&current_formspec, client, gamedef,
-					texture_src, device, simple_singleplayer_mode);
+				texture_src, device, &input->joystick,
+				simple_singleplayer_mode);
 		}
 	} else if (wasKeyDown(KeyType::CHAT)) {
 		openConsole(0.2, L"");
@@ -2813,12 +2832,21 @@ void Game::processItemSelection(u16 *new_playeritem)
 	u16 max_item = MYMIN(PLAYER_INVENTORY_SIZE - 1,
 		                 player->hud_hotbar_itemcount - 1);
 
-	if (wheel < 0)
+	s32 dir = wheel;
+
+	if (input->joystick.wasKeyDown(KeyType::SCROLL_DOWN)) {
+		dir = -1;
+	}
+
+	if (input->joystick.wasKeyDown(KeyType::SCROLL_UP)) {
+		dir = 1;
+	}
+
+	if (dir < 0)
 		*new_playeritem = *new_playeritem < max_item ? *new_playeritem + 1 : 0;
-	else if (wheel > 0)
+	else if (dir > 0)
 		*new_playeritem = *new_playeritem > 0 ? *new_playeritem - 1 : max_item;
-	// else wheel == 0
-
+	// else dir == 0
 
 	/* Item selection using keyboard
 	 */
@@ -2868,7 +2896,7 @@ void Game::openInventory()
 	TextDest *txt_dst = new TextDestPlayerInventory(client);
 
 	create_formspec_menu(&current_formspec, client, gamedef, texture_src,
-			device, fs_src, txt_dst, client);
+			device, &input->joystick, fs_src, txt_dst, client);
 
 	InventoryLocation inventoryloc;
 	inventoryloc.setCurrentPlayer();
@@ -3154,7 +3182,7 @@ void Game::toggleFullViewRange(float *statustext_time)
 
 
 void Game::updateCameraDirection(CameraOrientation *cam,
-		VolatileRunFlags *flags)
+		VolatileRunFlags *flags, float dtime)
 {
 	if ((device->isWindowActive() && noMenuActive()) || random_input) {
 
@@ -3169,7 +3197,7 @@ void Game::updateCameraDirection(CameraOrientation *cam,
 		if (flags->first_loop_after_window_activation)
 			flags->first_loop_after_window_activation = false;
 		else
-			updateCameraOrientation(cam, *flags);
+			updateCameraOrientation(cam, *flags, dtime);
 
 		input->setMousePos((driver->getScreenSize().Width / 2),
 				(driver->getScreenSize().Height / 2));
@@ -3187,9 +3215,8 @@ void Game::updateCameraDirection(CameraOrientation *cam,
 	}
 }
 
-
 void Game::updateCameraOrientation(CameraOrientation *cam,
-		const VolatileRunFlags &flags)
+		const VolatileRunFlags &flags, float dtime)
 {
 #ifdef HAVE_TOUCHSCREENGUI
 	if (g_touchscreengui) {
@@ -3197,6 +3224,7 @@ void Game::updateCameraOrientation(CameraOrientation *cam,
 		cam->camera_pitch = g_touchscreengui->getPitch();
 	} else {
 #endif
+
 		s32 dx = input->getMousePos().X - (driver->getScreenSize().Width / 2);
 		s32 dy = input->getMousePos().Y - (driver->getScreenSize().Height / 2);
 
@@ -3212,6 +3240,14 @@ void Game::updateCameraOrientation(CameraOrientation *cam,
 	}
 #endif
 
+	if (m_cache_enable_joysticks) {
+		f32 c = m_cache_joystick_frustum_sensitivity * (1.f / 32767.f) * dtime;
+		cam->camera_yaw -= input->joystick.getAxisWithoutDead(JA_FRUSTUM_HORIZONTAL) *
+			c;
+		cam->camera_pitch += input->joystick.getAxisWithoutDead(JA_FRUSTUM_VERTICAL) *
+			c;
+	}
+
 	cam->camera_pitch = rangelim(cam->camera_pitch, -89.5, 89.5);
 }
 
@@ -3220,30 +3256,36 @@ void Game::updatePlayerControl(const CameraOrientation &cam)
 {
 	//TimeTaker tt("update player control", NULL, PRECISION_NANO);
 
+	// DO NOT use the isKeyDown method for the forward, backward, left, right
+	// buttons, as the code that uses the controls needs to be able to
+	// distinguish between the two in order to know when to use joysticks.
+
 	PlayerControl control(
 		input->isKeyDown(keycache.key[KeyType::FORWARD]),
 		input->isKeyDown(keycache.key[KeyType::BACKWARD]),
 		input->isKeyDown(keycache.key[KeyType::LEFT]),
 		input->isKeyDown(keycache.key[KeyType::RIGHT]),
-		input->isKeyDown(keycache.key[KeyType::JUMP]),
-		input->isKeyDown(keycache.key[KeyType::SPECIAL1]),
-		input->isKeyDown(keycache.key[KeyType::SNEAK]),
-		input->getLeftState(),
-		input->getRightState(),
+		isKeyDown(KeyType::JUMP),
+		isKeyDown(KeyType::SPECIAL1),
+		isKeyDown(KeyType::SNEAK),
+		isLeftPressed(),
+		isRightPressed(),
 		cam.camera_pitch,
-		cam.camera_yaw
+		cam.camera_yaw,
+		input->joystick.getAxisWithoutDead(JA_SIDEWARD_MOVE),
+		input->joystick.getAxisWithoutDead(JA_FORWARD_MOVE)
 	);
 
 	u32 keypress_bits =
-			( (u32)(input->isKeyDown(keycache.key[KeyType::FORWARD])  & 0x1) << 0) |
-			( (u32)(input->isKeyDown(keycache.key[KeyType::BACKWARD]) & 0x1) << 1) |
-			( (u32)(input->isKeyDown(keycache.key[KeyType::LEFT])     & 0x1) << 2) |
-			( (u32)(input->isKeyDown(keycache.key[KeyType::RIGHT])    & 0x1) << 3) |
-			( (u32)(input->isKeyDown(keycache.key[KeyType::JUMP])     & 0x1) << 4) |
-			( (u32)(input->isKeyDown(keycache.key[KeyType::SPECIAL1]) & 0x1) << 5) |
-			( (u32)(input->isKeyDown(keycache.key[KeyType::SNEAK])    & 0x1) << 6) |
-			( (u32)(input->getLeftState()                                        & 0x1) << 7) |
-			( (u32)(input->getRightState()                                       & 0x1) << 8
+			( (u32)(isKeyDown(KeyType::FORWARD)                       & 0x1) << 0) |
+			( (u32)(isKeyDown(KeyType::BACKWARD)                      & 0x1) << 1) |
+			( (u32)(isKeyDown(KeyType::LEFT)                          & 0x1) << 2) |
+			( (u32)(isKeyDown(KeyType::RIGHT)                         & 0x1) << 3) |
+			( (u32)(isKeyDown(KeyType::JUMP)                          & 0x1) << 4) |
+			( (u32)(isKeyDown(KeyType::SPECIAL1)                      & 0x1) << 5) |
+			( (u32)(isKeyDown(KeyType::SNEAK)                         & 0x1) << 6) |
+			( (u32)(isLeftPressed()                                   & 0x1) << 7) |
+			( (u32)(isRightPressed()                                  & 0x1) << 8
 		);
 
 #ifdef ANDROID
@@ -3312,7 +3354,7 @@ void Game::processClientEvents(CameraOrientation *cam, float *damage_flash)
 			cam->camera_pitch = event.player_force_move.pitch;
 		} else if (event.type == CE_DEATHSCREEN) {
 			show_deathscreen(&current_formspec, client, gamedef, texture_src,
-					 device, client);
+				device, &input->joystick, client);
 
 			chat_backend->addMessage(L"", L"You died.");
 
@@ -3328,7 +3370,8 @@ void Game::processClientEvents(CameraOrientation *cam, float *damage_flash)
 				new TextDestPlayerInventory(client, *(event.show_formspec.formname));
 
 			create_formspec_menu(&current_formspec, client, gamedef,
-					     texture_src, device, fs_src, txt_dst, client);
+				texture_src, device, &input->joystick,
+				fs_src, txt_dst, client);
 
 			delete(event.show_formspec.formspec);
 			delete(event.show_formspec.formname);
@@ -3723,8 +3766,14 @@ void Game::processPlayerInteraction(GameRunData *runData,
 	input->resetLeftClicked();
 	input->resetRightClicked();
 
+	input->joystick.clearWasKeyDown(KeyType::MOUSE_L);
+	input->joystick.clearWasKeyDown(KeyType::MOUSE_R);
+
 	input->resetLeftReleased();
 	input->resetRightReleased();
+
+	input->joystick.clearWasKeyReleased(KeyType::MOUSE_L);
+	input->joystick.clearWasKeyReleased(KeyType::MOUSE_R);
 }
 
 
@@ -3785,7 +3834,7 @@ void Game::handlePointingAtNode(GameRunData *runData,
 			TextDest *txt_dst = new TextDestNodeMetadata(nodepos, client);
 
 			create_formspec_menu(&current_formspec, client, gamedef,
-					     texture_src, device, fs_src, txt_dst, client);
+				texture_src, device, &input->joystick, fs_src, txt_dst, client);
 
 			current_formspec->setFormSpec(meta->getString("formspec"), inventoryloc);
 		} else {
@@ -4468,12 +4517,14 @@ void Game::settingChangedCallback(const std::string &setting_name, void *data)
 
 void Game::readSettings()
 {
-	m_cache_doubletap_jump            = g_settings->getBool("doubletap_jump");
-	m_cache_enable_clouds             = g_settings->getBool("enable_clouds");
-	m_cache_enable_particles          = g_settings->getBool("enable_particles");
-	m_cache_enable_fog                = g_settings->getBool("enable_fog");
-	m_cache_mouse_sensitivity         = g_settings->getFloat("mouse_sensitivity");
-	m_repeat_right_click_time         = g_settings->getFloat("repeat_rightclick_time");
+	m_cache_doubletap_jump               = g_settings->getBool("doubletap_jump");
+	m_cache_enable_clouds                = g_settings->getBool("enable_clouds");
+	m_cache_enable_joysticks             = g_settings->getBool("enable_joysticks");
+	m_cache_enable_particles             = g_settings->getBool("enable_particles");
+	m_cache_enable_fog                   = g_settings->getBool("enable_fog");
+	m_cache_mouse_sensitivity            = g_settings->getFloat("mouse_sensitivity");
+	m_cache_joystick_frustum_sensitivity = g_settings->getFloat("joystick_frustum_sensitivity");
+	m_repeat_right_click_time            = g_settings->getFloat("repeat_rightclick_time");
 
 	m_cache_mouse_sensitivity = rangelim(m_cache_mouse_sensitivity, 0.001, 100.0);
 }
diff --git a/src/game.h b/src/game.h
index d8c2f78fc..df32e3397 100644
--- a/src/game.h
+++ b/src/game.h
@@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "irrlichttypes_extrabloated.h"
 #include <string>
 #include "client/keys.h"
+#include "client/joystick_controller.h"
 #include "keycode.h"
 #include <list>
 
@@ -135,6 +136,8 @@ class InputHandler
 	virtual void step(float dtime) {}
 
 	virtual void clear() {}
+
+	JoystickController joystick;
 };
 
 class ChatBackend;  /* to avoid having to include chat.h */
diff --git a/src/guiEngine.cpp b/src/guiEngine.cpp
index 96de7a4f7..b9d796ccb 100644
--- a/src/guiEngine.cpp
+++ b/src/guiEngine.cpp
@@ -131,6 +131,7 @@ void MenuMusicFetcher::fetchSounds(const std::string &name,
 /** GUIEngine                                                                 */
 /******************************************************************************/
 GUIEngine::GUIEngine(	irr::IrrlichtDevice* dev,
+						JoystickController *joystick,
 						gui::IGUIElement* parent,
 						IMenuManager *menumgr,
 						scene::ISceneManager* smgr,
@@ -189,6 +190,7 @@ GUIEngine::GUIEngine(	irr::IrrlichtDevice* dev,
 
 	/* Create menu */
 	m_menu = new GUIFormSpecMenu(m_device,
+			joystick,
 			m_parent,
 			-1,
 			m_menumanager,
diff --git a/src/guiEngine.h b/src/guiEngine.h
index fa98a21e4..a59436953 100644
--- a/src/guiEngine.h
+++ b/src/guiEngine.h
@@ -149,7 +149,8 @@ class GUIEngine {
 	 * @param smgr scene manager to add scene elements to
 	 * @param data struct to transfer data to main game handling
 	 */
-	GUIEngine(	irr::IrrlichtDevice* dev,
+	GUIEngine(irr::IrrlichtDevice* dev,
+			JoystickController *joystick,
 			gui::IGUIElement* parent,
 			IMenuManager *menumgr,
 			scene::ISceneManager* smgr,
diff --git a/src/guiFormSpecMenu.cpp b/src/guiFormSpecMenu.cpp
index cf01dc38c..a9cbb6254 100644
--- a/src/guiFormSpecMenu.cpp
+++ b/src/guiFormSpecMenu.cpp
@@ -79,6 +79,7 @@ static unsigned int font_line_height(gui::IGUIFont *font)
 }
 
 GUIFormSpecMenu::GUIFormSpecMenu(irr::IrrlichtDevice* dev,
+		JoystickController *joystick,
 		gui::IGUIElement* parent, s32 id, IMenuManager *menumgr,
 		InventoryManager *invmgr, IGameDef *gamedef,
 		ISimpleTextureSource *tsrc, IFormSource* fsrc, TextDest* tdst,
@@ -102,6 +103,7 @@ GUIFormSpecMenu::GUIFormSpecMenu(irr::IrrlichtDevice* dev,
 	m_text_dst(tdst),
 	m_formspec_version(0),
 	m_focused_element(""),
+	m_joystick(joystick),
 	m_font(NULL),
 	m_remap_dbl_click(remap_dbl_click)
 #ifdef __ANDROID__
@@ -2459,7 +2461,7 @@ void GUIFormSpecMenu::drawMenu()
 		Draw static text elements
 	*/
 	for (u32 i = 0; i < m_static_texts.size(); i++) {
-		const StaticTextSpec &spec = m_static_texts[i];	
+		const StaticTextSpec &spec = m_static_texts[i];
 		core::rect<s32> rect = spec.rect;
 		if (spec.parent_button && spec.parent_button->isPressed()) {
 #if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
@@ -3024,6 +3026,25 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
 	}
 	#endif
 
+	if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) {
+		/* TODO add a check like:
+		if (event.JoystickEvent != joystick_we_listen_for)
+			return false;
+		*/
+		bool handled = m_joystick->handleEvent(event.JoystickEvent);
+		if (handled) {
+			if (m_joystick->wasKeyDown(KeyType::ESC)) {
+				tryClose();
+			} else if (m_joystick->wasKeyDown(KeyType::JUMP)) {
+				if (m_allowclose) {
+					acceptInput(quit_mode_accept);
+					quitMenu();
+				}
+			}
+		}
+		return handled;
+	}
+
 	return false;
 }
 
@@ -3085,19 +3106,24 @@ bool GUIFormSpecMenu::DoubleClickDetection(const SEvent event)
 	return false;
 }
 
+void GUIFormSpecMenu::tryClose()
+{
+	if (m_allowclose) {
+		doPause = false;
+		acceptInput(quit_mode_cancel);
+		quitMenu();
+	} else {
+		m_text_dst->gotText(L"MenuQuit");
+	}
+}
+
 bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 {
 	if (event.EventType==EET_KEY_INPUT_EVENT) {
 		KeyPress kp(event.KeyInput);
 		if (event.KeyInput.PressedDown && ( (kp == EscapeKey) ||
 				(kp == getKeySetting("keymap_inventory")) || (kp == CancelKey))) {
-			if (m_allowclose) {
-				doPause = false;
-				acceptInput(quit_mode_cancel);
-				quitMenu();
-			} else {
-				m_text_dst->gotText(L"MenuQuit");
-			}
+			tryClose();
 			return true;
 		} else if (m_client != NULL && event.KeyInput.PressedDown &&
 				(kp == getKeySetting("keymap_screenshot"))) {
diff --git a/src/guiFormSpecMenu.h b/src/guiFormSpecMenu.h
index 4122b1f56..2fb55070d 100644
--- a/src/guiFormSpecMenu.h
+++ b/src/guiFormSpecMenu.h
@@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "modalMenu.h"
 #include "guiTable.h"
 #include "network/networkprotocol.h"
+#include "client/joystick_controller.h"
 #include "util/string.h"
 #include "util/enriched_string.h"
 
@@ -278,6 +279,7 @@ class GUIFormSpecMenu : public GUIModalMenu
 
 public:
 	GUIFormSpecMenu(irr::IrrlichtDevice* dev,
+			JoystickController *joystick,
 			gui::IGUIElement* parent, s32 id,
 			IMenuManager *menumgr,
 			InventoryManager *invmgr,
@@ -433,10 +435,11 @@ class GUIFormSpecMenu : public GUIModalMenu
 	video::SColor m_default_tooltip_color;
 
 private:
-	IFormSource      *m_form_src;
-	TextDest         *m_text_dst;
-	unsigned int      m_formspec_version;
-	std::string       m_focused_element;
+	IFormSource        *m_form_src;
+	TextDest           *m_text_dst;
+	unsigned int        m_formspec_version;
+	std::string         m_focused_element;
+	JoystickController *m_joystick;
 
 	typedef struct {
 		bool explicit_size;
@@ -494,6 +497,8 @@ class GUIFormSpecMenu : public GUIModalMenu
 	bool parseSizeDirect(parserData* data, std::string element);
 	void parseScrollBar(parserData* data, std::string element);
 
+	void tryClose();
+
 	/**
 	 * check if event is part of a double click
 	 * @param event event to evaluate
diff --git a/src/localplayer.cpp b/src/localplayer.cpp
index 507f31980..732ca8acf 100644
--- a/src/localplayer.cpp
+++ b/src/localplayer.cpp
@@ -528,18 +528,23 @@ void LocalPlayer::applyControl(float dtime)
 			speedH += move_direction;
 		}
 	}
-	if(control.down)
-	{
+	if (control.down) {
 		speedH -= move_direction;
 	}
-	if(control.left)
-	{
+	if (!control.up && !control.down) {
+		speedH -= move_direction *
+			(control.forw_move_joystick_axis / 32767.f);
+	}
+	if (control.left) {
 		speedH += move_direction.crossProduct(v3f(0,1,0));
 	}
-	if(control.right)
-	{
+	if (control.right) {
 		speedH += move_direction.crossProduct(v3f(0,-1,0));
 	}
+	if (!control.left && !control.right) {
+		speedH -= move_direction.crossProduct(v3f(0,1,0)) *
+			(control.sidew_move_joystick_axis / 32767.f);
+	}
 	if(control.jump)
 	{
 		if (free_move) {
diff --git a/src/player.h b/src/player.h
index b317cda4f..6687ca86e 100644
--- a/src/player.h
+++ b/src/player.h
@@ -46,6 +46,8 @@ struct PlayerControl
 		RMB = false;
 		pitch = 0;
 		yaw = 0;
+		sidew_move_joystick_axis = .0f;
+		forw_move_joystick_axis = .0f;
 	}
 	PlayerControl(
 		bool a_up,
@@ -58,7 +60,9 @@ struct PlayerControl
 		bool a_LMB,
 		bool a_RMB,
 		float a_pitch,
-		float a_yaw
+		float a_yaw,
+		float a_sidew_move_joystick_axis,
+		float a_forw_move_joystick_axis
 	)
 	{
 		up = a_up;
@@ -72,6 +76,8 @@ struct PlayerControl
 		RMB = a_RMB;
 		pitch = a_pitch;
 		yaw = a_yaw;
+		sidew_move_joystick_axis = a_sidew_move_joystick_axis;
+		forw_move_joystick_axis = a_forw_move_joystick_axis;
 	}
 	bool up;
 	bool down;
@@ -84,6 +90,8 @@ struct PlayerControl
 	bool RMB;
 	float pitch;
 	float yaw;
+	float sidew_move_joystick_axis;
+	float forw_move_joystick_axis;
 };
 
 class Map;
-- 
GitLab