From 46f7fa406f79864ea4283d771e094bfaa00e5aec Mon Sep 17 00:00:00 2001
From: Jaime Ita Passos <jp6781615@gmail.com>
Date: Mon, 14 Nov 2022 00:37:08 -0300
Subject: [PATCH] Refactor gamepad code Implements the SDL Game Controller API,
 haptics, and axis bindings.

---
 src/Sourcefile      |    1 +
 src/command.c       |   81 ++-
 src/console.c       |    9 +-
 src/d_clisrv.c      |   14 +-
 src/d_event.h       |   14 +-
 src/d_main.c        |    8 +
 src/d_netcmd.c      |  140 +++--
 src/d_netcmd.h      |   12 +-
 src/deh_tables.c    |   10 +-
 src/doomdef.h       |    5 +-
 src/f_finale.c      |   76 +--
 src/g_demo.c        |    6 +-
 src/g_game.c        |  707 +++++++++++++----------
 src/g_game.h        |   24 +-
 src/g_input.c       | 1341 +++++++++++++++++++++++++++++++------------
 src/g_input.h       |  216 ++++++-
 src/hu_stuff.c      |   22 +-
 src/i_gamepad.h     |   58 ++
 src/i_joy.h         |   58 --
 src/i_system.h      |   94 +--
 src/lua_inputlib.c  |    9 +-
 src/m_cheat.c       |   32 +-
 src/m_menu.c        |  768 ++++++++++++++-----------
 src/m_menu.h        |   28 +-
 src/p_haptic.c      |  115 ++++
 src/p_haptic.h      |   27 +
 src/p_inter.c       |   70 +--
 src/p_local.h       |   15 -
 src/p_user.c        |    6 +-
 src/sdl/Sourcefile  |    1 +
 src/sdl/i_gamepad.c |  921 +++++++++++++++++++++++++++++
 src/sdl/i_system.c  |  767 +------------------------
 src/sdl/i_video.c   |  446 ++------------
 src/sdl/sdlmain.h   |   72 +--
 34 files changed, 3564 insertions(+), 2609 deletions(-)
 create mode 100644 src/i_gamepad.h
 delete mode 100644 src/i_joy.h
 create mode 100644 src/p_haptic.c
 create mode 100644 src/p_haptic.h
 create mode 100644 src/sdl/i_gamepad.c

diff --git a/src/Sourcefile b/src/Sourcefile
index de90bb609..9de90eee4 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -51,6 +51,7 @@ p_spec.c
 p_telept.c
 p_tick.c
 p_user.c
+p_haptic.c
 p_slopes.c
 tables.c
 r_bsp.c
diff --git a/src/command.c b/src/command.c
index c849341ff..9be081fb7 100644
--- a/src/command.c
+++ b/src/command.c
@@ -77,7 +77,6 @@ CV_PossibleValue_t CV_Natural[] = {{1, "MIN"}, {999999999, "MAX"}, {0, NULL}};
 
 // Filter consvars by EXECVERSION
 // First implementation is 26 (2.1.21), so earlier configs default at 25 (2.1.20)
-// Also set CV_HIDEN during runtime, after config is loaded
 static boolean execversion_enabled = false;
 consvar_t cv_execversion = CVAR_INIT ("execversion","25",CV_CALL,CV_Unsigned, CV_EnforceExecVersion);
 
@@ -2230,12 +2229,12 @@ static boolean CV_FilterJoyAxisVars(consvar_t *v, const char *valstr)
 		// reset all axis settings to defaults
 		if (joyaxis_count == 6)
 		{
-			COM_BufInsertText(va("%s \"%s\"\n", cv_turnaxis.name, cv_turnaxis.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_moveaxis.name, cv_moveaxis.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_sideaxis.name, cv_sideaxis.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_lookaxis.name, cv_lookaxis.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_fireaxis.name, cv_fireaxis.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_firenaxis.name, cv_firenaxis.defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_turnaxis[0].name, cv_turnaxis[0].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_moveaxis[0].name, cv_moveaxis[0].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_sideaxis[0].name, cv_sideaxis[0].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_lookaxis[0].name, cv_lookaxis[0].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_fireaxis[0].name, cv_fireaxis[0].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_firenaxis[0].name, cv_firenaxis[0].defaultvalue));
 			joyaxis_count++;
 			return false;
 		}
@@ -2289,12 +2288,12 @@ static boolean CV_FilterJoyAxisVars(consvar_t *v, const char *valstr)
 		// reset all axis settings to defaults
 		if (joyaxis2_count == 6)
 		{
-			COM_BufInsertText(va("%s \"%s\"\n", cv_turnaxis2.name, cv_turnaxis2.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_moveaxis2.name, cv_moveaxis2.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_sideaxis2.name, cv_sideaxis2.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_lookaxis2.name, cv_lookaxis2.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_fireaxis2.name, cv_fireaxis2.defaultvalue));
-			COM_BufInsertText(va("%s \"%s\"\n", cv_firenaxis2.name, cv_firenaxis2.defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_turnaxis[1].name, cv_turnaxis[1].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_moveaxis[1].name, cv_moveaxis[1].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_sideaxis[1].name, cv_sideaxis[1].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_lookaxis[1].name, cv_lookaxis[1].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_fireaxis[1].name, cv_fireaxis[1].defaultvalue));
+			COM_BufInsertText(va("%s \"%s\"\n", cv_firenaxis[1].name, cv_firenaxis[1].defaultvalue));
 			joyaxis2_count++;
 			return false;
 		}
@@ -2304,6 +2303,49 @@ static boolean CV_FilterJoyAxisVars(consvar_t *v, const char *valstr)
 	return true;
 }
 
+#ifndef OLD_GAMEPAD_AXES
+static boolean CV_ConvertOldJoyAxisVars(consvar_t *v, const char *valstr)
+{
+	static struct {
+		const char *old;
+		const char *new;
+	} axis_names[] = {
+		{"X-Axis",    "Left Stick X"},
+		{"Y-Axis",    "Left Stick Y"},
+		{"X-Axis-",   "Left Stick X-"},
+		{"Y-Axis-",   "Left Stick Y-"},
+		{"X-Rudder",  "Right Stick X"},
+		{"Y-Rudder",  "Right Stick Y"},
+		{"X-Rudder-", "Right Stick X-"},
+		{"Y-Rudder-", "Right Stick Y-"},
+		{"Z-Axis",    "Left Trigger"},
+		{"Z-Rudder",  "Right Trigger"},
+		{"Z-Axis-",   "Left Trigger"},
+		{"Z-Rudder-", "Right Trigger"},
+		{NULL, NULL}
+	};
+
+	if (v->PossibleValue != joyaxis_cons_t)
+		return true;
+
+	for (unsigned i = 0;; i++)
+	{
+		if (axis_names[i].old == NULL)
+		{
+			CV_SetCVar(v, "None", false);
+			return false;
+		}
+		else if (!stricmp(valstr, axis_names[i].old))
+		{
+			CV_SetCVar(v, axis_names[i].new, false);
+			return false;
+		}
+	}
+
+	return true;
+}
+#endif
+
 static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr)
 {
 	// True means allow the CV change, False means block it
@@ -2332,8 +2374,8 @@ static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr)
 			&& atoi(valstr) == 35)
 			return false;
 
-		// JOYSTICK DEFAULTS
-		// use_joystick was changed from 0 to 1 to automatically use a joystick if available
+		// GAMEPAD DEFAULTS
+		// use_gamepad was changed from 0 to 1 to automatically use a gamepad if available
 #if defined(HAVE_SDL) || defined(_WINDOWS)
 		if ((!stricmp(v->name, "use_joystick")
 			|| !stricmp(v->name, "use_joystick2"))
@@ -2346,6 +2388,15 @@ static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr)
 		if (!CV_FilterJoyAxisVars(v, valstr))
 			return false;
 	}
+
+#ifndef OLD_GAMEPAD_AXES
+	if (GETMAJOREXECVERSION(cv_execversion.value) <= 51 && GETMINOREXECVERSION(cv_execversion.value) < 1)
+	{
+		if (!CV_ConvertOldJoyAxisVars(v, valstr))
+			return false;
+	}
+#endif
+
 	return true;
 }
 
diff --git a/src/console.c b/src/console.c
index 40fb43121..7cad4aee5 100644
--- a/src/console.c
+++ b/src/console.c
@@ -918,7 +918,8 @@ boolean CON_Responder(event_t *ev)
 	static INT32 alias_skips;
 
 	const char *cmd = NULL;
-	INT32 key;
+	INT32 key = ev->key;
+	boolean key_is_console = (key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1]);
 
 	if (chat_on)
 		return false;
@@ -926,20 +927,18 @@ boolean CON_Responder(event_t *ev)
 	// let go keyup events, don't eat them
 	if (ev->type != ev_keydown && ev->type != ev_console)
 	{
-		if (ev->key == gamecontrol[GC_CONSOLE][0] || ev->key == gamecontrol[GC_CONSOLE][1])
+		if (key_is_console)
 			consdown = false;
 		return false;
 	}
 
-	key = ev->key;
-
 	// check for console toggle key
 	if (ev->type != ev_console)
 	{
 		if (modeattacking || metalrecording || marathonmode)
 			return false;
 
-		if (key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1])
+		if (key_is_console)
 		{
 			if (consdown) // ignore repeat
 				return true;
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 4cd6333c5..f66b29d7b 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -25,7 +25,8 @@
 #include "st_stuff.h"
 #include "hu_stuff.h"
 #include "keys.h"
-#include "g_input.h" // JOY1
+#include "g_input.h"
+#include "i_gamepad.h"
 #include "m_menu.h"
 #include "console.h"
 #include "d_netfil.h"
@@ -33,6 +34,7 @@
 #include "p_saveg.h"
 #include "z_zone.h"
 #include "p_local.h"
+#include "p_haptic.h"
 #include "m_misc.h"
 #include "am_map.h"
 #include "m_random.h"
@@ -678,14 +680,14 @@ static void Snake_Handle(void)
 	UINT16 i;
 
 	// Handle retry
-	if (snake->gameover && (PLAYER1INPUTDOWN(GC_JUMP) || gamekeydown[KEY_ENTER]))
+	if (snake->gameover && (G_PlayerInputDown(0, GC_JUMP) || gamekeydown[KEY_ENTER]))
 	{
 		Snake_Initialise();
 		snake->pausepressed = true; // Avoid accidental pause on respawn
 	}
 
 	// Handle pause
-	if (PLAYER1INPUTDOWN(GC_PAUSE) || gamekeydown[KEY_ENTER])
+	if (G_PlayerInputDown(0, GC_PAUSE) || gamekeydown[KEY_ENTER])
 	{
 		if (!snake->pausepressed)
 			snake->paused = !snake->paused;
@@ -1646,6 +1648,8 @@ static void CL_LoadReceivedSavegame(boolean reloading)
 	titledemo = false;
 	automapactive = false;
 
+	P_StopRumble(NULL);
+
 	// load a base level
 	if (P_LoadNetGame(reloading))
 	{
@@ -2383,7 +2387,7 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 				G_MapEventsToControls(&events[eventtail]);
 		}
 
-		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
+		if (gamekeydown[KEY_ESCAPE] || gamepads[0].buttons[GAMEPAD_BUTTON_B] || cl_mode == CL_ABORTED)
 		{
 			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
 			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
@@ -5166,7 +5170,7 @@ static void Local_Maketic(INT32 realtics)
 	                   // game responder calls HU_Responder, AM_Responder,
 	                   // and G_MapEventsToControls
 	if (!dedicated) rendergametic = gametic;
-	// translate inputs (keyboard/mouse/joystick) into game controls
+	// translate inputs (keyboard/mouse/gamepad) into game controls
 	G_BuildTiccmd(&localcmds, realtics, 1);
 	if (splitscreen || botingame)
 		G_BuildTiccmd(&localcmds2, realtics, 2);
diff --git a/src/d_event.h b/src/d_event.h
index c0b9cef77..9448b9c5a 100644
--- a/src/d_event.h
+++ b/src/d_event.h
@@ -24,19 +24,21 @@ typedef enum
 	ev_keyup,
 	ev_console,
 	ev_mouse,
-	ev_joystick,
 	ev_mouse2,
-	ev_joystick2,
+	ev_gamepad_up,
+	ev_gamepad_down,
+	ev_gamepad_axis
 } evtype_t;
 
 // Event structure.
 typedef struct
 {
 	evtype_t type;
-	INT32 key; // keys/mouse/joystick buttons
-	INT32 x; // mouse/joystick x move
-	INT32 y; // mouse/joystick y move
-	boolean repeated; // key repeat
+	INT32 key; // key, mouse button, or gamepad button/axis type
+	INT32 x; // mouse x move, or gamepad axis value
+	INT32 y; // mouse y move
+	UINT8 which; // which gamepad or mouse ID
+	boolean repeated; // is the event repeated?
 } event_t;
 
 //
diff --git a/src/d_main.c b/src/d_main.c
index 3566e7f3d..1af8d090c 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -43,6 +43,7 @@
 #include "i_time.h"
 #include "i_threads.h"
 #include "i_video.h"
+#include "i_gamepad.h"
 #include "m_argv.h"
 #include "m_menu.h"
 #include "m_misc.h"
@@ -986,6 +987,7 @@ void D_StartTitle(void)
 	G_SetGametype(GT_COOP);
 	paused = false;
 	advancedemo = false;
+	P_StopRumble(NULL);
 	F_InitMenuPresValues();
 	F_StartTitleScreen();
 
@@ -1396,6 +1398,9 @@ void D_SRB2Main(void)
 	CONS_Printf("I_InitializeTime()...\n");
 	I_InitializeTime();
 
+	// Initializes the game logic side of gamepads
+	G_InitGamepads();
+
 	// Make backups of some SOCcable tables.
 	P_BackupTables();
 
@@ -1451,6 +1456,9 @@ void D_SRB2Main(void)
 
 	D_RegisterServerCommands();
 	D_RegisterClientCommands(); // be sure that this is called before D_CheckNetGame
+
+	I_InitGamepads();
+
 	R_RegisterEngineStuff();
 	S_RegisterSoundStuff();
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 4e90db0dc..c56e6e269 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -21,6 +21,7 @@
 #include "g_game.h"
 #include "hu_stuff.h"
 #include "g_input.h"
+#include "i_gamepad.h"
 #include "m_menu.h"
 #include "r_local.h"
 #include "r_skins.h"
@@ -181,14 +182,6 @@ static CV_PossibleValue_t mouse2port_cons_t[] = {{1, "COM1"}, {2, "COM2"}, {3, "
 	{0, NULL}};
 #endif
 
-#ifdef LJOYSTICK
-static CV_PossibleValue_t joyport_cons_t[] = {{1, "/dev/js0"}, {2, "/dev/js1"}, {3, "/dev/js2"},
-	{4, "/dev/js3"}, {0, NULL}};
-#else
-// accept whatever value - it is in fact the joystick device number
-#define usejoystick_cons_t NULL
-#endif
-
 static CV_PossibleValue_t teamscramble_cons_t[] = {{0, "Off"}, {1, "Random"}, {2, "Points"}, {0, NULL}};
 
 static CV_PossibleValue_t startingliveslimit_cons_t[] = {{1, "MIN"}, {99, "MAX"}, {0, NULL}};
@@ -247,19 +240,61 @@ INT32 cv_debug;
 consvar_t cv_usemouse = CVAR_INIT ("use_mouse", "On", CV_SAVE|CV_CALL,usemouse_cons_t, I_StartupMouse);
 consvar_t cv_usemouse2 = CVAR_INIT ("use_mouse2", "Off", CV_SAVE|CV_CALL,usemouse_cons_t, I_StartupMouse2);
 
-consvar_t cv_usejoystick = CVAR_INIT ("use_gamepad", "1", CV_SAVE|CV_CALL, usejoystick_cons_t, I_InitJoystick);
-consvar_t cv_usejoystick2 = CVAR_INIT ("use_gamepad2", "2", CV_SAVE|CV_CALL, usejoystick_cons_t, I_InitJoystick2);
-#if (defined (LJOYSTICK) || defined (HAVE_SDL))
-#ifdef LJOYSTICK
-consvar_t cv_joyport = CVAR_INIT ("padport", "/dev/js0", CV_SAVE, joyport_cons_t, NULL);
-consvar_t cv_joyport2 = CVAR_INIT ("padport2", "/dev/js0", CV_SAVE, joyport_cons_t, NULL); //Alam: for later
-#endif
-consvar_t cv_joyscale = CVAR_INIT ("padscale", "1", CV_SAVE|CV_CALL, NULL, I_JoyScale);
-consvar_t cv_joyscale2 = CVAR_INIT ("padscale2", "1", CV_SAVE|CV_CALL, NULL, I_JoyScale2);
-#else
-consvar_t cv_joyscale = CVAR_INIT ("padscale", "1", CV_SAVE|CV_HIDEN, NULL, NULL); //Alam: Dummy for save
-consvar_t cv_joyscale2 = CVAR_INIT ("padscale2", "1", CV_SAVE|CV_HIDEN, NULL, NULL); //Alam: Dummy for save
-#endif
+// We use cv_usegamepad.string as the USER-SET var
+// and cv_usegamepad.value as the INTERNAL var
+//
+// In practice, if cv_usegamepad.string == 0, this overrides
+// cv_usegamepad.value and always disables
+
+static void UseGamepad_OnChange(void)
+{
+	I_ChangeGamepad(0);
+}
+
+static void UseGamepad2_OnChange(void)
+{
+	I_ChangeGamepad(1);
+}
+
+consvar_t cv_usegamepad[2] = {
+	CVAR_INIT ("use_gamepad", "1", CV_SAVE|CV_CALL, NULL, UseGamepad_OnChange),
+	CVAR_INIT ("use_gamepad2", "2", CV_SAVE|CV_CALL, NULL, UseGamepad2_OnChange)
+};
+
+static void PadScale_OnChange(void)
+{
+	I_SetGamepadDigital(0, cv_gamepad_scale[0].value == 0);
+}
+
+static void PadScale2_OnChange(void)
+{
+	I_SetGamepadDigital(1, cv_gamepad_scale[1].value == 0);
+}
+
+consvar_t cv_gamepad_scale[2] = {
+	CVAR_INIT ("padscale", "1", CV_SAVE|CV_CALL, NULL, PadScale_OnChange),
+	CVAR_INIT ("padscale2", "1", CV_SAVE|CV_CALL, NULL, PadScale2_OnChange)
+};
+
+static void PadRumble_OnChange(void)
+{
+	if (!cv_gamepad_rumble[0].value)
+		I_StopGamepadRumble(0);
+}
+
+static void PadRumble2_OnChange(void)
+{
+	if (!cv_gamepad_rumble[1].value)
+		I_StopGamepadRumble(1);
+}
+
+consvar_t cv_gamepad_rumble[2] = {
+	CVAR_INIT ("padrumble", "Off", CV_SAVE|CV_CALL, NULL, PadRumble_OnChange),
+	CVAR_INIT ("padrumble2", "Off", CV_SAVE|CV_CALL, NULL, PadRumble2_OnChange)
+};
+
+consvar_t cv_gamepad_autopause = CVAR_INIT ("pauseongamepaddisconnect", "On", CV_SAVE, CV_OnOff, NULL);
+
 #if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 consvar_t cv_mouse2port = CVAR_INIT ("mouse2port", "/dev/gpmdata", CV_SAVE, mouse2port_cons_t, NULL);
 consvar_t cv_mouse2opt = CVAR_INIT ("mouse2opt", "0", CV_SAVE, NULL, NULL);
@@ -771,26 +806,26 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_pauseifunfocused);
 
 	// g_input.c
-	CV_RegisterVar(&cv_sideaxis);
-	CV_RegisterVar(&cv_sideaxis2);
-	CV_RegisterVar(&cv_turnaxis);
-	CV_RegisterVar(&cv_turnaxis2);
-	CV_RegisterVar(&cv_moveaxis);
-	CV_RegisterVar(&cv_moveaxis2);
-	CV_RegisterVar(&cv_lookaxis);
-	CV_RegisterVar(&cv_lookaxis2);
-	CV_RegisterVar(&cv_jumpaxis);
-	CV_RegisterVar(&cv_jumpaxis2);
-	CV_RegisterVar(&cv_spinaxis);
-	CV_RegisterVar(&cv_spinaxis2);
-	CV_RegisterVar(&cv_fireaxis);
-	CV_RegisterVar(&cv_fireaxis2);
-	CV_RegisterVar(&cv_firenaxis);
-	CV_RegisterVar(&cv_firenaxis2);
-	CV_RegisterVar(&cv_deadzone);
-	CV_RegisterVar(&cv_deadzone2);
-	CV_RegisterVar(&cv_digitaldeadzone);
-	CV_RegisterVar(&cv_digitaldeadzone2);
+	CV_RegisterVar(&cv_sideaxis[0]);
+	CV_RegisterVar(&cv_sideaxis[1]);
+	CV_RegisterVar(&cv_turnaxis[0]);
+	CV_RegisterVar(&cv_turnaxis[1]);
+	CV_RegisterVar(&cv_moveaxis[0]);
+	CV_RegisterVar(&cv_moveaxis[1]);
+	CV_RegisterVar(&cv_lookaxis[0]);
+	CV_RegisterVar(&cv_lookaxis[1]);
+	CV_RegisterVar(&cv_jumpaxis[0]);
+	CV_RegisterVar(&cv_jumpaxis[1]);
+	CV_RegisterVar(&cv_spinaxis[0]);
+	CV_RegisterVar(&cv_spinaxis[1]);
+	CV_RegisterVar(&cv_fireaxis[0]);
+	CV_RegisterVar(&cv_fireaxis[1]);
+	CV_RegisterVar(&cv_firenaxis[0]);
+	CV_RegisterVar(&cv_firenaxis[1]);
+	CV_RegisterVar(&cv_deadzone[0]);
+	CV_RegisterVar(&cv_deadzone[1]);
+	CV_RegisterVar(&cv_digitaldeadzone[0]);
+	CV_RegisterVar(&cv_digitaldeadzone[1]);
 
 	// filesrch.c
 	CV_RegisterVar(&cv_addons_option);
@@ -819,14 +854,14 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_mousemove);
 	CV_RegisterVar(&cv_mousemove2);
 
-	CV_RegisterVar(&cv_usejoystick);
-	CV_RegisterVar(&cv_usejoystick2);
-#ifdef LJOYSTICK
-	CV_RegisterVar(&cv_joyport);
-	CV_RegisterVar(&cv_joyport2);
-#endif
-	CV_RegisterVar(&cv_joyscale);
-	CV_RegisterVar(&cv_joyscale2);
+	for (i = 0; i < 2; i++)
+	{
+		CV_RegisterVar(&cv_usegamepad[i]);
+		CV_RegisterVar(&cv_gamepad_scale[i]);
+		CV_RegisterVar(&cv_gamepad_rumble[i]);
+	}
+
+	CV_RegisterVar(&cv_gamepad_autopause);
 
 	// Analog Control
 	CV_RegisterVar(&cv_analog[0]);
@@ -2215,9 +2250,14 @@ static void Got_Pause(UINT8 **cp, INT32 playernum)
 		{
 			if (!menuactive || netgame)
 				S_PauseAudio();
+
+			P_PauseRumble(NULL);
 		}
 		else
+		{
 			S_ResumeAudio();
+			P_UnpauseRumble(NULL);
+		}
 	}
 
 	I_UpdateMouseGrab();
@@ -4563,6 +4603,8 @@ void Command_ExitGame_f(void)
 	emeralds = 0;
 	memset(&luabanks, 0, sizeof(luabanks));
 
+	P_StopRumble(NULL);
+
 	if (dirmenu)
 		closefilemenu(true);
 
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 0beeae154..47f68a17e 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -33,14 +33,10 @@ extern consvar_t cv_defaultskin2;
 
 extern consvar_t cv_seenames, cv_allowseenames;
 extern consvar_t cv_usemouse;
-extern consvar_t cv_usejoystick;
-extern consvar_t cv_usejoystick2;
-#ifdef LJOYSTICK
-extern consvar_t cv_joyport;
-extern consvar_t cv_joyport2;
-#endif
-extern consvar_t cv_joyscale;
-extern consvar_t cv_joyscale2;
+extern consvar_t cv_usegamepad[2];
+extern consvar_t cv_gamepad_scale[2];
+extern consvar_t cv_gamepad_rumble[2];
+extern consvar_t cv_gamepad_autopause;
 
 // splitscreen with second mouse
 extern consvar_t cv_mouse2port;
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 11d8b1a01..d53fd9051 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -22,9 +22,9 @@
 #include "v_video.h" // video flags (for lua)
 #include "i_sound.h" // musictype_t (for lua)
 #include "g_state.h" // gamestate_t (for lua)
-#include "g_game.h" // Joystick axes (for lua)
-#include "i_joy.h"
+#include "g_game.h" // Gamepad axes (for lua)
 #include "g_input.h" // Game controls (for lua)
+#include "i_gamepad.h"
 
 #include "deh_tables.h"
 
@@ -4841,7 +4841,7 @@ const char *const MENUTYPES_LIST[] = {
 	"OP_CHANGECONTROLS", // OP_ChangeControlsDef shared with P2
 	"OP_P1MOUSE",
 	"OP_P1JOYSTICK",
-	"OP_JOYSTICKSET", // OP_JoystickSetDef shared with P2
+	"OP_JOYSTICKSET", // OP_GamepadSetDef shared with P2
 	"OP_P1CAMERA",
 
 	"OP_P2CONTROLS",
@@ -5642,7 +5642,7 @@ struct int_const_s const INT_CONST[] = {
 	{"GS_DEDICATEDSERVER",GS_DEDICATEDSERVER},
 	{"GS_WAITINGPLAYERS",GS_WAITINGPLAYERS},
 
-	// Joystick axes
+	// Gamepad axes
 	{"JA_NONE",JA_NONE},
 	{"JA_TURN",JA_TURN},
 	{"JA_MOVE",JA_MOVE},
@@ -5653,7 +5653,7 @@ struct int_const_s const INT_CONST[] = {
 	{"JA_SPIN",JA_SPIN},
 	{"JA_FIRE",JA_FIRE},
 	{"JA_FIRENORMAL",JA_FIRENORMAL},
-	{"JOYAXISRANGE",JOYAXISRANGE},
+	{"JOYAXISRANGE",OLDJOYAXISRANGE},
 
 	// Game controls
 	{"GC_NULL",GC_NULL},
diff --git a/src/doomdef.h b/src/doomdef.h
index 2b62bcd6e..24b4fa980 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -209,7 +209,7 @@ extern char logfilename[1024];
 // to an increment in MODVERSION. This might never happen in practice.
 // If MODVERSION increases, set MINOREXECVERSION to 0.
 #define MAJOREXECVERSION MODVERSION
-#define MINOREXECVERSION 0
+#define MINOREXECVERSION 1
 // (It would have been nice to use VERSION and SUBVERSION but those are zero'd out for DEVELOP builds)
 
 // Macros
@@ -556,9 +556,6 @@ UINT32 quickncasehash (const char *p, size_t n)
 #define max(x, y) (((x) > (y)) ? (x) : (y))
 #endif
 
-// Max gamepad/joysticks that can be detected/used.
-#define MAX_JOYSTICKS 4
-
 #ifndef M_PIl
 #define M_PIl 3.1415926535897932384626433832795029L
 #endif
diff --git a/src/f_finale.c b/src/f_finale.c
index bca8e3ba6..307e00aaa 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -37,6 +37,7 @@
 #include "m_cond.h"
 #include "p_local.h"
 #include "p_setup.h"
+#include "p_haptic.h"
 #include "st_stuff.h" // hud hiding
 #include "fastcmp.h"
 #include "console.h"
@@ -510,6 +511,7 @@ void F_StartIntro(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
+	P_StopRumble(NULL);
 	F_NewCutscene(introtext[0]);
 
 	intro_scenenum = 0;
@@ -991,9 +993,10 @@ void F_IntroTicker(void)
 //
 boolean F_IntroResponder(event_t *event)
 {
-	INT32 key = event->key;
+	INT32 type = event->type;
+	INT32 key = G_RemapGamepadEvent(event, &type);
 
-	// remap virtual keys (mouse & joystick buttons)
+	// remap virtual keys (mouse & gamepad buttons)
 	switch (key)
 	{
 		case KEY_MOUSE1:
@@ -1002,34 +1005,30 @@ boolean F_IntroResponder(event_t *event)
 		case KEY_MOUSE1 + 1:
 			key = KEY_BACKSPACE;
 			break;
-		case KEY_JOY1:
-		case KEY_JOY1 + 2:
+		case GAMEPAD_KEY(START):
+		case GAMEPAD_KEY(A):
+		case GAMEPAD_KEY(X):
+		case GAMEPAD_KEY(B):
 			key = KEY_ENTER;
 			break;
-		case KEY_JOY1 + 3:
-			key = 'n';
-			break;
-		case KEY_JOY1 + 1:
-			key = KEY_BACKSPACE;
-			break;
-		case KEY_HAT1:
+		case GAMEPAD_KEY(DPAD_UP):
 			key = KEY_UPARROW;
 			break;
-		case KEY_HAT1 + 1:
+		case GAMEPAD_KEY(DPAD_DOWN):
 			key = KEY_DOWNARROW;
 			break;
-		case KEY_HAT1 + 2:
+		case GAMEPAD_KEY(DPAD_LEFT):
 			key = KEY_LEFTARROW;
 			break;
-		case KEY_HAT1 + 3:
+		case GAMEPAD_KEY(DPAD_RIGHT):
 			key = KEY_RIGHTARROW;
 			break;
 	}
 
-	if (event->type != ev_keydown && key != 301)
+	if (type != ev_keydown)
 		return false;
 
-	if (key != 27 && key != KEY_ENTER && key != KEY_SPACE && key != KEY_BACKSPACE)
+	if (key != KEY_ESCAPE && key != KEY_ENTER && key != KEY_SPACE && key != KEY_BACKSPACE)
 		return false;
 
 	if (keypressed)
@@ -1264,6 +1263,7 @@ void F_StartCredits(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
+	P_StopRumble(NULL);
 	S_StopMusic();
 	S_StopSounds();
 
@@ -1376,9 +1376,10 @@ void F_CreditTicker(void)
 
 boolean F_CreditResponder(event_t *event)
 {
-	INT32 key = event->key;
+	INT32 type = event->type;
+	INT32 key = G_RemapGamepadEvent(event, &type);
 
-	// remap virtual keys (mouse & joystick buttons)
+	// remap virtual keys (mouse & gamepad buttons)
 	switch (key)
 	{
 		case KEY_MOUSE1:
@@ -1387,26 +1388,22 @@ boolean F_CreditResponder(event_t *event)
 		case KEY_MOUSE1 + 1:
 			key = KEY_BACKSPACE;
 			break;
-		case KEY_JOY1:
-		case KEY_JOY1 + 2:
+		case GAMEPAD_KEY(START):
+		case GAMEPAD_KEY(A):
+		case GAMEPAD_KEY(X):
+		case GAMEPAD_KEY(B):
 			key = KEY_ENTER;
 			break;
-		case KEY_JOY1 + 3:
-			key = 'n';
-			break;
-		case KEY_JOY1 + 1:
-			key = KEY_BACKSPACE;
-			break;
-		case KEY_HAT1:
+		case GAMEPAD_KEY(DPAD_UP):
 			key = KEY_UPARROW;
 			break;
-		case KEY_HAT1 + 1:
+		case GAMEPAD_KEY(DPAD_DOWN):
 			key = KEY_DOWNARROW;
 			break;
-		case KEY_HAT1 + 2:
+		case GAMEPAD_KEY(DPAD_LEFT):
 			key = KEY_LEFTARROW;
 			break;
-		case KEY_HAT1 + 3:
+		case GAMEPAD_KEY(DPAD_RIGHT):
 			key = KEY_RIGHTARROW;
 			break;
 	}
@@ -1414,7 +1411,7 @@ boolean F_CreditResponder(event_t *event)
 	if (!(timesBeaten) && !(netgame || multiplayer) && !cv_debug)
 		return false;
 
-	if (event->type != ev_keydown)
+	if (type != ev_keydown)
 		return false;
 
 	if (key != KEY_ESCAPE && key != KEY_ENTER && key != KEY_SPACE && key != KEY_BACKSPACE)
@@ -1455,6 +1452,7 @@ void F_StartGameEvaluation(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
+	P_StopRumble(NULL);
 
 	finalecount = -1;
 	sparklloop = 0;
@@ -1780,6 +1778,7 @@ void F_StartEnding(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
+	P_StopRumble(NULL);
 	S_StopMusic(); // todo: placeholder
 	S_StopSounds();
 
@@ -2225,6 +2224,7 @@ void F_StartGameEnd(void)
 	paused = false;
 	CON_ToggleOff();
 	S_StopSounds();
+	P_StopRumble(NULL);
 
 	// In case menus are still up?!!
 	M_ClearMenus(true);
@@ -3567,6 +3567,7 @@ void F_StartContinue(void)
 	keypressed = false;
 	paused = false;
 	CON_ToggleOff();
+	P_StopRumble(NULL);
 
 	// In case menus are still up?!!
 	M_ClearMenus(true);
@@ -3819,24 +3820,26 @@ void F_ContinueTicker(void)
 
 boolean F_ContinueResponder(event_t *event)
 {
-	INT32 key = event->key;
-
 	if (keypressed)
 		return true;
 
+	INT32 type = event->type;
+	INT32 key = G_RemapGamepadEvent(event, &type);
+
 	if (timetonext >= 21*TICRATE/2)
 		return false;
 	if (event->type != ev_keydown)
 		return false;
 
-	// remap virtual keys (mouse & joystick buttons)
+	// remap virtual keys (mouse & gamepad buttons)
 	switch (key)
 	{
 		case KEY_ENTER:
 		case KEY_SPACE:
 		case KEY_MOUSE1:
-		case KEY_JOY1:
-		case KEY_JOY1 + 2:
+		case GAMEPAD_KEY(START):
+		case GAMEPAD_KEY(A):
+		case GAMEPAD_KEY(X):
 			break;
 		default:
 			return false;
@@ -3954,6 +3957,7 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
+	P_StopRumble(NULL);
 
 	F_NewCutscene(cutscenes[cutscenenum]->scene[0].text);
 
diff --git a/src/g_demo.c b/src/g_demo.c
index 2da5a76ab..9099adc71 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -32,7 +32,7 @@
 #include "z_zone.h"
 #include "i_video.h"
 #include "byteptr.h"
-#include "i_joy.h"
+#include "i_gamepad.h"
 #include "r_local.h"
 #include "r_skins.h"
 #include "y_inter.h"
@@ -1527,9 +1527,9 @@ void G_BeginRecording(void)
 			buf |= 0x08;
 			pflags |= PF_AUTOBRAKE;
 		}
-		if (cv_usejoystick.value)
+		if (cv_usegamepad[0].value)
 			buf |= 0x10;
-		CV_SetValue(&cv_showinputjoy, !!(cv_usejoystick.value));
+		CV_SetValue(&cv_showinputjoy, !!(cv_usegamepad[0].value));
 
 		WRITEUINT8(demo_p,buf);
 		player->pflags = pflags;
diff --git a/src/g_game.c b/src/g_game.c
index 349d90558..911ba19fa 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -24,6 +24,7 @@
 #include "am_map.h"
 #include "m_random.h"
 #include "p_local.h"
+#include "p_haptic.h"
 #include "r_draw.h"
 #include "r_main.h"
 #include "s_sound.h"
@@ -38,7 +39,7 @@
 #include "z_zone.h"
 #include "i_video.h"
 #include "byteptr.h"
-#include "i_joy.h"
+#include "i_gamepad.h"
 #include "r_local.h"
 #include "r_skins.h"
 #include "y_inter.h"
@@ -59,9 +60,6 @@ boolean botingame;
 UINT8 botskin;
 UINT16 botcolor;
 
-JoyType_t Joystick;
-JoyType_t Joystick2;
-
 // 1024 bytes is plenty for a savegame
 #define SAVEGAMESIZE (1024)
 
@@ -256,12 +254,6 @@ UINT32 timesBeaten;
 UINT32 timesBeatenWithEmeralds;
 UINT32 timesBeatenUltimate;
 
-typedef struct joystickvector2_s
-{
-	INT32 xaxis;
-	INT32 yaxis;
-} joystickvector2_t;
-
 boolean precache = true; // if true, load all graphics at start
 
 INT16 prevmap, nextmap;
@@ -280,21 +272,42 @@ static void AutoBrake2_OnChange(void);
 void SendWeaponPref(void);
 void SendWeaponPref2(void);
 
-static CV_PossibleValue_t crosshair_cons_t[] = {{0, "Off"}, {1, "Cross"}, {2, "Angle"}, {3, "Point"}, {0, NULL}};
-static CV_PossibleValue_t joyaxis_cons_t[] = {{0, "None"},
-{1, "X-Axis"}, {2, "Y-Axis"}, {-1, "X-Axis-"}, {-2, "Y-Axis-"},
-#if JOYAXISSET > 1
-{3, "Z-Axis"}, {4, "X-Rudder"}, {-3, "Z-Axis-"}, {-4, "X-Rudder-"},
+CV_PossibleValue_t joyaxis_cons_t[] = {{0, "None"},
+#ifndef OLD_GAMEPAD_AXES
+	{1, "Left Stick X"}, {2, "Left Stick Y"},
+	{3, "Right Stick X"},{4, "Right Stick Y"},
+	{-1, "Left Stick X-"}, {-2, "Left Stick Y-"},
+	{-3, "Right Stick X-"}, {-4, "Right Stick Y-"},
+	{5, "Left Trigger"}, {6, "Right Trigger"},
+#else
+	{1, "X-Axis"}, {2, "Y-Axis"}, {-1, "X-Axis-"}, {-2, "Y-Axis-"},
+	#if JOYAXISSET > 1
+	{3, "Z-Axis"}, {4, "X-Rudder"}, {-3, "Z-Axis-"}, {-4, "X-Rudder-"},
+	#endif
+	#if JOYAXISSET > 2
+	{5, "Y-Rudder"}, {6, "Z-Rudder"}, {-5, "Y-Rudder-"}, {-6, "Z-Rudder-"},
+	#endif
+	#if JOYAXISSET > 3
+	{7, "U-Axis"}, {8, "V-Axis"}, {-7, "U-Axis-"}, {-8, "V-Axis-"},
+	#endif
 #endif
-#if JOYAXISSET > 2
-{5, "Y-Rudder"}, {6, "Z-Rudder"}, {-5, "Y-Rudder-"}, {-6, "Z-Rudder-"},
-#endif
-#if JOYAXISSET > 3
-{7, "U-Axis"}, {8, "V-Axis"}, {-7, "U-Axis-"}, {-8, "V-Axis-"},
-#endif
- {0, NULL}};
-#if JOYAXISSET > 4
-"More Axis Sets"
+	{0, NULL}
+};
+
+#ifndef OLD_GAMEPAD_AXES
+#define MOVEAXIS_DEFAULT "Left Stick Y"
+#define SIDEAXIS_DEFAULT "Left Stick X"
+#define LOOKAXIS_DEFAULT "Right Stick Y-"
+#define TURNAXIS_DEFAULT "Right Stick X"
+#define FIREAXIS_DEFAULT "Right Trigger"
+#define FIRENAXIS_DEFAULT "Left Trigger"
+#else
+#define MOVEAXIS_DEFAULT "Y-Axis"
+#define SIDEAXIS_DEFAULT "X-Axis"
+#define LOOKAXIS_DEFAULT "Y-Rudder-"
+#define TURNAXIS_DEFAULT "X-Rudder"
+#define FIREAXIS_DEFAULT "Z-Rudder"
+#define FIRENAXIS_DEFAULT "Z-Axis"
 #endif
 
 // don't mind me putting these here, I was lazy to figure out where else I could put those without blowing up the compiler.
@@ -330,6 +343,7 @@ consvar_t cv_consolechat = CVAR_INIT ("chatmode", "Window", CV_SAVE, consolechat
 // Pause game upon window losing focus
 consvar_t cv_pauseifunfocused = CVAR_INIT ("pauseifunfocused", "Yes", CV_SAVE, CV_YesNo, NULL);
 
+static CV_PossibleValue_t crosshair_cons_t[] = {{0, "Off"}, {1, "Cross"}, {2, "Angle"}, {3, "Point"}, {0, NULL}};
 consvar_t cv_crosshair = CVAR_INIT ("crosshair", "Cross", CV_SAVE, crosshair_cons_t, NULL);
 consvar_t cv_crosshair2 = CVAR_INIT ("crosshair2", "Cross", CV_SAVE, crosshair_cons_t, NULL);
 consvar_t cv_invertmouse = CVAR_INIT ("invertmouse", "Off", CV_SAVE, CV_OnOff, NULL);
@@ -409,27 +423,46 @@ consvar_t cv_cam_lockonboss[2] = {
 	CVAR_INIT ("cam2_lockaimassist", "Full", CV_SAVE, lockedassist_cons_t, NULL),
 };
 
-consvar_t cv_moveaxis = CVAR_INIT ("joyaxis_move", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_sideaxis = CVAR_INIT ("joyaxis_side", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_lookaxis = CVAR_INIT ("joyaxis_look", "Y-Rudder-", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_turnaxis = CVAR_INIT ("joyaxis_turn", "X-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_jumpaxis = CVAR_INIT ("joyaxis_jump", "None", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_spinaxis = CVAR_INIT ("joyaxis_spin", "None", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_fireaxis = CVAR_INIT ("joyaxis_fire", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_firenaxis = CVAR_INIT ("joyaxis_firenormal", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_deadzone = CVAR_INIT ("joy_deadzone", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
-consvar_t cv_digitaldeadzone = CVAR_INIT ("joy_digdeadzone", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
-
-consvar_t cv_moveaxis2 = CVAR_INIT ("joyaxis2_move", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_sideaxis2 = CVAR_INIT ("joyaxis2_side", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_lookaxis2 = CVAR_INIT ("joyaxis2_look", "Y-Rudder-", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_turnaxis2 = CVAR_INIT ("joyaxis2_turn", "X-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_jumpaxis2 = CVAR_INIT ("joyaxis2_jump", "None", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_spinaxis2 = CVAR_INIT ("joyaxis2_spin", "None", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_fireaxis2 = CVAR_INIT ("joyaxis2_fire", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_firenaxis2 = CVAR_INIT ("joyaxis2_firenormal", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_deadzone2 = CVAR_INIT ("joy_deadzone2", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
-consvar_t cv_digitaldeadzone2 = CVAR_INIT ("joy_digdeadzone2", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
+consvar_t cv_moveaxis[2] = {
+	CVAR_INIT ("joyaxis_move", MOVEAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_move", MOVEAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_sideaxis[2] = {
+	CVAR_INIT ("joyaxis_side", SIDEAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_side", SIDEAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_lookaxis[2] = {
+	CVAR_INIT ("joyaxis_look", LOOKAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_look", LOOKAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_turnaxis[2] = {
+	CVAR_INIT ("joyaxis_turn", TURNAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_turn", TURNAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_jumpaxis[2] = {
+	CVAR_INIT ("joyaxis_jump", "None", CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_jump", "None", CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_spinaxis[2] = {
+	CVAR_INIT ("joyaxis_spin", "None", CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_spin", "None", CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_fireaxis[2] = {
+	CVAR_INIT ("joyaxis_fire", FIREAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_fire", FIREAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_firenaxis[2] = {
+	CVAR_INIT ("joyaxis_firenormal", FIRENAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL),
+	CVAR_INIT ("joyaxis2_firenormal", FIRENAXIS_DEFAULT, CV_SAVE, joyaxis_cons_t, NULL)
+};
+consvar_t cv_deadzone[2] = {
+	CVAR_INIT ("joy_deadzone", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL),
+	CVAR_INIT ("joy_deadzone2", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL)
+};
+consvar_t cv_digitaldeadzone[2] = {
+	CVAR_INIT ("joy_digdeadzone", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL),
+	CVAR_INIT ("joy_digdeadzone2", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL)
+};
 
 player_t *seenplayer; // player we're aiming at right now
 
@@ -828,194 +861,171 @@ INT16 G_SoftwareClipAimingPitch(INT32 *aiming)
 	return (INT16)((*aiming)>>16);
 }
 
-INT32 JoyAxis(joyaxis_e axissel)
+#ifdef OLD_GAMEPAD_AXES
+static gamepad_axis_e ConvertXboxControllerAxes(int type)
+{
+	switch (type)
+	{
+		// Left stick
+		case 1: // X-Axis
+			return GAMEPAD_AXIS_LEFTX;
+		case 2: // Y-Axis
+			return GAMEPAD_AXIS_LEFTY;
+
+		// Right stick
+		case 4: // X-Rudder
+			return GAMEPAD_AXIS_RIGHTX;
+		case 5: // Y-Rudder
+			return GAMEPAD_AXIS_RIGHTY;
+
+		// Triggers
+		case 3: // Z-Axis
+			return GAMEPAD_AXIS_TRIGGERLEFT;
+		case 6: // Z-Rudder
+			return GAMEPAD_AXIS_TRIGGERRIGHT;
+
+		default: // All the other ones
+			return NUM_GAMEPAD_AXES;
+	}
+}
+#endif
+
+static INT16 GetJoystickAxisValue(UINT8 which, joyaxis_e axissel, INT32 axisval)
 {
-	INT32 retaxis;
-	INT32 axisval;
 	boolean flp = false;
 
-	//find what axis to get
+	if (axisval < 0) // odd -axes
+	{
+		axisval = -axisval;
+		flp = true;
+	}
+	else if (axisval == 0)
+		return 0;
+
+	if (axisval > JOYAXISSET*2)
+		return 0;
+
+	gamepad_axis_e gp_axis;
+
+#ifdef OLD_GAMEPAD_AXES
+	gp_axis = ConvertXboxControllerAxes(axisval);
+#else
+	gp_axis = axisval - 1;
+#endif
+
+	if (gp_axis >= NUM_GAMEPAD_AXES)
+		return 0;
+
+	if (axisval % 2)
+		axisval /= 2;
+	else
+	{
+		axisval--;
+		axisval /= 2;
+	}
+
+	INT16 retaxis = G_GetGamepadAxisValue(0, gp_axis);
+
+	if (gamepads[which].digital && axissel >= JA_DIGITAL)
+	{
+		const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(which) / 2;
+		if (-jdeadzone < retaxis && retaxis < jdeadzone)
+			return 0;
+	}
+
+	// flip it around
+	if (flp)
+		retaxis = -retaxis;
+
+	return retaxis;
+}
+
+INT16 G_JoyAxis(UINT8 which, joyaxis_e axissel)
+{
+	INT32 axisval;
+
+	// find what axis to get
 	switch (axissel)
 	{
 		case JA_TURN:
-			axisval = cv_turnaxis.value;
+			axisval = cv_turnaxis[which].value;
 			break;
 		case JA_MOVE:
-			axisval = cv_moveaxis.value;
+			axisval = cv_moveaxis[which].value;
 			break;
 		case JA_LOOK:
-			axisval = cv_lookaxis.value;
+			axisval = cv_lookaxis[which].value;
 			break;
 		case JA_STRAFE:
-			axisval = cv_sideaxis.value;
+			axisval = cv_sideaxis[which].value;
 			break;
 		case JA_JUMP:
-			axisval = cv_jumpaxis.value;
+			axisval = cv_jumpaxis[which].value;
 			break;
 		case JA_SPIN:
-			axisval = cv_spinaxis.value;
+			axisval = cv_spinaxis[which].value;
 			break;
 		case JA_FIRE:
-			axisval = cv_fireaxis.value;
+			axisval = cv_fireaxis[which].value;
 			break;
 		case JA_FIRENORMAL:
-			axisval = cv_firenaxis.value;
+			axisval = cv_firenaxis[which].value;
 			break;
 		default:
 			return 0;
 	}
 
-	if (axisval < 0) //odd -axises
-	{
-		axisval = -axisval;
-		flp = true;
-	}
-	if (axisval > JOYAXISSET*2 || axisval == 0) //not there in array or None
-		return 0;
-
-	if (axisval%2)
-	{
-		axisval /= 2;
-		retaxis = joyxmove[axisval];
-	}
-	else
-	{
-		axisval--;
-		axisval /= 2;
-		retaxis = joyymove[axisval];
-	}
-
-	if (retaxis < (-JOYAXISRANGE))
-		retaxis = -JOYAXISRANGE;
-	if (retaxis > (+JOYAXISRANGE))
-		retaxis = +JOYAXISRANGE;
-
-	if (!Joystick.bGamepadStyle && axissel >= JA_DIGITAL)
-	{
-		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_digitaldeadzone.value) >> FRACBITS;
-		if (-jdeadzone < retaxis && retaxis < jdeadzone)
-			return 0;
-	}
-
-	if (flp) retaxis = -retaxis; //flip it around
-	return retaxis;
+	return GetJoystickAxisValue(which, axissel, axisval);
 }
 
-INT32 Joy2Axis(joyaxis_e axissel)
+static INT16 GetAnalogInput(UINT8 which, gamecontrols_e gc)
 {
-	INT32 retaxis;
-	INT32 axisval;
-	boolean flp = false;
-
-	//find what axis to get
-	switch (axissel)
+	for (UINT8 i = 0; i < 2; i++)
 	{
-		case JA_TURN:
-			axisval = cv_turnaxis2.value;
-			break;
-		case JA_MOVE:
-			axisval = cv_moveaxis2.value;
-			break;
-		case JA_LOOK:
-			axisval = cv_lookaxis2.value;
-			break;
-		case JA_STRAFE:
-			axisval = cv_sideaxis2.value;
-			break;
-		case JA_JUMP:
-			axisval = cv_jumpaxis2.value;
-			break;
-		case JA_SPIN:
-			axisval = cv_spinaxis2.value;
-			break;
-		case JA_FIRE:
-			axisval = cv_fireaxis2.value;
-			break;
-		case JA_FIRENORMAL:
-			axisval = cv_firenaxis2.value;
-			break;
-		default:
-			return 0;
+		SINT8 isAnalog = G_PlayerInputIsAnalog(which, gc, i);
+		if (!isAnalog)
+			continue;
+
+		INT16 value = G_GetAnalogPlayerInput(which, gc, i);
+		if (value > 0 && isAnalog == 1)
+			return value;
+		else if (value < 0 && isAnalog == -1)
+			return max(min(-value, INT16_MAX), INT16_MIN);
 	}
 
-
-	if (axisval < 0) //odd -axises
-	{
-		axisval = -axisval;
-		flp = true;
-	}
-
-	if (axisval > JOYAXISSET*2 || axisval == 0) //not there in array or None
-		return 0;
-
-	if (axisval%2)
-	{
-		axisval /= 2;
-		retaxis = joy2xmove[axisval];
-	}
-	else
-	{
-		axisval--;
-		axisval /= 2;
-		retaxis = joy2ymove[axisval];
-	}
-
-	if (retaxis < (-JOYAXISRANGE))
-		retaxis = -JOYAXISRANGE;
-	if (retaxis > (+JOYAXISRANGE))
-		retaxis = +JOYAXISRANGE;
-
-	if (!Joystick2.bGamepadStyle && axissel >= JA_DIGITAL)
-	{
-		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_digitaldeadzone2.value) >> FRACBITS;
-		if (-jdeadzone < retaxis && retaxis < jdeadzone)
-			return 0;
-	}
-
-	if (flp) retaxis = -retaxis; //flip it around
-	return retaxis;
+	return 0;
 }
 
-
-#define PlayerJoyAxis(p, ax) ((p) == 1 ? JoyAxis(ax) : Joy2Axis(ax))
-
-// Take a magnitude of two axes, and adjust it to take out the deadzone
-// Will return a value between 0 and JOYAXISRANGE
-static INT32 G_BasicDeadZoneCalculation(INT32 magnitude, fixed_t deadZone)
+static boolean CheckAxesUsable(UINT8 which, gamecontrols_e gc1, gamecontrols_e gc2)
 {
-	const INT32 jdeadzone = (JOYAXISRANGE * deadZone) / FRACUNIT;
-	INT32 deadzoneAppliedValue = 0;
-	INT32 adjustedMagnitude = abs(magnitude);
+	INT32 (*controls)[2] = which == 0 ? gamecontrol : gamecontrolbis;
 
-	if (jdeadzone >= JOYAXISRANGE && adjustedMagnitude >= JOYAXISRANGE) // If the deadzone and magnitude are both 100%...
-		return JOYAXISRANGE; // ...return 100% input directly, to avoid dividing by 0
-	else if (adjustedMagnitude > jdeadzone) // Otherwise, calculate how much the magnitude exceeds the deadzone
-	{
-		adjustedMagnitude = min(adjustedMagnitude, JOYAXISRANGE);
+#define CHECK_RANGE(x, y, z) \
+	(controls[x][y] >= KEY_AXES && controls[x][y] < KEY_AXES + NUM_GAMEPAD_AXES \
+	&& controls[x][z] >= KEY_INV_AXES && controls[x][z] < KEY_INV_AXES + NUM_GAMEPAD_AXES)
 
-		adjustedMagnitude -= jdeadzone;
+	if (CHECK_RANGE(gc1, 0, 1) || CHECK_RANGE(gc2, 0, 1))
+		return false;
+	if (CHECK_RANGE(gc1, 1, 0) || CHECK_RANGE(gc2, 1, 0))
+		return false;
 
-		deadzoneAppliedValue = (adjustedMagnitude * JOYAXISRANGE) / (JOYAXISRANGE - jdeadzone);
-	}
+#undef CHECK_RANGE
 
-	return deadzoneAppliedValue;
+	return true;
 }
 
+typedef struct
+{
+	INT32 xaxis, yaxis;
+} joystickvector2_t;
+
 // Get the actual sensible radial value for a joystick axis when accounting for a deadzone
-static void G_HandleAxisDeadZone(UINT8 splitnum, joystickvector2_t *joystickvector)
+static void G_HandleAxisDeadZone(UINT8 playernum, joystickvector2_t *joystickvector)
 {
-	INT32 gamepadStyle = Joystick.bGamepadStyle;
-	fixed_t deadZone = cv_deadzone.value;
-
-	if (splitnum == 1)
+	if (!gamepads[playernum].digital)
 	{
-		gamepadStyle = Joystick2.bGamepadStyle;
-		deadZone = cv_deadzone2.value;
-	}
+		const UINT16 deadZone = G_GetGamepadDeadZone(playernum);
 
-	// When gamepadstyle is "true" the values are just -1, 0, or 1. This is done in the interface code.
-	if (!gamepadStyle)
-	{
 		// Get the total magnitude of the 2 axes
 		INT32 magnitude = (joystickvector->xaxis * joystickvector->xaxis) + (joystickvector->yaxis * joystickvector->yaxis);
 		INT32 normalisedXAxis;
@@ -1029,18 +1039,18 @@ static void G_HandleAxisDeadZone(UINT8 splitnum, joystickvector2_t *joystickvect
 		normalisedYAxis = (joystickvector->yaxis * magnitude) / JOYAXISRANGE;
 
 		// Apply the deadzone to the magnitude to give a correct value between 0 and JOYAXISRANGE
-		normalisedMagnitude = G_BasicDeadZoneCalculation(magnitude, deadZone);
+		normalisedMagnitude = G_BasicDeadZoneCalculation(abs(magnitude), deadZone);
 
 		// Apply the deadzone to the xy axes
 		joystickvector->xaxis = (normalisedXAxis * normalisedMagnitude) / JOYAXISRANGE;
 		joystickvector->yaxis = (normalisedYAxis * normalisedMagnitude) / JOYAXISRANGE;
-
-		// Cap the values so they don't go above the correct maximum
-		joystickvector->xaxis = min(joystickvector->xaxis, JOYAXISRANGE);
-		joystickvector->xaxis = max(joystickvector->xaxis, -JOYAXISRANGE);
-		joystickvector->yaxis = min(joystickvector->yaxis, JOYAXISRANGE);
-		joystickvector->yaxis = max(joystickvector->yaxis, -JOYAXISRANGE);
 	}
+
+	// Cap the values so they don't go above the correct maximum
+	joystickvector->xaxis = min(joystickvector->xaxis, JOYAXISRANGE);
+	joystickvector->xaxis = max(joystickvector->xaxis, -JOYAXISRANGE - 1);
+	joystickvector->yaxis = min(joystickvector->yaxis, JOYAXISRANGE);
+	joystickvector->yaxis = max(joystickvector->yaxis, -JOYAXISRANGE - 1);
 }
 
 //
@@ -1063,6 +1073,8 @@ boolean ticcmd_centerviewdown[2]; // For simple controls, lock the camera behind
 mobj_t *ticcmd_ztargetfocus[2]; // Locking onto an object?
 void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 {
+	UINT8 forplayer = ssplayer - 1;
+
 	boolean forcestrafe = false;
 	boolean forcefullinput = false;
 	INT32 tspeed, forward, side, axis, strafeaxis, moveaxis, turnaxis, lookaxis, i;
@@ -1071,15 +1083,17 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 	const INT32 speed = 1;
 	// these ones used for multiple conditions
-	boolean turnleft, turnright, strafelkey, straferkey, movefkey, movebkey, mouseaiming, analogjoystickmove, gamepadjoystickmove, thisjoyaiming;
+	boolean turnleft, turnright, strafelkey, straferkey, movefkey, movebkey, mouseaiming;
+	boolean analogaxismove, digitalaxismove, thisjoyaiming;
 	boolean strafeisturn; // Simple controls only
 	player_t *player = &players[ssplayer == 2 ? secondarydisplayplayer : consoleplayer];
 	camera_t *thiscam = ((ssplayer == 1 || player->bot == BOT_2PHUMAN) ? &camera : &camera2);
 	angle_t *myangle = (ssplayer == 1 ? &localangle : &localangle2);
 	INT32 *myaiming = (ssplayer == 1 ? &localaiming : &localaiming2);
+	gamepad_t *gamepad = &gamepads[forplayer];
 
 	angle_t drawangleoffset = (player->powers[pw_carry] == CR_ROLLOUT) ? ANGLE_180 : 0;
-	INT32 chasecam, chasefreelook, alwaysfreelook, usejoystick, invertmouse, turnmultiplier, mousemove;
+	INT32 chasecam, chasefreelook, alwaysfreelook, usegamepad, invertmouse, turnmultiplier, mousemove;
 	controlstyle_e controlstyle = G_ControlStyle(ssplayer);
 	INT32 mdx, mdy, mldy;
 
@@ -1093,14 +1107,11 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	static fixed_t tta_factor[2] = {FRACUNIT, FRACUNIT}; // disables turn-to-angle when manually turning camera until movement happens
 	boolean centerviewdown = false;
 
-	UINT8 forplayer = ssplayer-1;
-
 	if (ssplayer == 1)
 	{
 		chasecam = cv_chasecam.value;
 		chasefreelook = cv_chasefreelook.value;
 		alwaysfreelook = cv_alwaysfreelook.value;
-		usejoystick = cv_usejoystick.value;
 		invertmouse = cv_invertmouse.value;
 		turnmultiplier = cv_cam_turnmultiplier.value;
 		mousemove = cv_mousemove.value;
@@ -1114,7 +1125,6 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		chasecam = cv_chasecam2.value;
 		chasefreelook = cv_chasefreelook2.value;
 		alwaysfreelook = cv_alwaysfreelook2.value;
-		usejoystick = cv_usejoystick2.value;
 		invertmouse = cv_invertmouse2.value;
 		turnmultiplier = cv_cam2_turnmultiplier.value;
 		mousemove = cv_mousemove2.value;
@@ -1124,6 +1134,8 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		G_CopyTiccmd(cmd, I_BaseTiccmd2(), 1); // empty, or external driver
 	}
 
+	usegamepad = cv_usegamepad[forplayer].value;
+
 	if (menuactive || CON_Ready() || chat_on)
 		mdx = mdy = mldy = 0;
 
@@ -1142,13 +1154,14 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		return;
 	}
 
-	turnright = PLAYERINPUTDOWN(ssplayer, GC_TURNRIGHT);
-	turnleft = PLAYERINPUTDOWN(ssplayer, GC_TURNLEFT);
+	// Axes for turning or strafing are ignored here
+	turnright = G_CheckDigitalPlayerInput(forplayer, GC_TURNRIGHT);
+	turnleft = G_CheckDigitalPlayerInput(forplayer, GC_TURNLEFT);
 
-	straferkey = PLAYERINPUTDOWN(ssplayer, GC_STRAFERIGHT);
-	strafelkey = PLAYERINPUTDOWN(ssplayer, GC_STRAFELEFT);
-	movefkey = PLAYERINPUTDOWN(ssplayer, GC_FORWARD);
-	movebkey = PLAYERINPUTDOWN(ssplayer, GC_BACKWARD);
+	straferkey = G_CheckDigitalPlayerInput(forplayer, GC_STRAFERIGHT);
+	strafelkey = G_CheckDigitalPlayerInput(forplayer, GC_STRAFELEFT);
+	movefkey = G_CheckDigitalPlayerInput(forplayer, GC_FORWARD);
+	movebkey = G_CheckDigitalPlayerInput(forplayer, GC_BACKWARD);
 
 	if (strafeisturn)
 	{
@@ -1157,10 +1170,10 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		straferkey = strafelkey = false;
 	}
 
-	mouseaiming = (PLAYERINPUTDOWN(ssplayer, GC_MOUSEAIMING)) ^
+	mouseaiming = (G_PlayerInputDown(forplayer, GC_MOUSEAIMING)) ^
 		((chasecam && !player->spectator) ? chasefreelook : alwaysfreelook);
-	analogjoystickmove = usejoystick && !Joystick.bGamepadStyle;
-	gamepadjoystickmove = usejoystick && Joystick.bGamepadStyle;
+	analogaxismove = usegamepad && !gamepad->digital;
+	digitalaxismove = usegamepad && gamepad->digital;
 
 	thisjoyaiming = (chasecam && !player->spectator) ? chasefreelook : alwaysfreelook;
 
@@ -1169,19 +1182,38 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		*myaiming = 0;
 	joyaiming[forplayer] = thisjoyaiming;
 
-	turnaxis = PlayerJoyAxis(ssplayer, JA_TURN);
+	turnaxis = G_JoyAxis(forplayer, JA_TURN);
 	if (strafeisturn)
-		turnaxis += PlayerJoyAxis(ssplayer, JA_STRAFE);
-	lookaxis = PlayerJoyAxis(ssplayer, JA_LOOK);
+		turnaxis += G_JoyAxis(forplayer, JA_STRAFE);
+	lookaxis = G_JoyAxis(forplayer, JA_LOOK);
+
+	if (usegamepad)
+	{
+		turnaxis -= GetAnalogInput(forplayer, GC_TURNLEFT);
+		turnaxis += GetAnalogInput(forplayer, GC_TURNRIGHT);
+
+		if (strafeisturn)
+		{
+			turnaxis -= GetAnalogInput(forplayer, GC_STRAFELEFT);
+			turnaxis += GetAnalogInput(forplayer, GC_STRAFERIGHT);
+		}
+
+		lookaxis += GetAnalogInput(forplayer, GC_LOOKUP);
+		lookaxis -= GetAnalogInput(forplayer, GC_LOOKDOWN);
+	}
+
+	// Handle deadzones
 	lookjoystickvector.xaxis = turnaxis;
 	lookjoystickvector.yaxis = lookaxis;
 	G_HandleAxisDeadZone(forplayer, &lookjoystickvector);
 
-	if (gamepadjoystickmove && lookjoystickvector.xaxis != 0)
+	// Do digital axis turning
+	if (digitalaxismove && lookjoystickvector.xaxis != 0)
 	{
 		turnright = turnright || (lookjoystickvector.xaxis > 0);
 		turnleft = turnleft || (lookjoystickvector.xaxis < 0);
 	}
+
 	forward = side = 0;
 
 	// use two stage accelerative turning
@@ -1220,10 +1252,10 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		if (turnleft)
 			side -= sidemove[speed];
 
-		if (analogjoystickmove && lookjoystickvector.xaxis != 0)
+		if (analogaxismove && lookjoystickvector.xaxis != 0)
 		{
-			// JOYAXISRANGE is supposed to be 1023 (divide by 1024)
-			side += ((lookjoystickvector.xaxis * sidemove[1]) >> 10);
+			// JOYAXISRANGE is supposed to be 32767 (divide by 32768)
+			side += ((lookjoystickvector.xaxis * sidemove[1]) >> 15);
 		}
 	}
 	else if (controlstyle == CS_LMAOGALOG) // Analog
@@ -1241,47 +1273,69 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		else if (turnleft)
 			cmd->angleturn = (INT16)(cmd->angleturn + ((angleturn[tspeed] * turnmultiplier)>>FRACBITS));
 
-		if (analogjoystickmove && lookjoystickvector.xaxis != 0)
+		if (analogaxismove && lookjoystickvector.xaxis != 0)
 		{
-			// JOYAXISRANGE should be 1023 (divide by 1024)
-			cmd->angleturn = (INT16)(cmd->angleturn - ((((lookjoystickvector.xaxis * angleturn[1]) >> 10) * turnmultiplier)>>FRACBITS)); // ANALOG!
+			// JOYAXISRANGE should be 32767 (divide by 32768)
+			cmd->angleturn = (INT16)(cmd->angleturn - ((((lookjoystickvector.xaxis * angleturn[1]) >> 15) * turnmultiplier)>>FRACBITS)); // ANALOG!
 		}
 
 		if (turnright || turnleft || abs(cmd->angleturn) > angleturn[2])
 			tta_factor[forplayer] = 0; // suspend turn to angle
 	}
 
-	strafeaxis = strafeisturn ? 0 : PlayerJoyAxis(ssplayer, JA_STRAFE);
-	moveaxis = PlayerJoyAxis(ssplayer, JA_MOVE);
+	// Strafing axes (moving left and right)
+	if (strafeisturn)
+		strafeaxis = 0;
+	else
+	{
+		strafeaxis = G_JoyAxis(forplayer, JA_STRAFE);
+
+		if (usegamepad && CheckAxesUsable(forplayer, GC_STRAFELEFT, GC_STRAFERIGHT))
+		{
+			strafeaxis -= GetAnalogInput(forplayer, GC_STRAFELEFT);
+			strafeaxis += GetAnalogInput(forplayer, GC_STRAFERIGHT);
+		}
+	}
+
+	// Moving axes (moving forwards and backwards)
+	moveaxis = G_JoyAxis(forplayer, JA_MOVE);
+	if (usegamepad && CheckAxesUsable(forplayer, GC_FORWARD, GC_BACKWARD))
+	{
+		moveaxis -= GetAnalogInput(forplayer, GC_FORWARD);
+		moveaxis += GetAnalogInput(forplayer, GC_BACKWARD);
+	}
+
 	movejoystickvector.xaxis = strafeaxis;
 	movejoystickvector.yaxis = moveaxis;
 	G_HandleAxisDeadZone(forplayer, &movejoystickvector);
 
-	if (gamepadjoystickmove && movejoystickvector.xaxis != 0)
+	if (digitalaxismove && movejoystickvector.xaxis != 0)
 	{
+		// Do digital axis movement
 		if (movejoystickvector.xaxis > 0)
 			side += sidemove[speed];
 		else if (movejoystickvector.xaxis < 0)
 			side -= sidemove[speed];
 	}
-	else if (analogjoystickmove && movejoystickvector.xaxis != 0)
+	else if (analogaxismove && movejoystickvector.xaxis != 0)
 	{
-		// JOYAXISRANGE is supposed to be 1023 (divide by 1024)
-		side += ((movejoystickvector.xaxis * sidemove[1]) >> 10);
+		// JOYAXISRANGE is supposed to be 32767 (divide by 32768)
+		side += ((movejoystickvector.xaxis * sidemove[1]) >> 15);
 	}
 
 	// forward with key or button
-	if (movefkey || (gamepadjoystickmove && movejoystickvector.yaxis < 0)
+	// also handles digital axis movement
+	if (movefkey || (digitalaxismove && movejoystickvector.yaxis < 0)
 		|| ((player->powers[pw_carry] == CR_NIGHTSMODE)
-			&& (PLAYERINPUTDOWN(ssplayer, GC_LOOKUP) || (gamepadjoystickmove && lookjoystickvector.yaxis > 0))))
+			&& (G_CheckDigitalPlayerInput(forplayer, GC_LOOKUP) || (digitalaxismove && lookjoystickvector.yaxis > 0))))
 		forward = forwardmove[speed];
-	if (movebkey || (gamepadjoystickmove && movejoystickvector.yaxis > 0)
+	if (movebkey || (digitalaxismove && movejoystickvector.yaxis > 0)
 		|| ((player->powers[pw_carry] == CR_NIGHTSMODE)
-			&& (PLAYERINPUTDOWN(ssplayer, GC_LOOKDOWN) || (gamepadjoystickmove && lookjoystickvector.yaxis < 0))))
+			&& (G_CheckDigitalPlayerInput(forplayer, GC_LOOKDOWN) || (digitalaxismove && lookjoystickvector.yaxis < 0))))
 		forward -= forwardmove[speed];
 
-	if (analogjoystickmove && movejoystickvector.yaxis != 0)
-		forward -= ((movejoystickvector.yaxis * forwardmove[1]) >> 10); // ANALOG!
+	if (analogaxismove && movejoystickvector.yaxis != 0)
+		forward -= ((movejoystickvector.yaxis * forwardmove[1]) >> 15); // ANALOG!
 
 	// some people strafe left & right with mouse buttons
 	// those people are weird
@@ -1290,53 +1344,54 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (strafelkey)
 		side -= sidemove[speed];
 
-	if (PLAYERINPUTDOWN(ssplayer, GC_WEAPONNEXT))
+	if (G_PlayerInputDown(forplayer, GC_WEAPONNEXT))
 		cmd->buttons |= BT_WEAPONNEXT; // Next Weapon
-	if (PLAYERINPUTDOWN(ssplayer, GC_WEAPONPREV))
+	if (G_PlayerInputDown(forplayer, GC_WEAPONPREV))
 		cmd->buttons |= BT_WEAPONPREV; // Previous Weapon
 
 #if NUM_WEAPONS > 10
-"Add extra inputs to g_input.h/gamecontrols_e"
+#error "Add extra inputs to g_input.h/gamecontrols_e"
 #endif
+
 	//use the four avaliable bits to determine the weapon.
 	cmd->buttons &= ~BT_WEAPONMASK;
 	for (i = 0; i < NUM_WEAPONS; ++i)
-		if (PLAYERINPUTDOWN(ssplayer, GC_WEPSLOT1 + i))
+		if (G_PlayerInputDown(forplayer, GC_WEPSLOT1 + i))
 		{
 			cmd->buttons |= (UINT16)(i + 1);
 			break;
 		}
 
 	// fire with any button/key
-	axis = PlayerJoyAxis(ssplayer, JA_FIRE);
-	if (PLAYERINPUTDOWN(ssplayer, GC_FIRE) || (usejoystick && axis > 0))
+	axis = G_JoyAxis(forplayer, JA_FIRE);
+	if (G_PlayerInputDown(forplayer, GC_FIRE) || (usegamepad && axis > 0))
 		cmd->buttons |= BT_ATTACK;
 
 	// fire normal with any button/key
-	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
-	if (PLAYERINPUTDOWN(ssplayer, GC_FIRENORMAL) || (usejoystick && axis > 0))
+	axis = G_JoyAxis(forplayer, JA_FIRENORMAL);
+	if (G_PlayerInputDown(forplayer, GC_FIRENORMAL) || (usegamepad && axis > 0))
 		cmd->buttons |= BT_FIRENORMAL;
 
-	if (PLAYERINPUTDOWN(ssplayer, GC_TOSSFLAG))
+	if (G_PlayerInputDown(forplayer, GC_TOSSFLAG))
 		cmd->buttons |= BT_TOSSFLAG;
 
 	// Lua scriptable buttons
-	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM1))
+	if (G_PlayerInputDown(forplayer, GC_CUSTOM1))
 		cmd->buttons |= BT_CUSTOM1;
-	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM2))
+	if (G_PlayerInputDown(forplayer, GC_CUSTOM2))
 		cmd->buttons |= BT_CUSTOM2;
-	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM3))
+	if (G_PlayerInputDown(forplayer, GC_CUSTOM3))
 		cmd->buttons |= BT_CUSTOM3;
 
-	// use with any button/key
-	axis = PlayerJoyAxis(ssplayer, JA_SPIN);
-	if (PLAYERINPUTDOWN(ssplayer, GC_SPIN) || (usejoystick && axis > 0))
+	// spin with any button/key
+	axis = G_JoyAxis(forplayer, JA_SPIN);
+	if (G_PlayerInputDown(forplayer, GC_SPIN) || (usegamepad && axis > 0))
 		cmd->buttons |= BT_SPIN;
 
 	// Centerview can be a toggle in simple mode!
 	{
 		static boolean last_centerviewdown[2], centerviewhold[2]; // detect taps for toggle behavior
-		boolean down = PLAYERINPUTDOWN(ssplayer, GC_CENTERVIEW);
+		boolean down = G_PlayerInputDown(forplayer, GC_CENTERVIEW);
 
 		if (!(controlstyle == CS_SIMPLE && cv_cam_centertoggle[forplayer].value))
 			centerviewdown = down;
@@ -1435,7 +1490,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (ticcmd_centerviewdown[forplayer] && controlstyle == CS_SIMPLE)
 		controlstyle = CS_LEGACY;
 
-	if (PLAYERINPUTDOWN(ssplayer, GC_CAMRESET))
+	if (G_PlayerInputDown(forplayer, GC_CAMRESET))
 	{
 		if (thiscam->chase && !resetdown[forplayer])
 			P_ResetCamera(&players[ssplayer == 1 ? displayplayer : secondarydisplayplayer], thiscam);
@@ -1445,10 +1500,9 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	else
 		resetdown[forplayer] = false;
 
-
 	// jump button
-	axis = PlayerJoyAxis(ssplayer, JA_JUMP);
-	if (PLAYERINPUTDOWN(ssplayer, GC_JUMP) || (usejoystick && axis > 0))
+	axis = G_JoyAxis(forplayer, JA_JUMP);
+	if (G_PlayerInputDown(forplayer, GC_JUMP) || (usegamepad && axis > 0))
 		cmd->buttons |= BT_JUMP;
 
 	// player aiming shit, ahhhh...
@@ -1458,7 +1512,6 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			(player->mo && (player->mo->eflags & MFE_VERTICALFLIP)
 			 && (!thiscam->chase || player->pflags & PF_FLIPCAM)) //because chasecam's not inverted
 			 ? -1 : 1; // set to -1 or 1 to multiply
-		 INT32 configlookaxis = ssplayer == 1 ? cv_lookaxis.value : cv_lookaxis2.value;
 
 		// mouse look stuff (mouse look is not the same as mouse aim)
 		if (mouseaiming)
@@ -1469,21 +1522,21 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			*myaiming += (mldy<<19)*player_invert*screen_invert;
 		}
 
-		if (analogjoystickmove && joyaiming[forplayer] && lookjoystickvector.yaxis != 0 && configlookaxis != 0)
-			*myaiming += (lookjoystickvector.yaxis<<16) * screen_invert;
+		if (joyaiming[forplayer] && lookjoystickvector.yaxis != 0)
+			*myaiming += (lookjoystickvector.yaxis<<11) * screen_invert;
 
 		// spring back if not using keyboard neither mouselookin'
-		if (!keyboard_look[forplayer] && configlookaxis == 0 && !joyaiming[forplayer] && !mouseaiming)
+		if (!keyboard_look[forplayer] && !joyaiming[forplayer] && !mouseaiming)
 			*myaiming = 0;
 
 		if (!(player->powers[pw_carry] == CR_NIGHTSMODE))
 		{
-			if (PLAYERINPUTDOWN(ssplayer, GC_LOOKUP) || (gamepadjoystickmove && lookjoystickvector.yaxis < 0))
+			if (G_CheckDigitalPlayerInput(forplayer, GC_LOOKUP) || (digitalaxismove && lookjoystickvector.yaxis < 0))
 			{
 				*myaiming += KB_LOOKSPEED * screen_invert;
 				keyboard_look[forplayer] = true;
 			}
-			else if (PLAYERINPUTDOWN(ssplayer, GC_LOOKDOWN) || (gamepadjoystickmove && lookjoystickvector.yaxis > 0))
+			else if (G_CheckDigitalPlayerInput(forplayer, GC_LOOKDOWN) || (digitalaxismove && lookjoystickvector.yaxis > 0))
 			{
 				*myaiming -= KB_LOOKSPEED * screen_invert;
 				keyboard_look[forplayer] = true;
@@ -1716,6 +1769,57 @@ ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
 	return dest;
 }
 
+static player_t *G_GetInputPlayer(UINT8 which)
+{
+	if (which == 0)
+		return &players[consoleplayer];
+	else if (which == 1)
+	{
+		if (splitscreen)
+			return &players[secondarydisplayplayer];
+		else if (playeringame[1] && players[1].bot == BOT_2PHUMAN)
+			return &players[1];
+	}
+
+	return NULL;
+}
+
+// Returns a player's gamepad index, even if it's disabled
+// Gamepad indexes correspond to the local player index.
+INT16 G_GetGamepadForPlayer(player_t *player)
+{
+	for (UINT8 i = 0; i < 2; i++)
+	{
+		if (player == G_GetInputPlayer(i))
+			return i;
+	}
+
+	return -1;
+}
+
+// Gets the user-set gamepad device for a specific player
+INT32 G_GetGamepadDeviceIndex(INT32 player)
+{
+#ifdef GAMEPAD_HOTPLUG
+	if (atoi(cv_usegamepad[player].string) > I_NumGamepads())
+		return atoi(cv_usegamepad[player].string);
+	else
+#endif
+		return cv_usegamepad[player].value;
+}
+
+void G_OnGamepadDisconnect(UINT8 which)
+{
+	if (!cv_gamepad_autopause.value)
+		return;
+
+	if (gamestate != GS_LEVEL || paused || netgame || splitscreen)
+		return;
+
+	if (which == 0 || (which == 1 && playeringame[1] && players[1].bot == BOT_2PHUMAN))
+		COM_ImmedExecute("pause");
+}
+
 // User has designated that they want
 // analog ON, so tell the game to stop
 // fudging with it.
@@ -1787,6 +1891,25 @@ static void AutoBrake2_OnChange(void)
 	SendWeaponPref2();
 }
 
+static void G_ResetInputs(void)
+{
+	memset(gamekeydown, 0, sizeof (gamekeydown));
+
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		for (UINT8 j = 0; j < NUM_GAMEPAD_BUTTONS; j++)
+			gamepads[i].buttons[j] = 0;
+
+		for (UINT8 j = 0; j < NUM_GAMEPAD_AXES; j++)
+			gamepads[i].axes[j] = 0;
+	}
+
+	G_SetMouseDeltas(0, 0, 1);
+	G_SetMouseDeltas(0, 0, 2);
+
+	P_StopRumble(NULL);
+}
+
 //
 // G_DoLoadLevel
 //
@@ -1855,14 +1978,7 @@ void G_DoLoadLevel(boolean resetplayer)
 		P_ResetCamera(&players[secondarydisplayplayer], &camera2);
 
 	// clear cmd building stuff
-	memset(gamekeydown, 0, sizeof (gamekeydown));
-	for (i = 0;i < JOYAXISSET; i++)
-	{
-		joyxmove[i] = joyymove[i] = 0;
-		joy2xmove[i] = joy2ymove[i] = 0;
-	}
-	G_SetMouseDeltas(0, 0, 1);
-	G_SetMouseDeltas(0, 0, 2);
+	G_ResetInputs();
 
 	// clear hud messages remains (usually from game startup)
 	CON_ClearHUD();
@@ -1965,11 +2081,14 @@ static INT32 camtoggledelay, camtoggledelay2 = 0;
 //
 boolean G_Responder(event_t *ev)
 {
+	INT32 evtype = ev->type;
+	INT32 key = G_RemapGamepadEvent(ev, &evtype);
+
 	// any other key pops up menu if in demos
 	if (gameaction == ga_nothing && !singledemo &&
 		((demoplayback && !modeattacking && !titledemo) || gamestate == GS_TITLESCREEN))
 	{
-		if (ev->type == ev_keydown && ev->key != 301 && !(gamestate == GS_TITLESCREEN && finalecount < TICRATE))
+		if (evtype == ev_keydown && !(gamestate == GS_TITLESCREEN && finalecount < TICRATE))
 		{
 			M_StartControlPanel();
 			return true;
@@ -1994,7 +2113,7 @@ boolean G_Responder(event_t *ev)
 			return true; // chat ate the event
 		if (AM_Responder(ev))
 			return true; // automap ate it
-		// map the event (key/mouse/joy) to a gamecontrol
+		// map the event (key/mouse/gamepad) to a gamecontrol
 	}
 	// Intro
 	else if (gamestate == GS_INTRO)
@@ -2044,8 +2163,8 @@ boolean G_Responder(event_t *ev)
 			return true; // chat ate the event
 
 	// allow spy mode changes even during the demo
-	if (gamestate == GS_LEVEL && ev->type == ev_keydown
-		&& (ev->key == KEY_F12 || ev->key == gamecontrol[GC_VIEWPOINT][0] || ev->key == gamecontrol[GC_VIEWPOINT][1]))
+	if (gamestate == GS_LEVEL && evtype == ev_keydown
+		&& (key == KEY_F12 || key == gamecontrol[GC_VIEWPOINT][0] || key == gamecontrol[GC_VIEWPOINT][1]))
 	{
 		// ViewpointSwitch Lua hook.
 		UINT8 canSwitchView = 0;
@@ -2115,16 +2234,16 @@ boolean G_Responder(event_t *ev)
 	// update keys current state
 	G_MapEventsToControls(ev);
 
-	switch (ev->type)
+	switch (evtype)
 	{
 		case ev_keydown:
-			if (ev->key == gamecontrol[GC_PAUSE][0]
-				|| ev->key == gamecontrol[GC_PAUSE][1]
-				|| ev->key == KEY_PAUSE)
+			if (key == gamecontrol[GC_PAUSE][0]
+				|| key == gamecontrol[GC_PAUSE][1]
+				|| key == KEY_PAUSE)
 			{
 				if (modeattacking && !demoplayback && (gamestate == GS_LEVEL))
 				{
-					pausebreakkey = (ev->key == KEY_PAUSE);
+					pausebreakkey = (key == KEY_PAUSE);
 					if (menuactive || pausedelay < 0 || leveltime < 2)
 						return true;
 
@@ -2149,8 +2268,8 @@ boolean G_Responder(event_t *ev)
 					}
 				}
 			}
-			if (ev->key == gamecontrol[GC_CAMTOGGLE][0]
-				|| ev->key == gamecontrol[GC_CAMTOGGLE][1])
+			if (key == gamecontrol[GC_CAMTOGGLE][0]
+				|| key == gamecontrol[GC_CAMTOGGLE][1])
 			{
 				if (!camtoggledelay)
 				{
@@ -2158,8 +2277,8 @@ boolean G_Responder(event_t *ev)
 					CV_SetValue(&cv_chasecam, cv_chasecam.value ? 0 : 1);
 				}
 			}
-			if (ev->key == gamecontrolbis[GC_CAMTOGGLE][0]
-				|| ev->key == gamecontrolbis[GC_CAMTOGGLE][1])
+			if (key == gamecontrolbis[GC_CAMTOGGLE][0]
+				|| key == gamecontrolbis[GC_CAMTOGGLE][1])
 			{
 				if (!camtoggledelay2)
 				{
@@ -2172,15 +2291,9 @@ boolean G_Responder(event_t *ev)
 		case ev_keyup:
 			return false; // always let key up events filter down
 
-		case ev_mouse:
-			return true; // eat events
-
-		case ev_joystick:
-			return true; // eat events
-
-		case ev_joystick2:
-			return true; // eat events
-
+		case ev_mouse: // eat events
+		case ev_gamepad_axis:
+			return true;
 
 		default:
 			break;
@@ -3174,14 +3287,7 @@ void G_DoReborn(INT32 playernum)
 				P_ResetCamera(&players[secondarydisplayplayer], &camera2);
 
 			// clear cmd building stuff
-			memset(gamekeydown, 0, sizeof (gamekeydown));
-			for (i = 0; i < JOYAXISSET; i++)
-			{
-				joyxmove[i] = joyymove[i] = 0;
-				joy2xmove[i] = joy2ymove[i] = 0;
-			}
-			G_SetMouseDeltas(0, 0, 1);
-			G_SetMouseDeltas(0, 0, 2);
+			G_ResetInputs();
 
 			// clear hud messages remains (usually from game startup)
 			CON_ClearHUD();
@@ -4589,11 +4695,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 	}
 	save_p += VERSIONSIZE;
 
-//	if (demoplayback) // reset game engine
-//		G_StopDemo();
-
-//	paused = false;
-//	automapactive = false;
+	P_StopRumble(NULL);
 
 	// dearchive all the modifications
 	if (!P_LoadGame(mapoverride))
@@ -4856,6 +4958,7 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 {
 	INT32 i;
 
+	P_StopRumble(NULL);
 	Y_CleanupScreenBuffer();
 
 	if (paused)
diff --git a/src/g_game.h b/src/g_game.h
index dca043f2e..6c24054a0 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -68,10 +68,14 @@ typedef enum {
 #define P_ControlStyle(player) ((((player)->pflags & PF_ANALOGMODE) ? CS_LMAOGALOG : 0) | (((player)->pflags & PF_DIRECTIONCHAR) ? CS_STANDARD : 0))
 
 extern consvar_t cv_autobrake, cv_autobrake2;
-extern consvar_t cv_sideaxis,cv_turnaxis,cv_moveaxis,cv_lookaxis,cv_jumpaxis,cv_spinaxis,cv_fireaxis,cv_firenaxis,cv_deadzone,cv_digitaldeadzone;
-extern consvar_t cv_sideaxis2,cv_turnaxis2,cv_moveaxis2,cv_lookaxis2,cv_jumpaxis2,cv_spinaxis2,cv_fireaxis2,cv_firenaxis2,cv_deadzone2,cv_digitaldeadzone2;
 extern consvar_t cv_ghost_bestscore, cv_ghost_besttime, cv_ghost_bestrings, cv_ghost_last, cv_ghost_guest;
 
+extern consvar_t cv_sideaxis[2], cv_turnaxis[2], cv_moveaxis[2], cv_lookaxis[2],
+	cv_jumpaxis[2], cv_spinaxis[2], cv_fireaxis[2], cv_firenaxis[2],
+	cv_deadzone[2], cv_digitaldeadzone[2];
+
+extern CV_PossibleValue_t joyaxis_cons_t[];
+
 // hi here's some new controls
 extern consvar_t cv_cam_shiftfacing[2], cv_cam_turnfacing[2],
 	cv_cam_turnfacingability[2], cv_cam_turnfacingspindash[2], cv_cam_turnfacinginput[2],
@@ -84,10 +88,12 @@ typedef enum
 	LOCK_INTERESTS = 1<<2,
 } lockassist_e;
 
+// Legacy axis stuff
+#define JOYAXISSET   4 // 4 Sets of 2 axes
 
 typedef enum
 {
-	JA_NONE = 0,
+	JA_NONE,
 	JA_TURN,
 	JA_MOVE,
 	JA_LOOK,
@@ -101,8 +107,7 @@ typedef enum
 	JA_FIRENORMAL,
 } joyaxis_e;
 
-INT32 JoyAxis(joyaxis_e axissel);
-INT32 Joy2Axis(joyaxis_e axissel);
+INT16 G_JoyAxis(UINT8 which, joyaxis_e axissel);
 
 // mouseaiming (looking up/down with the mouse or keyboard)
 #define KB_LOOKSPEED (1<<25)
@@ -122,6 +127,15 @@ ticcmd_t *G_CopyTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n);
 // copy ticcmd_t to and fro network packets
 ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n);
 
+// gets the user-set gamepad device for a specific player
+INT32 G_GetGamepadDeviceIndex(INT32 player);
+
+// returns a player's gamepad index
+INT16 G_GetGamepadForPlayer(player_t *player);
+
+// called when a player's gamepad is disconnected
+void G_OnGamepadDisconnect(UINT8 which);
+
 // clip the console player aiming to the view
 INT16 G_ClipAimingPitch(INT32 *aiming);
 INT16 G_SoftwareClipAimingPitch(INT32 *aiming);
diff --git a/src/g_input.c b/src/g_input.c
index 7bb2e799d..a0a3fd56c 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -8,12 +8,14 @@
 // See the 'LICENSE' file for more details.
 //-----------------------------------------------------------------------------
 /// \file  g_input.c
-/// \brief handle mouse/keyboard/joystick inputs,
+/// \brief handle mouse/keyboard/gamepad inputs,
 ///        maps inputs to game controls (forward, spin, jump...)
 
 #include "doomdef.h"
 #include "doomstat.h"
+#include "g_game.h"
 #include "g_input.h"
+#include "i_gamepad.h"
 #include "keys.h"
 #include "hu_stuff.h" // need HUFONT start & end
 #include "d_net.h"
@@ -34,8 +36,7 @@ consvar_t cv_controlperkey = CVAR_INIT ("controlperkey", "One", CV_SAVE, onecont
 mouse_t mouse;
 mouse_t mouse2;
 
-// joystick values are repeated
-INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
+gamepad_t gamepads[NUM_GAMEPADS];
 
 // current state of the keys: true if pushed
 UINT8 gamekeydown[NUMINPUTS];
@@ -86,114 +87,77 @@ const INT32 gcl_jump_spin[num_gcl_jump_spin] = {
 	GC_JUMP, GC_SPIN
 };
 
+static boolean CheckInputDown(UINT8 which, gamecontrols_e gc, boolean checkaxes)
+{
+	INT32 (*controls)[2] = which == 0 ? gamecontrol : gamecontrolbis;
+
+	for (unsigned i = 0; i < 2; i++)
+	{
+		INT32 key = controls[gc][i];
+
+		if (key >= KEY_GAMEPAD && key < KEY_AXES)
+		{
+			if (gamepads[which].buttons[key - KEY_GAMEPAD])
+				return true;
+		}
+		else if (checkaxes && (key >= KEY_AXES && key < KEY_INV_AXES + NUM_GAMEPAD_AXES))
+		{
+			const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(which);
+			const INT16 value = G_GetGamepadAxisValue(which, (key - KEY_AXES) % NUM_GAMEPAD_AXES);
+
+			if (abs(value) > jdeadzone)
+				return true;
+		}
+		else if (gamekeydown[key])
+			return true;
+	}
+
+	return false;
+}
+
+boolean G_PlayerInputDown(UINT8 which, gamecontrols_e gc)
+{
+	return CheckInputDown(which, gc, true);
+}
+
+boolean G_CheckDigitalPlayerInput(UINT8 which, gamecontrols_e gc)
+{
+	return CheckInputDown(which, gc, false);
+}
+
+SINT8 G_PlayerInputIsAnalog(UINT8 which, gamecontrols_e gc, UINT8 settings)
+{
+	INT32 (*controls)[2] = which == 0 ? gamecontrol : gamecontrolbis;
+	INT32 key = controls[gc][settings];
+
+	if (key >= KEY_AXES && key < KEY_AXES + NUM_GAMEPAD_AXES)
+		return 1;
+	else if (key >= KEY_INV_AXES && key < KEY_INV_AXES + NUM_GAMEPAD_AXES)
+		return -1;
+
+	return 0;
+}
+
+INT16 G_GetAnalogPlayerInput(UINT8 which, gamecontrols_e gc, UINT8 settings)
+{
+	INT32 (*controls)[2] = which == 0 ? gamecontrol : gamecontrolbis;
+	INT32 key = controls[gc][settings];
+
+	if (key >= KEY_AXES && key < KEY_INV_AXES + NUM_GAMEPAD_AXES)
+		return G_GetGamepadAxisValue(which, (key - KEY_AXES) % NUM_GAMEPAD_AXES);
+
+	return 0;
+}
+
 typedef struct
 {
 	UINT8 time;
 	UINT8 state;
 	UINT8 clicks;
 } dclick_t;
+
 static dclick_t mousedclicks[MOUSEBUTTONS];
-static dclick_t joydclicks[JOYBUTTONS + JOYHATS*4];
 static dclick_t mouse2dclicks[MOUSEBUTTONS];
-static dclick_t joy2dclicks[JOYBUTTONS + JOYHATS*4];
-
-// protos
-static UINT8 G_CheckDoubleClick(UINT8 state, dclick_t *dt);
-
-//
-// Remaps the inputs to game controls.
-//
-// A game control can be triggered by one or more keys/buttons.
-//
-// Each key/mousebutton/joybutton triggers ONLY ONE game control.
-//
-void G_MapEventsToControls(event_t *ev)
-{
-	INT32 i;
-	UINT8 flag;
-
-	switch (ev->type)
-	{
-		case ev_keydown:
-			if (ev->key < NUMINPUTS)
-				gamekeydown[ev->key] = 1;
-#ifdef PARANOIA
-			else
-			{
-				CONS_Debug(DBG_GAMELOGIC, "Bad downkey input %d\n",ev->key);
-			}
-
-#endif
-			break;
-
-		case ev_keyup:
-			if (ev->key < NUMINPUTS)
-				gamekeydown[ev->key] = 0;
-#ifdef PARANOIA
-			else
-			{
-				CONS_Debug(DBG_GAMELOGIC, "Bad upkey input %d\n",ev->key);
-			}
-#endif
-			break;
-
-		case ev_mouse: // buttons are virtual keys
-			mouse.rdx = ev->x;
-			mouse.rdy = ev->y;
-			break;
-
-		case ev_joystick: // buttons are virtual keys
-			i = ev->key;
-			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
-				break;
-			if (ev->x != INT32_MAX) joyxmove[i] = ev->x;
-			if (ev->y != INT32_MAX) joyymove[i] = ev->y;
-			break;
-
-		case ev_joystick2: // buttons are virtual keys
-			i = ev->key;
-			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
-				break;
-			if (ev->x != INT32_MAX) joy2xmove[i] = ev->x;
-			if (ev->y != INT32_MAX) joy2ymove[i] = ev->y;
-			break;
-
-		case ev_mouse2: // buttons are virtual keys
-			if (menuactive || CON_Ready() || chat_on)
-				break;
-			mouse2.rdx = ev->x;
-			mouse2.rdy = ev->y;
-			break;
-
-		default:
-			break;
-	}
-
-	// ALWAYS check for mouse & joystick double-clicks even if no mouse event
-	for (i = 0; i < MOUSEBUTTONS; i++)
-	{
-		flag = G_CheckDoubleClick(gamekeydown[KEY_MOUSE1+i], &mousedclicks[i]);
-		gamekeydown[KEY_DBLMOUSE1+i] = flag;
-	}
-
-	for (i = 0; i < JOYBUTTONS + JOYHATS*4; i++)
-	{
-		flag = G_CheckDoubleClick(gamekeydown[KEY_JOY1+i], &joydclicks[i]);
-		gamekeydown[KEY_DBLJOY1+i] = flag;
-	}
-
-	for (i = 0; i < MOUSEBUTTONS; i++)
-	{
-		flag = G_CheckDoubleClick(gamekeydown[KEY_2MOUSE1+i], &mouse2dclicks[i]);
-		gamekeydown[KEY_DBL2MOUSE1+i] = flag;
-	}
-
-	for (i = 0; i < JOYBUTTONS + JOYHATS*4; i++)
-	{
-		flag = G_CheckDoubleClick(gamekeydown[KEY_2JOY1+i], &joy2dclicks[i]);
-		gamekeydown[KEY_DBL2JOY1+i] = flag;
-	}
-}
 
 //
 // General double-click detection routine for any kind of input.
@@ -225,6 +189,641 @@ static UINT8 G_CheckDoubleClick(UINT8 state, dclick_t *dt)
 	return false;
 }
 
+//
+// Remaps the inputs to game controls.
+//
+// A game control can be triggered by one or more keys/buttons.
+//
+// Each key/mouse button/gamepad button triggers ONLY ONE game control.
+//
+void G_MapEventsToControls(event_t *ev)
+{
+	INT32 i;
+	UINT8 flag;
+
+	switch (ev->type)
+	{
+		case ev_keydown:
+			if (ev->key < NUMINPUTS)
+				gamekeydown[ev->key] = 1;
+#ifdef PARANOIA
+			else
+				CONS_Debug(DBG_GAMELOGIC, "Bad downkey input %d\n",ev->key);
+
+#endif
+			break;
+
+		case ev_keyup:
+			if (ev->key < NUMINPUTS)
+				gamekeydown[ev->key] = 0;
+#ifdef PARANOIA
+			else
+				CONS_Debug(DBG_GAMELOGIC, "Bad upkey input %d\n",ev->key);
+#endif
+			break;
+
+		case ev_gamepad_down:
+		case ev_gamepad_up:
+#ifdef PARANOIA
+			if (ev->which < NUM_GAMEPADS)
+#endif
+				gamepads[ev->which].buttons[ev->key] = ev->type == ev_gamepad_down ? 1 : 0;
+			break;
+
+		case ev_gamepad_axis:
+#ifdef PARANOIA
+			if (ev->which < NUM_GAMEPADS)
+#endif
+				gamepads[ev->which].axes[ev->key] = ev->x;
+			break;
+
+		case ev_mouse:
+			mouse.rdx = ev->x;
+			mouse.rdy = ev->y;
+			break;
+
+		case ev_mouse2:
+			mouse2.rdx = ev->x;
+			mouse2.rdy = ev->y;
+			break;
+
+		default:
+			break;
+	}
+
+	// ALWAYS check for mouse double-clicks even if there were no such events
+	for (i = 0; i < MOUSEBUTTONS; i++)
+	{
+		flag = G_CheckDoubleClick(gamekeydown[KEY_MOUSE1+i], &mousedclicks[i]);
+		gamekeydown[KEY_DBLMOUSE1+i] = flag;
+	}
+
+	for (i = 0; i < MOUSEBUTTONS; i++)
+	{
+		flag = G_CheckDoubleClick(gamekeydown[KEY_2MOUSE1+i], &mouse2dclicks[i]);
+		gamekeydown[KEY_DBL2MOUSE1+i] = flag;
+	}
+}
+
+const char *const gamepad_button_names[NUM_GAMEPAD_BUTTONS + 1] = {
+	"a",
+	"b",
+	"x",
+	"y",
+	"back",
+	"guide",
+	"start",
+	"left-stick",
+	"right-stick",
+	"left-shoulder",
+	"right-shoulder",
+	"dpad-up",
+	"dpad-down",
+	"dpad-left",
+	"dpad-right",
+	"misc1",
+	"paddle1",
+	"paddle2",
+	"paddle3",
+	"paddle4",
+	"touchpad",
+	NULL};
+
+const char *const gamepad_axis_names[NUM_GAMEPAD_AXES + 1] = {
+	"left-x",
+	"left-y",
+	"right-x",
+	"right-y",
+	"trigger-left",
+	"trigger-right",
+	NULL};
+
+boolean G_GamepadTypeIsXbox(gamepadtype_e type)
+{
+	switch (type)
+	{
+	case GAMEPAD_TYPE_XBOX360:
+	case GAMEPAD_TYPE_XBOXONE:
+	case GAMEPAD_TYPE_XBOX_SERIES_XS:
+	case GAMEPAD_TYPE_XBOX_ELITE:
+		return true;
+	default:
+		return false;
+	}
+}
+
+boolean G_GamepadTypeIsPlayStation(gamepadtype_e type)
+{
+	switch (type)
+	{
+	case GAMEPAD_TYPE_PS3:
+	case GAMEPAD_TYPE_PS4:
+	case GAMEPAD_TYPE_PS5:
+		return true;
+	default:
+		return false;
+	}
+}
+
+boolean G_GamepadTypeIsNintendoSwitch(gamepadtype_e type)
+{
+	switch (type)
+	{
+	case GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
+	case GAMEPAD_TYPE_NINTENDO_SWITCH_JOY_CON_GRIP:
+		return true;
+	default:
+		return G_GamepadTypeIsJoyCon(type);
+	}
+}
+
+boolean G_GamepadTypeIsJoyCon(gamepadtype_e type)
+{
+	switch (type)
+	{
+	case GAMEPAD_TYPE_NINTENDO_SWITCH_JOY_CON_LEFT:
+	case GAMEPAD_TYPE_NINTENDO_SWITCH_JOY_CON_RIGHT:
+		return true;
+	default:
+		return false;
+	}
+}
+
+boolean G_RumbleSupported(UINT8 which)
+{
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS)
+		return 0;
+
+	return I_GetGamepadRumbleSupported(which);
+}
+
+boolean G_RumbleGamepad(UINT8 which, fixed_t large_magnitude, fixed_t small_magnitude, tic_t duration)
+{
+	haptic_t effect;
+
+	if (!G_RumbleSupported(which))
+		return false;
+
+	effect.large_magnitude = large_magnitude;
+	effect.small_magnitude = small_magnitude;
+	effect.duration = duration;
+
+	return I_RumbleGamepad(which, &effect);
+}
+
+void G_StopGamepadRumble(UINT8 which)
+{
+	if (G_RumbleSupported(which))
+		I_StopGamepadRumble(which);
+}
+
+fixed_t G_GetLargeMotorFreq(UINT8 which)
+{
+	if (!G_RumbleSupported(which) || which >= NUM_GAMEPADS)
+		return 0;
+
+	gamepad_t *gamepad = &gamepads[which];
+	return gamepad->rumble.data.large_magnitude;
+}
+
+fixed_t G_GetSmallMotorFreq(UINT8 which)
+{
+	if (!G_RumbleSupported(which) || which >= NUM_GAMEPADS)
+		return 0;
+
+	gamepad_t *gamepad = &gamepads[which];
+	return gamepad->rumble.data.small_magnitude;
+}
+
+boolean G_GetGamepadRumblePaused(UINT8 which)
+{
+	return I_GetGamepadRumblePaused(which);
+}
+
+boolean G_SetLargeMotorFreq(UINT8 which, fixed_t freq)
+{
+	return I_SetGamepadLargeMotorFreq(which, freq);
+}
+
+boolean G_SetSmallMotorFreq(UINT8 which, fixed_t freq)
+{
+	return I_SetGamepadSmallMotorFreq(which, freq);
+}
+
+void G_SetGamepadRumblePaused(UINT8 which, boolean pause)
+{
+	if (G_RumbleSupported(which))
+		I_SetGamepadRumblePaused(which, pause);
+}
+
+// Obtains the value of an axis, and makes it digital if needed
+INT16 G_GamepadAxisEventValue(UINT8 which, INT16 value)
+{
+	gamepad_t *gamepad = &gamepads[which];
+
+	if (gamepad->digital)
+	{
+		const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(which);
+
+		if (value < -jdeadzone)
+			value = -JOYAXISRANGE - 1;
+		else if (value > jdeadzone)
+			value = JOYAXISRANGE;
+		else
+			value = 0;
+	}
+
+	return value;
+}
+
+INT16 G_GetGamepadAxisValue(UINT8 which, gamepad_axis_e axis)
+{
+	gamepad_t *gamepad = &gamepads[which];
+
+	if (axis >= NUM_GAMEPAD_AXES)
+		return 0;
+
+	return G_GamepadAxisEventValue(which, gamepad->axes[axis]);
+}
+
+fixed_t G_GetAdjustedGamepadAxis(UINT8 which, gamepad_axis_e axis, boolean applyDeadzone)
+{
+	gamepad_t *gamepad = &gamepads[which];
+
+	if (axis >= NUM_GAMEPAD_AXES)
+		return 0;
+
+	INT32 value = gamepad->axes[axis];
+
+	if (applyDeadzone && gamepad->digital)
+	{
+		INT16 deadzone = G_GetGamepadDigitalDeadZone(which);
+
+		if (value < -deadzone)
+			value = -JOYAXISRANGE;
+		else if (value > deadzone)
+			value = JOYAXISRANGE;
+		else
+			value = 0;
+	}
+	else if (applyDeadzone)
+	{
+		INT32 sign = value < 0 ? -1 : 1;
+		INT16 deadzone = G_GetGamepadDeadZone(which);
+		INT32 magnitude = value * value;
+		INT32 nAxis = magnitude / JOYAXISRANGE;
+		INT32 nMagnitude = G_BasicDeadZoneCalculation(magnitude, deadzone);
+
+		value = (nAxis * nMagnitude) / JOYAXISRANGE;
+		value = min(value * sign, JOYAXISRANGE);
+		value = max(value, -JOYAXISRANGE);
+	}
+
+	return (value / 32767.0) * FRACUNIT;
+}
+
+static UINT16 CalcGamepadDeadZone(fixed_t deadzone)
+{
+	INT32 value = (JOYAXISRANGE * deadzone) / FRACUNIT;
+
+	if (value < 0)
+		value = 0;
+	else if (value > JOYAXISRANGE)
+		value = JOYAXISRANGE;
+
+	return value;
+}
+
+UINT16 G_GetGamepadDeadZone(UINT8 which)
+{
+	return CalcGamepadDeadZone(cv_deadzone[which].value);
+}
+
+UINT16 G_GetGamepadDigitalDeadZone(UINT8 which)
+{
+	return CalcGamepadDeadZone(cv_digitaldeadzone[which].value);
+}
+
+// Take a magnitude of two axes, and adjust it to take out the deadzone
+// Will return a value between 0 and JOYAXISRANGE
+INT32 G_BasicDeadZoneCalculation(INT32 magnitude, const UINT16 jdeadzone)
+{
+	INT32 deadzoneAppliedValue = 0;
+	INT32 adjustedMagnitude = abs(magnitude);
+
+	if (jdeadzone >= JOYAXISRANGE && adjustedMagnitude >= JOYAXISRANGE) // If the deadzone and magnitude are both 100%...
+		return JOYAXISRANGE; // ...return 100% input directly, to avoid dividing by 0
+	else if (adjustedMagnitude > jdeadzone) // Otherwise, calculate how much the magnitude exceeds the deadzone
+	{
+		adjustedMagnitude = min(adjustedMagnitude, JOYAXISRANGE);
+
+		adjustedMagnitude -= jdeadzone;
+
+		deadzoneAppliedValue = (adjustedMagnitude * JOYAXISRANGE) / (JOYAXISRANGE - jdeadzone);
+	}
+
+	return deadzoneAppliedValue;
+}
+
+INT32 G_RemapGamepadEvent(event_t *event, INT32 *type)
+{
+	if (event->type == ev_gamepad_down)
+	{
+		*type = ev_keydown;
+		return KEY_GAMEPAD + event->key;
+	}
+	else if (event->type == ev_gamepad_up)
+	{
+		*type = ev_keyup;
+		return KEY_GAMEPAD + event->key;
+	}
+	else if (event->type == ev_gamepad_axis)
+	{
+		const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(event->which);
+		const INT16 value = G_GetGamepadAxisValue(event->which, event->key);
+
+		if (value < -jdeadzone || value > jdeadzone)
+			*type = ev_keyup;
+		else
+			*type = ev_keydown;
+
+		if (value < -jdeadzone)
+			return KEY_INV_AXES + event->key;
+		else
+			return KEY_AXES + event->key;
+	}
+
+	return event->key;
+}
+
+typedef struct
+{
+	const char *name;
+	const char *menu1;
+	const char *menu2;
+} button_strings_t;
+
+#define DEF_NAME_BUTTON(str) {.name = str, .menu1 = str " Button", .menu2 = "the " str " Button"}
+#define DEF_NAME_SIMPLE(str) {.name = str, .menu1 = NULL, .menu2 = "the " str}
+#define DEF_NAME_DPAD(a, b)  {.name = "D-Pad " a, .menu1 = "D-Pad " b, .menu2 = a}
+
+#define PARTIAL_DEF_START [GAMEPAD_BUTTON_A] = { NULL }
+#define PARTIAL_DEF_END [NUM_GAMEPAD_BUTTONS - 1] = { NULL }
+
+static const char *GetStringFromButtonList(const button_strings_t *names, gamepad_button_e button, gamepad_string_e type)
+{
+	switch (type)
+	{
+	case GAMEPAD_STRING_DEFAULT:
+		return names[button].name;
+	case GAMEPAD_STRING_MENU1:
+		if (names[button].menu1)
+			return names[button].menu1;
+		else
+			return names[button].name;
+	case GAMEPAD_STRING_MENU2:
+		if (names[button].menu2)
+			return names[button].menu2;
+		else
+			return names[button].name;
+	}
+
+	return NULL;
+}
+
+const char *G_GetGamepadButtonString(gamepadtype_e type, gamepad_button_e button, gamepad_string_e strtype)
+{
+	static const button_strings_t base_names[] = {
+		[GAMEPAD_BUTTON_A]             = DEF_NAME_BUTTON("A"),
+		[GAMEPAD_BUTTON_B]             = DEF_NAME_BUTTON("B"),
+		[GAMEPAD_BUTTON_X]             = DEF_NAME_BUTTON("X"),
+		[GAMEPAD_BUTTON_Y]             = DEF_NAME_BUTTON("Y"),
+		[GAMEPAD_BUTTON_BACK]          = DEF_NAME_BUTTON("Back"),
+		[GAMEPAD_BUTTON_GUIDE]         = DEF_NAME_BUTTON("Guide"),
+		[GAMEPAD_BUTTON_START]         = DEF_NAME_BUTTON("Start"),
+		[GAMEPAD_BUTTON_LEFTSTICK]     = DEF_NAME_SIMPLE("Left Stick"),
+		[GAMEPAD_BUTTON_RIGHTSTICK]    = DEF_NAME_SIMPLE("Right Stick"),
+		[GAMEPAD_BUTTON_LEFTSHOULDER]  = DEF_NAME_SIMPLE("Left Shoulder"),
+		[GAMEPAD_BUTTON_RIGHTSHOULDER] = DEF_NAME_SIMPLE("Right Shoulder"),
+		[GAMEPAD_BUTTON_DPAD_UP]       = DEF_NAME_DPAD("Up", "\x1A"),
+		[GAMEPAD_BUTTON_DPAD_DOWN]     = DEF_NAME_DPAD("Down", "\x1B"),
+		[GAMEPAD_BUTTON_DPAD_LEFT]     = DEF_NAME_DPAD("Left", "\x1C"),
+		[GAMEPAD_BUTTON_DPAD_RIGHT]    = DEF_NAME_DPAD("Right", "\x1D"),
+		[GAMEPAD_BUTTON_PADDLE1]       = DEF_NAME_SIMPLE("Paddle 1"),
+		[GAMEPAD_BUTTON_PADDLE2]       = DEF_NAME_SIMPLE("Paddle 2"),
+		[GAMEPAD_BUTTON_PADDLE3]       = DEF_NAME_SIMPLE("Paddle 3"),
+		[GAMEPAD_BUTTON_PADDLE4]       = DEF_NAME_SIMPLE("Paddle 4"),
+		[GAMEPAD_BUTTON_TOUCHPAD]      = DEF_NAME_SIMPLE("Touchpad"),
+
+		// This one's a bit weird
+		// Suffix the numbers in the event SDL adds more misc buttons
+		[GAMEPAD_BUTTON_MISC1] = {
+			.name  = "Misc. Button",
+			.menu1 = "Gamepad Misc.",
+			.menu2 = "the Misc. Button"
+		},
+	};
+
+	button_strings_t const *names = NULL;
+
+	if (G_GamepadTypeIsXbox(type))
+	{
+		#define BASE_XBOX_NAMES \
+			[GAMEPAD_BUTTON_LEFTSHOULDER]  = DEF_NAME_SIMPLE("Left Bumper"), \
+			[GAMEPAD_BUTTON_RIGHTSHOULDER] = DEF_NAME_SIMPLE("Right Bumper")
+
+		static const button_strings_t xbox_names[] = { PARTIAL_DEF_START,
+			BASE_XBOX_NAMES,
+		PARTIAL_DEF_END };
+
+		static const button_strings_t series_xs_names[] = { PARTIAL_DEF_START,
+			BASE_XBOX_NAMES,
+			[GAMEPAD_BUTTON_MISC1]         = DEF_NAME_BUTTON("Share"),
+		PARTIAL_DEF_END };
+
+		static const button_strings_t elite_names[] = { PARTIAL_DEF_START,
+			BASE_XBOX_NAMES,
+			[GAMEPAD_BUTTON_PADDLE1]       = DEF_NAME_SIMPLE("P1 Paddle"),
+			[GAMEPAD_BUTTON_PADDLE2]       = DEF_NAME_SIMPLE("P2 Paddle"),
+			[GAMEPAD_BUTTON_PADDLE3]       = DEF_NAME_SIMPLE("P3 Paddle"),
+			[GAMEPAD_BUTTON_PADDLE4]       = DEF_NAME_SIMPLE("P4 Paddle"),
+		PARTIAL_DEF_END };
+
+		if (type == GAMEPAD_TYPE_XBOX_SERIES_XS) // X|S controllers have a Share button
+			names = series_xs_names;
+		else if (type == GAMEPAD_TYPE_XBOX_ELITE) // Elite controller has paddles
+			names = elite_names;
+		else
+			names = xbox_names;
+
+		#undef BASE_XBOX_NAMES
+	}
+	else if (G_GamepadTypeIsPlayStation(type))
+	{
+		#define BASE_PS_NAMES \
+			[GAMEPAD_BUTTON_A]             = DEF_NAME_BUTTON("Cross"), \
+			[GAMEPAD_BUTTON_B]             = DEF_NAME_BUTTON("Circle"), \
+			[GAMEPAD_BUTTON_X]             = DEF_NAME_BUTTON("Square"), \
+			[GAMEPAD_BUTTON_Y]             = DEF_NAME_BUTTON("Triangle"), \
+			[GAMEPAD_BUTTON_BACK]          = DEF_NAME_BUTTON("Select"), \
+			[GAMEPAD_BUTTON_GUIDE]         = DEF_NAME_BUTTON("PS"), \
+			[GAMEPAD_BUTTON_LEFTSTICK]     = DEF_NAME_BUTTON("L3"), \
+			[GAMEPAD_BUTTON_RIGHTSTICK]    = DEF_NAME_BUTTON("R3"), \
+			[GAMEPAD_BUTTON_LEFTSHOULDER]  = DEF_NAME_BUTTON("L1"), \
+			[GAMEPAD_BUTTON_RIGHTSHOULDER] = DEF_NAME_BUTTON("R1")
+
+		static const button_strings_t ps_names[] = {
+			BASE_PS_NAMES,
+		PARTIAL_DEF_END };
+
+		static const button_strings_t ps5_names[] = {
+			BASE_PS_NAMES,
+			[GAMEPAD_BUTTON_MISC1] = DEF_NAME_BUTTON("Microphone"),
+		PARTIAL_DEF_END };
+
+		names = type == GAMEPAD_TYPE_PS5 ? ps5_names : ps_names;
+		#undef BASE_PS_NAMES
+	}
+	else if (G_GamepadTypeIsNintendoSwitch(type))
+	{
+		static const button_strings_t switch_names[] = { PARTIAL_DEF_START,
+			[GAMEPAD_BUTTON_BACK]          = DEF_NAME_BUTTON("-"),
+			[GAMEPAD_BUTTON_GUIDE]         = DEF_NAME_BUTTON("HOME"),
+			[GAMEPAD_BUTTON_START]         = DEF_NAME_BUTTON("+"),
+			[GAMEPAD_BUTTON_LEFTSHOULDER]  = DEF_NAME_BUTTON("L"),
+			[GAMEPAD_BUTTON_RIGHTSHOULDER] = DEF_NAME_BUTTON("R"),
+			[GAMEPAD_BUTTON_MISC1]         = DEF_NAME_BUTTON("Capture"),
+		PARTIAL_DEF_END };
+
+		names = switch_names;
+	}
+	else if (type == GAMEPAD_TYPE_AMAZON_LUNA)
+	{
+		static const button_strings_t luna_names[] = { PARTIAL_DEF_START,
+			[GAMEPAD_BUTTON_MISC1] = DEF_NAME_BUTTON("Microphone"),
+		PARTIAL_DEF_END };
+
+		names = luna_names;
+	}
+
+	const char *str = NULL;
+
+	if (names)
+		str = GetStringFromButtonList(names, button, strtype);
+	if (str == NULL)
+		str = GetStringFromButtonList(base_names, button, strtype);
+	if (str)
+		return str;
+
+	return "Unknown";
+}
+
+#undef DEF_NAME_BUTTON
+#undef DEF_NAME_SIMPLE
+#undef DEF_NAME_DPAD
+
+#undef PARTIAL_DEF_START
+#undef PARTIAL_DEF_END
+
+typedef struct
+{
+	const char *name;
+	const char *menu1;
+	const char *menu2;
+	const char *name_inv;
+	const char *menu1_inv;
+	const char *menu2_inv;
+} axis_strings_t;
+
+#define DEF_NAME_AXIS(str, a, inv_a, b, inv_b) {\
+	str " " a, str " " b, "the " str " " b, \
+	str " " inv_a, str " " inv_b, "the " str " " inv_b}
+#define DEF_NAME_TRIGGER(str) {str, NULL, "the " str, NULL, NULL, NULL}
+#define DEF_NAME_BUTTON(str) {str, str " Button", "the " str " Button", NULL, NULL, NULL}
+
+#define PARTIAL_DEF_START [GAMEPAD_AXIS_LEFTX] = { NULL }
+
+static const char *GetStringFromAxisList(const axis_strings_t *names, gamepad_axis_e axis, gamepad_string_e type, boolean inv)
+{
+	switch (type)
+	{
+	case GAMEPAD_STRING_DEFAULT:
+		if (inv && names[axis].name_inv)
+			return names[axis].name_inv;
+		else
+			return names[axis].name;
+		break;
+	case GAMEPAD_STRING_MENU1:
+		if (inv && names[axis].menu1_inv)
+			return names[axis].menu1_inv;
+		if (names[axis].menu1)
+			return names[axis].menu1;
+		else
+			return names[axis].name;
+		break;
+	case GAMEPAD_STRING_MENU2:
+		if (inv && names[axis].menu2_inv)
+			return names[axis].menu2_inv;
+		if (names[axis].menu2)
+			return names[axis].menu2;
+		else
+			return names[axis].name;
+		break;
+	}
+
+	return NULL;
+}
+
+const char *G_GetGamepadAxisString(gamepadtype_e type, gamepad_axis_e axis, gamepad_string_e strtype, boolean inv)
+{
+	static const axis_strings_t base_names[] = {
+		[GAMEPAD_AXIS_LEFTX]        = DEF_NAME_AXIS("Left Stick",  "X", "X-", "\x1D", "\x1C"),
+		[GAMEPAD_AXIS_LEFTY]        = DEF_NAME_AXIS("Left Stick",  "Y", "Y-", "\x1B", "\x1A"),
+		[GAMEPAD_AXIS_RIGHTX]       = DEF_NAME_AXIS("Right Stick", "X", "X-", "\x1D", "\x1C"),
+		[GAMEPAD_AXIS_RIGHTY]       = DEF_NAME_AXIS("Right Stick", "Y", "Y-", "\x1B", "\x1A"),
+		[GAMEPAD_AXIS_TRIGGERLEFT]  = DEF_NAME_TRIGGER("Left Trigger"),
+		[GAMEPAD_AXIS_TRIGGERRIGHT] = DEF_NAME_TRIGGER("Right Trigger")
+	};
+
+	axis_strings_t const *names = NULL;
+
+	if (G_GamepadTypeIsPlayStation(type))
+	{
+		static const axis_strings_t ps_names[] = { PARTIAL_DEF_START,
+			[GAMEPAD_AXIS_TRIGGERLEFT]  = DEF_NAME_BUTTON("L2"),
+			[GAMEPAD_AXIS_TRIGGERRIGHT] = DEF_NAME_BUTTON("R2"),
+		};
+
+		names = ps_names;
+	}
+	else if (G_GamepadTypeIsNintendoSwitch(type))
+	{
+		static const axis_strings_t switch_names[] = { PARTIAL_DEF_START,
+			[GAMEPAD_AXIS_TRIGGERLEFT]  = DEF_NAME_BUTTON("ZL"),
+			[GAMEPAD_AXIS_TRIGGERRIGHT] = DEF_NAME_BUTTON("ZR"),
+		};
+
+		names = switch_names;
+	}
+
+	const char *str = NULL;
+
+	if (names)
+		str = GetStringFromAxisList(names, axis, strtype, inv);
+	if (str == NULL)
+		str = GetStringFromAxisList(base_names, axis, strtype, inv);
+	if (str)
+		return str;
+
+	return "Unknown";
+}
+
+#undef DEF_NAME_AXIS
+#undef DEF_NAME_TRIGGER
+#undef DEF_NAME_BUTTON
+
+#undef PARTIAL_DEF_START
+
 typedef struct
 {
 	INT32 keynum;
@@ -243,20 +842,17 @@ static keyname_t keynames[] =
 	{KEY_NUMLOCK, "numlock"},
 	{KEY_SCROLLLOCK, "scrolllock"},
 
-	// bill gates keys
+	// satya nadella keys
 	{KEY_LEFTWIN, "leftwin"},
 	{KEY_RIGHTWIN, "rightwin"},
 	{KEY_MENU, "menu"},
 
 	{KEY_LSHIFT, "lshift"},
 	{KEY_RSHIFT, "rshift"},
-	{KEY_LSHIFT, "shift"},
 	{KEY_LCTRL, "lctrl"},
 	{KEY_RCTRL, "rctrl"},
-	{KEY_LCTRL, "ctrl"},
 	{KEY_LALT, "lalt"},
 	{KEY_RALT, "ralt"},
-	{KEY_LALT, "alt"},
 
 	// keypad keys
 	{KEY_KPADSLASH, "keypad /"},
@@ -304,7 +900,7 @@ static keyname_t keynames[] =
 	{'`', "TILDE"},
 	{KEY_PAUSE, "pause/break"},
 
-	// virtual keys for mouse buttons and joystick buttons
+	// virtual keys for mouse buttons and gamepad buttons
 	{KEY_MOUSE1+0,"mouse1"},
 	{KEY_MOUSE1+1,"mouse2"},
 	{KEY_MOUSE1+2,"mouse3"},
@@ -313,8 +909,8 @@ static keyname_t keynames[] =
 	{KEY_MOUSE1+5,"mouse6"},
 	{KEY_MOUSE1+6,"mouse7"},
 	{KEY_MOUSE1+7,"mouse8"},
-	{KEY_2MOUSE1+0,"sec_mouse2"}, // BP: sorry my mouse handler swap button 1 and 2
-	{KEY_2MOUSE1+1,"sec_mouse1"},
+	{KEY_2MOUSE1+0,"sec_mouse1"},
+	{KEY_2MOUSE1+1,"sec_mouse2"},
 	{KEY_2MOUSE1+2,"sec_mouse3"},
 	{KEY_2MOUSE1+3,"sec_mouse4"},
 	{KEY_2MOUSE1+4,"sec_mouse5"},
@@ -326,58 +922,48 @@ static keyname_t keynames[] =
 	{KEY_2MOUSEWHEELUP, "wheel 2 up"},
 	{KEY_2MOUSEWHEELDOWN, "wheel 2 down"},
 
-	{KEY_JOY1+0, "joy1"},
-	{KEY_JOY1+1, "joy2"},
-	{KEY_JOY1+2, "joy3"},
-	{KEY_JOY1+3, "joy4"},
-	{KEY_JOY1+4, "joy5"},
-	{KEY_JOY1+5, "joy6"},
-	{KEY_JOY1+6, "joy7"},
-	{KEY_JOY1+7, "joy8"},
-	{KEY_JOY1+8, "joy9"},
-#if !defined (NOMOREJOYBTN_1S)
-	// we use up to 32 buttons in DirectInput
-	{KEY_JOY1+9, "joy10"},
-	{KEY_JOY1+10, "joy11"},
-	{KEY_JOY1+11, "joy12"},
-	{KEY_JOY1+12, "joy13"},
-	{KEY_JOY1+13, "joy14"},
-	{KEY_JOY1+14, "joy15"},
-	{KEY_JOY1+15, "joy16"},
-	{KEY_JOY1+16, "joy17"},
-	{KEY_JOY1+17, "joy18"},
-	{KEY_JOY1+18, "joy19"},
-	{KEY_JOY1+19, "joy20"},
-	{KEY_JOY1+20, "joy21"},
-	{KEY_JOY1+21, "joy22"},
-	{KEY_JOY1+22, "joy23"},
-	{KEY_JOY1+23, "joy24"},
-	{KEY_JOY1+24, "joy25"},
-	{KEY_JOY1+25, "joy26"},
-	{KEY_JOY1+26, "joy27"},
-	{KEY_JOY1+27, "joy28"},
-	{KEY_JOY1+28, "joy29"},
-	{KEY_JOY1+29, "joy30"},
-	{KEY_JOY1+30, "joy31"},
-	{KEY_JOY1+31, "joy32"},
-#endif
-	// the DOS version uses Allegro's joystick support
-	{KEY_HAT1+0, "hatup"},
-	{KEY_HAT1+1, "hatdown"},
-	{KEY_HAT1+2, "hatleft"},
-	{KEY_HAT1+3, "hatright"},
-	{KEY_HAT1+4, "hatup2"},
-	{KEY_HAT1+5, "hatdown2"},
-	{KEY_HAT1+6, "hatleft2"},
-	{KEY_HAT1+7, "hatright2"},
-	{KEY_HAT1+8, "hatup3"},
-	{KEY_HAT1+9, "hatdown3"},
-	{KEY_HAT1+10, "hatleft3"},
-	{KEY_HAT1+11, "hatright3"},
-	{KEY_HAT1+12, "hatup4"},
-	{KEY_HAT1+13, "hatdown4"},
-	{KEY_HAT1+14, "hatleft4"},
-	{KEY_HAT1+15, "hatright4"},
+#define DEF_GAMEPAD_NAME(btn, name) {KEY_GAMEPAD+GAMEPAD_BUTTON_##btn, name}
+#define DEF_GAMEPAD_AXIS(ax, name) \
+	{KEY_AXES+GAMEPAD_AXIS_##ax, name}, \
+	{KEY_INV_AXES+GAMEPAD_AXIS_##ax, name "-"}
+
+	DEF_GAMEPAD_NAME(A, "a button"),
+	DEF_GAMEPAD_NAME(B, "b button"),
+	DEF_GAMEPAD_NAME(X, "x button"),
+	DEF_GAMEPAD_NAME(Y, "y button"),
+
+	DEF_GAMEPAD_NAME(BACK, "back button"),
+	DEF_GAMEPAD_NAME(GUIDE, "guide button"),
+	DEF_GAMEPAD_NAME(START, "start button"),
+	DEF_GAMEPAD_NAME(LEFTSTICK, "left stick"),
+	DEF_GAMEPAD_NAME(RIGHTSTICK, "right stick"),
+
+	DEF_GAMEPAD_NAME(LEFTSHOULDER, "left shoulder"),
+	DEF_GAMEPAD_NAME(RIGHTSHOULDER, "right shoulder"),
+
+	DEF_GAMEPAD_NAME(DPAD_UP, "d-pad up"),
+	DEF_GAMEPAD_NAME(DPAD_DOWN, "d-pad down"),
+	DEF_GAMEPAD_NAME(DPAD_LEFT, "d-pad left"),
+	DEF_GAMEPAD_NAME(DPAD_RIGHT, "d-pad right"),
+
+	DEF_GAMEPAD_NAME(MISC1, "gamepad misc 1"),
+	DEF_GAMEPAD_NAME(PADDLE1, "paddle 1"),
+	DEF_GAMEPAD_NAME(PADDLE2, "paddle 2"),
+	DEF_GAMEPAD_NAME(PADDLE3, "paddle 3"),
+	DEF_GAMEPAD_NAME(PADDLE4, "paddle 4"),
+	DEF_GAMEPAD_NAME(TOUCHPAD, "touchpad"),
+
+	DEF_GAMEPAD_AXIS(LEFTX, "left stick x"),
+	DEF_GAMEPAD_AXIS(LEFTY, "left stick y"),
+
+	DEF_GAMEPAD_AXIS(RIGHTX, "right stick x"),
+	DEF_GAMEPAD_AXIS(RIGHTY, "right stick y"),
+
+	DEF_GAMEPAD_AXIS(TRIGGERLEFT, "left trigger"),
+	DEF_GAMEPAD_AXIS(TRIGGERRIGHT, "right trigger"),
+
+#undef DEF_GAMEPAD_NAME
+#undef DEF_GAMEPAD_AXIS
 
 	{KEY_DBLMOUSE1+0, "dblmouse1"},
 	{KEY_DBLMOUSE1+1, "dblmouse2"},
@@ -387,172 +973,154 @@ static keyname_t keynames[] =
 	{KEY_DBLMOUSE1+5, "dblmouse6"},
 	{KEY_DBLMOUSE1+6, "dblmouse7"},
 	{KEY_DBLMOUSE1+7, "dblmouse8"},
-	{KEY_DBL2MOUSE1+0, "dblsec_mouse2"}, // BP: sorry my mouse handler swap button 1 and 2
-	{KEY_DBL2MOUSE1+1, "dblsec_mouse1"},
+	{KEY_DBL2MOUSE1+0, "dblsec_mouse1"},
+	{KEY_DBL2MOUSE1+1, "dblsec_mouse2"},
 	{KEY_DBL2MOUSE1+2, "dblsec_mouse3"},
 	{KEY_DBL2MOUSE1+3, "dblsec_mouse4"},
 	{KEY_DBL2MOUSE1+4, "dblsec_mouse5"},
 	{KEY_DBL2MOUSE1+5, "dblsec_mouse6"},
 	{KEY_DBL2MOUSE1+6, "dblsec_mouse7"},
-	{KEY_DBL2MOUSE1+7, "dblsec_mouse8"},
-
-	{KEY_DBLJOY1+0, "dbljoy1"},
-	{KEY_DBLJOY1+1, "dbljoy2"},
-	{KEY_DBLJOY1+2, "dbljoy3"},
-	{KEY_DBLJOY1+3, "dbljoy4"},
-	{KEY_DBLJOY1+4, "dbljoy5"},
-	{KEY_DBLJOY1+5, "dbljoy6"},
-	{KEY_DBLJOY1+6, "dbljoy7"},
-	{KEY_DBLJOY1+7, "dbljoy8"},
-#if !defined (NOMOREJOYBTN_1DBL)
-	{KEY_DBLJOY1+8, "dbljoy9"},
-	{KEY_DBLJOY1+9, "dbljoy10"},
-	{KEY_DBLJOY1+10, "dbljoy11"},
-	{KEY_DBLJOY1+11, "dbljoy12"},
-	{KEY_DBLJOY1+12, "dbljoy13"},
-	{KEY_DBLJOY1+13, "dbljoy14"},
-	{KEY_DBLJOY1+14, "dbljoy15"},
-	{KEY_DBLJOY1+15, "dbljoy16"},
-	{KEY_DBLJOY1+16, "dbljoy17"},
-	{KEY_DBLJOY1+17, "dbljoy18"},
-	{KEY_DBLJOY1+18, "dbljoy19"},
-	{KEY_DBLJOY1+19, "dbljoy20"},
-	{KEY_DBLJOY1+20, "dbljoy21"},
-	{KEY_DBLJOY1+21, "dbljoy22"},
-	{KEY_DBLJOY1+22, "dbljoy23"},
-	{KEY_DBLJOY1+23, "dbljoy24"},
-	{KEY_DBLJOY1+24, "dbljoy25"},
-	{KEY_DBLJOY1+25, "dbljoy26"},
-	{KEY_DBLJOY1+26, "dbljoy27"},
-	{KEY_DBLJOY1+27, "dbljoy28"},
-	{KEY_DBLJOY1+28, "dbljoy29"},
-	{KEY_DBLJOY1+29, "dbljoy30"},
-	{KEY_DBLJOY1+30, "dbljoy31"},
-	{KEY_DBLJOY1+31, "dbljoy32"},
-#endif
-	{KEY_DBLHAT1+0, "dblhatup"},
-	{KEY_DBLHAT1+1, "dblhatdown"},
-	{KEY_DBLHAT1+2, "dblhatleft"},
-	{KEY_DBLHAT1+3, "dblhatright"},
-	{KEY_DBLHAT1+4, "dblhatup2"},
-	{KEY_DBLHAT1+5, "dblhatdown2"},
-	{KEY_DBLHAT1+6, "dblhatleft2"},
-	{KEY_DBLHAT1+7, "dblhatright2"},
-	{KEY_DBLHAT1+8, "dblhatup3"},
-	{KEY_DBLHAT1+9, "dblhatdown3"},
-	{KEY_DBLHAT1+10, "dblhatleft3"},
-	{KEY_DBLHAT1+11, "dblhatright3"},
-	{KEY_DBLHAT1+12, "dblhatup4"},
-	{KEY_DBLHAT1+13, "dblhatdown4"},
-	{KEY_DBLHAT1+14, "dblhatleft4"},
-	{KEY_DBLHAT1+15, "dblhatright4"},
-
-	{KEY_2JOY1+0, "sec_joy1"},
-	{KEY_2JOY1+1, "sec_joy2"},
-	{KEY_2JOY1+2, "sec_joy3"},
-	{KEY_2JOY1+3, "sec_joy4"},
-	{KEY_2JOY1+4, "sec_joy5"},
-	{KEY_2JOY1+5, "sec_joy6"},
-	{KEY_2JOY1+6, "sec_joy7"},
-	{KEY_2JOY1+7, "sec_joy8"},
-#if !defined (NOMOREJOYBTN_2S)
-	// we use up to 32 buttons in DirectInput
-	{KEY_2JOY1+8, "sec_joy9"},
-	{KEY_2JOY1+9, "sec_joy10"},
-	{KEY_2JOY1+10, "sec_joy11"},
-	{KEY_2JOY1+11, "sec_joy12"},
-	{KEY_2JOY1+12, "sec_joy13"},
-	{KEY_2JOY1+13, "sec_joy14"},
-	{KEY_2JOY1+14, "sec_joy15"},
-	{KEY_2JOY1+15, "sec_joy16"},
-	{KEY_2JOY1+16, "sec_joy17"},
-	{KEY_2JOY1+17, "sec_joy18"},
-	{KEY_2JOY1+18, "sec_joy19"},
-	{KEY_2JOY1+19, "sec_joy20"},
-	{KEY_2JOY1+20, "sec_joy21"},
-	{KEY_2JOY1+21, "sec_joy22"},
-	{KEY_2JOY1+22, "sec_joy23"},
-	{KEY_2JOY1+23, "sec_joy24"},
-	{KEY_2JOY1+24, "sec_joy25"},
-	{KEY_2JOY1+25, "sec_joy26"},
-	{KEY_2JOY1+26, "sec_joy27"},
-	{KEY_2JOY1+27, "sec_joy28"},
-	{KEY_2JOY1+28, "sec_joy29"},
-	{KEY_2JOY1+29, "sec_joy30"},
-	{KEY_2JOY1+30, "sec_joy31"},
-	{KEY_2JOY1+31, "sec_joy32"},
-#endif
-	// the DOS version uses Allegro's joystick support
-	{KEY_2HAT1+0,  "sec_hatup"},
-	{KEY_2HAT1+1,  "sec_hatdown"},
-	{KEY_2HAT1+2,  "sec_hatleft"},
-	{KEY_2HAT1+3,  "sec_hatright"},
-	{KEY_2HAT1+4, "sec_hatup2"},
-	{KEY_2HAT1+5, "sec_hatdown2"},
-	{KEY_2HAT1+6, "sec_hatleft2"},
-	{KEY_2HAT1+7, "sec_hatright2"},
-	{KEY_2HAT1+8, "sec_hatup3"},
-	{KEY_2HAT1+9, "sec_hatdown3"},
-	{KEY_2HAT1+10, "sec_hatleft3"},
-	{KEY_2HAT1+11, "sec_hatright3"},
-	{KEY_2HAT1+12, "sec_hatup4"},
-	{KEY_2HAT1+13, "sec_hatdown4"},
-	{KEY_2HAT1+14, "sec_hatleft4"},
-	{KEY_2HAT1+15, "sec_hatright4"},
-
-	{KEY_DBL2JOY1+0, "dblsec_joy1"},
-	{KEY_DBL2JOY1+1, "dblsec_joy2"},
-	{KEY_DBL2JOY1+2, "dblsec_joy3"},
-	{KEY_DBL2JOY1+3, "dblsec_joy4"},
-	{KEY_DBL2JOY1+4, "dblsec_joy5"},
-	{KEY_DBL2JOY1+5, "dblsec_joy6"},
-	{KEY_DBL2JOY1+6, "dblsec_joy7"},
-	{KEY_DBL2JOY1+7, "dblsec_joy8"},
-#if !defined (NOMOREJOYBTN_2DBL)
-	{KEY_DBL2JOY1+8, "dblsec_joy9"},
-	{KEY_DBL2JOY1+9, "dblsec_joy10"},
-	{KEY_DBL2JOY1+10, "dblsec_joy11"},
-	{KEY_DBL2JOY1+11, "dblsec_joy12"},
-	{KEY_DBL2JOY1+12, "dblsec_joy13"},
-	{KEY_DBL2JOY1+13, "dblsec_joy14"},
-	{KEY_DBL2JOY1+14, "dblsec_joy15"},
-	{KEY_DBL2JOY1+15, "dblsec_joy16"},
-	{KEY_DBL2JOY1+16, "dblsec_joy17"},
-	{KEY_DBL2JOY1+17, "dblsec_joy18"},
-	{KEY_DBL2JOY1+18, "dblsec_joy19"},
-	{KEY_DBL2JOY1+19, "dblsec_joy20"},
-	{KEY_DBL2JOY1+20, "dblsec_joy21"},
-	{KEY_DBL2JOY1+21, "dblsec_joy22"},
-	{KEY_DBL2JOY1+22, "dblsec_joy23"},
-	{KEY_DBL2JOY1+23, "dblsec_joy24"},
-	{KEY_DBL2JOY1+24, "dblsec_joy25"},
-	{KEY_DBL2JOY1+25, "dblsec_joy26"},
-	{KEY_DBL2JOY1+26, "dblsec_joy27"},
-	{KEY_DBL2JOY1+27, "dblsec_joy28"},
-	{KEY_DBL2JOY1+28, "dblsec_joy29"},
-	{KEY_DBL2JOY1+29, "dblsec_joy30"},
-	{KEY_DBL2JOY1+30, "dblsec_joy31"},
-	{KEY_DBL2JOY1+31, "dblsec_joy32"},
-#endif
-	{KEY_DBL2HAT1+0, "dblsec_hatup"},
-	{KEY_DBL2HAT1+1, "dblsec_hatdown"},
-	{KEY_DBL2HAT1+2, "dblsec_hatleft"},
-	{KEY_DBL2HAT1+3, "dblsec_hatright"},
-	{KEY_DBL2HAT1+4, "dblsec_hatup2"},
-	{KEY_DBL2HAT1+5, "dblsec_hatdown2"},
-	{KEY_DBL2HAT1+6, "dblsec_hatleft2"},
-	{KEY_DBL2HAT1+7, "dblsec_hatright2"},
-	{KEY_DBL2HAT1+8, "dblsec_hatup3"},
-	{KEY_DBL2HAT1+9, "dblsec_hatdown3"},
-	{KEY_DBL2HAT1+10, "dblsec_hatleft3"},
-	{KEY_DBL2HAT1+11, "dblsec_hatright3"},
-	{KEY_DBL2HAT1+12, "dblsec_hatup4"},
-	{KEY_DBL2HAT1+13, "dblsec_hatdown4"},
-	{KEY_DBL2HAT1+14, "dblsec_hatleft4"},
-	{KEY_DBL2HAT1+15, "dblsec_hatright4"},
-
+	{KEY_DBL2MOUSE1+7, "dblsec_mouse8"}
 };
 
+#define NUMKEYNAMES (sizeof(keynames) / sizeof(keyname_t))
+
+static keyname_t displaykeynames[] =
+{
+	{KEY_SPACE, "Space Bar"},
+	{KEY_CAPSLOCK, "Caps Lock"},
+	{KEY_ENTER, "Enter"},
+	{KEY_TAB, "Tab"},
+	{KEY_ESCAPE, "Escape"},
+	{KEY_BACKSPACE, "Backspace"},
+
+	{KEY_NUMLOCK, "Num Lock"},
+	{KEY_SCROLLLOCK, "Scroll Lock"},
+
+#ifdef _WIN32
+	{KEY_LEFTWIN, "Left Windows"},
+	{KEY_RIGHTWIN, "Right Windows"},
+#else
+	{KEY_LEFTWIN, "Left Super"},
+	{KEY_RIGHTWIN, "Right Super"},
+#endif
+
+	{KEY_MENU, "Menu"},
+
+	{KEY_LSHIFT, "Left Shift"},
+	{KEY_RSHIFT, "Right Shift"},
+	{KEY_LCTRL, "Left Ctrl"},
+	{KEY_RCTRL, "Right Ctrl"},
+	{KEY_LALT, "Left Alt"},
+	{KEY_RALT, "Right Alt"},
+
+	{KEY_KEYPAD0, "Keypad 0"},
+	{KEY_KEYPAD1, "Keypad 1"},
+	{KEY_KEYPAD2, "Keypad 2"},
+	{KEY_KEYPAD3, "Keypad 3"},
+	{KEY_KEYPAD4, "Keypad 4"},
+	{KEY_KEYPAD5, "Keypad 5"},
+	{KEY_KEYPAD6, "Keypad 6"},
+	{KEY_KEYPAD7, "Keypad 7"},
+	{KEY_KEYPAD8, "Keypad 8"},
+	{KEY_KEYPAD9, "Keypad 9"},
+	{KEY_PLUSPAD, "Keypad +"},
+	{KEY_MINUSPAD, "Keypad -"},
+	{KEY_KPADSLASH, "Keypad /"},
+	{KEY_KPADDEL, "Keypad ."},
+
+	{KEY_UPARROW, "Up Arrow"},
+	{KEY_DOWNARROW, "Down Arrow"},
+	{KEY_LEFTARROW, "Left Arrow"},
+	{KEY_RIGHTARROW, "Right Arrow"},
+
+	{KEY_HOME, "Home"},
+	{KEY_END, "End"},
+	{KEY_PGUP, "Page Up"},
+	{KEY_PGDN, "Page Down"},
+	{KEY_INS, "Insert"},
+	{KEY_DEL, "Delete"},
+
+	{KEY_F1, "F1"},
+	{KEY_F2, "F2"},
+	{KEY_F3, "F3"},
+	{KEY_F4, "F4"},
+	{KEY_F5, "F5"},
+	{KEY_F6, "F6"},
+	{KEY_F7, "F7"},
+	{KEY_F8, "F8"},
+	{KEY_F9, "F9"},
+	{KEY_F10, "F10"},
+	{KEY_F11, "F11"},
+	{KEY_F12, "F12"},
+
+	{'`', "Tilde"},
+	{KEY_PAUSE, "Pause/Break"},
+
+	{KEY_MOUSE1+0, "Left Mouse Button"},
+	{KEY_MOUSE1+1, "Right Mouse Button"},
+	{KEY_MOUSE1+2, "Middle Mouse Button"},
+	{KEY_MOUSE1+3, "X1 Mouse Button"},
+	{KEY_MOUSE1+4, "X2 Mouse Button"},
+
+	{KEY_2MOUSE1+0, "Sec. Mouse Left Button"},
+	{KEY_2MOUSE1+1, "Sec. Mouse Right Button"},
+	{KEY_2MOUSE1+2, "Sec. Mouse Middle Button"},
+	{KEY_2MOUSE1+3, "Sec. Mouse X1 Button"},
+	{KEY_2MOUSE1+4, "Sec. Mouse X2 Button"},
+
+	{KEY_MOUSEWHEELUP, "Mouse Wheel Up"},
+	{KEY_MOUSEWHEELDOWN, "Mouse Wheel Down"},
+	{KEY_2MOUSEWHEELUP, "Sec. Mouse Wheel Up"},
+	{KEY_2MOUSEWHEELDOWN, "Sec. Mouse Wheel Down"},
+
+#define DEF_GAMEPAD_NAME(btn, name) {KEY_GAMEPAD+GAMEPAD_BUTTON_##btn, name}
+#define DEF_GAMEPAD_AXIS(ax, name) {KEY_AXES+GAMEPAD_AXIS_##ax, name}
+
+	DEF_GAMEPAD_NAME(A, "A Button"),
+	DEF_GAMEPAD_NAME(B, "B Button"),
+	DEF_GAMEPAD_NAME(X, "X Button"),
+	DEF_GAMEPAD_NAME(Y, "Y Button"),
+
+	DEF_GAMEPAD_NAME(BACK, "Back Button"),
+	DEF_GAMEPAD_NAME(GUIDE, "Guide Button"),
+	DEF_GAMEPAD_NAME(START, "Start Button"),
+	DEF_GAMEPAD_NAME(LEFTSTICK, "Left Stick Button"),
+	DEF_GAMEPAD_NAME(RIGHTSTICK, "Right Stick Button"),
+
+	DEF_GAMEPAD_NAME(LEFTSHOULDER, "Left Shoulder"),
+	DEF_GAMEPAD_NAME(RIGHTSHOULDER, "Right Shoulder"),
+
+	DEF_GAMEPAD_NAME(DPAD_UP, "D-Pad Up"),
+	DEF_GAMEPAD_NAME(DPAD_DOWN, "D-Pad Down"),
+	DEF_GAMEPAD_NAME(DPAD_LEFT, "D-Pad Left"),
+	DEF_GAMEPAD_NAME(DPAD_RIGHT, "D-Pad Right"),
+
+	DEF_GAMEPAD_NAME(MISC1, "Gamepad Misc. 1"),
+	DEF_GAMEPAD_NAME(PADDLE1, "Paddle 1"),
+	DEF_GAMEPAD_NAME(PADDLE2, "Paddle 2"),
+	DEF_GAMEPAD_NAME(PADDLE3, "Paddle 3"),
+	DEF_GAMEPAD_NAME(PADDLE4, "Paddle 4"),
+	DEF_GAMEPAD_NAME(TOUCHPAD, "Touchpad"),
+
+	{KEY_INV_AXES + GAMEPAD_AXIS_LEFTX, "Left Stick \x1C"},
+	{KEY_AXES     + GAMEPAD_AXIS_LEFTX, "Left Stick \x1D"},
+	{KEY_INV_AXES + GAMEPAD_AXIS_LEFTY, "Left Stick \x1A"},
+	{KEY_AXES     + GAMEPAD_AXIS_LEFTY, "Left Stick \x1B"},
+	{KEY_INV_AXES + GAMEPAD_AXIS_RIGHTX, "Right Stick \x1C"},
+	{KEY_AXES     + GAMEPAD_AXIS_RIGHTX, "Right Stick \x1D"},
+	{KEY_INV_AXES + GAMEPAD_AXIS_RIGHTY, "Right Stick \x1A"},
+	{KEY_AXES     + GAMEPAD_AXIS_RIGHTY, "Right Stick \x1B"},
+
+	DEF_GAMEPAD_AXIS(TRIGGERLEFT, "Left Trigger"),
+	DEF_GAMEPAD_AXIS(TRIGGERRIGHT, "Right Trigger"),
+
+#undef DEF_GAMEPAD_NAME
+#undef DEF_GAMEPAD_AXIS
+};
+
+#define NUMDISPLAYKEYNAMES (sizeof(displaykeynames) / sizeof(keyname_t))
+
 static const char *gamecontrolname[NUM_GAMECONTROLS] =
 {
 	"nothing", // a key/button mapped to GC_NULL has no effect
@@ -599,8 +1167,6 @@ static const char *gamecontrolname[NUM_GAMECONTROLS] =
 	"custom3",
 };
 
-#define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t))
-
 //
 // Detach any keys associated to the given game control
 // - pass the pointer to the gamecontrol table for the player being edited
@@ -621,7 +1187,7 @@ void G_ClearAllControlKeys(void)
 }
 
 //
-// Returns the name of a key (or virtual key for mouse and joy)
+// Returns the name of a key (or virtual key for mouse and gamepad)
 // the input value being an keynum
 //
 const char *G_KeyNumToName(INT32 keynum)
@@ -644,7 +1210,44 @@ const char *G_KeyNumToName(INT32 keynum)
 			return keynames[j].name;
 
 	// create a name for unknown keys
-	sprintf(keynamestr, "KEY%d", keynum);
+	snprintf(keynamestr, sizeof keynamestr, "KEY%d", keynum);
+	return keynamestr;
+}
+
+const char *G_GetDisplayNameForKey(INT32 keynum)
+{
+	static char keynamestr[32];
+
+	UINT32 j;
+
+	// find a description for special keys
+	for (j = 0; j < NUMDISPLAYKEYNAMES; j++)
+		if (displaykeynames[j].keynum == keynum)
+			return displaykeynames[j].name;
+
+	// return a string with the ascii char if displayable
+	if (keynum > ' ' && keynum <= 'z' && keynum != KEY_CONSOLE)
+	{
+		snprintf(keynamestr, sizeof keynamestr, "%c Key", toupper((char)keynum));
+		return keynamestr;
+	}
+
+	// unnamed mouse buttons
+	if (keynum >= KEY_MOUSE1 && keynum <= KEY_MOUSE1+7)
+	{
+		j = (keynum - KEY_MOUSE1) + 1;
+		snprintf(keynamestr, sizeof keynamestr, "Mouse Button #%d", j);
+		return keynamestr;
+	}
+	else if (keynum >= KEY_2MOUSE1 && keynum <= KEY_2MOUSE1+7)
+	{
+		j = (keynum - KEY_2MOUSE1) + 1;
+		snprintf(keynamestr, sizeof keynamestr, "Sec. Mouse Button #%d", j);
+		return keynamestr;
+	}
+
+	// create a name for unknown keys
+	snprintf(keynamestr, sizeof keynamestr, "Unknown Key %d", keynum);
 	return keynamestr;
 }
 
@@ -671,6 +1274,36 @@ INT32 G_KeyNameToNum(const char *keystr)
 	return 0;
 }
 
+const char *G_GamepadTypeToString(gamepadtype_e type)
+{
+	static const char *names[] = {
+		"xbox-360",
+		"xbox-one",
+		"xbox-series-xs",
+		"xbox-elite",
+		"ps3",
+		"ps4",
+		"ps5",
+		"switch-pro",
+		"switch-joy-con-grip",
+		"switch-joy-con-left",
+		"switch-joy-con-right",
+		"stadia",
+		"amazon-luna",
+		"steam-controller",
+		"virtual",
+		"unknown"
+	};
+
+	return names[type];
+}
+
+void G_InitGamepads(void)
+{
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+		gamepads[i].num = i;
+}
+
 void G_DefineDefaultControls(void)
 {
 	INT32 i;
@@ -738,36 +1371,36 @@ void G_DefineDefaultControls(void)
 		gamecontroldefault[i][GC_VIEWPOINT  ][0] = KEY_F12;
 
 		// Gamepad controls -- same for both schemes
-		gamecontroldefault[i][GC_JUMP       ][1] = KEY_JOY1+0; // A
-		gamecontroldefault[i][GC_SPIN       ][1] = KEY_JOY1+2; // X
-		gamecontroldefault[i][GC_CUSTOM1    ][1] = KEY_JOY1+1; // B
-		gamecontroldefault[i][GC_CUSTOM2    ][1] = KEY_JOY1+3; // Y
-		gamecontroldefault[i][GC_CUSTOM3    ][1] = KEY_JOY1+8; // Left Stick
-		gamecontroldefault[i][GC_CENTERVIEW ][1] = KEY_JOY1+9; // Right Stick
-		gamecontroldefault[i][GC_WEAPONPREV ][1] = KEY_JOY1+4; // LB
-		gamecontroldefault[i][GC_WEAPONNEXT ][1] = KEY_JOY1+5; // RB
-		gamecontroldefault[i][GC_SCREENSHOT ][1] = KEY_JOY1+6; // Back
-		gamecontroldefault[i][GC_SYSTEMMENU ][0] = KEY_JOY1+7; // Start
-		gamecontroldefault[i][GC_CAMTOGGLE  ][1] = KEY_HAT1+0; // D-Pad Up
-		gamecontroldefault[i][GC_VIEWPOINT  ][1] = KEY_HAT1+1; // D-Pad Down
-		gamecontroldefault[i][GC_TOSSFLAG   ][1] = KEY_HAT1+2; // D-Pad Left
-		gamecontroldefault[i][GC_SCORES     ][1] = KEY_HAT1+3; // D-Pad Right
+		gamecontroldefault[i][GC_JUMP       ][1] = GAMEPAD_KEY(A); // A
+		gamecontroldefault[i][GC_SPIN       ][1] = GAMEPAD_KEY(X); // X
+		gamecontroldefault[i][GC_CUSTOM1    ][1] = GAMEPAD_KEY(B); // B
+		gamecontroldefault[i][GC_CUSTOM2    ][1] = GAMEPAD_KEY(Y); // Y
+		gamecontroldefault[i][GC_CUSTOM3    ][1] = GAMEPAD_KEY(LEFTSTICK); // Left Stick
+		gamecontroldefault[i][GC_CENTERVIEW ][1] = GAMEPAD_KEY(RIGHTSTICK); // Right Stick
+		gamecontroldefault[i][GC_WEAPONPREV ][1] = GAMEPAD_KEY(LEFTSHOULDER); // LB
+		gamecontroldefault[i][GC_WEAPONNEXT ][1] = GAMEPAD_KEY(RIGHTSHOULDER); // RB
+		gamecontroldefault[i][GC_SCREENSHOT ][1] = GAMEPAD_KEY(BACK); // Back
+		gamecontroldefault[i][GC_SYSTEMMENU ][0] = GAMEPAD_KEY(START); // Start
+		gamecontroldefault[i][GC_CAMTOGGLE  ][1] = GAMEPAD_KEY(DPAD_UP); // D-Pad Up
+		gamecontroldefault[i][GC_VIEWPOINT  ][1] = GAMEPAD_KEY(DPAD_DOWN); // D-Pad Down
+		gamecontroldefault[i][GC_TOSSFLAG   ][1] = GAMEPAD_KEY(DPAD_LEFT); // D-Pad Left
+		gamecontroldefault[i][GC_SCORES     ][1] = GAMEPAD_KEY(DPAD_RIGHT); // D-Pad Right
 
-		// Second player controls only have joypad defaults
-		gamecontrolbisdefault[i][GC_JUMP       ][1] = KEY_2JOY1+0; // A
-		gamecontrolbisdefault[i][GC_SPIN       ][1] = KEY_2JOY1+2; // X
-		gamecontrolbisdefault[i][GC_CUSTOM1    ][1] = KEY_2JOY1+1; // B
-		gamecontrolbisdefault[i][GC_CUSTOM2    ][1] = KEY_2JOY1+3; // Y
-		gamecontrolbisdefault[i][GC_CUSTOM3    ][1] = KEY_2JOY1+8; // Left Stick
-		gamecontrolbisdefault[i][GC_CENTERVIEW ][1] = KEY_2JOY1+9; // Right Stick
-		gamecontrolbisdefault[i][GC_WEAPONPREV ][1] = KEY_2JOY1+4; // LB
-		gamecontrolbisdefault[i][GC_WEAPONNEXT ][1] = KEY_2JOY1+5; // RB
-		gamecontrolbisdefault[i][GC_SCREENSHOT ][1] = KEY_2JOY1+6; // Back
-		//gamecontrolbisdefault[i][GC_SYSTEMMENU ][0] = KEY_2JOY1+7; // Start
-		gamecontrolbisdefault[i][GC_CAMTOGGLE  ][1] = KEY_2HAT1+0; // D-Pad Up
-		gamecontrolbisdefault[i][GC_VIEWPOINT  ][1] = KEY_2HAT1+1; // D-Pad Down
-		gamecontrolbisdefault[i][GC_TOSSFLAG   ][1] = KEY_2HAT1+2; // D-Pad Left
-		//gamecontrolbisdefault[i][GC_SCORES     ][1] = KEY_2HAT1+3; // D-Pad Right
+		// Second player only has gamepad defaults
+		gamecontrolbisdefault[i][GC_JUMP       ][1] = GAMEPAD_KEY(A); // A
+		gamecontrolbisdefault[i][GC_SPIN       ][1] = GAMEPAD_KEY(X); // X
+		gamecontrolbisdefault[i][GC_CUSTOM1    ][1] = GAMEPAD_KEY(B); // B
+		gamecontrolbisdefault[i][GC_CUSTOM2    ][1] = GAMEPAD_KEY(Y); // Y
+		gamecontrolbisdefault[i][GC_CUSTOM3    ][1] = GAMEPAD_KEY(LEFTSTICK); // Left Stick
+		gamecontrolbisdefault[i][GC_CENTERVIEW ][1] = GAMEPAD_KEY(RIGHTSTICK); // Right Stick
+		gamecontrolbisdefault[i][GC_WEAPONPREV ][1] = GAMEPAD_KEY(LEFTSHOULDER); // LB
+		gamecontrolbisdefault[i][GC_WEAPONNEXT ][1] = GAMEPAD_KEY(RIGHTSHOULDER); // RB
+		gamecontrolbisdefault[i][GC_SCREENSHOT ][1] = GAMEPAD_KEY(BACK); // Back
+		//gamecontrolbisdefault[i][GC_SYSTEMMENU ][0] = GAMEPAD_KEY(START); // Start
+		gamecontrolbisdefault[i][GC_CAMTOGGLE  ][1] = GAMEPAD_KEY(DPAD_UP); // D-Pad Up
+		gamecontrolbisdefault[i][GC_VIEWPOINT  ][1] = GAMEPAD_KEY(DPAD_DOWN); // D-Pad Down
+		gamecontrolbisdefault[i][GC_TOSSFLAG   ][1] = GAMEPAD_KEY(DPAD_LEFT); // D-Pad Left
+		//gamecontrolbisdefault[i][GC_SCORES     ][1] = GAMEPAD_KEY(DPAD_RIGHT); // D-Pad Right
 	}
 }
 
diff --git a/src/g_input.h b/src/g_input.h
index bf6ad39b3..bfe4724f8 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -8,7 +8,7 @@
 // See the 'LICENSE' file for more details.
 //-----------------------------------------------------------------------------
 /// \file  g_input.h
-/// \brief handle mouse/keyboard/joystick inputs,
+/// \brief handle mouse/keyboard/gamepad inputs,
 ///        maps inputs to game controls (forward, spin, jump...)
 
 #ifndef __G_INPUT__
@@ -17,45 +17,178 @@
 #include "d_event.h"
 #include "keys.h"
 #include "command.h"
+#include "m_fixed.h"
 
 // number of total 'button' inputs, include keyboard keys, plus virtual
 // keys (mousebuttons and joybuttons becomes keys)
 #define NUMKEYS 256
 
+// Max gamepads that can be used by every player
+#define NUM_GAMEPADS 2
+
+// Max gamepads that can be detected
+#define MAX_CONNECTED_GAMEPADS 4
+
+// Max mouse buttons
 #define MOUSEBUTTONS 8
-#define JOYBUTTONS   32 // 32 buttons
-#define JOYHATS      4  // 4 hats
-#define JOYAXISSET   4  // 4 Sets of 2 axises
+
+typedef enum
+{
+	GAMEPAD_TYPE_UNKNOWN,
+
+	GAMEPAD_TYPE_XBOX360,
+	GAMEPAD_TYPE_XBOXONE,
+	GAMEPAD_TYPE_XBOX_SERIES_XS,
+	GAMEPAD_TYPE_XBOX_ELITE,
+
+	GAMEPAD_TYPE_PS3,
+	GAMEPAD_TYPE_PS4,
+	GAMEPAD_TYPE_PS5,
+
+	GAMEPAD_TYPE_NINTENDO_SWITCH_PRO,
+	GAMEPAD_TYPE_NINTENDO_SWITCH_JOY_CON_GRIP,
+	GAMEPAD_TYPE_NINTENDO_SWITCH_JOY_CON_LEFT,
+	GAMEPAD_TYPE_NINTENDO_SWITCH_JOY_CON_RIGHT,
+
+	GAMEPAD_TYPE_GOOGLE_STADIA,
+	GAMEPAD_TYPE_AMAZON_LUNA,
+	GAMEPAD_TYPE_STEAM_CONTROLLER,
+
+	GAMEPAD_TYPE_VIRTUAL
+} gamepadtype_e;
+
+boolean G_GamepadTypeIsXbox(gamepadtype_e type);
+boolean G_GamepadTypeIsPlayStation(gamepadtype_e type);
+boolean G_GamepadTypeIsNintendoSwitch(gamepadtype_e type);
+boolean G_GamepadTypeIsJoyCon(gamepadtype_e type);
+
+const char *G_GamepadTypeToString(gamepadtype_e type);
+
+typedef enum
+{
+	GAMEPAD_BUTTON_A,
+	GAMEPAD_BUTTON_B,
+	GAMEPAD_BUTTON_X,
+	GAMEPAD_BUTTON_Y,
+	GAMEPAD_BUTTON_BACK,
+	GAMEPAD_BUTTON_GUIDE,
+	GAMEPAD_BUTTON_START,
+	GAMEPAD_BUTTON_LEFTSTICK,
+	GAMEPAD_BUTTON_RIGHTSTICK,
+	GAMEPAD_BUTTON_LEFTSHOULDER,
+	GAMEPAD_BUTTON_RIGHTSHOULDER,
+	GAMEPAD_BUTTON_DPAD_UP,
+	GAMEPAD_BUTTON_DPAD_DOWN,
+	GAMEPAD_BUTTON_DPAD_LEFT,
+	GAMEPAD_BUTTON_DPAD_RIGHT,
+
+	// According to SDL, this button can be:
+	// the Xbox Series X|S share button
+	// the PS5 microphone button
+	// the Nintendo Switch (Pro or Joy-Con) capture button
+	// the Amazon Luna microphone button
+	GAMEPAD_BUTTON_MISC1,
+
+	// Xbox Elite paddles
+	GAMEPAD_BUTTON_PADDLE1,
+	GAMEPAD_BUTTON_PADDLE2,
+	GAMEPAD_BUTTON_PADDLE3,
+	GAMEPAD_BUTTON_PADDLE4,
+
+	// PS4/PS5 touchpad button
+	GAMEPAD_BUTTON_TOUCHPAD,
+
+	NUM_GAMEPAD_BUTTONS
+} gamepad_button_e;
+
+typedef enum
+{
+	GAMEPAD_AXIS_LEFTX,
+	GAMEPAD_AXIS_LEFTY,
+	GAMEPAD_AXIS_RIGHTX,
+	GAMEPAD_AXIS_RIGHTY,
+	GAMEPAD_AXIS_TRIGGERLEFT,
+	GAMEPAD_AXIS_TRIGGERRIGHT,
+
+	NUM_GAMEPAD_AXES
+} gamepad_axis_e;
+
+extern const char *const gamepad_button_names[NUM_GAMEPAD_BUTTONS + 1];
+extern const char *const gamepad_axis_names[NUM_GAMEPAD_AXES + 1];
+
+// Haptic effects
+typedef struct
+{
+	fixed_t large_magnitude; // Magnitude of the large motor
+	fixed_t small_magnitude; // Magnitude of the small motor
+	tic_t duration;          // The total duration of the effect, in tics
+} haptic_t;
+
+// Gamepad info for each player on the system
+typedef struct
+{
+	// Gamepad index
+	UINT8 num;
+
+	// Gamepad is connected and being used by a player
+	boolean connected;
+
+	// What kind of controller this is (Xbox 360, DualShock, Joy-Con, etc.)
+	gamepadtype_e type;
+
+	// Treat this gamepad's axes as if it they were buttons
+	boolean digital;
+
+	struct {
+		boolean supported; // Gamepad can rumble
+		boolean active;    // Rumble is active
+		boolean paused;    // Rumble is paused
+		haptic_t data;     // Current haptic effect status
+	} rumble;
+
+	UINT8 buttons[NUM_GAMEPAD_BUTTONS]; // Current state of all buttons
+	INT16 axes[NUM_GAMEPAD_AXES]; // Current state of all axes
+} gamepad_t;
+
+void G_InitGamepads(void);
+
+typedef enum
+{
+	GAMEPAD_STRING_DEFAULT, // A
+	GAMEPAD_STRING_MENU1,   // A Button
+	GAMEPAD_STRING_MENU2    // the A Button
+} gamepad_string_e;
+
+const char *G_GetGamepadButtonString(gamepadtype_e type, gamepad_button_e button, gamepad_string_e strtype);
+const char *G_GetGamepadAxisString(gamepadtype_e type, gamepad_axis_e button, gamepad_string_e strtype, boolean inv);
+
+extern gamepad_t gamepads[NUM_GAMEPADS];
 
 //
-// mouse and joystick buttons are handled as 'virtual' keys
+// mouse and gamepad buttons are handled as 'virtual' keys
 //
 typedef enum
 {
 	KEY_MOUSE1 = NUMKEYS,
-	KEY_JOY1 = KEY_MOUSE1 + MOUSEBUTTONS,
-	KEY_HAT1 = KEY_JOY1 + JOYBUTTONS,
+	KEY_GAMEPAD = KEY_MOUSE1 + MOUSEBUTTONS,
+	KEY_AXES = KEY_GAMEPAD + NUM_GAMEPAD_BUTTONS, // Sure, why not.
+	KEY_INV_AXES = KEY_AXES + NUM_GAMEPAD_AXES,
 
-	KEY_DBLMOUSE1 =KEY_HAT1 + JOYHATS*4, // double clicks
-	KEY_DBLJOY1 = KEY_DBLMOUSE1 + MOUSEBUTTONS,
-	KEY_DBLHAT1 = KEY_DBLJOY1 + JOYBUTTONS,
+	KEY_DBLMOUSE1 = KEY_INV_AXES + NUM_GAMEPAD_AXES, // double clicks
 
-	KEY_2MOUSE1 = KEY_DBLHAT1 + JOYHATS*4,
-	KEY_2JOY1 = KEY_2MOUSE1 + MOUSEBUTTONS,
-	KEY_2HAT1 = KEY_2JOY1 + JOYBUTTONS,
+	KEY_2MOUSE1 = KEY_DBLMOUSE1 + MOUSEBUTTONS,
+	KEY_DBL2MOUSE1 = KEY_2MOUSE1 + MOUSEBUTTONS,
 
-	KEY_DBL2MOUSE1 = KEY_2HAT1 + JOYHATS*4,
-	KEY_DBL2JOY1 = KEY_DBL2MOUSE1 + MOUSEBUTTONS,
-	KEY_DBL2HAT1 = KEY_DBL2JOY1 + JOYBUTTONS,
+	KEY_MOUSEWHEELUP = KEY_DBL2MOUSE1 + MOUSEBUTTONS,
+	KEY_MOUSEWHEELDOWN,
+	KEY_2MOUSEWHEELUP,
+	KEY_2MOUSEWHEELDOWN,
 
-	KEY_MOUSEWHEELUP = KEY_DBL2HAT1 + JOYHATS*4,
-	KEY_MOUSEWHEELDOWN = KEY_MOUSEWHEELUP + 1,
-	KEY_2MOUSEWHEELUP = KEY_MOUSEWHEELDOWN + 1,
-	KEY_2MOUSEWHEELDOWN = KEY_2MOUSEWHEELUP + 1,
-
-	NUMINPUTS = KEY_2MOUSEWHEELDOWN + 1,
+	NUMINPUTS
 } key_input_e;
 
+#define GAMEPAD_KEY(key) (KEY_GAMEPAD + GAMEPAD_BUTTON_##key)
+
 typedef enum
 {
 	GC_NULL = 0, // a key/button mapped to GC_NULL has no effect
@@ -140,19 +273,22 @@ typedef struct
 extern mouse_t mouse;
 extern mouse_t mouse2;
 
-extern INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
-
 // current state of the keys: true if pushed
 extern UINT8 gamekeydown[NUMINPUTS];
 
 // two key codes (or virtual key) per game control
 extern INT32 gamecontrol[NUM_GAMECONTROLS][2];
 extern INT32 gamecontrolbis[NUM_GAMECONTROLS][2]; // secondary splitscreen player
-extern INT32 gamecontroldefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2]; // default control storage, use 0 (gcs_custom) for memory retention
+
+// default control storage, use 0 (gcs_custom) for memory retention
+extern INT32 gamecontroldefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2];
 extern INT32 gamecontrolbisdefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2];
-#define PLAYER1INPUTDOWN(gc) (gamekeydown[gamecontrol[gc][0]] || gamekeydown[gamecontrol[gc][1]])
-#define PLAYER2INPUTDOWN(gc) (gamekeydown[gamecontrolbis[gc][0]] || gamekeydown[gamecontrolbis[gc][1]])
-#define PLAYERINPUTDOWN(p, gc) ((p) == 2 ? PLAYER2INPUTDOWN(gc) : PLAYER1INPUTDOWN(gc))
+
+boolean G_PlayerInputDown(UINT8 which, gamecontrols_e gc);
+boolean G_CheckDigitalPlayerInput(UINT8 which, gamecontrols_e gc);
+
+SINT8 G_PlayerInputIsAnalog(UINT8 which, gamecontrols_e gc, UINT8 settings);
+INT16 G_GetAnalogPlayerInput(UINT8 which, gamecontrols_e gc, UINT8 settings);
 
 #define num_gcl_tutorial_check 6
 #define num_gcl_tutorial_used 8
@@ -180,13 +316,37 @@ extern const INT32 gcl_jump_spin[num_gcl_jump_spin];
 // remaps the input event to a game control.
 void G_MapEventsToControls(event_t *ev);
 
+boolean G_RumbleSupported(UINT8 which);
+boolean G_RumbleGamepad(UINT8 which, fixed_t large_magnitude, fixed_t small_magnitude, tic_t duration);
+void G_StopGamepadRumble(UINT8 which);
+
+fixed_t G_GetLargeMotorFreq(UINT8 which);
+fixed_t G_GetSmallMotorFreq(UINT8 which);
+boolean G_GetGamepadRumblePaused(UINT8 which);
+boolean G_SetLargeMotorFreq(UINT8 which, fixed_t freq);
+boolean G_SetSmallMotorFreq(UINT8 which, fixed_t freq);
+void G_SetGamepadRumblePaused(UINT8 which, boolean pause);
+
+INT16 G_GamepadAxisEventValue(UINT8 which, INT16 value);
+INT16 G_GetGamepadAxisValue(UINT8 which, gamepad_axis_e axis);
+fixed_t G_GetAdjustedGamepadAxis(UINT8 which, gamepad_axis_e axis, boolean applyDeadzone);
+
+UINT16 G_GetGamepadDeadZone(UINT8 which);
+UINT16 G_GetGamepadDigitalDeadZone(UINT8 which);
+INT32 G_BasicDeadZoneCalculation(INT32 magnitude, const UINT16 jdeadzone);
+
+INT32 G_RemapGamepadEvent(event_t *event, INT32 *type);
+
 // returns the name of a key
 const char *G_KeyNumToName(INT32 keynum);
 INT32 G_KeyNameToNum(const char *keystr);
 
+const char *G_GetDisplayNameForKey(INT32 keynum);
+
 // detach any keys associated to the given game control
 void G_ClearControlKeys(INT32 (*setupcontrols)[2], INT32 control);
 void G_ClearAllControlKeys(void);
+
 void Command_Setcontrol_f(void);
 void Command_Setcontrol2_f(void);
 void G_DefineDefaultControls(void);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index c037abcd7..805aa694f 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -877,7 +877,7 @@ void HU_Ticker(void)
 	hu_tick++;
 	hu_tick &= 7; // currently only to blink chat input cursor
 
-	if (PLAYER1INPUTDOWN(GC_SCORES))
+	if (G_PlayerInputDown(0, GC_SCORES))
 		hu_showscores = !chat_on;
 	else
 		hu_showscores = false;
@@ -1052,26 +1052,6 @@ boolean HU_Responder(event_t *ev)
 
 	// only KeyDown events now...
 
-	/*// Shoot, to prevent P1 chatting from ruining the game for everyone else, it's either:
-	// A. completely disallow opening chat entirely in online splitscreen
-	// or B. iterate through all controls to make sure it's bound to player 1 before eating
-	// You can see which one I chose.
-	// (Unless if you're sharing a keyboard, since you probably establish when you start chatting that you have dibs on it...)
-	// (Ahhh, the good ol days when I was a kid who couldn't afford an extra USB controller...)
-
-	if (ev->key >= KEY_MOUSE1)
-	{
-		INT32 i;
-		for (i = 0; i < NUM_GAMECONTROLS; i++)
-		{
-			if (gamecontrol[i][0] == ev->key || gamecontrol[i][1] == ev->key)
-				break;
-		}
-
-		if (i == NUM_GAMECONTROLS)
-			return false;
-	}*/	//We don't actually care about that unless we get splitscreen netgames. :V
-
 #ifndef NONET
 	c = (INT32)ev->key;
 
diff --git a/src/i_gamepad.h b/src/i_gamepad.h
new file mode 100644
index 000000000..730109b54
--- /dev/null
+++ b/src/i_gamepad.h
@@ -0,0 +1,58 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  i_gamepad.h
+/// \brief Gamepads
+
+#ifndef __I_GAMEPAD_H__
+#define __I_GAMEPAD_H__
+
+#include "g_input.h"
+#include "p_haptic.h"
+
+// So m_menu knows whether to store cv_usegamepad value or string
+#define GAMEPAD_HOTPLUG
+
+// Value range for axes
+#define JOYAXISRANGE INT16_MAX
+#define OLDJOYAXISRANGE 1023
+
+// Starts all gamepads
+void I_InitGamepads(void);
+
+// Returns the number of gamepads on the system
+INT32 I_NumGamepads(void);
+
+// Changes a gamepad's device
+void I_ChangeGamepad(UINT8 which);
+
+// Toggles a gamepad's digital axis setting
+void I_SetGamepadDigital(UINT8 which, boolean enable);
+
+// Shuts down all gamepads
+void I_ShutdownGamepads(void);
+
+// Returns the name of a gamepad from its index
+const char *I_GetGamepadName(INT32 joyindex);
+
+// Gamepad rumble interface
+boolean I_RumbleSupported(void);
+boolean I_RumbleGamepad(UINT8 which, const haptic_t *effect);
+
+boolean I_GetGamepadRumblePaused(UINT8 which);
+
+boolean I_SetGamepadLargeMotorFreq(UINT8 which, fixed_t freq);
+boolean I_SetGamepadSmallMotorFreq(UINT8 which, fixed_t freq);
+void I_SetGamepadRumblePaused(UINT8 which, boolean pause);
+
+boolean I_GetGamepadRumbleSupported(UINT8 which);
+
+void I_StopGamepadRumble(UINT8 which);
+
+#endif // __I_GAMEPAD_H__
diff --git a/src/i_joy.h b/src/i_joy.h
deleted file mode 100644
index 27584cea6..000000000
--- a/src/i_joy.h
+++ /dev/null
@@ -1,58 +0,0 @@
-// SONIC ROBO BLAST 2
-//-----------------------------------------------------------------------------
-// Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2022 by Sonic Team Junior.
-//
-// This program is free software distributed under the
-// terms of the GNU General Public License, version 2.
-// See the 'LICENSE' file for more details.
-//-----------------------------------------------------------------------------
-/// \file  i_joy.h
-/// \brief share joystick information with game control code
-
-#ifndef __I_JOY_H__
-#define __I_JOY_H__
-
-#include "g_input.h"
-
-/*!
-  \brief	-JOYAXISRANGE to +JOYAXISRANGE for each axis
-
-	(1024-1) so we can do a right shift instead of division
-	(doesnt matter anyway, just give enough precision)
-	a gamepad will return -1, 0, or 1 in the event data
-	an analog type joystick will return a value
-	from -JOYAXISRANGE to +JOYAXISRANGE for each axis
-*/
-
-#define JOYAXISRANGE 1023
-
-// detect a bug if we increase JOYBUTTONS above DIJOYSTATE's number of buttons
-#if (JOYBUTTONS > 64)
-"JOYBUTTONS is greater than INT64 bits can hold"
-#endif
-
-/**	\brief	The struct JoyType_s
-
- share some joystick information (maybe 2 for splitscreen), to the game input code,
- actually, we need to know if it is a gamepad or analog controls
-*/
-
-struct JoyType_s
-{
-	/*! if true, we MUST Poll() to get new joystick data,
-	that is: we NEED the DIRECTINPUTDEVICE2 ! (watchout NT compatibility) */
-	INT32 bJoyNeedPoll;
-	/*! this joystick is a gamepad, read: digital axes
-	if FALSE, interpret the joystick event data as JOYAXISRANGE (see above) */
-	INT32 bGamepadStyle;
-
-};
-typedef struct JoyType_s JoyType_t;
-/**	\brief Joystick info
-	for palyer 1 and 2's joystick/gamepad
-*/
-
-extern JoyType_t Joystick, Joystick2;
-
-#endif // __I_JOY_H__
diff --git a/src/i_system.h b/src/i_system.h
index 7153aa735..f3ca5aac5 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -101,90 +101,6 @@ ticcmd_t *I_BaseTiccmd2(void);
 */
 void I_Quit(void) FUNCNORETURN;
 
-typedef enum
-{
-	EvilForce = -1,
-	//Constant
-	ConstantForce = 0,
-	//Ramp
-	RampForce,
-	//Periodics
-	SquareForce,
-	SineForce,
-	TriangleForce,
-	SawtoothUpForce,
-	SawtoothDownForce,
-	//MAX
-	NumberofForces,
-} FFType;
-
-typedef struct JoyFF_s
-{
-	INT32 ForceX; ///< The X of the Force's Vel
-	INT32 ForceY; ///< The Y of the Force's Vel
-	//All
-	UINT32 Duration; ///< The total duration of the effect, in microseconds
-	INT32 Gain; //< /The gain to be applied to the effect, in the range from 0 through 10,000.
-	//All, CONSTANTFORCE -10,000 to 10,000
-	INT32 Magnitude; ///< Magnitude of the effect, in the range from 0 through 10,000.
-	//RAMPFORCE
-	INT32 Start; ///< Magnitude at the start of the effect, in the range from -10,000 through 10,000.
-	INT32 End; ///< Magnitude at the end of the effect, in the range from -10,000 through 10,000.
-	//PERIODIC
-	INT32 Offset; ///< Offset of the effect.
-	UINT32 Phase; ///< Position in the cycle of the periodic effect at which playback begins, in the range from 0 through 35,999
-	UINT32 Period; ///< Period of the effect, in microseconds.
-} JoyFF_t;
-
-/**	\brief	Forcefeedback for the first joystick
-
-	\param	Type   what kind of Effect
-	\param	Effect Effect Info
-
-	\return	void
-*/
-
-void I_Tactile(FFType Type, const JoyFF_t *Effect);
-
-/**	\brief	Forcefeedback for the second joystick
-
-	\param	Type   what kind of Effect
-	\param	Effect Effect Info
-
-	\return	void
-*/
-void I_Tactile2(FFType Type, const JoyFF_t *Effect);
-
-/**	\brief to set up the first joystick scale
-*/
-void I_JoyScale(void);
-
-/**	\brief to set up the second joystick scale
-*/
-void I_JoyScale2(void);
-
-// Called by D_SRB2Main.
-
-/**	\brief to startup the first joystick
-*/
-void I_InitJoystick(void);
-
-/**	\brief to startup the second joystick
-*/
-void I_InitJoystick2(void);
-
-/**	\brief return the number of joystick on the system
-*/
-INT32 I_NumJoys(void);
-
-/**	\brief	The *I_GetJoyName function
-
-	\param	joyindex	which joystick
-
-	\return	joystick name
-*/
-const char *I_GetJoyName(INT32 joyindex);
-
 #ifndef NOMUMBLE
 #include "p_mobj.h" // mobj_t
 #include "s_sound.h" // listener_t
@@ -293,15 +209,7 @@ const CPUInfoFlags *I_CPUInfo(void);
 */
 const char *I_LocateWad(void);
 
-/**	\brief First Joystick's events
-*/
-void I_GetJoystickEvents(void);
-
-/**	\brief Second Joystick's events
-*/
-void I_GetJoystick2Events(void);
-
-/**	\brief Mouses events
+/**	\brief Mice events
 */
 void I_GetMouseEvents(void);
 
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 1710b0355..88efc3490 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -15,6 +15,7 @@
 #include "g_game.h"
 #include "hu_stuff.h"
 #include "i_system.h"
+#include "i_gamepad.h"
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -30,7 +31,7 @@ static int lib_gameControlDown(lua_State *L)
 	int i = luaL_checkinteger(L, 1);
 	if (i < 0 || i >= NUM_GAMECONTROLS)
 		return luaL_error(L, "GC_* constant %d out of range (0 - %d)", i, NUM_GAMECONTROLS-1);
-	lua_pushinteger(L, PLAYER1INPUTDOWN(i));
+	lua_pushinteger(L, G_PlayerInputDown(0, i));
 	return 1;
 }
 
@@ -39,7 +40,7 @@ static int lib_gameControl2Down(lua_State *L)
 	int i = luaL_checkinteger(L, 1);
 	if (i < 0 || i >= NUM_GAMECONTROLS)
 		return luaL_error(L, "GC_* constant %d out of range (0 - %d)", i, NUM_GAMECONTROLS-1);
-	lua_pushinteger(L, PLAYER2INPUTDOWN(i));
+	lua_pushinteger(L, G_PlayerInputDown(1, i));
 	return 1;
 }
 
@@ -66,14 +67,14 @@ static int lib_gameControl2ToKeyNum(lua_State *L)
 static int lib_joyAxis(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	lua_pushinteger(L, JoyAxis(i));
+	lua_pushinteger(L, G_JoyAxis(0, i) / 32);
 	return 1;
 }
 
 static int lib_joy2Axis(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	lua_pushinteger(L, Joy2Axis(i));
+	lua_pushinteger(L, G_JoyAxis(1, i) / 32);
 	return 1;
 }
 
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 89c8009ae..f94377450 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -199,39 +199,41 @@ static UINT8 cht_CheckCheat(cheatseq_t *cht, char key)
 
 boolean cht_Responder(event_t *ev)
 {
-	UINT8 ret = 0, ch = 0;
-	if (ev->type != ev_keydown)
-		return false;
+	UINT8 ch = 0;
 
-	if (ev->key > 0xFF)
+	if (ev->type == ev_gamepad_down)
 	{
-		// map some fake (joy) inputs into keys
-		// map joy inputs into keys
 		switch (ev->key)
 		{
-			case KEY_JOY1:
-			case KEY_JOY1 + 2:
-				ch = KEY_ENTER;
-				break;
-			case KEY_HAT1:
+			case GAMEPAD_BUTTON_DPAD_UP:
 				ch = KEY_UPARROW;
 				break;
-			case KEY_HAT1 + 1:
+			case GAMEPAD_BUTTON_DPAD_DOWN:
 				ch = KEY_DOWNARROW;
 				break;
-			case KEY_HAT1 + 2:
+			case GAMEPAD_BUTTON_DPAD_LEFT:
 				ch = KEY_LEFTARROW;
 				break;
-			case KEY_HAT1 + 3:
+			case GAMEPAD_BUTTON_DPAD_RIGHT:
 				ch = KEY_RIGHTARROW;
 				break;
+			case GAMEPAD_BUTTON_START:
+				ch = KEY_ENTER;
+				break;
 			default:
 				// no mapping
 				return false;
 		}
 	}
-	else
+	else if (ev->type == ev_keydown)
+	{
+		if (ev->key > 0xFF)
+			return false;
+
 		ch = (UINT8)ev->key;
+	}
+
+	UINT8 ret = 0;
 
 	ret += cht_CheckCheat(&cheat_ultimate, (char)ch);
 	ret += cht_CheckCheat(&cheat_ultimate_joy, (char)ch);
diff --git a/src/m_menu.c b/src/m_menu.c
index 83b788fd5..2a357acf9 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -41,6 +41,8 @@
 
 #include "v_video.h"
 #include "i_video.h"
+#include "i_gamepad.h"
+#include "g_input.h"
 #include "keys.h"
 #include "z_zone.h"
 #include "w_wad.h"
@@ -62,8 +64,6 @@
 #include "i_sound.h"
 #include "fastcmp.h"
 
-#include "i_joy.h" // for joystick menu controls
-
 #include "p_saveg.h" // Only for NEWSKINSAVES
 
 // Condition Sets
@@ -72,13 +72,6 @@
 // And just some randomness for the exits.
 #include "m_random.h"
 
-#if defined(HAVE_SDL)
-#include "SDL.h"
-#if SDL_VERSION_ATLEAST(2,0,0)
-#include "sdl/sdlmain.h" // JOYSTICK_HOTPLUG
-#endif
-#endif
-
 #if defined (__GNUC__) && (__GNUC__ >= 4)
 #define FIXUPO0
 #endif
@@ -148,7 +141,12 @@ typedef enum
 levellist_mode_t levellistmode = LLM_CREATESERVER;
 UINT8 maplistoption = 0;
 
-static char joystickInfo[MAX_JOYSTICKS+1][29];
+static struct
+{
+	char name[29];
+	INT32 index;
+} gamepadInfo[MAX_CONNECTED_GAMEPADS + 1];
+
 #ifndef NONET
 static UINT32 serverlistpage;
 #endif
@@ -163,6 +161,7 @@ static INT16 itemOn = 1; // menu item skull is on, Hack by Tails 09-18-2002
 static INT16 skullAnimCounter = 10; // skull animation counter
 
 static  boolean setupcontrols_secondaryplayer;
+static  consvar_t *setupcontrols_joycvar = NULL;
 static  INT32   (*setupcontrols)[2];  // pointer to the gamecontrols of the player being edited
 
 // shhh... what am I doing... nooooo!
@@ -311,17 +310,17 @@ menu_t MP_MainDef;
 menu_t OP_ChangeControlsDef;
 menu_t OP_MPControlsDef, OP_MiscControlsDef;
 menu_t OP_P1ControlsDef, OP_P2ControlsDef, OP_MouseOptionsDef;
-menu_t OP_Mouse2OptionsDef, OP_Joystick1Def, OP_Joystick2Def;
+menu_t OP_Mouse2OptionsDef, OP_Gamepad1Def, OP_Gamepad2Def;
 menu_t OP_CameraOptionsDef, OP_Camera2OptionsDef;
 menu_t OP_PlaystyleDef;
 static void M_VideoModeMenu(INT32 choice);
 static void M_Setup1PControlsMenu(INT32 choice);
 static void M_Setup2PControlsMenu(INT32 choice);
-static void M_Setup1PJoystickMenu(INT32 choice);
-static void M_Setup2PJoystickMenu(INT32 choice);
+static void M_Setup1PGamepadMenu(INT32 choice);
+static void M_Setup2PGamepadMenu(INT32 choice);
 static void M_Setup1PPlaystyleMenu(INT32 choice);
 static void M_Setup2PPlaystyleMenu(INT32 choice);
-static void M_AssignJoystick(INT32 choice);
+static void M_AssignGamepad(INT32 choice);
 static void M_ChangeControl(INT32 choice);
 
 // Video & Sound
@@ -375,7 +374,8 @@ static void M_DrawSetupChoosePlayerMenu(void);
 static void M_DrawControlsDefMenu(void);
 static void M_DrawCameraOptionsMenu(void);
 static void M_DrawPlaystyleMenu(void);
-static void M_DrawControl(void);
+static void M_DrawGamepadMenu(void);
+static void M_DrawControlConfigMenu(void);
 static void M_DrawMainVideoMenu(void);
 static void M_DrawVideoMode(void);
 static void M_DrawColorMenu(void);
@@ -386,7 +386,7 @@ static void M_DrawConnectMenu(void);
 static void M_DrawMPMainMenu(void);
 static void M_DrawRoomMenu(void);
 #endif
-static void M_DrawJoystick(void);
+static void M_DrawGamepadList(void);
 static void M_DrawSetupMultiPlayerMenu(void);
 
 // Handling functions
@@ -1084,7 +1084,7 @@ static menuitem_t OP_P1ControlsMenu[] =
 {
 	{IT_CALL    | IT_STRING, NULL, "Control Configuration...", M_Setup1PControlsMenu,   10},
 	{IT_SUBMENU | IT_STRING, NULL, "Mouse Options...", &OP_MouseOptionsDef, 20},
-	{IT_SUBMENU | IT_STRING, NULL, "Gamepad Options...", &OP_Joystick1Def  ,  30},
+	{IT_SUBMENU | IT_STRING, NULL, "Gamepad Options...", &OP_Gamepad1Def  ,  30},
 
 	{IT_SUBMENU | IT_STRING, NULL, "Camera Options...", &OP_CameraOptionsDef,	50},
 
@@ -1096,7 +1096,7 @@ static menuitem_t OP_P2ControlsMenu[] =
 {
 	{IT_CALL    | IT_STRING, NULL, "Control Configuration...", M_Setup2PControlsMenu,   10},
 	{IT_SUBMENU | IT_STRING, NULL, "Second Mouse Options...", &OP_Mouse2OptionsDef, 20},
-	{IT_SUBMENU | IT_STRING, NULL, "Second Gamepad Options...", &OP_Joystick2Def  ,  30},
+	{IT_SUBMENU | IT_STRING, NULL, "Second Gamepad Options...", &OP_Gamepad2Def  ,  30},
 
 	{IT_SUBMENU | IT_STRING, NULL, "Camera Options...", &OP_Camera2OptionsDef,	50},
 
@@ -1159,43 +1159,43 @@ static menuitem_t OP_ChangeControlsMenu[] =
 	{IT_CALL | IT_STRING2, NULL, "Custom Action 3",  M_ChangeControl, GC_CUSTOM3     },
 };
 
-static menuitem_t OP_Joystick1Menu[] =
+static menuitem_t OP_Gamepad1Menu[] =
 {
-	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup1PJoystickMenu, 10},
-	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis         , 30},
-	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis         , 40},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis         , 50},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis         , 60},
-	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis         , 70},
-	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis         , 80},
-	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis         , 90},
-	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis        ,100},
+	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup1PGamepadMenu, 10},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis[0]      , 30},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis[0]      , 40},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis[0]      , 50},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis[0]      , 60},
+	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis[0]      , 70},
+	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis[0]      , 80},
+	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis[0]      , 90},
+	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis[0]     ,100},
 
 	{IT_STRING | IT_CVAR, NULL, "First-Person Vert-Look", &cv_alwaysfreelook, 120},
 	{IT_STRING | IT_CVAR, NULL, "Third-Person Vert-Look", &cv_chasefreelook,  130},
-	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Analog Deadzone", &cv_deadzone, 140},
-	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Digital Deadzone", &cv_digitaldeadzone, 150},
+	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Analog Deadzone", &cv_deadzone[0], 140},
+	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Digital Deadzone", &cv_digitaldeadzone[0], 150},
 };
 
-static menuitem_t OP_Joystick2Menu[] =
+static menuitem_t OP_Gamepad2Menu[] =
 {
-	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup2PJoystickMenu, 10},
-	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis2        , 30},
-	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis2        , 40},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis2        , 50},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis2        , 60},
-	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis2        , 70},
-	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis2        , 80},
-	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis2        , 90},
-	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis2       ,100},
+	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup2PGamepadMenu, 10},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis[1]      , 30},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis[1]      , 40},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis[1]      , 50},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis[1]      , 60},
+	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis[1]      , 70},
+	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis[1]      , 80},
+	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis[1]      , 90},
+	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis[1]     ,100},
 
 	{IT_STRING | IT_CVAR, NULL, "First-Person Vert-Look", &cv_alwaysfreelook2,120},
 	{IT_STRING | IT_CVAR, NULL, "Third-Person Vert-Look", &cv_chasefreelook2, 130},
-	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Analog Deadzone", &cv_deadzone2,140},
-	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Digital Deadzone", &cv_digitaldeadzone2,150},
+	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Analog Deadzone", &cv_deadzone[1],140},
+	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Digital Deadzone", &cv_digitaldeadzone[1],150},
 };
 
-static menuitem_t OP_JoystickSetMenu[1+MAX_JOYSTICKS];
+static menuitem_t OP_GamepadSetMenu[MAX_CONNECTED_GAMEPADS + 1];
 
 static menuitem_t OP_MouseOptionsMenu[] =
 {
@@ -2092,21 +2092,21 @@ menu_t OP_Mouse2OptionsDef = DEFAULTMENUSTYLE(
 	MTREE3(MN_OP_MAIN, MN_OP_P2CONTROLS, MN_OP_P2MOUSE),
 	"M_CONTRO", OP_Mouse2OptionsMenu, &OP_P2ControlsDef, 35, 30);
 
-menu_t OP_Joystick1Def = DEFAULTMENUSTYLE(
+menu_t OP_Gamepad1Def = GAMEPADMENUSTYLE(
 	MTREE3(MN_OP_MAIN, MN_OP_P1CONTROLS, MN_OP_P1JOYSTICK),
-	"M_CONTRO", OP_Joystick1Menu, &OP_P1ControlsDef, 50, 30);
-menu_t OP_Joystick2Def = DEFAULTMENUSTYLE(
+	"M_CONTRO", OP_Gamepad1Menu, &OP_P1ControlsDef, 50, 30);
+menu_t OP_Gamepad2Def = GAMEPADMENUSTYLE(
 	MTREE3(MN_OP_MAIN, MN_OP_P2CONTROLS, MN_OP_P2JOYSTICK),
-	"M_CONTRO", OP_Joystick2Menu, &OP_P2ControlsDef, 50, 30);
+	"M_CONTRO", OP_Gamepad2Menu, &OP_P2ControlsDef, 50, 30);
 
-menu_t OP_JoystickSetDef =
+menu_t OP_GamepadSetDef =
 {
 	MTREE4(MN_OP_MAIN, 0, 0, MN_OP_JOYSTICKSET), // second and third level set on runtime
 	"M_CONTRO",
-	sizeof (OP_JoystickSetMenu)/sizeof (menuitem_t),
-	&OP_Joystick1Def,
-	OP_JoystickSetMenu,
-	M_DrawJoystick,
+	sizeof (OP_GamepadSetMenu)/sizeof (menuitem_t),
+	&OP_Gamepad1Def,
+	OP_GamepadSetMenu,
+	M_DrawGamepadList,
 	60, 40,
 	0,
 	NULL
@@ -3197,13 +3197,28 @@ static void Command_Manual_f(void)
 	itemOn = 0;
 }
 
+static INT32 RemapGamepadButton(event_t *ev)
+{
+	switch (ev->key)
+	{
+		case GAMEPAD_BUTTON_A: return KEY_ENTER;
+		case GAMEPAD_BUTTON_B: return KEY_ESCAPE;
+		case GAMEPAD_BUTTON_X: return KEY_BACKSPACE;
+		case GAMEPAD_BUTTON_DPAD_UP: return KEY_UPARROW;
+		case GAMEPAD_BUTTON_DPAD_DOWN: return KEY_DOWNARROW;
+		case GAMEPAD_BUTTON_DPAD_LEFT: return KEY_LEFTARROW;
+		case GAMEPAD_BUTTON_DPAD_RIGHT: return KEY_RIGHTARROW;
+	}
+
+	return KEY_GAMEPAD + ev->key;
+}
+
 //
 // M_Responder
 //
 boolean M_Responder(event_t *ev)
 {
 	INT32 ch = -1;
-//	INT32 i;
 	static tic_t joywait = 0, mousewait = 0;
 	static INT32 pjoyx = 0, pjoyy = 0;
 	static INT32 pmousex = 0, pmousey = 0;
@@ -3221,6 +3236,8 @@ boolean M_Responder(event_t *ev)
 	if (CON_Ready() && gamestate != GS_WAITINGPLAYERS)
 		return false;
 
+	boolean useEventHandler = false;
+
 	if (noFurtherInput)
 	{
 		// Ignore input after enter/escape/other buttons
@@ -3229,80 +3246,69 @@ boolean M_Responder(event_t *ev)
 	}
 	else if (menuactive)
 	{
+		if (currentMenu->menuitems[itemOn].status == IT_MSGHANDLER)
+			useEventHandler = currentMenu->menuitems[itemOn].alphaKey == MM_EVENTHANDLER;
+
 		if (ev->type == ev_keydown)
 		{
 			keydown++;
 			ch = ev->key;
 
-			// added 5-2-98 remap virtual keys (mouse & joystick buttons)
+			// added 5-2-98 remap virtual keys (mouse buttons)
 			switch (ch)
 			{
 				case KEY_MOUSE1:
-				case KEY_JOY1:
 					ch = KEY_ENTER;
 					break;
-				case KEY_JOY1 + 3:
-					ch = 'n';
-					break;
 				case KEY_MOUSE1 + 1:
-				case KEY_JOY1 + 1:
 					ch = KEY_ESCAPE;
 					break;
-				case KEY_JOY1 + 2:
-					ch = KEY_BACKSPACE;
-					break;
-				case KEY_HAT1:
-					ch = KEY_UPARROW;
-					break;
-				case KEY_HAT1 + 1:
-					ch = KEY_DOWNARROW;
-					break;
-				case KEY_HAT1 + 2:
-					ch = KEY_LEFTARROW;
-					break;
-				case KEY_HAT1 + 3:
-					ch = KEY_RIGHTARROW;
-					break;
 			}
 		}
-		else if (ev->type == ev_joystick  && ev->key == 0 && joywait < I_GetTime())
+		else if (ev->type == ev_gamepad_down)
 		{
-			const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
-			if (ev->y != INT32_MAX)
+			keydown++;
+			ch = RemapGamepadButton(ev);
+		}
+		else if (ev->type == ev_gamepad_axis && ev->which == 0 && joywait < I_GetTime())
+		{
+			const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(0);
+			const INT16 value = G_GamepadAxisEventValue(0, ev->x);
+
+			if (ev->key == GAMEPAD_AXIS_LEFTY)
 			{
-				if (Joystick.bGamepadStyle || abs(ev->y) > jdeadzone)
+				if (abs(value) > jdeadzone)
 				{
-					if (ev->y < 0 && pjoyy >= 0)
+					if (value < 0 && pjoyy >= 0)
 					{
 						ch = KEY_UPARROW;
 						joywait = I_GetTime() + NEWTICRATE/7;
 					}
-					else if (ev->y > 0 && pjoyy <= 0)
+					else if (value > 0 && pjoyy <= 0)
 					{
 						ch = KEY_DOWNARROW;
 						joywait = I_GetTime() + NEWTICRATE/7;
 					}
-					pjoyy = ev->y;
+					pjoyy = value;
 				}
 				else
 					pjoyy = 0;
 			}
-
-			if (ev->x != INT32_MAX)
+			else if (ev->key == GAMEPAD_AXIS_LEFTX)
 			{
-				if (Joystick.bGamepadStyle || abs(ev->x) > jdeadzone)
+				if (abs(value) > jdeadzone)
 				{
-					if (ev->x < 0 && pjoyx >= 0)
+					if (value < 0 && pjoyx >= 0)
 					{
 						ch = KEY_LEFTARROW;
 						joywait = I_GetTime() + NEWTICRATE/17;
 					}
-					else if (ev->x > 0 && pjoyx <= 0)
+					else if (value > 0 && pjoyx <= 0)
 					{
 						ch = KEY_RIGHTARROW;
 						joywait = I_GetTime() + NEWTICRATE/17;
 					}
-					pjoyx = ev->x;
+					pjoyx = value;
 				}
 				else
 					pjoyx = 0;
@@ -3338,13 +3344,29 @@ boolean M_Responder(event_t *ev)
 				pmousex = lastx += 30;
 			}
 		}
-		else if (ev->type == ev_keyup) // Preserve event for other responders
+		else if (ev->type == ev_keyup || ev->type == ev_gamepad_up) // Preserve event for other responders
 			keydown = 0;
 	}
-	else if (ev->type == ev_keydown) // Preserve event for other responders
+	// Preserve event for other responders
+	else if (ev->type == ev_keydown || ev->type == ev_gamepad_down)
+	{
 		ch = ev->key;
 
-	if (ch == -1)
+		if (ev->type == ev_gamepad_down)
+			ch += KEY_GAMEPAD;
+	}
+	else if (ev->type == ev_gamepad_axis)
+	{
+		const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(0);
+		const INT16 value = G_GamepadAxisEventValue(0, ev->x);
+
+		if (value > jdeadzone)
+			ch = KEY_AXES + ev->key;
+		else if (value < -jdeadzone)
+			ch = KEY_INV_AXES + ev->key;
+	}
+
+	if (!useEventHandler && ch == -1)
 		return false;
 	else if (ch == gamecontrol[GC_SYSTEMMENU][0] || ch == gamecontrol[GC_SYSTEMMENU][1]) // allow remappable ESC key
 		ch = KEY_ESCAPE;
@@ -3432,27 +3454,25 @@ boolean M_Responder(event_t *ev)
 
 	if (currentMenu->menuitems[itemOn].status == IT_MSGHANDLER)
 	{
-		if (currentMenu->menuitems[itemOn].alphaKey != MM_EVENTHANDLER)
+		if (!useEventHandler)
 		{
-			if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER || ch == KEY_DEL)
+			UINT16 type = currentMenu->menuitems[itemOn].alphaKey;
+			if (type == MM_YESNO && !(ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER || ch == KEY_DEL))
+				return true;
+			if (routine)
+				routine(ch);
+			if (type == MM_YESNO)
 			{
-				if (routine)
-					routine(ch);
 				if (stopstopmessage)
 					stopstopmessage = false;
 				else
 					M_StopMessage(0);
 				noFurtherInput = true;
-				return true;
 			}
 			return true;
 		}
 		else
 		{
-			// dirty hack: for customising controls, I want only buttons/keys, not moves
-			if (ev->type == ev_mouse || ev->type == ev_mouse2 || ev->type == ev_joystick
-				|| ev->type == ev_joystick2)
-				return true;
 			if (routine)
 			{
 				void (*otherroutine)(event_t *sev) = currentMenu->menuitems[itemOn].itemaction;
@@ -3718,6 +3738,7 @@ void M_StartControlPanel(void)
 
 		currentMenu = &SPauseDef;
 		itemOn = spause_continue;
+
 	}
 	else // multiplayer
 	{
@@ -3762,6 +3783,9 @@ void M_StartControlPanel(void)
 	}
 
 	CON_ToggleOff(); // move away console
+
+	if (P_AutoPause())
+		P_PauseRumble(NULL);
 }
 
 void M_EndModeAttackRun(void)
@@ -3790,6 +3814,7 @@ void M_ClearMenus(boolean callexitmenufunc)
 	hidetitlemap = false;
 
 	I_UpdateMouseGrab();
+	P_UnpauseRumble(NULL);
 }
 
 //
@@ -3959,10 +3984,10 @@ void M_Init(void)
 	at all if every item just calls the same function, and
 	nothing more. Now just automate the definition.
 	*/
-	for (i = 0; i <= MAX_JOYSTICKS; ++i)
+	for (i = 0; i < MAX_CONNECTED_GAMEPADS + 1; ++i)
 	{
-		OP_JoystickSetMenu[i].status = ( IT_NOTHING|IT_CALL );
-		OP_JoystickSetMenu[i].itemaction = M_AssignJoystick;
+		OP_GamepadSetMenu[i].status = ( IT_NOTHING|IT_CALL );
+		OP_GamepadSetMenu[i].itemaction = M_AssignGamepad;
 	}
 
 #ifndef NONET
@@ -4192,26 +4217,6 @@ static void M_DrawStaticBox(fixed_t x, fixed_t y, INT32 flags, fixed_t w, fixed_
 	W_UnlockCachedPatch(patch);
 }
 
-//
-// Draw border for the savegame description
-//
-#if 0 // once used for joysticks and savegames, now no longer
-static void M_DrawSaveLoadBorder(INT32 x,INT32 y)
-{
-	INT32 i;
-
-	V_DrawScaledPatch (x-8,y+7,0,W_CachePatchName("M_LSLEFT",PU_PATCH));
-
-	for (i = 0;i < 24;i++)
-	{
-		V_DrawScaledPatch (x,y+7,0,W_CachePatchName("M_LSCNTR",PU_PATCH));
-		x += 8;
-	}
-
-	V_DrawScaledPatch (x,y+7,0,W_CachePatchName("M_LSRGHT",PU_PATCH));
-}
-#endif
-
 // horizontally centered text
 static void M_CentreText(INT32 y, const char *string)
 {
@@ -6150,7 +6155,10 @@ void M_StartMessage(const char *string, void *routine,
 
 	MessageDef.menuitems[0].text     = message;
 	MessageDef.menuitems[0].alphaKey = (UINT8)itemtype;
-	if (!routine && itemtype != MM_NOTHING) itemtype = MM_NOTHING;
+
+	if (routine == NULL && itemtype != MM_NOTHING)
+		itemtype = MM_NOTHING;
+
 	switch (itemtype)
 	{
 		case MM_NOTHING:
@@ -6158,9 +6166,7 @@ void M_StartMessage(const char *string, void *routine,
 			MessageDef.menuitems[0].itemaction = M_StopMessage;
 			break;
 		case MM_YESNO:
-			MessageDef.menuitems[0].status     = IT_MSGHANDLER;
-			MessageDef.menuitems[0].itemaction = routine;
-			break;
+		case MM_KEYHANDLER:
 		case MM_EVENTHANDLER:
 			MessageDef.menuitems[0].status     = IT_MSGHANDLER;
 			MessageDef.menuitems[0].itemaction = routine;
@@ -6192,7 +6198,6 @@ void M_StartMessage(const char *string, void *routine,
 
 	MessageDef.lastOn = (INT16)((strlines<<8)+max);
 
-	//M_SetupNextMenu();
 	currentMenu = &MessageDef;
 	itemOn = 0;
 }
@@ -11403,7 +11408,7 @@ static void M_ConnectMenuModChecks(INT32 choice)
 
 	if (modifiedgame)
 	{
-		M_StartMessage(M_GetText("You have add-ons loaded.\nYou won't be able to join netgames!\n\nTo play online, restart the game\nand don't load any addons.\nSRB2 will automatically add\neverything you need when you join.\n\n(Press a key)\n"),M_ConnectMenu,MM_EVENTHANDLER);
+		M_StartMessage(M_GetText("You have add-ons loaded.\nYou won't be able to join netgames!\n\nTo play online, restart the game\nand don't load any addons.\nSRB2 will automatically add\neverything you need when you join.\n\n(Press a key)\n"),M_ConnectMenu,MM_KEYHANDLER);
 		return;
 	}
 
@@ -12550,183 +12555,147 @@ static void M_SetupScreenshotMenu(void)
 		item->status = (IT_STRING | IT_CVAR);
 }
 
-// =============
-// JOYSTICK MENU
-// =============
+// ============
+// GAMEPAD MENU
+// ============
 
 // Start the controls menu, setting it up for either the console player,
 // or the secondary splitscreen player
 
-static void M_DrawJoystick(void)
+static void M_DrawGamepadList(void)
 {
-	INT32 i, compareval2, compareval;
+	INT32 i;
+
+	INT32 compareval = G_GetGamepadDeviceIndex(0);
+	INT32 compareval2 = G_GetGamepadDeviceIndex(1);
 
 	// draw title (or big pic)
 	M_DrawMenuTitle();
 
-	for (i = 0; i <= MAX_JOYSTICKS; i++) // See MAX_JOYSTICKS
+	for (i = 0; i < MAX_CONNECTED_GAMEPADS + 1; i++)
 	{
-		M_DrawTextBox(OP_JoystickSetDef.x-8, OP_JoystickSetDef.y+LINEHEIGHT*i-12, 28, 1);
-		//M_DrawSaveLoadBorder(OP_JoystickSetDef.x+4, OP_JoystickSetDef.y+1+LINEHEIGHT*i);
-
-#ifdef JOYSTICK_HOTPLUG
-		if (atoi(cv_usejoystick2.string) > I_NumJoys())
-			compareval2 = atoi(cv_usejoystick2.string);
-		else
-			compareval2 = cv_usejoystick2.value;
-
-		if (atoi(cv_usejoystick.string) > I_NumJoys())
-			compareval = atoi(cv_usejoystick.string);
-		else
-			compareval = cv_usejoystick.value;
-#else
-		compareval2 = cv_usejoystick2.value;
-		compareval = cv_usejoystick.value;
-#endif
+		M_DrawTextBox(OP_GamepadSetDef.x-8, OP_GamepadSetDef.y+LINEHEIGHT*i-12, 28, 1);
 
 		if ((setupcontrols_secondaryplayer && (i == compareval2))
 			|| (!setupcontrols_secondaryplayer && (i == compareval)))
-			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4,V_GREENMAP,joystickInfo[i]);
+			V_DrawString(OP_GamepadSetDef.x, OP_GamepadSetDef.y+LINEHEIGHT*i-4,V_GREENMAP,gamepadInfo[i].name);
 		else
-			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4,0,joystickInfo[i]);
+			V_DrawString(OP_GamepadSetDef.x, OP_GamepadSetDef.y+LINEHEIGHT*i-4,0,gamepadInfo[i].name);
 
 		if (i == itemOn)
 		{
-			V_DrawScaledPatch(currentMenu->x - 24, OP_JoystickSetDef.y+LINEHEIGHT*i-4, 0,
+			V_DrawScaledPatch(currentMenu->x - 24, OP_GamepadSetDef.y+LINEHEIGHT*i-4, 0,
 				W_CachePatchName("M_CURSOR", PU_PATCH));
 		}
 	}
 }
 
-void M_SetupJoystickMenu(INT32 choice)
+boolean M_OnGamepadMenu(void)
 {
-	INT32 i = 0;
-	const char *joyNA = "Unavailable";
-	INT32 n = I_NumJoys();
-	(void)choice;
-
-	strcpy(joystickInfo[i], "None");
-
-	for (i = 1; i <= MAX_JOYSTICKS; i++)
-	{
-		if (i <= n && (I_GetJoyName(i)) != NULL)
-			strncpy(joystickInfo[i], I_GetJoyName(i), 28);
-		else
-			strcpy(joystickInfo[i], joyNA);
-
-#ifdef JOYSTICK_HOTPLUG
-		// We use cv_usejoystick.string as the USER-SET var
-		// and cv_usejoystick.value as the INTERNAL var
-		//
-		// In practice, if cv_usejoystick.string == 0, this overrides
-		// cv_usejoystick.value and always disables
-		//
-		// Update cv_usejoystick.string here so that the user can
-		// properly change this value.
-		if (i == cv_usejoystick.value)
-			CV_SetValue(&cv_usejoystick, i);
-		if (i == cv_usejoystick2.value)
-			CV_SetValue(&cv_usejoystick2, i);
-#endif
-	}
-
-	M_SetupNextMenu(&OP_JoystickSetDef);
+	return currentMenu == &OP_GamepadSetDef;
 }
 
-static void M_Setup1PJoystickMenu(INT32 choice)
+void M_UpdateGamepadMenu(void)
+{
+	INT32 i = 0, j = 1;
+	INT32 n = I_NumGamepads();
+
+	strcpy(gamepadInfo[i].name, "None");
+	gamepadInfo[i].index = 0;
+
+	for (i = 1; i < MAX_CONNECTED_GAMEPADS + 1; i++)
+	{
+		if (i <= n && (I_GetGamepadName(i)) != NULL)
+			strlcpy(gamepadInfo[j].name, I_GetGamepadName(i), sizeof gamepadInfo[j].name);
+		else
+			strlcpy(gamepadInfo[j].name, "Unavailable", sizeof gamepadInfo[j].name);
+
+		gamepadInfo[j].index = j;
+
+#ifdef GAMEPAD_HOTPLUG
+		// Update cv_usegamepad.string here so that the user can
+		// properly change this value.
+		for (INT32 jn = 0; jn < NUM_GAMEPADS; jn++)
+		{
+			if (i == cv_usegamepad[jn].value)
+				CV_SetValue(&cv_usegamepad[jn], i);
+		}
+#endif
+
+		j++;
+	}
+}
+
+static void M_SetupGamepadMenu(void)
+{
+	M_UpdateGamepadMenu();
+	M_SetupNextMenu(&OP_GamepadSetDef);
+
+	if (setupcontrols_secondaryplayer)
+		itemOn = G_GetGamepadDeviceIndex(1);
+	else
+		itemOn = G_GetGamepadDeviceIndex(0);
+}
+
+static void M_Setup1PGamepadMenu(INT32 choice)
 {
 	setupcontrols_secondaryplayer = false;
-	OP_JoystickSetDef.prevMenu = &OP_Joystick1Def;
-	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS);
-	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << (MENUBITS*2));
-	OP_JoystickSetDef.menuid |= MN_OP_P1CONTROLS << MENUBITS;
-	OP_JoystickSetDef.menuid |= MN_OP_P1JOYSTICK << (MENUBITS*2);
-	M_SetupJoystickMenu(choice);
+	setupcontrols_joycvar = &cv_usegamepad[0];
+	OP_GamepadSetDef.prevMenu = &OP_Gamepad1Def;
+	OP_GamepadSetDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS);
+	OP_GamepadSetDef.menuid &= ~(((1 << MENUBITS) - 1) << (MENUBITS*2));
+	OP_GamepadSetDef.menuid |= MN_OP_P1CONTROLS << MENUBITS;
+	OP_GamepadSetDef.menuid |= MN_OP_P1JOYSTICK << (MENUBITS*2);
+
+	M_SetupGamepadMenu();
+	(void)choice;
 }
 
-static void M_Setup2PJoystickMenu(INT32 choice)
+static void M_Setup2PGamepadMenu(INT32 choice)
 {
 	setupcontrols_secondaryplayer = true;
-	OP_JoystickSetDef.prevMenu = &OP_Joystick2Def;
-	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS);
-	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << (MENUBITS*2));
-	OP_JoystickSetDef.menuid |= MN_OP_P2CONTROLS << MENUBITS;
-	OP_JoystickSetDef.menuid |= MN_OP_P2JOYSTICK << (MENUBITS*2);
-	M_SetupJoystickMenu(choice);
+	setupcontrols_joycvar = &cv_usegamepad[1];
+	OP_GamepadSetDef.prevMenu = &OP_Gamepad2Def;
+	OP_GamepadSetDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS);
+	OP_GamepadSetDef.menuid &= ~(((1 << MENUBITS) - 1) << (MENUBITS*2));
+	OP_GamepadSetDef.menuid |= MN_OP_P2CONTROLS << MENUBITS;
+	OP_GamepadSetDef.menuid |= MN_OP_P2JOYSTICK << (MENUBITS*2);
+
+	M_SetupGamepadMenu();
+	(void)choice;
 }
 
-static void M_AssignJoystick(INT32 choice)
+static void M_AssignGamepad(INT32 choice)
 {
-#ifdef JOYSTICK_HOTPLUG
-	INT32 oldchoice, oldstringchoice;
-	INT32 numjoys = I_NumJoys();
+#ifdef GAMEPAD_HOTPLUG
+	INT32 this = gamepadInfo[choice].index;
 
-	if (setupcontrols_secondaryplayer)
+	// Detect if other players are using this gamepad index
+	for (INT32 i = 0; this && i < NUM_GAMEPADS; i++)
 	{
-		oldchoice = oldstringchoice = atoi(cv_usejoystick2.string) > numjoys ? atoi(cv_usejoystick2.string) : cv_usejoystick2.value;
-		CV_SetValue(&cv_usejoystick2, choice);
+		// Ignore yourself
+		if (i == (INT32)setupcontrols_secondaryplayer)
+			continue;
 
-		// Just in case last-minute changes were made to cv_usejoystick.value,
-		// update the string too
-		// But don't do this if we're intentionally setting higher than numjoys
-		if (choice <= numjoys)
+		INT32 other = G_GetGamepadDeviceIndex(i);
+
+		// Ignore gamepads that are disconnected
+		// (the game will deal with it when they are connected)
+		if (other > I_NumGamepads())
+			continue;
+
+		if (other == this)
 		{
-			CV_SetValue(&cv_usejoystick2, cv_usejoystick2.value);
-
-			// reset this so the comparison is valid
-			if (oldchoice > numjoys)
-				oldchoice = cv_usejoystick2.value;
-
-			if (oldchoice != choice)
-			{
-				if (choice && oldstringchoice > numjoys) // if we did not select "None", we likely selected a used device
-					CV_SetValue(&cv_usejoystick2, (oldstringchoice > numjoys ? oldstringchoice : oldchoice));
-
-				if (oldstringchoice ==
-					(atoi(cv_usejoystick2.string) > numjoys ? atoi(cv_usejoystick2.string) : cv_usejoystick2.value))
-					M_StartMessage("This gamepad is used by another\n"
-					               "player. Reset the gamepad\n"
-					               "for that player first.\n\n"
-					               "(Press a key)\n", NULL, MM_NOTHING);
-			}
+			M_StartMessage("This gamepad is used by another\n"
+			               "player. Reset the gamepad\n"
+			               "for that player first.\n\n"
+			               "(Press a key)\n", NULL, MM_NOTHING);
+			return;
 		}
 	}
-	else
-	{
-		oldchoice = oldstringchoice = atoi(cv_usejoystick.string) > numjoys ? atoi(cv_usejoystick.string) : cv_usejoystick.value;
-		CV_SetValue(&cv_usejoystick, choice);
-
-		// Just in case last-minute changes were made to cv_usejoystick.value,
-		// update the string too
-		// But don't do this if we're intentionally setting higher than numjoys
-		if (choice <= numjoys)
-		{
-			CV_SetValue(&cv_usejoystick, cv_usejoystick.value);
-
-			// reset this so the comparison is valid
-			if (oldchoice > numjoys)
-				oldchoice = cv_usejoystick.value;
-
-			if (oldchoice != choice)
-			{
-				if (choice && oldstringchoice > numjoys) // if we did not select "None", we likely selected a used device
-					CV_SetValue(&cv_usejoystick, (oldstringchoice > numjoys ? oldstringchoice : oldchoice));
-
-				if (oldstringchoice ==
-					(atoi(cv_usejoystick.string) > numjoys ? atoi(cv_usejoystick.string) : cv_usejoystick.value))
-					M_StartMessage("This gamepad is used by another\n"
-					               "player. Reset the gamepad\n"
-					               "for that player first.\n\n"
-					               "(Press a key)\n", NULL, MM_NOTHING);
-			}
-		}
-	}
-#else
-	if (setupcontrols_secondaryplayer)
-		CV_SetValue(&cv_usejoystick2, choice);
-	else
-		CV_SetValue(&cv_usejoystick, choice);
 #endif
+
+	CV_SetValue(setupcontrols_joycvar, this);
 }
 
 // =============
@@ -12795,10 +12764,26 @@ static void M_Setup2PControlsMenu(INT32 choice)
 	M_SetupNextMenu(&OP_ChangeControlsDef);
 }
 
+static const char *M_GetKeyName(UINT8 player, INT32 key)
+{
+	gamepadtype_e type = GAMEPAD_TYPE_UNKNOWN;
+	if (cv_usegamepad[player].value)
+		type = gamepads[player].type;
+
+	if (key >= KEY_GAMEPAD && key < KEY_GAMEPAD + NUM_GAMEPAD_BUTTONS)
+		return G_GetGamepadButtonString(type, key - KEY_GAMEPAD, GAMEPAD_STRING_MENU1);
+	else if (key >= KEY_AXES && key < KEY_AXES + NUM_GAMEPAD_AXES)
+		return G_GetGamepadAxisString(type, key - KEY_AXES, GAMEPAD_STRING_MENU1, false);
+	else if (key >= KEY_INV_AXES && key < KEY_INV_AXES + NUM_GAMEPAD_AXES)
+		return G_GetGamepadAxisString(type, key - KEY_INV_AXES, GAMEPAD_STRING_MENU1, true);
+
+	return G_GetDisplayNameForKey(key);
+}
+
 #define controlheight 18
 
 // Draws the Customise Controls menu
-static void M_DrawControl(void)
+static void M_DrawControlConfigMenu(void)
 {
 	char     tmp[50];
 	INT32    x, y, i, max, cursory = 0, iter;
@@ -12878,40 +12863,38 @@ static void M_DrawControl(void)
 
 		if (currentMenu->menuitems[i].status == IT_CONTROL)
 		{
+			INT32 right = x + V_StringWidth(currentMenu->menuitems[i].text, 0);
 			V_DrawString(x, y, ((i == itemOn) ? V_YELLOWMAP : 0), currentMenu->menuitems[i].text);
+
 			keys[0] = setupcontrols[currentMenu->menuitems[i].alphaKey][0];
 			keys[1] = setupcontrols[currentMenu->menuitems[i].alphaKey][1];
 
 			tmp[0] ='\0';
 			if (keys[0] == KEY_NULL && keys[1] == KEY_NULL)
-			{
 				strcpy(tmp, "---");
-			}
 			else
 			{
 				if (keys[0] != KEY_NULL)
-					strcat (tmp, G_KeyNumToName (keys[0]));
-
+					strcat(tmp, M_GetKeyName(setupcontrols_secondaryplayer, keys[0]));
 				if (keys[0] != KEY_NULL && keys[1] != KEY_NULL)
-					strcat(tmp," or ");
-
+					strcat(tmp, " or ");
 				if (keys[1] != KEY_NULL)
-					strcat (tmp, G_KeyNumToName (keys[1]));
-
-
+					strcat(tmp, M_GetKeyName(setupcontrols_secondaryplayer, keys[1]));
 			}
-			V_DrawRightAlignedString(BASEVIDWIDTH-currentMenu->x, y, V_YELLOWMAP, tmp);
+
+			INT32 left = BASEVIDWIDTH-currentMenu->x-V_StringWidth(tmp, V_ALLOWLOWERCASE);
+			if (left - 8 <= right)
+				V_DrawRightAlignedThinString(BASEVIDWIDTH-currentMenu->x, y+1, V_ALLOWLOWERCASE | V_YELLOWMAP, tmp);
+			else
+				V_DrawRightAlignedString(BASEVIDWIDTH-currentMenu->x, y, V_ALLOWLOWERCASE | V_YELLOWMAP, tmp);
 		}
-		/*else if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2)
-			V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text);*/
 		else if ((currentMenu->menuitems[i].status == IT_HEADER) && (i != max-1))
 			M_DrawLevelPlatterHeader(y, currentMenu->menuitems[i].text, true, false);
 
 		y += SMALLLINEHEIGHT;
 	}
 
-	V_DrawScaledPatch(currentMenu->x - 20, cursory, 0,
-		W_CachePatchName("M_CURSOR", PU_PATCH));
+	V_DrawScaledPatch(currentMenu->x - 20, cursory, 0, W_CachePatchName("M_CURSOR", PU_PATCH));
 }
 
 #undef controlbuffer
@@ -12919,55 +12902,55 @@ static void M_DrawControl(void)
 static INT32 controltochange;
 static char controltochangetext[33];
 
-static void M_ChangecontrolResponse(event_t *ev)
+static void M_ChangeControlResponse(event_t *ev)
 {
-	INT32        control;
-	INT32        found;
-	INT32        ch = ev->key;
+	// dirty hack: for customising controls, I want only buttons/keys, not moves
+	if (ev->type == ev_mouse || ev->type == ev_mouse2)
+		return;
+
+	INT32 ch = ev->key;
+
+	// Remap gamepad events
+	if (ev->type == ev_gamepad_down)
+		ch += KEY_GAMEPAD;
+	else if (ev->type == ev_gamepad_axis)
+	{
+		const UINT16 jdeadzone = G_GetGamepadDigitalDeadZone(ev->which);
+		const INT16 value = G_GamepadAxisEventValue(ev->which, ev->x);
+
+		if (value > jdeadzone)
+			ch += KEY_AXES;
+		else if (value < -jdeadzone)
+			ch += KEY_INV_AXES;
+		else
+			return;
+	}
+	else if (ev->type != ev_keydown)
+		return;
 
 	// ESCAPE cancels; dummy out PAUSE
 	if (ch != KEY_ESCAPE && ch != KEY_PAUSE)
 	{
-
-		switch (ev->type)
-		{
-			// ignore mouse/joy movements, just get buttons
-			case ev_mouse:
-			case ev_mouse2:
-			case ev_joystick:
-			case ev_joystick2:
-				ch = KEY_NULL;      // no key
-			break;
-
-			// keypad arrows are converted for the menu in cursor arrows
-			// so use the event instead of ch
-			case ev_keydown:
-				ch = ev->key;
-			break;
-
-			default:
-			break;
-		}
-
-		control = controltochange;
+		INT32 control = controltochange;
 
 		// check if we already entered this key
-		found = -1;
-		if (setupcontrols[control][0] ==ch)
+		INT32 found = -1;
+		if (setupcontrols[control][0] == ch)
 			found = 0;
-		else if (setupcontrols[control][1] ==ch)
+		else if (setupcontrols[control][1] == ch)
 			found = 1;
+
 		if (found >= 0)
 		{
-			// replace mouse and joy clicks by double clicks
-			if (ch >= KEY_MOUSE1 && ch <= KEY_MOUSE1+MOUSEBUTTONS)
-				setupcontrols[control][found] = ch-KEY_MOUSE1+KEY_DBLMOUSE1;
-			else if (ch >= KEY_JOY1 && ch <= KEY_JOY1+JOYBUTTONS)
-				setupcontrols[control][found] = ch-KEY_JOY1+KEY_DBLJOY1;
-			else if (ch >= KEY_2MOUSE1 && ch <= KEY_2MOUSE1+MOUSEBUTTONS)
-				setupcontrols[control][found] = ch-KEY_2MOUSE1+KEY_DBL2MOUSE1;
-			else if (ch >= KEY_2JOY1 && ch <= KEY_2JOY1+JOYBUTTONS)
-				setupcontrols[control][found] = ch-KEY_2JOY1+KEY_DBL2JOY1;
+#define CHECK_DBL(key, length) (ch >= key && ch <= key+length)
+#define SET_DBL(key, dblkey) ch-key+dblkey
+			// replace mouse clicks by double clicks
+			if (CHECK_DBL(KEY_MOUSE1, MOUSEBUTTONS))
+				setupcontrols[control][found] = SET_DBL(KEY_MOUSE1, KEY_DBLMOUSE1);
+			else if (CHECK_DBL(KEY_2MOUSE1, MOUSEBUTTONS))
+				setupcontrols[control][found] = SET_DBL(KEY_2MOUSE1, KEY_DBL2MOUSE1);
+#undef CHECK_DBL
+#undef SET_DBL
 		}
 		else
 		{
@@ -12982,9 +12965,11 @@ static void M_ChangecontrolResponse(event_t *ev)
 				found = 0;
 				setupcontrols[control][1] = KEY_NULL;  //replace key 1,clear key2
 			}
-			(void)G_CheckDoubleUsage(ch, true);
+
+			G_CheckDoubleUsage(ch, true);
 			setupcontrols[control][found] = ch;
 		}
+
 		S_StartSound(NULL, sfx_strpst);
 	}
 	else if (ch == KEY_PAUSE)
@@ -13000,7 +12985,7 @@ static void M_ChangecontrolResponse(event_t *ev)
 			sprintf(tmp, M_GetText("The \x82Pause Key \x80is enabled, but \nit is not configurable. \n\nHit another key for\n%s\nESC for Cancel"),
 				controltochangetext);
 
-		M_StartMessage(tmp, M_ChangecontrolResponse, MM_EVENTHANDLER);
+		M_StartMessage(tmp, M_ChangeControlResponse, MM_EVENTHANDLER);
 		currentMenu->prevMenu = prev;
 
 		S_StartSound(NULL, sfx_s3k42);
@@ -13026,7 +13011,150 @@ static void M_ChangeControl(INT32 choice)
 		currentMenu->menuitems[choice].text);
 	strlcpy(controltochangetext, currentMenu->menuitems[choice].text, 33);
 
-	M_StartMessage(tmp, M_ChangecontrolResponse, MM_EVENTHANDLER);
+	M_StartMessage(tmp, M_ChangeControlResponse, MM_EVENTHANDLER);
+}
+
+static const char *M_GetGamepadAxisName(consvar_t *cv)
+{
+	switch (cv->value)
+	{
+		case 0:
+			return "None";
+
+		case 1: // X
+			return "L. Stick X";
+		case 2: // Y
+			return "L. Stick Y";
+		case 3: // X
+			return "R. Stick X";
+		case 4: // Y
+			return "R. Stick Y";
+
+		case -1: // X-
+			return "L. Stick X (inv.)";
+		case -2: // Y-
+			return "L. Stick Y (inv.)";
+		case -3: // X-
+			return "R. Stick X (inv.)";
+		case -4: // Y-
+			return "R. Stick Y (inv.)";
+
+		case 5:
+			return "L. Trigger";
+		case 6:
+			return "R. Trigger";
+
+		default:
+			return cv->string;
+	}
+}
+
+static void M_DrawGamepadMenu(void)
+{
+	INT32 x, y, i, cursory = 0;
+	INT32 right, left;
+
+	// DRAW MENU
+	x = currentMenu->x;
+	y = currentMenu->y;
+
+	// draw title (or big pic)
+	M_DrawMenuTitle();
+
+	for (i = 0; i < currentMenu->numitems; i++)
+	{
+		if (i == itemOn)
+			cursory = y;
+		switch (currentMenu->menuitems[i].status & IT_DISPLAY)
+		{
+			case IT_NOTHING:
+			case IT_DYBIGSPACE:
+				y += LINEHEIGHT;
+				break;
+			case IT_STRING:
+			case IT_WHITESTRING:
+				if (currentMenu->menuitems[i].alphaKey)
+					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
+				if (i == itemOn)
+					cursory = y;
+
+				if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
+					V_DrawString(x, y, 0, currentMenu->menuitems[i].text);
+				else
+					V_DrawString(x, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
+
+				right = x + V_StringWidth(currentMenu->menuitems[i].text, 0);
+
+				// Cvar specific handling
+				switch (currentMenu->menuitems[i].status & IT_TYPE)
+					case IT_CVAR:
+					{
+						consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
+						switch (currentMenu->menuitems[i].status & IT_CVARTYPE)
+						{
+							case IT_CV_SLIDER:
+								M_DrawSlider(x, y, cv, (i == itemOn));
+								break;
+							default:
+							{
+								const char *str = cv->string;
+								INT32 flags = V_YELLOWMAP;
+								INT32 width = V_StringWidth(str, flags);
+
+								if (cv->PossibleValue == joyaxis_cons_t)
+								{
+									str = M_GetGamepadAxisName(cv);
+									flags |= V_ALLOWLOWERCASE;
+
+									width = V_StringWidth(str, flags);
+									left = BASEVIDWIDTH - x - width;
+
+									if (left - 16 <= right)
+									{
+										width = V_ThinStringWidth(str, flags);
+										V_DrawRightAlignedThinString(BASEVIDWIDTH - x, y + 1, flags, str);
+									}
+									else
+										V_DrawRightAlignedString(BASEVIDWIDTH - x, y, flags, str);
+								}
+								else
+									V_DrawRightAlignedString(BASEVIDWIDTH - x, y, flags, str);
+
+								if (i == itemOn)
+								{
+									V_DrawCharacter(BASEVIDWIDTH - x - 10 - width - (skullAnimCounter/5), y,
+											'\x1C' | V_YELLOWMAP, false);
+									V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
+											'\x1D' | V_YELLOWMAP, false);
+								}
+								break;
+							}
+						}
+						break;
+					}
+					y += STRINGHEIGHT;
+					break;
+			case IT_TRANSTEXT:
+				if (currentMenu->menuitems[i].alphaKey)
+					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
+				V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text);
+				y += SMALLLINEHEIGHT;
+				break;
+			case IT_HEADERTEXT: // draws 16 pixels to the left, in yellow text
+				if (currentMenu->menuitems[i].alphaKey)
+					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
+
+				//V_DrawString(x-16, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
+				M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), currentMenu->menuitems[i].text, true, false);
+				y += SMALLLINEHEIGHT;
+				break;
+		}
+	}
+
+	// DRAW THE SKULL CURSOR
+	V_DrawScaledPatch(currentMenu->x - 24, cursory, 0,
+		W_CachePatchName("M_CURSOR", PU_PATCH));
+	V_DrawString(currentMenu->x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
 }
 
 static void M_Setup1PPlaystyleMenu(INT32 choice)
diff --git a/src/m_menu.h b/src/m_menu.h
index a7072b0c1..8d023811d 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -223,8 +223,9 @@ typedef enum
 {
 	MM_NOTHING = 0, // is just displayed until the user do someting
 	MM_YESNO,       // routine is called with only 'y' or 'n' in param
-	MM_EVENTHANDLER // the same of above but without 'y' or 'n' restriction
-	                // and routine is void routine(event_t *) (ex: set control)
+	MM_KEYHANDLER,  // the same of above but without 'y' or 'n' restriction
+	MM_EVENTHANDLER // the same of above but routine is void routine(event_t *)
+	                // (ex: set control)
 } menumessagetype_t;
 void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtype);
 
@@ -361,9 +362,11 @@ extern menu_t *currentMenu;
 extern menu_t MainDef;
 extern menu_t SP_LoadDef;
 
-// Call upon joystick hotplug
-void M_SetupJoystickMenu(INT32 choice);
-extern menu_t OP_JoystickSetDef;
+// Call when a gamepad is connected or disconnected
+void M_UpdateGamepadMenu(void);
+
+// Returns true if the player is on the gamepad selection menu
+boolean M_OnGamepadMenu(void);
 
 // Stuff for customizing the player select screen
 typedef struct
@@ -538,6 +541,19 @@ void M_FreePlayerSetupColors(void);
 	NULL\
 }
 
+#define GAMEPADMENUSTYLE(id, header, source, prev, x, y)\
+{\
+	id,\
+	header,\
+	sizeof(source)/sizeof(menuitem_t),\
+	prev,\
+	source,\
+	M_DrawGamepadMenu,\
+	x, y,\
+	0,\
+	NULL\
+}
+
 #define MAPPLATTERMENUSTYLE(id, header, source)\
 {\
 	id,\
@@ -558,7 +574,7 @@ void M_FreePlayerSetupColors(void);
 	sizeof (source)/sizeof (menuitem_t),\
 	prev,\
 	source,\
-	M_DrawControl,\
+	M_DrawControlConfigMenu,\
 	24, 40,\
 	0,\
 	NULL\
diff --git a/src/p_haptic.c b/src/p_haptic.c
new file mode 100644
index 000000000..dbfa58737
--- /dev/null
+++ b/src/p_haptic.c
@@ -0,0 +1,115 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2021-2022 by Jaime "Lactozilla" Passos.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  p_haptic.c
+/// \brief Haptic feedback
+
+#include "p_haptic.h"
+#include "g_game.h"
+#include "d_netcmd.h"
+#include "i_gamepad.h"
+#include "doomstat.h"
+
+// Helper function: Returns the gamepad index for a player if it's enabled
+static INT16 GetGamepadIndex(player_t *player)
+{
+	INT16 index = G_GetGamepadForPlayer(player);
+
+	if (index >= 0 && cv_usegamepad[index].value)
+		return index;
+
+	return -1;
+}
+
+// Rumbles a player's gamepad, or all gamepads
+boolean P_DoRumble(player_t *player, fixed_t large_magnitude, fixed_t small_magnitude, tic_t duration)
+{
+	if (!I_RumbleSupported())
+		return false;
+
+	// Rumble every gamepad
+	if (player == NULL)
+	{
+		for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+		{
+			if (cv_gamepad_rumble[i].value)
+				G_RumbleGamepad(i, large_magnitude, small_magnitude, duration);
+		}
+
+		return true;
+	}
+
+	INT16 which = GetGamepadIndex(player);
+	if (which < 0 || !cv_gamepad_rumble[which].value)
+		return false;
+
+	return G_RumbleGamepad((UINT8)which, large_magnitude, small_magnitude, duration);
+}
+
+// Pauses or unpauses gamepad rumble for a player (or all of them)
+// Rumble is paused or unpaused regardless if it's enabled or not
+static void SetRumblePaused(player_t *player, boolean pause)
+{
+	INT16 which = GetGamepadIndex(player);
+
+	if (which >= 0)
+		G_SetGamepadRumblePaused((UINT8)which, pause);
+	else if (player == NULL)
+	{
+		// Pause or unpause every gamepad
+		for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+			G_SetGamepadRumblePaused(i, pause);
+	}
+}
+
+void P_PauseRumble(player_t *player)
+{
+	SetRumblePaused(player, true);
+}
+
+void P_UnpauseRumble(player_t *player)
+{
+	SetRumblePaused(player, false);
+}
+
+boolean P_IsRumbleEnabled(player_t *player)
+{
+	INT16 which = GetGamepadIndex(player);
+	if (which < 0 || !cv_gamepad_rumble[which].value)
+		return false;
+
+	return G_RumbleSupported((UINT8)which);
+}
+
+boolean P_IsRumblePaused(player_t *player)
+{
+	INT16 which = GetGamepadIndex(player);
+	if (which < 0 || !cv_gamepad_rumble[which].value)
+		return false;
+
+	return G_GetGamepadRumblePaused((UINT8)which);
+}
+
+// Stops gamepad rumble for a player (or all of them)
+void P_StopRumble(player_t *player)
+{
+	if (!I_RumbleSupported())
+		return;
+
+	if (player)
+	{
+		INT16 which = GetGamepadIndex(player);
+		if (which >= 0)
+			G_StopGamepadRumble((UINT8)which);
+		return;
+	}
+
+	// Stop every gamepad instead
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+		G_StopGamepadRumble(i);
+}
diff --git a/src/p_haptic.h b/src/p_haptic.h
new file mode 100644
index 000000000..1bd4f9199
--- /dev/null
+++ b/src/p_haptic.h
@@ -0,0 +1,27 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2021-2022 by Jaime "Lactozilla" Passos.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  p_haptic.h
+/// \brief Haptic feedback
+
+#ifndef __P_HAPTIC__
+#define __P_HAPTIC__
+
+#include "doomdef.h"
+#include "p_local.h"
+
+boolean P_DoRumble(player_t *player, fixed_t large_magnitude, fixed_t small_magnitude, tic_t duration);
+void P_PauseRumble(player_t *player);
+void P_UnpauseRumble(player_t *player);
+boolean P_IsRumbleEnabled(player_t *player);
+boolean P_IsRumblePaused(player_t *player);
+void P_StopRumble(player_t *player);
+
+#define P_DoRumbleCombined(player, magnitude, dur) P_DoRumble(player, magnitude, magnitude, dur);
+
+#endif // __P_HAPTIC__
diff --git a/src/p_inter.c b/src/p_inter.c
index dd3e0f9c2..f3c13e315 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -13,6 +13,7 @@
 
 #include "doomdef.h"
 #include "i_system.h"
+#include "i_gamepad.h"
 #include "am_map.h"
 #include "g_game.h"
 #include "m_random.h"
@@ -24,6 +25,7 @@
 #include "lua_hook.h"
 #include "m_cond.h" // unlockables, emblems, etc
 #include "p_setup.h"
+#include "p_haptic.h"
 #include "m_cheat.h" // objectplace
 #include "m_misc.h"
 #include "v_video.h" // video flags for CEchos
@@ -33,54 +35,6 @@
 #define CTFTEAMCODE(pl) pl->ctfteam ? (pl->ctfteam == 1 ? "\x85" : "\x84") : ""
 #define CTFTEAMENDCODE(pl) pl->ctfteam ? "\x80" : ""
 
-void P_ForceFeed(const player_t *player, INT32 attack, INT32 fade, tic_t duration, INT32 period)
-{
-	BasicFF_t Basicfeed;
-	if (!player)
-		return;
-	Basicfeed.Duration = (UINT32)(duration * (100L/TICRATE));
-	Basicfeed.ForceX = Basicfeed.ForceY = 1;
-	Basicfeed.Gain = 25000;
-	Basicfeed.Magnitude = period*10;
-	Basicfeed.player = player;
-	/// \todo test FFB
-	P_RampConstant(&Basicfeed, attack, fade);
-}
-
-void P_ForceConstant(const BasicFF_t *FFInfo)
-{
-	JoyFF_t ConstantQuake;
-	if (!FFInfo || !FFInfo->player)
-		return;
-	ConstantQuake.ForceX    = FFInfo->ForceX;
-	ConstantQuake.ForceY    = FFInfo->ForceY;
-	ConstantQuake.Duration  = FFInfo->Duration;
-	ConstantQuake.Gain      = FFInfo->Gain;
-	ConstantQuake.Magnitude = FFInfo->Magnitude;
-	if (FFInfo->player == &players[consoleplayer])
-		I_Tactile(ConstantForce, &ConstantQuake);
-	else if (splitscreen && FFInfo->player == &players[secondarydisplayplayer])
-		I_Tactile2(ConstantForce, &ConstantQuake);
-}
-void P_RampConstant(const BasicFF_t *FFInfo, INT32 Start, INT32 End)
-{
-	JoyFF_t RampQuake;
-	if (!FFInfo || !FFInfo->player)
-		return;
-	RampQuake.ForceX    = FFInfo->ForceX;
-	RampQuake.ForceY    = FFInfo->ForceY;
-	RampQuake.Duration  = FFInfo->Duration;
-	RampQuake.Gain      = FFInfo->Gain;
-	RampQuake.Magnitude = FFInfo->Magnitude;
-	RampQuake.Start     = Start;
-	RampQuake.End       = End;
-	if (FFInfo->player == &players[consoleplayer])
-		I_Tactile(ConstantForce, &RampQuake);
-	else if (splitscreen && FFInfo->player == &players[secondarydisplayplayer])
-		I_Tactile2(ConstantForce, &RampQuake);
-}
-
-
 //
 // GET STUFF
 //
@@ -3057,6 +3011,8 @@ static boolean P_TagDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, IN
 	player_t *player = target->player;
 	(void)damage; //unused parm
 
+	P_DoRumbleCombined(player, FRACUNIT, TICRATE / 6);
+
 	// If flashing or invulnerable, ignore the tag,
 	if (player->powers[pw_flashing] || player->powers[pw_invulnerability])
 		return false;
@@ -3160,6 +3116,8 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou
 {
 	player_t *player = target->player;
 
+	(void)damage;
+
 	if (!(damagetype & DMG_CANHURTSELF))
 	{
 		// You can't kill yourself, idiot...
@@ -3222,6 +3180,8 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou
 
 static void P_KillPlayer(player_t *player, mobj_t *source, INT32 damage)
 {
+	(void)damage;
+
 	player->pflags &= ~PF_SLIDING;
 
 	player->powers[pw_carry] = CR_NONE;
@@ -3242,7 +3202,7 @@ static void P_KillPlayer(player_t *player, mobj_t *source, INT32 damage)
 	// Get rid of emeralds
 	player->powers[pw_emeralds] = 0;
 
-	P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
+	P_DoRumbleCombined(player, FRACUNIT, TICRATE / 3);
 
 	P_ResetPlayer(player);
 
@@ -3282,7 +3242,9 @@ static void P_SuperDamage(player_t *player, mobj_t *inflictor, mobj_t *source, I
 	fixed_t fallbackspeed;
 	angle_t ang;
 
-	P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
+	(void)damage;
+
+	P_DoRumbleCombined(player, FRACUNIT, TICRATE / 6);
 
 	if (player->mo->eflags & MFE_VERTICALFLIP)
 		player->mo->z--;
@@ -3363,12 +3325,14 @@ void P_RemoveShield(player_t *player)
 
 static void P_ShieldDamage(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
 {
+	(void)damage;
+
 	// Must do pain first to set flashing -- P_RemoveShield can cause damage
 	P_DoPlayerPain(player, source, inflictor);
 
 	P_RemoveShield(player);
 
-	P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
+	P_DoRumbleCombined(player, FRACUNIT, TICRATE / 6);
 
 	if (damagetype == DMG_SPIKE) // spikes
 		S_StartSound(player->mo, sfx_spkdth);
@@ -3397,7 +3361,7 @@ static void P_RingDamage(player_t *player, mobj_t *inflictor, mobj_t *source, IN
 {
 	P_DoPlayerPain(player, source, inflictor);
 
-	P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
+	P_DoRumbleCombined(player, FRACUNIT, TICRATE / 6);
 
 	if (damagetype == DMG_SPIKE) // spikes
 		S_StartSound(player->mo, sfx_spkdth);
@@ -3728,8 +3692,6 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			damage = 1;
 			P_KillPlayer(player, source, damage);
 		}
-
-		P_ForceFeed(player, 40, 10, TICRATE, 40 + min(damage, 100)*2);
 	}
 
 	// Killing dead. Just for kicks.
diff --git a/src/p_local.h b/src/p_local.h
index 2b3020997..31a9e6c9d 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -453,18 +453,6 @@ extern mobj_t **blocklinks; // for thing chains
 //
 // P_INTER
 //
-typedef struct BasicFF_s
-{
-	INT32 ForceX; ///< The X of the Force's Vel
-	INT32 ForceY; ///< The Y of the Force's Vel
-	const player_t *player; ///< Player of Rumble
-	//All
-	UINT32 Duration; ///< The total duration of the effect, in microseconds
-	INT32 Gain; ///< /The gain to be applied to the effect, in the range from 0 through 10,000.
-	//All, CONSTANTFORCE �10,000 to 10,000
-	INT32 Magnitude; ///< Magnitude of the effect, in the range from 0 through 10,000.
-} BasicFF_t;
-
 /* Damage/death types, for P_DamageMobj and related */
 //// Damage types
 //#define DMG_NORMAL 0 (unneeded?)
@@ -485,9 +473,6 @@ typedef struct BasicFF_s
 #define DMG_CANHURTSELF 0x40 // Flag - can hurt self/team indirectly, such as through mines
 #define DMG_DEATHMASK  DMG_INSTAKILL // if bit 7 is set, this is a death type instead of a damage type
 
-void P_ForceFeed(const player_t *player, INT32 attack, INT32 fade, tic_t duration, INT32 period);
-void P_ForceConstant(const BasicFF_t *FFInfo);
-void P_RampConstant(const BasicFF_t *FFInfo, INT32 Start, INT32 End);
 void P_RemoveShield(player_t *player);
 void P_SpecialStageDamage(player_t *player, mobj_t *inflictor, mobj_t *source);
 boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype);
diff --git a/src/p_user.c b/src/p_user.c
index 45978d105..6fd24eb13 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -5327,9 +5327,9 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						// disabled because it seemed to disorient people and Z-targeting exists now
 						/*if (!demoplayback)
 						{
-							if (player == &players[consoleplayer] && cv_cam_turnfacingability[0].value > 0 && !(PLAYER1INPUTDOWN(GC_TURNLEFT) || PLAYER1INPUTDOWN(GC_TURNRIGHT)))
+							if (player == &players[consoleplayer] && cv_cam_turnfacingability[0].value > 0 && !(G_PlayerInputDown(0, GC_TURNLEFT) || G_PlayerInputDown(0, GC_TURNRIGHT)))
 								P_SetPlayerAngle(player, player->mo->angle);;
-							else if (player == &players[secondarydisplayplayer] && cv_cam_turnfacingability[1].value > 0 && !(PLAYER2INPUTDOWN(GC_TURNLEFT) || PLAYER2INPUTDOWN(GC_TURNRIGHT)))
+							else if (player == &players[secondarydisplayplayer] && cv_cam_turnfacingability[1].value > 0 && !(G_PlayerInputDown(1, GC_TURNLEFT) || G_PlayerInputDown(1, GC_TURNRIGHT)))
 								P_SetPlayerAngle(player, player->mo->angle);
 						}*/
 					}
@@ -7335,7 +7335,7 @@ static void P_NiGHTSMovement(player_t *player)
 			else if (cmd->forwardmove < 0)
 				newangle = 270;
 		}
-		else // AngleFixed(R_PointToAngle2()) results in slight inaccuracy! Don't use it unless movement is on both axises.
+		else // AngleFixed(R_PointToAngle2()) results in slight inaccuracy! Don't use it unless movement is on both axes.
 			newangle = (INT16)FixedInt(AngleFixed(R_PointToAngle2(0,0, cmd->sidemove*FRACUNIT, cmd->forwardmove*FRACUNIT)));
 
 		newangle -= player->viewrollangle / ANG1;
diff --git a/src/sdl/Sourcefile b/src/sdl/Sourcefile
index 82d5ce073..ef6a8b0dc 100644
--- a/src/sdl/Sourcefile
+++ b/src/sdl/Sourcefile
@@ -2,6 +2,7 @@ i_net.c
 i_system.c
 i_main.c
 i_video.c
+i_gamepad.c
 dosstr.c
 endtxt.c
 hwsym_sdl.c
diff --git a/src/sdl/i_gamepad.c b/src/sdl/i_gamepad.c
new file mode 100644
index 000000000..c3150ac6e
--- /dev/null
+++ b/src/sdl/i_gamepad.c
@@ -0,0 +1,921 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  i_gamepad.c
+/// \brief Gamepads
+
+#ifdef HAVE_SDL
+#include "../i_gamepad.h"
+#include "../i_system.h"
+#include "../doomdef.h"
+#include "../d_main.h"
+#include "../d_netcmd.h"
+#include "../g_game.h"
+#include "../m_argv.h"
+#include "../m_menu.h"
+#include "../z_zone.h"
+
+#include "SDL.h"
+#include "SDL_joystick.h"
+#include "sdlmain.h"
+
+static void Controller_ChangeDevice(UINT8 num);
+static void Controller_Close(UINT8 num);
+static void Controller_StopRumble(UINT8 num);
+
+static ControllerInfo controllers[NUM_GAMEPADS];
+
+static boolean rumble_supported = false;
+static boolean rumble_paused = false;
+
+// This attempts to initialize the gamepad subsystems
+static boolean InitGamepadSubsystems(void)
+{
+	if (M_CheckParm("-noxinput"))
+		SDL_SetHintWithPriority(SDL_HINT_XINPUT_ENABLED, "0", SDL_HINT_OVERRIDE);
+	if (M_CheckParm("-nohidapi"))
+		SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_HIDAPI, "0", SDL_HINT_OVERRIDE);
+
+	if (SDL_WasInit(GAMEPAD_INIT_FLAGS) == 0)
+	{
+		if (SDL_InitSubSystem(GAMEPAD_INIT_FLAGS) == -1)
+		{
+			CONS_Printf(M_GetText("Couldn't initialize game controller subsystems: %s\n"), SDL_GetError());
+			return false;
+		}
+	}
+
+	return true;
+}
+
+void I_InitGamepads(void)
+{
+	if (M_CheckParm("-nojoy"))
+		return;
+
+	CONS_Printf("I_InitGamepads()...\n");
+
+	if (!InitGamepadSubsystems())
+		return;
+
+	rumble_supported = !M_CheckParm("-norumble");
+
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+		controllers[i].info = &gamepads[i];
+
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+		Controller_ChangeDevice(i);
+}
+
+INT32 I_NumGamepads(void)
+{
+	if (SDL_WasInit(GAMEPAD_INIT_FLAGS) == GAMEPAD_INIT_FLAGS)
+		return SDL_NumJoysticks();
+	else
+		return 0;
+}
+
+// From the SDL source code
+#define USB_VENDOR_MICROSOFT    0x045e
+#define USB_VENDOR_PDP          0x0e6f
+#define USB_VENDOR_POWERA_ALT   0x20d6
+
+#define USB_PRODUCT_XBOX_ONE_ELITE_SERIES_1                 0x02e3
+#define USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2                 0x0b00
+#define USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH       0x0b05
+#define USB_PRODUCT_XBOX_SERIES_X                           0x0b12
+#define USB_PRODUCT_XBOX_SERIES_X_BLE                       0x0b13
+#define USB_PRODUCT_XBOX_SERIES_X_VICTRIX_GAMBIT            0x02d6
+#define USB_PRODUCT_XBOX_SERIES_X_PDP_BLUE                  0x02d9
+#define USB_PRODUCT_XBOX_SERIES_X_PDP_AFTERGLOW             0x02da
+#define USB_PRODUCT_XBOX_SERIES_X_POWERA_FUSION_PRO2        0x4001
+#define USB_PRODUCT_XBOX_SERIES_X_POWERA_SPECTRA            0x4002
+
+static boolean IsJoystickXboxOneElite(Uint16 vendor_id, Uint16 product_id)
+{
+	if (vendor_id == USB_VENDOR_MICROSOFT) {
+		if (product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_1 ||
+			product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2 ||
+			product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static boolean IsJoystickXboxSeriesXS(Uint16 vendor_id, Uint16 product_id)
+{
+	if (vendor_id == USB_VENDOR_MICROSOFT) {
+		if (product_id == USB_PRODUCT_XBOX_SERIES_X ||
+			product_id == USB_PRODUCT_XBOX_SERIES_X_BLE) {
+			return true;
+		}
+	}
+	else if (vendor_id == USB_VENDOR_PDP) {
+		if (product_id == USB_PRODUCT_XBOX_SERIES_X_VICTRIX_GAMBIT ||
+			product_id == USB_PRODUCT_XBOX_SERIES_X_PDP_BLUE ||
+			product_id == USB_PRODUCT_XBOX_SERIES_X_PDP_AFTERGLOW) {
+			return true;
+		}
+	}
+	else if (vendor_id == USB_VENDOR_POWERA_ALT) {
+		if ((product_id >= 0x2001 && product_id <= 0x201a) ||
+			product_id == USB_PRODUCT_XBOX_SERIES_X_POWERA_FUSION_PRO2 ||
+			product_id == USB_PRODUCT_XBOX_SERIES_X_POWERA_SPECTRA) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+// Opens a controller device
+static boolean Controller_OpenDevice(UINT8 which, INT32 devindex)
+{
+	if (SDL_WasInit(GAMEPAD_INIT_FLAGS) == 0)
+	{
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("Game controller subsystems not started\n"));
+		return false;
+	}
+
+	if (devindex <= 0)
+		return false;
+
+	if (SDL_NumJoysticks() == 0)
+	{
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("Found no controllers on this system\n"));
+		return false;
+	}
+
+	devindex--;
+
+	if (!SDL_IsGameController(devindex))
+	{
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("Device index %d isn't a game controller\n"), devindex);
+		return false;
+	}
+
+	ControllerInfo *controller = &controllers[which];
+	SDL_GameController *newdev = SDL_GameControllerOpen(devindex);
+
+	// Handle the edge case where the device <-> controller index assignment can change due to hotplugging
+	// This indexing is SDL's responsibility and there's not much we can do about it.
+	//
+	// Example:
+	// 1. Plug Controller A   -> Index 0 opened
+	// 2. Plug Controller B   -> Index 1 opened
+	// 3. Unplug Controller A -> Index 0 closed, Index 1 active
+	// 4. Unplug Controller B -> Index 0 inactive, Index 1 closed
+	// 5. Plug Controller B   -> Index 0 opened
+	// 6. Plug Controller A   -> Index 0 REPLACED, opened as Controller A; Index 1 is now Controller B
+	if (controller->dev)
+	{
+		if (controller->dev == newdev // same device, nothing to do
+			|| (newdev == NULL && SDL_GameControllerGetAttached(controller->dev))) // we failed, but already have a working device
+			return true;
+
+		// Else, we're changing devices, so close the controller
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("Controller %d device is changing; closing controller...\n"), which);
+		Controller_Close(which);
+	}
+
+	if (newdev == NULL)
+	{
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("Controller %d: Couldn't open device - %s\n"), which, SDL_GetError());
+		controller->started = false;
+	}
+	else
+	{
+		controller->dev = newdev;
+		controller->joydev = SDL_GameControllerGetJoystick(controller->dev);
+		controller->started = true;
+
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("Controller %d: %s\n"), which, SDL_GameControllerName(controller->dev));
+
+	#define GAMEPAD_TYPE_CASE(ctrl) \
+		case SDL_CONTROLLER_TYPE_##ctrl: \
+			controller->info->type = GAMEPAD_TYPE_##ctrl; \
+			break
+
+		switch (SDL_GameControllerGetType(newdev))
+		{
+			GAMEPAD_TYPE_CASE(UNKNOWN);
+			GAMEPAD_TYPE_CASE(XBOX360);
+			GAMEPAD_TYPE_CASE(XBOXONE);
+			GAMEPAD_TYPE_CASE(PS3);
+			GAMEPAD_TYPE_CASE(PS4);
+			GAMEPAD_TYPE_CASE(PS5);
+			GAMEPAD_TYPE_CASE(NINTENDO_SWITCH_PRO);
+			GAMEPAD_TYPE_CASE(GOOGLE_STADIA);
+			GAMEPAD_TYPE_CASE(AMAZON_LUNA);
+			GAMEPAD_TYPE_CASE(VIRTUAL);
+			default: break;
+		}
+
+	#undef GAMEPAD_BUTTON_CASE
+
+		// Check the device vendor and product to find out what controller this actually is
+		Uint16 vendor = SDL_JoystickGetDeviceVendor(devindex);
+		Uint16 product = SDL_JoystickGetDeviceProduct(devindex);
+
+		if (IsJoystickXboxSeriesXS(vendor, product))
+			controller->info->type = GAMEPAD_TYPE_XBOX_SERIES_XS;
+		else if (IsJoystickXboxOneElite(vendor, product))
+			controller->info->type = GAMEPAD_TYPE_XBOX_ELITE;
+
+		CONS_Debug(DBG_GAMELOGIC, M_GetText("    Type: %s\n"), G_GamepadTypeToString(controller->info->type));
+
+		// Change the ring LEDs on Xbox 360 controllers
+		// TODO: Doesn't seem to work?
+		SDL_GameControllerSetPlayerIndex(controller->dev, which);
+
+		// Check if rumble is supported
+		if (SDL_GameControllerHasRumble(controller->dev) == SDL_TRUE)
+		{
+			controller->info->rumble.supported = true;
+			CONS_Debug(DBG_GAMELOGIC, M_GetText("    Rumble supported: Yes\n"));
+		}
+		else
+		{
+			controller->info->rumble.supported = false;
+			CONS_Debug(DBG_GAMELOGIC, M_GetText("    Rumble supported: No\n"));;
+		}
+
+		controller->info->connected = true;
+	}
+
+	return controller->started;
+}
+
+// Initializes a controller
+static INT32 Controller_Init(SDL_GameController **newcontroller, UINT8 which, INT32 *index)
+{
+	ControllerInfo *info = &controllers[which];
+	SDL_GameController *controller = NULL;
+	INT32 device = (*index);
+
+	if (device && SDL_IsGameController(device - 1))
+		controller = SDL_GameControllerOpen(device - 1);
+	if (newcontroller)
+		(*newcontroller) = controller;
+
+	if (controller && info->dev == controller) // don't override an active device
+		(*index) = I_GetControllerIndex(info->dev) + 1;
+	else if (controller && Controller_OpenDevice(which, device))
+	{
+		// SDL's device indexes are unstable, so cv_usegamepad may not match
+		// the actual device index. So let's cheat a bit and find the device's current index.
+		info->lastindex = I_GetControllerIndex(info->dev) + 1;
+		return 1;
+	}
+	else
+	{
+		(*index) = 0;
+		return 0;
+	}
+
+	return -1;
+}
+
+// Changes a controller's device
+static void Controller_ChangeDevice(UINT8 num)
+{
+	SDL_GameController *newjoy = NULL;
+
+	if (!Controller_Init(&newjoy, num, &cv_usegamepad[num].value) && controllers[num].lastindex)
+		Controller_Close(num);
+
+	I_CloseInactiveController(newjoy);
+}
+
+static boolean Controller_IsAnyUsingDevice(SDL_GameController *dev)
+{
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		if (controllers[i].dev == dev)
+			return true;
+	}
+
+	return false;
+}
+
+static boolean Controller_IsAnyOtherUsingDevice(SDL_GameController *dev, UINT8 thisjoy)
+{
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		if (i == thisjoy)
+			continue;
+		else if (controllers[i].dev == dev)
+			return true;
+	}
+
+	return false;
+}
+
+void I_ControllerDeviceAdded(INT32 which)
+{
+	if (!SDL_IsGameController(which))
+		return;
+
+	SDL_GameController *newjoy = SDL_GameControllerOpen(which);
+
+	CONS_Debug(DBG_GAMELOGIC, "Gamepad device index %d added\n", which + 1);
+
+	// Because SDL's device index is unstable, we're going to cheat here a bit:
+	// For the first controller setting that is NOT active:
+	// 1. Set cv_usegamepadX.value to the new device index (this does not change what is written to config.cfg)
+	// 2. Set OTHERS' cv_usegamepadX.value to THEIR new device index, because it likely changed
+	//    * If device doesn't exist, switch cv_usegamepad back to default value (.string)
+	//      * BUT: If that default index is being occupied, use ANOTHER cv_usegamepad's default value!
+	for (UINT8 this = 0; this < NUM_GAMEPADS && newjoy; this++)
+	{
+		if ((!controllers[this].dev || !SDL_GameControllerGetAttached(controllers[this].dev))
+			&& !Controller_IsAnyOtherUsingDevice(newjoy, this)) // don't override a currently active device
+		{
+			cv_usegamepad[this].value = which + 1;
+
+			// Go through every other device
+			for (UINT8 other = 0; other < NUM_GAMEPADS; other++)
+			{
+				if (other == this)
+				{
+					// Don't change this controller's index
+					continue;
+				}
+				else if (controllers[other].dev)
+				{
+					// Update this controller's index if the device is open
+					cv_usegamepad[other].value = I_GetControllerIndex(controllers[other].dev) + 1;
+				}
+				else if (atoi(cv_usegamepad[other].string) != controllers[this].lastindex
+						&& atoi(cv_usegamepad[other].string) != cv_usegamepad[this].value)
+				{
+					// If the user-set index for the other controller doesn't
+					// match this controller's current or former internal index,
+					// then use the other controller's internal index
+					cv_usegamepad[other].value = atoi(cv_usegamepad[other].string);
+				}
+				else if (atoi(cv_usegamepad[this].string) != controllers[this].lastindex
+						&& atoi(cv_usegamepad[this].string) != cv_usegamepad[this].value)
+				{
+					// If the user-set index for this controller doesn't match
+					// its current or former internal index, then use this
+					// controller's internal index
+					cv_usegamepad[other].value = atoi(cv_usegamepad[this].string);
+				}
+				else
+				{
+					// Try again
+					cv_usegamepad[other].value = 0;
+					continue;
+				}
+
+				break;
+			}
+
+			break;
+		}
+	}
+
+	// Was cv_usegamepad disabled in settings?
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		if (!strcmp(cv_usegamepad[i].string, "0") || !cv_usegamepad[i].value)
+			cv_usegamepad[i].value = 0;
+		else if (atoi(cv_usegamepad[i].string) <= I_NumGamepads() // don't mess if we intentionally set higher than NumJoys
+			     && cv_usegamepad[i].value) // update the cvar ONLY if a device exists
+			CV_SetValue(&cv_usegamepad[i], cv_usegamepad[i].value);
+	}
+
+	// Update all gamepads' init states
+	// This is a little wasteful since cv_usegamepad already calls this, but
+	// we need to do this in case CV_SetValue did nothing because the string was already same.
+	// if the device is already active, this should do nothing, effectively.
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		Controller_ChangeDevice(i);
+		CONS_Debug(DBG_GAMELOGIC, "Controller %d device index: %d\n", i, controllers[i].lastindex);
+	}
+
+	if (M_OnGamepadMenu())
+		M_UpdateGamepadMenu();
+
+	I_CloseInactiveController(newjoy);
+}
+
+void I_ControllerDeviceRemoved(void)
+{
+	for (UINT8 this = 0; this < NUM_GAMEPADS; this++)
+	{
+		if (controllers[this].dev && !SDL_GameControllerGetAttached(controllers[this].dev))
+		{
+			CONS_Debug(DBG_GAMELOGIC, "Controller %d removed, device index: %d\n", this, controllers[this].lastindex);
+			G_OnGamepadDisconnect(this);
+			Controller_Close(this);
+		}
+
+		// Update the device indexes, because they likely changed
+		// * If device doesn't exist, switch cv_usegamepad back to default value (.string)
+		//   * BUT: If that default index is being occupied, use ANOTHER cv_usegamepad's default value!
+		if (controllers[this].dev)
+			cv_usegamepad[this].value = controllers[this].lastindex = I_GetControllerIndex(controllers[this].dev) + 1;
+		else
+		{
+			for (UINT8 other = 0; other < NUM_GAMEPADS; other++)
+			{
+				if (other == this)
+					continue;
+
+				if (atoi(cv_usegamepad[this].string) != controllers[other].lastindex)
+				{
+					// Update this internal index if this user-set index
+					// doesn't match the other's former internal index
+					cv_usegamepad[this].value = atoi(cv_usegamepad[this].string);
+				}
+				else if (atoi(cv_usegamepad[other].string) != controllers[other].lastindex)
+				{
+					// Otherwise, set this internal index to the other's
+					// user-set index, if the other user-set index is not the
+					// same as the other's former internal index
+					cv_usegamepad[this].value = atoi(cv_usegamepad[other].string);
+				}
+				else
+				{
+					// Try again
+					cv_usegamepad[this].value = 0;
+					continue;
+				}
+
+				break;
+			}
+		}
+
+		// Was cv_usegamepad disabled in settings?
+		if (!strcmp(cv_usegamepad[this].string, "0"))
+			cv_usegamepad[this].value = 0;
+		else if (atoi(cv_usegamepad[this].string) <= I_NumGamepads() // don't mess if we intentionally set higher than NumJoys
+				 && cv_usegamepad[this].value) // update the cvar ONLY if a device exists
+			CV_SetValue(&cv_usegamepad[this], cv_usegamepad[this].value);
+
+		CONS_Debug(DBG_GAMELOGIC, "Controller %d device index: %d\n", this, controllers[this].lastindex);
+	}
+
+	if (M_OnGamepadMenu())
+		M_UpdateGamepadMenu();
+}
+
+// Close the controller device if there isn't any controller using it
+void I_CloseInactiveController(SDL_GameController *dev)
+{
+	if (!Controller_IsAnyUsingDevice(dev))
+		SDL_GameControllerClose(dev);
+}
+
+// Cheat to get the device index for a game controller handle
+INT32 I_GetControllerIndex(SDL_GameController *dev)
+{
+	INT32 i, count = SDL_NumJoysticks();
+
+	for (i = 0; dev && i < count; i++)
+	{
+		SDL_GameController *test = SDL_GameControllerOpen(i);
+		if (test && test == dev)
+			return i;
+		else
+			I_CloseInactiveController(test);
+	}
+
+	return -1;
+}
+
+// Changes a gamepad's device
+void I_ChangeGamepad(UINT8 which)
+{
+	if (which >= NUM_GAMEPADS)
+		return;
+
+	if (controllers[which].started)
+		Controller_StopRumble(which);
+
+	Controller_ChangeDevice(which);
+}
+
+// Returns the name of a controller from its index
+const char *I_GetGamepadName(INT32 joyindex)
+{
+	static char joyname[256];
+	joyname[0] = '\0';
+
+	if (SDL_WasInit(GAMEPAD_INIT_FLAGS) == GAMEPAD_INIT_FLAGS)
+	{
+		const char *tempname = SDL_GameControllerNameForIndex(joyindex - 1);
+		if (tempname)
+			strlcpy(joyname, tempname, sizeof joyname);
+	}
+
+	return joyname;
+}
+
+// Toggles a gamepad's digital axis setting
+void I_SetGamepadDigital(UINT8 which, boolean enable)
+{
+	if (which >= NUM_GAMEPADS)
+		return;
+
+	gamepads[which].digital = enable;
+}
+
+static gamepad_t *Controller_GetFromID(SDL_JoystickID which, UINT8 *found)
+{
+	SDL_JoystickID joyid[NUM_GAMEPADS];
+
+	UINT8 i;
+
+	// Determine the joystick IDs for each current open controller
+	for (i = 0; i < NUM_GAMEPADS; i++)
+		joyid[i] = SDL_JoystickInstanceID(controllers[i].joydev);
+
+	for (i = 0; i < NUM_GAMEPADS; i++)
+	{
+		if (which == joyid[i])
+		{
+			(*found) = i;
+			return &gamepads[i];
+		}
+	}
+
+	(*found) = UINT8_MAX;
+
+	return NULL;
+}
+
+void I_HandleControllerButtonEvent(SDL_ControllerButtonEvent evt, Uint32 type)
+{
+	event_t event;
+
+	gamepad_t *gamepad = Controller_GetFromID(evt.which, &event.which);
+	if (gamepad == NULL)
+		return;
+
+	if (type == SDL_CONTROLLERBUTTONUP)
+		event.type = ev_gamepad_up;
+	else if (type == SDL_CONTROLLERBUTTONDOWN)
+		event.type = ev_gamepad_down;
+	else
+		return;
+
+#define GAMEPAD_BUTTON_CASE(btn) \
+	case SDL_CONTROLLER_BUTTON_##btn: \
+		event.key = GAMEPAD_BUTTON_##btn; \
+		break
+
+	switch (evt.button)
+	{
+		GAMEPAD_BUTTON_CASE(A);
+		GAMEPAD_BUTTON_CASE(B);
+		GAMEPAD_BUTTON_CASE(X);
+		GAMEPAD_BUTTON_CASE(Y);
+		GAMEPAD_BUTTON_CASE(BACK);
+		GAMEPAD_BUTTON_CASE(GUIDE);
+		GAMEPAD_BUTTON_CASE(START);
+		GAMEPAD_BUTTON_CASE(LEFTSTICK);
+		GAMEPAD_BUTTON_CASE(RIGHTSTICK);
+		GAMEPAD_BUTTON_CASE(LEFTSHOULDER);
+		GAMEPAD_BUTTON_CASE(RIGHTSHOULDER);
+		GAMEPAD_BUTTON_CASE(DPAD_UP);
+		GAMEPAD_BUTTON_CASE(DPAD_DOWN);
+		GAMEPAD_BUTTON_CASE(DPAD_LEFT);
+		GAMEPAD_BUTTON_CASE(DPAD_RIGHT);
+		GAMEPAD_BUTTON_CASE(MISC1);
+		GAMEPAD_BUTTON_CASE(PADDLE1);
+		GAMEPAD_BUTTON_CASE(PADDLE2);
+		GAMEPAD_BUTTON_CASE(PADDLE3);
+		GAMEPAD_BUTTON_CASE(PADDLE4);
+		GAMEPAD_BUTTON_CASE(TOUCHPAD);
+		default: return;
+	}
+
+#undef GAMEPAD_BUTTON_CASE
+
+	D_PostEvent(&event);
+}
+
+void I_HandleControllerAxisEvent(SDL_ControllerAxisEvent evt)
+{
+	event_t event;
+
+	gamepad_t *gamepad = Controller_GetFromID(evt.which, &event.which);
+	if (gamepad == NULL)
+		return;
+
+#define GAMEPAD_AXIS_CASE(btn) \
+	case SDL_CONTROLLER_AXIS_##btn: \
+		event.key = GAMEPAD_AXIS_##btn; \
+		break
+
+	switch (evt.axis)
+	{
+		GAMEPAD_AXIS_CASE(LEFTX);
+		GAMEPAD_AXIS_CASE(LEFTY);
+		GAMEPAD_AXIS_CASE(RIGHTX);
+		GAMEPAD_AXIS_CASE(RIGHTY);
+		GAMEPAD_AXIS_CASE(TRIGGERLEFT);
+		GAMEPAD_AXIS_CASE(TRIGGERRIGHT);
+		default: return;
+	}
+
+#undef GAMEPAD_AXIS_CASE
+
+	event.type = ev_gamepad_axis;
+	event.x = evt.value;
+
+	D_PostEvent(&event);
+}
+
+static void Controller_StopRumble(UINT8 num)
+{
+	ControllerInfo *controller = &controllers[num];
+
+	controller->rumble.large_magnitude = 0;
+	controller->rumble.small_magnitude = 0;
+	controller->rumble.time_left = 0;
+	controller->rumble.expiration = 0;
+
+	gamepad_t *gamepad = controller->info;
+
+	gamepad->rumble.active = false;
+	gamepad->rumble.paused = false;
+	gamepad->rumble.data.large_magnitude = 0;
+	gamepad->rumble.data.small_magnitude = 0;
+	gamepad->rumble.data.duration = 0;
+
+	if (gamepad->rumble.supported)
+		SDL_GameControllerRumble(controller->dev, 0, 0, 0);
+}
+
+static void Controller_Close(UINT8 num)
+{
+	ControllerInfo *controller = &controllers[num];
+
+	// Close the game controller device
+	if (controller->dev)
+	{
+		Controller_StopRumble(num);
+		SDL_GameControllerClose(controller->dev);
+	}
+
+	controller->dev = NULL;
+	controller->joydev = NULL;
+	controller->lastindex = -1;
+	controller->started = false;
+
+	// Reset gamepad info
+	gamepad_t *gamepad = controller->info;
+
+	if (gamepad)
+	{
+		gamepad->type = GAMEPAD_TYPE_UNKNOWN;
+		gamepad->connected = false;
+		gamepad->digital = false;
+		gamepad->rumble.supported = false;
+
+		for (UINT8 i = 0; i < NUM_GAMEPAD_BUTTONS; i++)
+			gamepad->buttons[i] = 0;
+
+		for (UINT8 i = 0; i < NUM_GAMEPAD_AXES; i++)
+			gamepad->axes[i] = 0;
+	}
+}
+
+void I_ShutdownGamepads(void)
+{
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+		Controller_Close(i);
+}
+
+boolean I_RumbleSupported(void)
+{
+	return rumble_supported;
+}
+
+static boolean Controller_Rumble(ControllerInfo *c)
+{
+	if (SDL_GameControllerRumble(c->dev, c->rumble.large_magnitude, c->rumble.small_magnitude, 0) == -1)
+		return false;
+
+	return true;
+}
+
+void I_ToggleControllerRumble(boolean unpause)
+{
+	if (!I_RumbleSupported() || rumble_paused == !unpause)
+		return;
+
+	rumble_paused = !unpause;
+
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		ControllerInfo *controller = &controllers[i];
+		if (!controller->started || !controller->info->rumble.supported)
+			continue;
+
+		if (rumble_paused)
+			SDL_GameControllerRumble(controller->dev, 0, 0, 0);
+		else if (!controller->info->rumble.paused)
+		{
+			if (!Controller_Rumble(controller))
+				controller->rumble.expiration = controller->rumble.time_left = 0;
+		}
+	}
+}
+
+void I_UpdateControllers(void)
+{
+	if (SDL_WasInit(GAMEPAD_INIT_FLAGS) != GAMEPAD_INIT_FLAGS)
+		return;
+
+	for (UINT8 i = 0; i < NUM_GAMEPADS; i++)
+	{
+		ControllerInfo *controller = &controllers[i];
+		if (!controller->started || !controller->info->rumble.supported || controller->info->rumble.paused)
+			continue;
+
+		if (controller->rumble.expiration &&
+			SDL_TICKS_PASSED(SDL_GetTicks(), controller->rumble.expiration))
+		{
+			// Enough time has passed, so stop the effect
+			Controller_StopRumble(i);
+		}
+	}
+
+	SDL_JoystickUpdate();
+}
+
+// Converts duration in tics to milliseconds
+#define TICS_TO_MS(tics) ((INT32)(tics * (1000.0f/TICRATE)))
+
+boolean I_RumbleGamepad(UINT8 which, const haptic_t *effect)
+{
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS)
+		return false;
+
+	ControllerInfo *controller = &controllers[which];
+	if (!controller->started || !controller->info->rumble.supported)
+		return false;
+
+	UINT16 duration = min(TICS_TO_MS(effect->duration), UINT16_MAX);
+	UINT16 large_magnitude = max(0, min(effect->large_magnitude, UINT16_MAX));
+	UINT16 small_magnitude = max(0, min(effect->small_magnitude, UINT16_MAX));
+
+	CONS_Debug(DBG_GAMELOGIC, "Starting rumble effect for controller %d:\n", which);
+	CONS_Debug(DBG_GAMELOGIC, "    Large motor magnitude: %f\n", large_magnitude / 65535.0f);
+	CONS_Debug(DBG_GAMELOGIC, "    Small motor magnitude: %f\n", small_magnitude / 65535.0f);
+
+	if (!duration)
+		CONS_Debug(DBG_GAMELOGIC, "    Duration: forever\n");
+	else
+		CONS_Debug(DBG_GAMELOGIC, "    Duration: %dms\n", duration);
+
+	controller->rumble.large_magnitude = large_magnitude;
+	controller->rumble.small_magnitude = small_magnitude;
+
+	if (!rumble_paused && !Controller_Rumble(controller))
+	{
+		Controller_StopRumble(which);
+		return false;
+	}
+
+	controller->rumble.time_left = 0;
+
+	if (duration)
+		controller->rumble.expiration = SDL_GetTicks() + duration;
+	else
+		controller->rumble.expiration = 0;
+
+	// Update gamepad rumble info
+	gamepad_t *gamepad = controller->info;
+
+	gamepad->rumble.active = true;
+	gamepad->rumble.paused = false;
+	gamepad->rumble.data.large_magnitude = effect->large_magnitude;
+	gamepad->rumble.data.small_magnitude = effect->small_magnitude;
+	gamepad->rumble.data.duration = effect->duration;
+
+	return true;
+}
+
+#undef TICS_TO_MS
+
+#define SET_MOTOR_FREQ(type) \
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS) \
+		return false; \
+ \
+	ControllerInfo *controller = &controllers[which]; \
+	if (!controller->started || !controller->info->rumble.supported) \
+		return false; \
+ \
+	gamepad_t *gamepad = controller->info; \
+	if (gamepad->rumble.data.type##_magnitude == freq) \
+		return true; \
+ \
+	UINT16 frequency = max(0, min(freq, UINT16_MAX)); \
+ \
+	controller->rumble.type##_magnitude = frequency; \
+ \
+	if (!rumble_paused && !gamepad->rumble.paused && !Controller_Rumble(controller)) \
+	{ \
+		Controller_StopRumble(which); \
+		return false; \
+	} \
+ \
+	gamepad->rumble.data.type##_magnitude = freq; \
+	gamepad->rumble.active = true; \
+	return true
+
+boolean I_SetGamepadLargeMotorFreq(UINT8 which, fixed_t freq)
+{
+	SET_MOTOR_FREQ(large);
+}
+
+boolean I_SetGamepadSmallMotorFreq(UINT8 which, fixed_t freq)
+{
+	SET_MOTOR_FREQ(small);
+}
+
+void I_SetGamepadRumblePaused(UINT8 which, boolean pause)
+{
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS)
+		return;
+
+	ControllerInfo *controller = &controllers[which];
+	if (!controller->started || !controller->info->rumble.supported)
+		return;
+
+	if (pause == controller->info->rumble.paused)
+		return;
+	else if (pause)
+	{
+		if (!rumble_paused)
+			SDL_GameControllerRumble(controller->dev, 0, 0, 0);
+
+		if (controller->rumble.expiration)
+		{
+			controller->rumble.time_left = controller->rumble.expiration - SDL_GetTicks();
+			controller->rumble.expiration = 0;
+		}
+	}
+	else
+	{
+		if (!rumble_paused)
+			SDL_GameControllerRumble(controller->dev, controller->rumble.large_magnitude, controller->rumble.small_magnitude, 0);
+
+		if (controller->rumble.time_left)
+			controller->rumble.expiration = SDL_GetTicks() + controller->rumble.time_left;
+	}
+
+	controller->info->rumble.paused = pause;
+}
+
+boolean I_GetGamepadRumbleSupported(UINT8 which)
+{
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS)
+		return false;
+
+	ControllerInfo *controller = &controllers[which];
+	if (!controller->started)
+		return false;
+
+	return controller->info->rumble.supported;
+}
+
+boolean I_GetGamepadRumblePaused(UINT8 which)
+{
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS)
+		return false;
+
+	ControllerInfo *controller = &controllers[which];
+	if (!controller->started || !controller->info->rumble.supported)
+		return false;
+
+	return controller->info->rumble.paused;
+}
+
+void I_StopGamepadRumble(UINT8 which)
+{
+	if (!I_RumbleSupported() || which >= NUM_GAMEPADS)
+		return;
+
+	ControllerInfo *controller = &controllers[which];
+	if (!controller->started || !controller->info->rumble.supported)
+		return;
+
+	Controller_StopRumble(which);
+}
+#endif
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 818d0f0c4..ee082fd5d 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -185,6 +185,7 @@ static char returnWadPath[256];
 #include "../i_video.h"
 #include "../i_sound.h"
 #include "../i_system.h"
+#include "../i_gamepad.h"
 #include "../i_threads.h"
 #include "../screen.h" //vid.WndParent
 #include "../d_net.h"
@@ -193,8 +194,6 @@ static char returnWadPath[256];
 #include "endtxt.h"
 #include "sdlmain.h"
 
-#include "../i_joy.h"
-
 #include "../m_argv.h"
 
 #include "../r_main.h" // Frame interpolation/uncapped
@@ -212,41 +211,6 @@ static char returnWadPath[256];
 #include "../byteptr.h"
 #endif
 
-/**	\brief	The JoyReset function
-
-	\param	JoySet	Joystick info to reset
-
-	\return	void
-*/
-static void JoyReset(SDLJoyInfo_t *JoySet)
-{
-	if (JoySet->dev)
-	{
-		SDL_JoystickClose(JoySet->dev);
-	}
-	JoySet->dev = NULL;
-	JoySet->oldjoy = -1;
-	JoySet->axises = JoySet->buttons = JoySet->hats = JoySet->balls = 0;
-	//JoySet->scale
-}
-
-/**	\brief First joystick up and running
-*/
-static INT32 joystick_started  = 0;
-
-/**	\brief SDL info about joystick 1
-*/
-SDLJoyInfo_t JoyInfo;
-
-
-/**	\brief Second joystick up and running
-*/
-static INT32 joystick2_started = 0;
-
-/**	\brief SDL inof about joystick 2
-*/
-SDLJoyInfo_t JoyInfo2;
-
 #ifdef HAVE_TERMIOS
 static INT32 fdmouse2 = -1;
 static INT32 mouse2_started = 0;
@@ -939,721 +903,17 @@ INT32 I_GetKey (void)
 	return rc;
 }
 
-//
-// I_JoyScale
-//
-void I_JoyScale(void)
-{
-	Joystick.bGamepadStyle = cv_joyscale.value==0;
-	JoyInfo.scale = Joystick.bGamepadStyle?1:cv_joyscale.value;
-}
-
-void I_JoyScale2(void)
-{
-	Joystick2.bGamepadStyle = cv_joyscale2.value==0;
-	JoyInfo2.scale = Joystick2.bGamepadStyle?1:cv_joyscale2.value;
-}
-
-// Cheat to get the device index for a joystick handle
-INT32 I_GetJoystickDeviceIndex(SDL_Joystick *dev)
-{
-	INT32 i, count = SDL_NumJoysticks();
-
-	for (i = 0; dev && i < count; i++)
-	{
-		SDL_Joystick *test = SDL_JoystickOpen(i);
-		if (test && test == dev)
-			return i;
-		else if (JoyInfo.dev != test && JoyInfo2.dev != test)
-			SDL_JoystickClose(test);
-	}
-
-	return -1;
-}
-
-/**	\brief Joystick 1 buttons states
-*/
-static UINT64 lastjoybuttons = 0;
-
-/**	\brief Joystick 1 hats state
-*/
-static UINT64 lastjoyhats = 0;
-
-/**	\brief	Shuts down joystick 1
-
-
-	\return void
-
-
-*/
-void I_ShutdownJoystick(void)
-{
-	INT32 i;
-	event_t event;
-	event.type=ev_keyup;
-	event.x = 0;
-	event.y = 0;
-
-	lastjoybuttons = lastjoyhats = 0;
-
-	// emulate the up of all joystick buttons
-	for (i=0;i<JOYBUTTONS;i++)
-	{
-		event.key=KEY_JOY1+i;
-		D_PostEvent(&event);
-	}
-
-	// emulate the up of all joystick hats
-	for (i=0;i<JOYHATS*4;i++)
-	{
-		event.key=KEY_HAT1+i;
-		D_PostEvent(&event);
-	}
-
-	// reset joystick position
-	event.type = ev_joystick;
-	for (i=0;i<JOYAXISSET; i++)
-	{
-		event.key = i;
-		D_PostEvent(&event);
-	}
-
-	joystick_started = 0;
-	JoyReset(&JoyInfo);
-
-	// don't shut down the subsystem here, because hotplugging
-}
-
-void I_GetJoystickEvents(void)
-{
-	static event_t event = {0,0,0,0,false};
-	INT32 i = 0;
-	UINT64 joyhats = 0;
-#if 0
-	UINT64 joybuttons = 0;
-	Sint16 axisx, axisy;
-#endif
-
-	if (!joystick_started) return;
-
-	if (!JoyInfo.dev) //I_ShutdownJoystick();
-		return;
-
-#if 0
-	//faB: look for as much buttons as g_input code supports,
-	//  we don't use the others
-	for (i = JoyInfo.buttons - 1; i >= 0; i--)
-	{
-		joybuttons <<= 1;
-		if (SDL_JoystickGetButton(JoyInfo.dev,i))
-			joybuttons |= 1;
-	}
-
-	if (joybuttons != lastjoybuttons)
-	{
-		INT64 j = 1; // keep only bits that changed since last time
-		INT64 newbuttons = joybuttons ^ lastjoybuttons;
-		lastjoybuttons = joybuttons;
-
-		for (i = 0; i < JOYBUTTONS; i++, j <<= 1)
-		{
-			if (newbuttons & j) // button changed state?
-			{
-				if (joybuttons & j)
-					event.type = ev_keydown;
-				else
-					event.type = ev_keyup;
-				event.key = KEY_JOY1 + i;
-				D_PostEvent(&event);
-			}
-		}
-	}
-#endif
-
-	for (i = JoyInfo.hats - 1; i >= 0; i--)
-	{
-		Uint8 hat = SDL_JoystickGetHat(JoyInfo.dev, i);
-
-		if (hat & SDL_HAT_UP   ) joyhats|=(UINT64)0x1<<(0 + 4*i);
-		if (hat & SDL_HAT_DOWN ) joyhats|=(UINT64)0x1<<(1 + 4*i);
-		if (hat & SDL_HAT_LEFT ) joyhats|=(UINT64)0x1<<(2 + 4*i);
-		if (hat & SDL_HAT_RIGHT) joyhats|=(UINT64)0x1<<(3 + 4*i);
-	}
-
-	if (joyhats != lastjoyhats)
-	{
-		INT64 j = 1; // keep only bits that changed since last time
-		INT64 newhats = joyhats ^ lastjoyhats;
-		lastjoyhats = joyhats;
-
-		for (i = 0; i < JOYHATS*4; i++, j <<= 1)
-		{
-			if (newhats & j) // hat changed state?
-			{
-				if (joyhats & j)
-					event.type = ev_keydown;
-				else
-					event.type = ev_keyup;
-				event.key = KEY_HAT1 + i;
-				D_PostEvent(&event);
-			}
-		}
-	}
-
-#if 0
-	// send joystick axis positions
-	event.type = ev_joystick;
-
-	for (i = JOYAXISSET - 1; i >= 0; i--)
-	{
-		event.key = i;
-		if (i*2 + 1 <= JoyInfo.axises)
-			axisx = SDL_JoystickGetAxis(JoyInfo.dev, i*2 + 0);
-		else axisx = 0;
-		if (i*2 + 2 <= JoyInfo.axises)
-			axisy = SDL_JoystickGetAxis(JoyInfo.dev, i*2 + 1);
-		else axisy = 0;
-
-
-		// -32768 to 32767
-		axisx = axisx/32;
-		axisy = axisy/32;
-
-
-		if (Joystick.bGamepadStyle)
-		{
-			// gamepad control type, on or off, live or die
-			if (axisx < -(JOYAXISRANGE/2))
-				event.x = -1;
-			else if (axisx > (JOYAXISRANGE/2))
-				event.x = 1;
-			else event.x = 0;
-			if (axisy < -(JOYAXISRANGE/2))
-				event.y = -1;
-			else if (axisy > (JOYAXISRANGE/2))
-				event.y = 1;
-			else event.y = 0;
-		}
-		else
-		{
-
-			axisx = JoyInfo.scale?((axisx/JoyInfo.scale)*JoyInfo.scale):axisx;
-			axisy = JoyInfo.scale?((axisy/JoyInfo.scale)*JoyInfo.scale):axisy;
-
-#ifdef SDL_JDEADZONE
-			if (-SDL_JDEADZONE <= axisx && axisx <= SDL_JDEADZONE) axisx = 0;
-			if (-SDL_JDEADZONE <= axisy && axisy <= SDL_JDEADZONE) axisy = 0;
-#endif
-
-			// analog control style , just send the raw data
-			event.x = axisx; // x axis
-			event.y = axisy; // y axis
-		}
-		D_PostEvent(&event);
-	}
-#endif
-}
-
-/**	\brief	Open joystick handle
-
-	\param	fname	name of joystick
-
-	\return	axises
-
-
-*/
-static int joy_open(int joyindex)
-{
-	SDL_Joystick *newdev = NULL;
-	int num_joy = 0;
-
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
-	{
-		CONS_Printf(M_GetText("Joystick subsystem not started\n"));
-		return -1;
-	}
-
-	if (joyindex <= 0)
-		return -1;
-
-	num_joy = SDL_NumJoysticks();
-
-	if (num_joy == 0)
-	{
-		CONS_Printf("%s", M_GetText("Found no joysticks on this system\n"));
-		return -1;
-	}
-
-	newdev = SDL_JoystickOpen(joyindex-1);
-
-	// Handle the edge case where the device <-> joystick index assignment can change due to hotplugging
-	// This indexing is SDL's responsibility and there's not much we can do about it.
-	//
-	// Example:
-	// 1. Plug Controller A   -> Index 0 opened
-	// 2. Plug Controller B   -> Index 1 opened
-	// 3. Unplug Controller A -> Index 0 closed, Index 1 active
-	// 4. Unplug Controller B -> Index 0 inactive, Index 1 closed
-	// 5. Plug Controller B   -> Index 0 opened
-	// 6. Plug Controller A   -> Index 0 REPLACED, opened as Controller A; Index 1 is now Controller B
-	if (JoyInfo.dev)
-	{
-		if (JoyInfo.dev == newdev // same device, nothing to do
-			|| (newdev == NULL && SDL_JoystickGetAttached(JoyInfo.dev))) // we failed, but already have a working device
-			return JoyInfo.axises;
-		// Else, we're changing devices, so send neutral joy events
-		CONS_Debug(DBG_GAMELOGIC, "Joystick1 device is changing; resetting events...\n");
-		I_ShutdownJoystick();
-	}
-
-	JoyInfo.dev = newdev;
-
-	if (JoyInfo.dev == NULL)
-	{
-		CONS_Debug(DBG_GAMELOGIC, M_GetText("Joystick1: Couldn't open device - %s\n"), SDL_GetError());
-		return -1;
-	}
-	else
-	{
-		CONS_Debug(DBG_GAMELOGIC, M_GetText("Joystick1: %s\n"), SDL_JoystickName(JoyInfo.dev));
-		JoyInfo.axises = SDL_JoystickNumAxes(JoyInfo.dev);
-		if (JoyInfo.axises > JOYAXISSET*2)
-			JoyInfo.axises = JOYAXISSET*2;
-	/*		if (joyaxes<2)
-		{
-			I_OutputMsg("Not enought axes?\n");
-			return 0;
-		}*/
-
-		JoyInfo.buttons = SDL_JoystickNumButtons(JoyInfo.dev);
-		if (JoyInfo.buttons > JOYBUTTONS)
-			JoyInfo.buttons = JOYBUTTONS;
-
-		JoyInfo.hats = SDL_JoystickNumHats(JoyInfo.dev);
-		if (JoyInfo.hats > JOYHATS)
-			JoyInfo.hats = JOYHATS;
-
-		JoyInfo.balls = SDL_JoystickNumBalls(JoyInfo.dev);
-
-		//Joystick.bGamepadStyle = !stricmp(SDL_JoystickName(JoyInfo.dev), "pad");
-
-		return JoyInfo.axises;
-	}
-}
-
-//Joystick2
-
-/**	\brief Joystick 2 buttons states
-*/
-static UINT64 lastjoy2buttons = 0;
-
-/**	\brief Joystick 2 hats state
-*/
-static UINT64 lastjoy2hats = 0;
-
-/**	\brief	Shuts down joystick 2
-
-
-	\return	void
-*/
-void I_ShutdownJoystick2(void)
-{
-	INT32 i;
-	event_t event;
-	event.type = ev_keyup;
-	event.x = 0;
-	event.y = 0;
-
-	lastjoy2buttons = lastjoy2hats = 0;
-
-	// emulate the up of all joystick buttons
-	for (i = 0; i < JOYBUTTONS; i++)
-	{
-		event.key = KEY_2JOY1 + i;
-		D_PostEvent(&event);
-	}
-
-	// emulate the up of all joystick hats
-	for (i = 0; i < JOYHATS*4; i++)
-	{
-		event.key = KEY_2HAT1 + i;
-		D_PostEvent(&event);
-	}
-
-	// reset joystick position
-	event.type = ev_joystick2;
-	for (i = 0; i < JOYAXISSET; i++)
-	{
-		event.key = i;
-		D_PostEvent(&event);
-	}
-
-	joystick2_started = 0;
-	JoyReset(&JoyInfo2);
-
-	// don't shut down the subsystem here, because hotplugging
-}
-
-void I_GetJoystick2Events(void)
-{
-	static event_t event = {0,0,0,0,false};
-	INT32 i = 0;
-	UINT64 joyhats = 0;
-#if 0
-	INT64 joybuttons = 0;
-	INT32 axisx, axisy;
-#endif
-
-	if (!joystick2_started)
-		return;
-
-	if (!JoyInfo2.dev) //I_ShutdownJoystick2();
-		return;
-
-
-#if 0
-	//faB: look for as much buttons as g_input code supports,
-	//  we don't use the others
-	for (i = JoyInfo2.buttons - 1; i >= 0; i--)
-	{
-		joybuttons <<= 1;
-		if (SDL_JoystickGetButton(JoyInfo2.dev,i))
-			joybuttons |= 1;
-	}
-
-	if (joybuttons != lastjoy2buttons)
-	{
-		INT64 j = 1; // keep only bits that changed since last time
-		INT64 newbuttons = joybuttons ^ lastjoy2buttons;
-		lastjoy2buttons = joybuttons;
-
-		for (i = 0; i < JOYBUTTONS; i++, j <<= 1)
-		{
-			if (newbuttons & j) // button changed state?
-			{
-				if (joybuttons & j)
-					event.type = ev_keydown;
-				else
-					event.type = ev_keyup;
-				event.key = KEY_2JOY1 + i;
-				D_PostEvent(&event);
-			}
-		}
-	}
-#endif
-
-	for (i = JoyInfo2.hats - 1; i >= 0; i--)
-	{
-		Uint8 hat = SDL_JoystickGetHat(JoyInfo2.dev, i);
-
-		if (hat & SDL_HAT_UP   ) joyhats|=(UINT64)0x1<<(0 + 4*i);
-		if (hat & SDL_HAT_DOWN ) joyhats|=(UINT64)0x1<<(1 + 4*i);
-		if (hat & SDL_HAT_LEFT ) joyhats|=(UINT64)0x1<<(2 + 4*i);
-		if (hat & SDL_HAT_RIGHT) joyhats|=(UINT64)0x1<<(3 + 4*i);
-	}
-
-	if (joyhats != lastjoy2hats)
-	{
-		INT64 j = 1; // keep only bits that changed since last time
-		INT64 newhats = joyhats ^ lastjoy2hats;
-		lastjoy2hats = joyhats;
-
-		for (i = 0; i < JOYHATS*4; i++, j <<= 1)
-		{
-			if (newhats & j) // hat changed state?
-			{
-				if (joyhats & j)
-					event.type = ev_keydown;
-				else
-					event.type = ev_keyup;
-				event.key = KEY_2HAT1 + i;
-				D_PostEvent(&event);
-			}
-		}
-	}
-
-#if 0
-	// send joystick axis positions
-	event.type = ev_joystick2;
-
-	for (i = JOYAXISSET - 1; i >= 0; i--)
-	{
-		event.key = i;
-		if (i*2 + 1 <= JoyInfo2.axises)
-			axisx = SDL_JoystickGetAxis(JoyInfo2.dev, i*2 + 0);
-		else axisx = 0;
-		if (i*2 + 2 <= JoyInfo2.axises)
-			axisy = SDL_JoystickGetAxis(JoyInfo2.dev, i*2 + 1);
-		else axisy = 0;
-
-		// -32768 to 32767
-		axisx = axisx/32;
-		axisy = axisy/32;
-
-		if (Joystick2.bGamepadStyle)
-		{
-			// gamepad control type, on or off, live or die
-			if (axisx < -(JOYAXISRANGE/2))
-				event.x = -1;
-			else if (axisx > (JOYAXISRANGE/2))
-				event.x = 1;
-			else
-				event.x = 0;
-			if (axisy < -(JOYAXISRANGE/2))
-				event.y = -1;
-			else if (axisy > (JOYAXISRANGE/2))
-				event.y = 1;
-			else
-				event.y = 0;
-		}
-		else
-		{
-
-			axisx = JoyInfo2.scale?((axisx/JoyInfo2.scale)*JoyInfo2.scale):axisx;
-			axisy = JoyInfo2.scale?((axisy/JoyInfo2.scale)*JoyInfo2.scale):axisy;
-
-#ifdef SDL_JDEADZONE
-			if (-SDL_JDEADZONE <= axisx && axisx <= SDL_JDEADZONE) axisx = 0;
-			if (-SDL_JDEADZONE <= axisy && axisy <= SDL_JDEADZONE) axisy = 0;
-#endif
-
-			// analog control style , just send the raw data
-			event.x = axisx; // x axis
-			event.y = axisy; // y axis
-		}
-		D_PostEvent(&event);
-	}
-#endif
-}
-
-/**	\brief	Open joystick handle
-
-	\param	fname	name of joystick
-
-	\return	axises
-
-
-*/
-static int joy_open2(int joyindex)
-{
-	SDL_Joystick *newdev = NULL;
-	int num_joy = 0;
-
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
-	{
-		CONS_Printf(M_GetText("Joystick subsystem not started\n"));
-		return -1;
-	}
-
-	if (joyindex <= 0)
-		return -1;
-
-	num_joy = SDL_NumJoysticks();
-
-	if (num_joy == 0)
-	{
-		CONS_Printf("%s", M_GetText("Found no joysticks on this system\n"));
-		return -1;
-	}
-
-	newdev = SDL_JoystickOpen(joyindex-1);
-
-	// Handle the edge case where the device <-> joystick index assignment can change due to hotplugging
-	// This indexing is SDL's responsibility and there's not much we can do about it.
-	//
-	// Example:
-	// 1. Plug Controller A   -> Index 0 opened
-	// 2. Plug Controller B   -> Index 1 opened
-	// 3. Unplug Controller A -> Index 0 closed, Index 1 active
-	// 4. Unplug Controller B -> Index 0 inactive, Index 1 closed
-	// 5. Plug Controller B   -> Index 0 opened
-	// 6. Plug Controller A   -> Index 0 REPLACED, opened as Controller A; Index 1 is now Controller B
-	if (JoyInfo2.dev)
-	{
-		if (JoyInfo2.dev == newdev // same device, nothing to do
-			|| (newdev == NULL && SDL_JoystickGetAttached(JoyInfo2.dev))) // we failed, but already have a working device
-			return JoyInfo.axises;
-		// Else, we're changing devices, so send neutral joy events
-		CONS_Debug(DBG_GAMELOGIC, "Joystick2 device is changing; resetting events...\n");
-		I_ShutdownJoystick2();
-	}
-
-	JoyInfo2.dev = newdev;
-
-	if (JoyInfo2.dev == NULL)
-	{
-		CONS_Debug(DBG_GAMELOGIC, M_GetText("Joystick2: couldn't open device - %s\n"), SDL_GetError());
-		return -1;
-	}
-	else
-	{
-		CONS_Debug(DBG_GAMELOGIC, M_GetText("Joystick2: %s\n"), SDL_JoystickName(JoyInfo2.dev));
-		JoyInfo2.axises = SDL_JoystickNumAxes(JoyInfo2.dev);
-		if (JoyInfo2.axises > JOYAXISSET*2)
-			JoyInfo2.axises = JOYAXISSET*2;
-/*		if (joyaxes<2)
-		{
-			I_OutputMsg("Not enought axes?\n");
-			return 0;
-		}*/
-
-		JoyInfo2.buttons = SDL_JoystickNumButtons(JoyInfo2.dev);
-		if (JoyInfo2.buttons > JOYBUTTONS)
-			JoyInfo2.buttons = JOYBUTTONS;
-
-		JoyInfo2.hats = SDL_JoystickNumHats(JoyInfo2.dev);
-		if (JoyInfo2.hats > JOYHATS)
-			JoyInfo2.hats = JOYHATS;
-
-		JoyInfo2.balls = SDL_JoystickNumBalls(JoyInfo2.dev);
-
-		//Joystick.bGamepadStyle = !stricmp(SDL_JoystickName(JoyInfo2.dev), "pad");
-
-		return JoyInfo2.axises;
-	}
-}
-
-//
-// I_InitJoystick
-//
-void I_InitJoystick(void)
-{
-	SDL_Joystick *newjoy = NULL;
-
-	//I_ShutdownJoystick();
-	if (M_CheckParm("-nojoy"))
-		return;
-
-	if (M_CheckParm("-noxinput"))
-		SDL_SetHintWithPriority("SDL_XINPUT_ENABLED", "0", SDL_HINT_OVERRIDE);
-
-	if (M_CheckParm("-nohidapi"))
-		SDL_SetHintWithPriority("SDL_JOYSTICK_HIDAPI", "0", SDL_HINT_OVERRIDE);
-
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
-	{
-		CONS_Printf("I_InitJoystick()...\n");
-
-		if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) == -1)
-		{
-			CONS_Printf(M_GetText("Couldn't initialize joystick: %s\n"), SDL_GetError());
-			return;
-		}
-	}
-
-	if (cv_usejoystick.value)
-		newjoy = SDL_JoystickOpen(cv_usejoystick.value-1);
-
-	if (newjoy && JoyInfo2.dev == newjoy) // don't override an active device
-		cv_usejoystick.value = I_GetJoystickDeviceIndex(JoyInfo.dev) + 1;
-	else if (newjoy && joy_open(cv_usejoystick.value) != -1)
-	{
-		// SDL's device indexes are unstable, so cv_usejoystick may not match
-		// the actual device index. So let's cheat a bit and find the device's current index.
-		JoyInfo.oldjoy = I_GetJoystickDeviceIndex(JoyInfo.dev) + 1;
-		joystick_started = 1;
-	}
-	else
-	{
-		if (JoyInfo.oldjoy)
-			I_ShutdownJoystick();
-		cv_usejoystick.value = 0;
-		joystick_started = 0;
-	}
-
-	if (JoyInfo.dev != newjoy && JoyInfo2.dev != newjoy)
-		SDL_JoystickClose(newjoy);
-}
-
-void I_InitJoystick2(void)
-{
-	SDL_Joystick *newjoy = NULL;
-
-	//I_ShutdownJoystick2();
-	if (M_CheckParm("-nojoy"))
-		return;
-
-	if (M_CheckParm("-noxinput"))
-		SDL_SetHintWithPriority("SDL_XINPUT_ENABLED", "0", SDL_HINT_OVERRIDE);
-
-	if (M_CheckParm("-nohidapi"))
-		SDL_SetHintWithPriority("SDL_JOYSTICK_HIDAPI", "0", SDL_HINT_OVERRIDE);
-
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
-	{
-		CONS_Printf("I_InitJoystick2()...\n");
-
-		if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) == -1)
-		{
-			CONS_Printf(M_GetText("Couldn't initialize joystick: %s\n"), SDL_GetError());
-			return;
-		}
-	}
-
-	if (cv_usejoystick2.value)
-		newjoy = SDL_JoystickOpen(cv_usejoystick2.value-1);
-
-	if (newjoy && JoyInfo.dev == newjoy) // don't override an active device
-		cv_usejoystick2.value = I_GetJoystickDeviceIndex(JoyInfo2.dev) + 1;
-	else if (newjoy && joy_open2(cv_usejoystick2.value) != -1)
-	{
-		// SDL's device indexes are unstable, so cv_usejoystick may not match
-		// the actual device index. So let's cheat a bit and find the device's current index.
-		JoyInfo2.oldjoy = I_GetJoystickDeviceIndex(JoyInfo2.dev) + 1;
-		joystick2_started = 1;
-	}
-	else
-	{
-		if (JoyInfo2.oldjoy)
-			I_ShutdownJoystick2();
-		cv_usejoystick2.value = 0;
-		joystick2_started = 0;
-	}
-
-	if (JoyInfo.dev != newjoy && JoyInfo2.dev != newjoy)
-		SDL_JoystickClose(newjoy);
-}
-
 static void I_ShutdownInput(void)
 {
-	// Yes, the name is misleading: these send neutral events to
-	// clean up the unplugged joystick's input
-	// Note these methods are internal to this file, not called elsewhere.
-	I_ShutdownJoystick();
-	I_ShutdownJoystick2();
+	I_ShutdownGamepads();
 
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
+	if (SDL_WasInit(GAMEPAD_INIT_FLAGS) == GAMEPAD_INIT_FLAGS)
 	{
-		CONS_Printf("Shutting down joy system\n");
-		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
-		I_OutputMsg("I_Joystick: SDL's Joystick system has been shutdown\n");
+		CONS_Printf("Shutting down game controller subsystems\n");
+		SDL_QuitSubSystem(GAMEPAD_INIT_FLAGS);
 	}
 }
 
-INT32 I_NumJoys(void)
-{
-	INT32 numjoy = 0;
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
-		numjoy = SDL_NumJoysticks();
-	return numjoy;
-}
-
-static char joyname[255]; // joystick name is straight from the driver
-
-const char *I_GetJoyName(INT32 joyindex)
-{
-	const char *tempname = NULL;
-	joyname[0] = 0;
-	joyindex--; //SDL's Joystick System starts at 0, not 1
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
-	{
-		tempname = SDL_JoystickNameForIndex(joyindex);
-		if (tempname)
-			strncpy(joyname, tempname, 255);
-	}
-	return joyname;
-}
-
 #ifndef NOMUMBLE
 #ifdef HAVE_MUMBLE
 // Best Mumble positional audio settings:
@@ -2113,23 +1373,6 @@ void I_StartupMouse2(void)
 #endif
 }
 
-//
-// I_Tactile
-//
-void I_Tactile(FFType pFFType, const JoyFF_t *FFEffect)
-{
-	// UNUSED.
-	(void)pFFType;
-	(void)FFEffect;
-}
-
-void I_Tactile2(FFType pFFType, const JoyFF_t *FFEffect)
-{
-	// UNUSED.
-	(void)pFFType;
-	(void)FFEffect;
-}
-
 /**	\brief empty ticcmd for player 1
 */
 static ticcmd_t emptycmd;
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 6e971a5d8..81b20b51e 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -66,7 +66,7 @@
 #include "../m_menu.h"
 #include "../d_main.h"
 #include "../s_sound.h"
-#include "../i_joy.h"
+#include "../i_gamepad.h"
 #include "../st_stuff.h"
 #include "../hu_stuff.h"
 #include "../g_game.h"
@@ -449,51 +449,10 @@ static void SurfaceInfo(const SDL_Surface *infoSurface, const char *SurfaceText)
 
 static void VID_Command_Info_f (void)
 {
-#if 0
-	SDL2STUB();
-#else
-#if 0
-	const SDL_VideoInfo *videoInfo;
-	videoInfo = SDL_GetVideoInfo(); //Alam: Double-Check
-	if (videoInfo)
-	{
-		CONS_Printf("%s", M_GetText("Video Interface Capabilities:\n"));
-		if (videoInfo->hw_available)
-			CONS_Printf("%s", M_GetText(" Hardware surfaces\n"));
-		if (videoInfo->wm_available)
-			CONS_Printf("%s", M_GetText(" Window manager\n"));
-		//UnusedBits1  :6
-		//UnusedBits2  :1
-		if (videoInfo->blit_hw)
-			CONS_Printf("%s", M_GetText(" Accelerated blits HW-2-HW\n"));
-		if (videoInfo->blit_hw_CC)
-			CONS_Printf("%s", M_GetText(" Accelerated blits HW-2-HW with Colorkey\n"));
-		if (videoInfo->wm_available)
-			CONS_Printf("%s", M_GetText(" Accelerated blits HW-2-HW with Alpha\n"));
-		if (videoInfo->blit_sw)
-		{
-			CONS_Printf("%s", M_GetText(" Accelerated blits SW-2-HW\n"));
-			if (!M_CheckParm("-noblit")) videoblitok = SDL_TRUE;
-		}
-		if (videoInfo->blit_sw_CC)
-			CONS_Printf("%s", M_GetText(" Accelerated blits SW-2-HW with Colorkey\n"));
-		if (videoInfo->blit_sw_A)
-			CONS_Printf("%s", M_GetText(" Accelerated blits SW-2-HW with Alpha\n"));
-		if (videoInfo->blit_fill)
-			CONS_Printf("%s", M_GetText(" Accelerated Color filling\n"));
-		//UnusedBits3  :16
-		if (videoInfo->video_mem)
-			CONS_Printf(M_GetText(" There is %i KB of video memory\n"), videoInfo->video_mem);
-		else
-			CONS_Printf("%s", M_GetText(" There no video memory for SDL\n"));
-		//*vfmt
-	}
-#else
 	if (!M_CheckParm("-noblit")) videoblitok = SDL_TRUE;
-#endif
+
 	SurfaceInfo(bufSurface, M_GetText("Current Engine Mode"));
 	SurfaceInfo(vidSurface, M_GetText("Current Video Mode"));
-#endif
 }
 
 static void VID_Command_ModeList_f(void)
@@ -528,61 +487,6 @@ static void VID_Command_Mode_f (void)
 		setmodeneeded = modenum+1; // request vid mode change
 }
 
-static inline void SDLJoyRemap(event_t *event)
-{
-	(void)event;
-}
-
-static INT32 SDLJoyAxis(const Sint16 axis, evtype_t which)
-{
-	// -32768 to 32767
-	INT32 raxis = axis/32;
-	if (which == ev_joystick)
-	{
-		if (Joystick.bGamepadStyle)
-		{
-			// gamepad control type, on or off, live or die
-			if (raxis < -(JOYAXISRANGE/2))
-				raxis = -1;
-			else if (raxis > (JOYAXISRANGE/2))
-				raxis = 1;
-			else
-				raxis = 0;
-		}
-		else
-		{
-			raxis = JoyInfo.scale!=1?((raxis/JoyInfo.scale)*JoyInfo.scale):raxis;
-
-#ifdef SDL_JDEADZONE
-			if (-SDL_JDEADZONE <= raxis && raxis <= SDL_JDEADZONE)
-				raxis = 0;
-#endif
-		}
-	}
-	else if (which == ev_joystick2)
-	{
-		if (Joystick2.bGamepadStyle)
-		{
-			// gamepad control type, on or off, live or die
-			if (raxis < -(JOYAXISRANGE/2))
-				raxis = -1;
-			else if (raxis > (JOYAXISRANGE/2))
-				raxis = 1;
-			else raxis = 0;
-		}
-		else
-		{
-			raxis = JoyInfo2.scale!=1?((raxis/JoyInfo2.scale)*JoyInfo2.scale):raxis;
-
-#ifdef SDL_JDEADZONE
-			if (-SDL_JDEADZONE <= raxis && raxis <= SDL_JDEADZONE)
-				raxis = 0;
-#endif
-		}
-	}
-	return raxis;
-}
-
 static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 {
 	static SDL_bool firsttimeonmouse = SDL_TRUE;
@@ -614,13 +518,13 @@ static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 		// Tell game we got focus back, resume music if necessary
 		window_notinfocus = false;
 		if (!paused)
-			S_ResumeAudio(); //resume it
+			S_ResumeAudio();
 
-		if (!firsttimeonmouse)
-		{
-			if (cv_usemouse.value) I_StartupMouse();
-		}
-		//else firsttimeonmouse = SDL_FALSE;
+		I_ToggleControllerRumble(true);
+		P_UnpauseRumble(NULL);
+
+		if (!firsttimeonmouse && cv_usemouse.value)
+			I_StartupMouse();
 
 		if (USE_MOUSEINPUT && !IgnoreMouse())
 			SDLdoGrabMouse();
@@ -629,43 +533,45 @@ static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 	{
 		// Tell game we lost focus, pause music
 		window_notinfocus = true;
-		if (! cv_playmusicifunfocused.value)
+
+		if (!cv_playmusicifunfocused.value)
 			S_PauseAudio();
-		if (! cv_playsoundsifunfocused.value)
+		if (!cv_playsoundsifunfocused.value)
 			S_StopSounds();
 
 		if (!disable_mouse)
-		{
 			SDLforceUngrabMouse();
-		}
+
 		memset(gamekeydown, 0, NUMKEYS); // TODO this is a scary memset
 
-		if (MOUSE_MENU)
-		{
-			SDLdoUngrabMouse();
-		}
-	}
+		I_ToggleControllerRumble(false);
+		if (P_AutoPause())
+			P_PauseRumble(NULL);
 
+		if (MOUSE_MENU)
+			SDLdoUngrabMouse();
+	}
 }
 
 static void Impl_HandleKeyboardEvent(SDL_KeyboardEvent evt, Uint32 type)
 {
 	event_t event;
+
 	if (type == SDL_KEYUP)
-	{
 		event.type = ev_keyup;
-	}
 	else if (type == SDL_KEYDOWN)
-	{
 		event.type = ev_keydown;
-	}
 	else
-	{
 		return;
-	}
+
 	event.key = Impl_SDL_Scancode_To_Keycode(evt.keysym.scancode);
+	if (!event.key)
+		return;
+
 	event.repeated = (evt.repeat != 0);
-	if (event.key) D_PostEvent(&event);
+	event.which = 0;
+
+	D_PostEvent(&event);
 }
 
 static void Impl_HandleMouseMotionEvent(SDL_MouseMotionEvent evt)
@@ -730,32 +636,35 @@ static void Impl_HandleMouseButtonEvent(SDL_MouseButtonEvent evt, Uint32 type)
 	if (SDL_GetMouseFocus() != window || IgnoreMouse())
 		return;
 
-	/// \todo inputEvent.button.which
 	if (USE_MOUSEINPUT)
 	{
 		if (type == SDL_MOUSEBUTTONUP)
-		{
 			event.type = ev_keyup;
-		}
 		else if (type == SDL_MOUSEBUTTONDOWN)
-		{
 			event.type = ev_keydown;
-		}
-		else return;
-		if (evt.button == SDL_BUTTON_MIDDLE)
-			event.key = KEY_MOUSE1+2;
-		else if (evt.button == SDL_BUTTON_RIGHT)
-			event.key = KEY_MOUSE1+1;
-		else if (evt.button == SDL_BUTTON_LEFT)
-			event.key = KEY_MOUSE1;
-		else if (evt.button == SDL_BUTTON_X1)
-			event.key = KEY_MOUSE1+3;
-		else if (evt.button == SDL_BUTTON_X2)
-			event.key = KEY_MOUSE1+4;
-		if (event.type == ev_keyup || event.type == ev_keydown)
+		else
+			return;
+
+		switch (evt.button)
 		{
-			D_PostEvent(&event);
+		case SDL_BUTTON_LEFT:
+			event.key = KEY_MOUSE1+0;
+			break;
+		case SDL_BUTTON_RIGHT:
+			event.key = KEY_MOUSE1+1;
+			break;
+		case SDL_BUTTON_MIDDLE:
+			event.key = KEY_MOUSE1+2;
+			break;
+		case SDL_BUTTON_X1:
+			event.key = KEY_MOUSE1+3;
+			break;
+		case SDL_BUTTON_X2:
+			event.key = KEY_MOUSE1+4;
+			break;
 		}
+
+		D_PostEvent(&event);
 	}
 }
 
@@ -786,111 +695,6 @@ static void Impl_HandleMouseWheelEvent(SDL_MouseWheelEvent evt)
 	}
 }
 
-static void Impl_HandleJoystickAxisEvent(SDL_JoyAxisEvent evt)
-{
-	event_t event;
-	SDL_JoystickID joyid[2];
-
-	// Determine the Joystick IDs for each current open joystick
-	joyid[0] = SDL_JoystickInstanceID(JoyInfo.dev);
-	joyid[1] = SDL_JoystickInstanceID(JoyInfo2.dev);
-
-	evt.axis++;
-	event.key = event.x = event.y = INT32_MAX;
-
-	if (evt.which == joyid[0])
-	{
-		event.type = ev_joystick;
-	}
-	else if (evt.which == joyid[1])
-	{
-		event.type = ev_joystick2;
-	}
-	else return;
-	//axis
-	if (evt.axis > JOYAXISSET*2)
-		return;
-	//vaule
-	if (evt.axis%2)
-	{
-		event.key = evt.axis / 2;
-		event.x = SDLJoyAxis(evt.value, event.type);
-	}
-	else
-	{
-		evt.axis--;
-		event.key = evt.axis / 2;
-		event.y = SDLJoyAxis(evt.value, event.type);
-	}
-	D_PostEvent(&event);
-}
-
-#if 0
-static void Impl_HandleJoystickHatEvent(SDL_JoyHatEvent evt)
-{
-	event_t event;
-	SDL_JoystickID joyid[2];
-
-	// Determine the Joystick IDs for each current open joystick
-	joyid[0] = SDL_JoystickInstanceID(JoyInfo.dev);
-	joyid[1] = SDL_JoystickInstanceID(JoyInfo2.dev);
-
-	if (evt.hat >= JOYHATS)
-		return; // ignore hats with too high an index
-
-	if (evt.which == joyid[0])
-	{
-		event.key = KEY_HAT1 + (evt.hat*4);
-	}
-	else if (evt.which == joyid[1])
-	{
-		event.key = KEY_2HAT1 + (evt.hat*4);
-	}
-	else return;
-
-	// NOTE: UNFINISHED
-}
-#endif
-
-static void Impl_HandleJoystickButtonEvent(SDL_JoyButtonEvent evt, Uint32 type)
-{
-	event_t event;
-	SDL_JoystickID joyid[2];
-
-	// Determine the Joystick IDs for each current open joystick
-	joyid[0] = SDL_JoystickInstanceID(JoyInfo.dev);
-	joyid[1] = SDL_JoystickInstanceID(JoyInfo2.dev);
-
-	if (evt.which == joyid[0])
-	{
-		event.key = KEY_JOY1;
-	}
-	else if (evt.which == joyid[1])
-	{
-		event.key = KEY_2JOY1;
-	}
-	else return;
-	if (type == SDL_JOYBUTTONUP)
-	{
-		event.type = ev_keyup;
-	}
-	else if (type == SDL_JOYBUTTONDOWN)
-	{
-		event.type = ev_keydown;
-	}
-	else return;
-	if (evt.button < JOYBUTTONS)
-	{
-		event.key += evt.button;
-	}
-	else return;
-
-	SDLJoyRemap(&event);
-	if (event.type != ev_console) D_PostEvent(&event);
-}
-
-
-
 void I_GetEvent(void)
 {
 	SDL_Event evt;
@@ -928,147 +732,18 @@ void I_GetEvent(void)
 			case SDL_MOUSEWHEEL:
 				Impl_HandleMouseWheelEvent(evt.wheel);
 				break;
-			case SDL_JOYAXISMOTION:
-				Impl_HandleJoystickAxisEvent(evt.jaxis);
+			case SDL_CONTROLLERAXISMOTION:
+				I_HandleControllerAxisEvent(evt.caxis);
 				break;
-#if 0
-			case SDL_JOYHATMOTION:
-				Impl_HandleJoystickHatEvent(evt.jhat)
+			case SDL_CONTROLLERBUTTONUP:
+			case SDL_CONTROLLERBUTTONDOWN:
+				I_HandleControllerButtonEvent(evt.cbutton, evt.type);
 				break;
-#endif
-			case SDL_JOYBUTTONUP:
-			case SDL_JOYBUTTONDOWN:
-				Impl_HandleJoystickButtonEvent(evt.jbutton, evt.type);
+			case SDL_CONTROLLERDEVICEADDED:
+				I_ControllerDeviceAdded(evt.cdevice.which);
 				break;
-			case SDL_JOYDEVICEADDED:
-				{
-					SDL_Joystick *newjoy = SDL_JoystickOpen(evt.jdevice.which);
-
-					CONS_Debug(DBG_GAMELOGIC, "Joystick device index %d added\n", evt.jdevice.which + 1);
-
-					// Because SDL's device index is unstable, we're going to cheat here a bit:
-					// For the first joystick setting that is NOT active:
-					// 1. Set cv_usejoystickX.value to the new device index (this does not change what is written to config.cfg)
-					// 2. Set OTHERS' cv_usejoystickX.value to THEIR new device index, because it likely changed
-					//    * If device doesn't exist, switch cv_usejoystick back to default value (.string)
-					//      * BUT: If that default index is being occupied, use ANOTHER cv_usejoystick's default value!
-					if (newjoy && (!JoyInfo.dev || !SDL_JoystickGetAttached(JoyInfo.dev))
-						&& JoyInfo2.dev != newjoy) // don't override a currently active device
-					{
-						cv_usejoystick.value = evt.jdevice.which + 1;
-
-						if (JoyInfo2.dev)
-							cv_usejoystick2.value = I_GetJoystickDeviceIndex(JoyInfo2.dev) + 1;
-						else if (atoi(cv_usejoystick2.string) != JoyInfo.oldjoy
-								&& atoi(cv_usejoystick2.string) != cv_usejoystick.value)
-							cv_usejoystick2.value = atoi(cv_usejoystick2.string);
-						else if (atoi(cv_usejoystick.string) != JoyInfo.oldjoy
-								&& atoi(cv_usejoystick.string) != cv_usejoystick.value)
-							cv_usejoystick2.value = atoi(cv_usejoystick.string);
-						else // we tried...
-							cv_usejoystick2.value = 0;
-					}
-					else if (newjoy && (!JoyInfo2.dev || !SDL_JoystickGetAttached(JoyInfo2.dev))
-						&& JoyInfo.dev != newjoy) // don't override a currently active device
-					{
-						cv_usejoystick2.value = evt.jdevice.which + 1;
-
-						if (JoyInfo.dev)
-							cv_usejoystick.value = I_GetJoystickDeviceIndex(JoyInfo.dev) + 1;
-						else if (atoi(cv_usejoystick.string) != JoyInfo2.oldjoy
-								&& atoi(cv_usejoystick.string) != cv_usejoystick2.value)
-							cv_usejoystick.value = atoi(cv_usejoystick.string);
-						else if (atoi(cv_usejoystick2.string) != JoyInfo2.oldjoy
-								&& atoi(cv_usejoystick2.string) != cv_usejoystick2.value)
-							cv_usejoystick.value = atoi(cv_usejoystick2.string);
-						else // we tried...
-							cv_usejoystick.value = 0;
-					}
-
-					// Was cv_usejoystick disabled in settings?
-					if (!strcmp(cv_usejoystick.string, "0") || !cv_usejoystick.value)
-						cv_usejoystick.value = 0;
-					else if (atoi(cv_usejoystick.string) <= I_NumJoys() // don't mess if we intentionally set higher than NumJoys
-						     && cv_usejoystick.value) // update the cvar ONLY if a device exists
-						CV_SetValue(&cv_usejoystick, cv_usejoystick.value);
-
-					if (!strcmp(cv_usejoystick2.string, "0") || !cv_usejoystick2.value)
-						cv_usejoystick2.value = 0;
-					else if (atoi(cv_usejoystick2.string) <= I_NumJoys() // don't mess if we intentionally set higher than NumJoys
-					         && cv_usejoystick2.value) // update the cvar ONLY if a device exists
-						CV_SetValue(&cv_usejoystick2, cv_usejoystick2.value);
-
-					// Update all joysticks' init states
-					// This is a little wasteful since cv_usejoystick already calls this, but
-					// we need to do this in case CV_SetValue did nothing because the string was already same.
-					// if the device is already active, this should do nothing, effectively.
-					I_InitJoystick();
-					I_InitJoystick2();
-
-					CONS_Debug(DBG_GAMELOGIC, "Joystick1 device index: %d\n", JoyInfo.oldjoy);
-					CONS_Debug(DBG_GAMELOGIC, "Joystick2 device index: %d\n", JoyInfo2.oldjoy);
-
-					// update the menu
-					if (currentMenu == &OP_JoystickSetDef)
-						M_SetupJoystickMenu(0);
-
-					if (JoyInfo.dev != newjoy && JoyInfo2.dev != newjoy)
-						SDL_JoystickClose(newjoy);
-				}
-				break;
-			case SDL_JOYDEVICEREMOVED:
-				if (JoyInfo.dev && !SDL_JoystickGetAttached(JoyInfo.dev))
-				{
-					CONS_Debug(DBG_GAMELOGIC, "Joystick1 removed, device index: %d\n", JoyInfo.oldjoy);
-					I_ShutdownJoystick();
-				}
-
-				if (JoyInfo2.dev && !SDL_JoystickGetAttached(JoyInfo2.dev))
-				{
-					CONS_Debug(DBG_GAMELOGIC, "Joystick2 removed, device index: %d\n", JoyInfo2.oldjoy);
-					I_ShutdownJoystick2();
-				}
-
-				// Update the device indexes, because they likely changed
-				// * If device doesn't exist, switch cv_usejoystick back to default value (.string)
-				//   * BUT: If that default index is being occupied, use ANOTHER cv_usejoystick's default value!
-				if (JoyInfo.dev)
-					cv_usejoystick.value = JoyInfo.oldjoy = I_GetJoystickDeviceIndex(JoyInfo.dev) + 1;
-				else if (atoi(cv_usejoystick.string) != JoyInfo2.oldjoy)
-					cv_usejoystick.value = atoi(cv_usejoystick.string);
-				else if (atoi(cv_usejoystick2.string) != JoyInfo2.oldjoy)
-					cv_usejoystick.value = atoi(cv_usejoystick2.string);
-				else // we tried...
-					cv_usejoystick.value = 0;
-
-				if (JoyInfo2.dev)
-					cv_usejoystick2.value = JoyInfo2.oldjoy = I_GetJoystickDeviceIndex(JoyInfo2.dev) + 1;
-				else if (atoi(cv_usejoystick2.string) != JoyInfo.oldjoy)
-					cv_usejoystick2.value = atoi(cv_usejoystick2.string);
-				else if (atoi(cv_usejoystick.string) != JoyInfo.oldjoy)
-					cv_usejoystick2.value = atoi(cv_usejoystick.string);
-				else // we tried...
-					cv_usejoystick2.value = 0;
-
-				// Was cv_usejoystick disabled in settings?
-				if (!strcmp(cv_usejoystick.string, "0"))
-					cv_usejoystick.value = 0;
-				else if (atoi(cv_usejoystick.string) <= I_NumJoys() // don't mess if we intentionally set higher than NumJoys
-						 && cv_usejoystick.value) // update the cvar ONLY if a device exists
-					CV_SetValue(&cv_usejoystick, cv_usejoystick.value);
-
-				if (!strcmp(cv_usejoystick2.string, "0"))
-					cv_usejoystick2.value = 0;
-				else if (atoi(cv_usejoystick2.string) <= I_NumJoys() // don't mess if we intentionally set higher than NumJoys
-						 && cv_usejoystick2.value) // update the cvar ONLY if a device exists
-					CV_SetValue(&cv_usejoystick2, cv_usejoystick2.value);
-
-				CONS_Debug(DBG_GAMELOGIC, "Joystick1 device index: %d\n", JoyInfo.oldjoy);
-				CONS_Debug(DBG_GAMELOGIC, "Joystick2 device index: %d\n", JoyInfo2.oldjoy);
-
-				// update the menu
-				if (currentMenu == &OP_JoystickSetDef)
-					M_SetupJoystickMenu(0);
+			case SDL_CONTROLLERDEVICEREMOVED:
+				I_ControllerDeviceRemoved();
 				break;
 			case SDL_QUIT:
 				LUA_HookBool(true, HOOK(GameQuit));
@@ -1086,6 +761,7 @@ void I_GetEvent(void)
 		//SDL_memset(&event, 0, sizeof(event_t));
 		event.type = ev_mouse;
 		event.key = 0;
+		event.which = 0;
 		event.x = (INT32)lround(mousemovex * ((float)wwidth / (float)realwidth));
 		event.y = (INT32)lround(mousemovey * ((float)wheight / (float)realheight));
 		D_PostEvent(&event);
@@ -1124,15 +800,9 @@ void I_OsPolling(void)
 
 	if (consolevent)
 		I_GetConsoleEvents();
-	if (SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
-	{
-		SDL_JoystickUpdate();
-		I_GetJoystickEvents();
-		I_GetJoystick2Events();
-	}
 
+	I_UpdateControllers();
 	I_GetMouseEvents();
-
 	I_GetEvent();
 
 	mod = SDL_GetModState();
diff --git a/src/sdl/sdlmain.h b/src/sdl/sdlmain.h
index 6b6e79d97..bb178b233 100644
--- a/src/sdl/sdlmain.h
+++ b/src/sdl/sdlmain.h
@@ -23,59 +23,41 @@ extern SDL_bool consolevent;
 extern SDL_bool framebuffer;
 
 #include "../m_fixed.h"
+#include "../i_gamepad.h"
 
-// SDL2 stub macro
-#ifdef _MSC_VER
-#define SDL2STUB() CONS_Printf("SDL2: stubbed: %s:%d\n", __FUNCTION__, __LINE__)
-#else
-#define SDL2STUB() CONS_Printf("SDL2: stubbed: %s:%d\n", __func__, __LINE__)
-#endif
-
-// So m_menu knows whether to store cv_usejoystick value or string
-#define JOYSTICK_HOTPLUG
-
-/**	\brief	The JoyInfo_s struct
-
-  info about joystick
-*/
-typedef struct SDLJoyInfo_s
+// SDL info about all controllers
+typedef struct
 {
-	/// Joystick handle
-	SDL_Joystick *dev;
-	/// number of old joystick
-	int oldjoy;
-	/// number of axies
-	int axises;
-	/// scale of axises
-	INT32 scale;
-	/// number of buttons
-	int buttons;
-	/// number of hats
-	int hats;
-	/// number of balls
-	int balls;
+	boolean started; // started
+	int lastindex; // last gamepad ID
 
-} SDLJoyInfo_t;
+	SDL_GameController *dev;
+	SDL_Joystick *joydev;
 
-/**	\brief SDL info about joystick 1
-*/
-extern SDLJoyInfo_t JoyInfo;
+	gamepad_t *info; // pointer to gamepad info
 
-/**	\brief joystick axis deadzone
-*/
-#define SDL_JDEADZONE 153
-#undef SDL_JDEADZONE
+	struct {
+		Uint16 large_magnitude;
+		Uint16 small_magnitude;
+		Uint32 expiration, time_left;
+	} rumble;
+} ControllerInfo;
 
-/**	\brief SDL inof about joystick 2
-*/
-extern SDLJoyInfo_t JoyInfo2;
+#define GAMEPAD_INIT_FLAGS (SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER)
 
-// So we can call this from i_video event loop
-void I_ShutdownJoystick(void);
-void I_ShutdownJoystick2(void);
+void I_UpdateControllers(void);
 
-// Cheat to get the device index for a joystick handle
-INT32 I_GetJoystickDeviceIndex(SDL_Joystick *dev);
+void I_ControllerDeviceAdded(INT32 which);
+void I_ControllerDeviceRemoved(void);
+
+void I_HandleControllerButtonEvent(SDL_ControllerButtonEvent evt, Uint32 type);
+void I_HandleControllerAxisEvent(SDL_ControllerAxisEvent evt);
+
+INT32 I_GetControllerIndex(SDL_GameController *dev);
+void I_CloseInactiveController(SDL_GameController *dev);
+void I_CloseInactiveHapticDevice(SDL_Haptic *dev);
+
+void I_ToggleControllerRumble(boolean unpause);
 
 void I_GetConsoleEvents(void);