diff --git a/src/g_game.c b/src/g_game.c
index ce41ccaf68a1f8c7f0495e9a81851b0d717fb13b..b406b8f9488bb7baf273ca825800e906cdf0c333 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1808,8 +1808,15 @@ INT32 G_GetGamepadDeviceIndex(INT32 player)
 		return cv_usegamepad[player].value;
 }
 
+void G_OnGamepadConnect(UINT8 which)
+{
+	LUA_HookGamepadEvent(which, HOOK(GamepadAdded));
+}
+
 void G_OnGamepadDisconnect(UINT8 which)
 {
+	LUA_HookGamepadEvent(which, HOOK(GamepadRemoved));
+
 	if (!cv_gamepad_autopause.value)
 		return;
 
@@ -2333,6 +2340,10 @@ boolean G_LuaResponder(event_t *ev)
 		cancelled = LUA_HookKey(ev, HOOK(KeyUp));
 		LUA_InvalidateUserdata(ev);
 	}
+	else if (ev->type == ev_gamepad_down)
+		cancelled = LUA_HookGamepadButton(ev, HOOK(GamepadButtonDown));
+	else if (ev->type == ev_gamepad_up)
+		cancelled = LUA_HookGamepadButton(ev, HOOK(GamepadButtonUp));
 
 	return cancelled;
 }
diff --git a/src/g_game.h b/src/g_game.h
index 6c24054a0750fe7f16fb09fd489b06720b8ce32f..e798176af06851822ec7956f88c5917751b7fb61 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -133,6 +133,9 @@ INT32 G_GetGamepadDeviceIndex(INT32 player);
 // returns a player's gamepad index
 INT16 G_GetGamepadForPlayer(player_t *player);
 
+// called when a player's gamepad is connected
+void G_OnGamepadConnect(UINT8 which);
+
 // called when a player's gamepad is disconnected
 void G_OnGamepadDisconnect(UINT8 which);
 
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 29adb478abfffd58a5d564769382c6290c6acefd..6212a77f0093f0a0c7f73b649148ca041c9e1752 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -220,6 +220,7 @@ static const struct {
 	{META_LUABANKS,     "luabanks[]"},
 
 	{META_KEYEVENT,     "keyevent_t"},
+	{META_GAMEPAD,      "gamepad_t"},
 	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 5a14294c3b312f2cac04ce434e4af2d92c713b5d..37a38e0563358ea0c3d8ea7a7d2fd051d17d3043 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -74,6 +74,10 @@ automatically.
 	X (PlayerCanEnterSpinGaps),\
 	X (KeyDown),\
 	X (KeyUp),\
+	X (GamepadButtonDown),\
+	X (GamepadButtonUp),\
+	X (GamepadAdded),\
+	X (GamepadRemoved),\
 
 #define STRING_HOOK_LIST(X) \
 	X (BotAI),/* B_BuildTailsTiccmd by skin name */\
@@ -125,6 +129,8 @@ void LUA_HookBool(boolean value, int hook);
 int  LUA_HookPlayer(player_t *, int hook);
 int  LUA_HookTiccmd(player_t *, ticcmd_t *, int hook);
 int  LUA_HookKey(event_t *event, int hook); // Hooks for key events
+int  LUA_HookGamepadButton(event_t *event, int hook);
+void LUA_HookGamepadEvent(UINT8 which, int hook);
 
 void LUA_HookThinkFrame(void);
 int  LUA_HookMobjLineCollide(mobj_t *, line_t *);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 0b24b7b535b917fa095510db4564d2564688a508..6506d3dc65503386249e6c733eb6801d7fe68264 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -14,6 +14,7 @@
 #include "doomstat.h"
 #include "p_mobj.h"
 #include "g_game.h"
+#include "g_input.h"
 #include "r_skins.h"
 #include "b_bot.h"
 #include "z_zone.h"
@@ -641,6 +642,28 @@ int LUA_HookKey(event_t *event, int hook_type)
 	return hook.status;
 }
 
+int  LUA_HookGamepadButton(event_t *event, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, &gamepads[event->which], META_GAMEPAD);
+		lua_pushstring(gL, gamepad_button_names[event->key]);
+		call_hooks(&hook, 1, res_true);
+	}
+	return hook.status;
+}
+
+void LUA_HookGamepadEvent(UINT8 which, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, hook_type))
+	{
+		LUA_PushUserdata(gL, &gamepads[which], META_GAMEPAD);
+		call_hooks(&hook, 0, res_none);
+	}
+}
+
 void LUA_HookHUD(int hook_type, huddrawlist_h list)
 {
 	const hook_t * map = &hudHookIds[hook_type];
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 88efc3490cf93dbbb8234a3ebc81a01b6aaccf5b..e4962a0fb85c18fb4b4722e30d490cab8002315c 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -129,6 +129,21 @@ static int lib_getCursorPosition(lua_State *L)
 	return 2;
 }
 
+static int lib_getPlayerGamepad(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+
+	INT16 which = G_GetGamepadForPlayer(player);
+	if (which >= 0)
+		LUA_PushUserdata(L, &gamepads[which], META_GAMEPAD);
+	else
+		lua_pushnil(L);
+
+	return 1;
+}
+
 static luaL_Reg lib[] = {
 	{"gameControlDown", lib_gameControlDown},
 	{"gameControl2Down", lib_gameControl2Down},
@@ -143,6 +158,7 @@ static luaL_Reg lib[] = {
 	{"getMouseGrab", lib_getMouseGrab},
 	{"setMouseGrab", lib_setMouseGrab},
 	{"getCursorPosition", lib_getCursorPosition},
+	{"getPlayerGamepad", lib_getPlayerGamepad},
 	{NULL, NULL}
 };
 
@@ -198,6 +214,341 @@ static int keyevent_get(lua_State *L)
 	return 1;
 }
 
+/////////////
+// GAMEPAD //
+/////////////
+
+enum gamepad_leftright_e {
+	gamepad_opt_left,
+	gamepad_opt_right
+};
+
+static const char *const gamepad_leftright_opt[] = {
+	"left",
+	"right",
+	NULL};
+
+// Buttons
+static int gamepad_isButtonDown(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	gamepad_button_e button = luaL_checkoption(L, 2, NULL, gamepad_button_names);
+	lua_pushboolean(L, gamepad->buttons[button] == 1);
+	return 1;
+}
+
+// Axes
+static int gamepad_getAxis(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	gamepad_axis_e axis = luaL_checkoption(L, 2, NULL, gamepad_axis_names);
+	boolean applyDeadzone = luaL_opt(L, luaL_checkboolean, 3, true);
+	lua_pushfixed(L, G_GetAdjustedGamepadAxis(gamepad->num, axis, applyDeadzone));
+	return 1;
+}
+
+// Sticks
+static int gamepad_getStick(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	enum gamepad_leftright_e stick = luaL_checkoption(L, 2, NULL, gamepad_leftright_opt);
+	boolean applyDeadzone = luaL_opt(L, luaL_checkboolean, 3, true);
+
+	switch (stick)
+	{
+	case gamepad_opt_left:
+		lua_pushfixed(L, G_GetAdjustedGamepadAxis(gamepad->num, GAMEPAD_AXIS_LEFTX, applyDeadzone));
+		lua_pushfixed(L, G_GetAdjustedGamepadAxis(gamepad->num, GAMEPAD_AXIS_LEFTY, applyDeadzone));
+		break;
+	case gamepad_opt_right:
+		lua_pushfixed(L, G_GetAdjustedGamepadAxis(gamepad->num, GAMEPAD_AXIS_RIGHTX, applyDeadzone));
+		lua_pushfixed(L, G_GetAdjustedGamepadAxis(gamepad->num, GAMEPAD_AXIS_RIGHTY, applyDeadzone));
+		break;
+	}
+
+	return 2;
+}
+
+// Triggers
+static int gamepad_getTrigger(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	enum gamepad_leftright_e stick = luaL_checkoption(L, 2, NULL, gamepad_leftright_opt);
+	boolean applyDeadzone = luaL_opt(L, luaL_checkboolean, 3, true);
+	gamepad_axis_e axis = 0;
+
+	switch (stick)
+	{
+	case gamepad_opt_left:
+		axis = GAMEPAD_AXIS_TRIGGERLEFT;
+		break;
+	case gamepad_opt_right:
+		axis = GAMEPAD_AXIS_TRIGGERRIGHT;
+		break;
+	}
+
+	lua_pushfixed(L, G_GetAdjustedGamepadAxis(gamepad->num, axis, applyDeadzone));
+	return 1;
+}
+
+// Button and axis names
+static int gamepad_getButtonName(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	gamepad_button_e button = luaL_checkoption(L, 2, NULL, gamepad_button_names);
+	lua_pushstring(L, G_GetGamepadButtonString(gamepad->type, button, GAMEPAD_STRING_DEFAULT));
+	return 1;
+}
+
+static int gamepad_getAxisName(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	gamepad_axis_e axis = luaL_checkoption(L, 2, NULL, gamepad_axis_names);
+	lua_pushstring(L, G_GetGamepadAxisString(gamepad->type, axis, GAMEPAD_STRING_DEFAULT, false));
+	return 1;
+}
+
+static int gamepad_getTriggerName(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	enum gamepad_leftright_e stick = luaL_checkoption(L, 2, NULL, gamepad_leftright_opt);
+	gamepad_axis_e axis = 0;
+
+	switch (stick)
+	{
+	case gamepad_opt_left:
+		axis = GAMEPAD_AXIS_TRIGGERLEFT;
+		break;
+	case gamepad_opt_right:
+		axis = GAMEPAD_AXIS_TRIGGERRIGHT;
+		break;
+	}
+
+	lua_pushstring(L, G_GetGamepadAxisString(gamepad->type, axis, GAMEPAD_STRING_DEFAULT, false));
+	return 1;
+}
+
+// Rumble
+static int gamepad_doRumble(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	fixed_t large_magnitude = luaL_checkfixed(L, 2);
+	fixed_t small_magnitude = luaL_optfixed(L, 3, large_magnitude);
+	tic_t duration = luaL_optinteger(L, 4, 0);
+
+#define CHECK_MAGNITUDE(which) \
+	if (which##_magnitude < 0 || which##_magnitude > FRACUNIT) \
+		return luaL_error(L, va(#which " motor frequency %f out of range (minimum is 0.0, maximum is 1.0)", \
+			FixedToFloat(which##_magnitude)))
+
+	CHECK_MAGNITUDE(large);
+	CHECK_MAGNITUDE(small);
+
+#undef CHECK_MAGNITUDE
+
+	lua_pushboolean(L, G_RumbleGamepad(gamepad->num, large_magnitude, small_magnitude, duration));
+	return 1;
+}
+
+static int gamepad_stopRumble(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	G_StopGamepadRumble(gamepad->num);
+	return 0;
+}
+
+// Accessing gamepad userdata
+enum gamepad_opt_e {
+	gamepad_opt_connected,
+	gamepad_opt_type,
+	gamepad_opt_isXbox,
+	gamepad_opt_isPlayStation,
+	gamepad_opt_isNintendoSwitch,
+	gamepad_opt_isJoyCon,
+	gamepad_opt_hasRumble,
+	gamepad_opt_isRumbling,
+	gamepad_opt_isRumblePaused,
+	gamepad_opt_largeMotorFrequency,
+	gamepad_opt_smallMotorFrequency,
+	gamepad_opt_isButtonDown,
+	gamepad_opt_getAxis,
+	gamepad_opt_getStick,
+	gamepad_opt_getTrigger,
+	gamepad_opt_getButtonName,
+	gamepad_opt_getAxisName,
+	gamepad_opt_getTriggerName,
+	gamepad_opt_rumble,
+	gamepad_opt_stopRumble
+};
+
+static const char *const gamepad_opt[] = {
+	"connected",
+	"type",
+	"isXbox",
+	"isPlayStation",
+	"isNintendoSwitch",
+	"isJoyCon",
+	"hasRumble",
+	"isRumbling",
+	"isRumblePaused",
+	"largeMotorFrequency",
+	"smallMotorFrequency",
+	"isButtonDown",
+	"getAxis",
+	"getStick",
+	"getTrigger",
+	"getButtonName",
+	"getAxisName",
+	"getTriggerName",
+	"rumble",
+	"stopRumble",
+	NULL};
+
+static int (*gamepad_fn_list[9])(lua_State *L) = {
+	gamepad_isButtonDown,
+	gamepad_getAxis,
+	gamepad_getStick,
+	gamepad_getTrigger,
+	gamepad_getButtonName,
+	gamepad_getAxisName,
+	gamepad_getTriggerName,
+	gamepad_doRumble,
+	gamepad_stopRumble
+};
+
+static int gamepad_get(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	enum gamepad_opt_e field = luaL_checkoption(L, 2, NULL, gamepad_opt);
+
+	switch (field)
+	{
+	case gamepad_opt_connected:
+		lua_pushboolean(L, gamepad->connected);
+		break;
+	case gamepad_opt_type:
+		lua_pushstring(L, G_GamepadTypeToString(gamepad->type));
+		break;
+	case gamepad_opt_isXbox:
+		lua_pushboolean(L, G_GamepadTypeIsXbox(gamepad->type));
+		break;
+	case gamepad_opt_isPlayStation:
+		lua_pushboolean(L, G_GamepadTypeIsPlayStation(gamepad->type));
+		break;
+	case gamepad_opt_isNintendoSwitch:
+		lua_pushboolean(L, G_GamepadTypeIsNintendoSwitch(gamepad->type));
+		break;
+	case gamepad_opt_isJoyCon:
+		// No, this does not include the grip.
+		lua_pushboolean(L, G_GamepadTypeIsJoyCon(gamepad->type));
+		break;
+	case gamepad_opt_hasRumble:
+		lua_pushboolean(L, G_RumbleSupported(gamepad->num));
+		break;
+	case gamepad_opt_isRumbling:
+		lua_pushboolean(L, gamepad->rumble.active);
+		break;
+	case gamepad_opt_isRumblePaused:
+		lua_pushboolean(L, G_GetGamepadRumblePaused(gamepad->num));
+		break;
+	case gamepad_opt_largeMotorFrequency:
+		lua_pushfixed(L, G_GetLargeMotorFreq(gamepad->num));
+		break;
+	case gamepad_opt_smallMotorFrequency:
+		lua_pushfixed(L, G_GetSmallMotorFreq(gamepad->num));
+		break;
+	case gamepad_opt_isButtonDown:
+	case gamepad_opt_getAxis:
+	case gamepad_opt_getStick:
+	case gamepad_opt_getTrigger:
+	case gamepad_opt_getButtonName:
+	case gamepad_opt_getAxisName:
+	case gamepad_opt_getTriggerName:
+	case gamepad_opt_rumble:
+	case gamepad_opt_stopRumble:
+		lua_pushcfunction(L, gamepad_fn_list[field - gamepad_opt_isButtonDown]);
+		break;
+	}
+	return 1;
+}
+
+static int gamepad_set(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	enum gamepad_opt_e field = luaL_checkoption(L, 2, NULL, gamepad_opt);
+
+	switch (field)
+	{
+	case gamepad_opt_isRumblePaused:
+		G_SetGamepadRumblePaused(gamepad->num, luaL_checkboolean(L, 3));
+		break;
+	case gamepad_opt_largeMotorFrequency:
+		G_SetLargeMotorFreq(gamepad->num, luaL_checkfixed(L, 3));
+		break;
+	case gamepad_opt_smallMotorFrequency:
+		G_SetSmallMotorFreq(gamepad->num, luaL_checkfixed(L, 3));
+		break;
+	default:
+		return luaL_error(L, LUA_QL("gamepad") " field " LUA_QS " should not be set directly.", gamepad_opt[field]);
+	}
+	return 1;
+}
+
+static int gamepad_num(lua_State *L)
+{
+	gamepad_t *gamepad = *((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD));
+	lua_pushinteger(L, gamepad->num + 1);
+	return 1;
+}
+
+static int lib_iterateGamepads(lua_State *L)
+{
+	INT32 i = -1;
+	if (lua_gettop(L) < 2)
+	{
+		lua_pushcfunction(L, lib_iterateGamepads);
+		return 1;
+	}
+	lua_settop(L, 2);
+	lua_remove(L, 1); // State is unused
+	if (!lua_isnil(L, 1))
+		i = (INT32)(*((gamepad_t **)luaL_checkudata(L, 1, META_GAMEPAD)) - gamepads);
+	for (i++; i < NUM_GAMEPADS; i++)
+	{
+		if (!gamepads[i].connected)
+			continue;
+		LUA_PushUserdata(L, &gamepads[i], META_GAMEPAD);
+		return 1;
+	}
+	return 0;
+}
+
+static int lib_getGamepad(lua_State *L)
+{
+	if (lua_type(L, 2) == LUA_TNUMBER)
+	{
+		lua_Integer i = luaL_checkinteger(L, 2);
+		if (i < 1 || i > NUM_GAMEPADS)
+			return luaL_error(L, "gamepads[] index %d out of range (1 - %d)", i, NUM_GAMEPADS);
+		LUA_PushUserdata(L, &gamepads[i - 1], META_GAMEPAD);
+		return 1;
+	}
+
+	if (fastcmp(luaL_checkstring(L, 2), "iterate"))
+	{
+		lua_pushcfunction(L, lib_iterateGamepads);
+		return 1;
+	}
+
+	return 0;
+}
+
+static int lib_lenGamepad(lua_State *L)
+{
+	lua_pushinteger(L, NUM_GAMEPADS);
+	return 1;
+}
+
 ///////////
 // MOUSE //
 ///////////
@@ -258,6 +609,27 @@ int LUA_InputLib(lua_State *L)
 		lua_setfield(L, -2, "__index");
 	lua_pop(L, 1);
 
+	luaL_newmetatable(L, META_GAMEPAD);
+		lua_pushcfunction(L, gamepad_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, gamepad_set);
+		lua_setfield(L, -2, "__newindex");
+
+		lua_pushcfunction(L, gamepad_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_getGamepad);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_lenGamepad);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "gamepads");
+
 	luaL_newmetatable(L, META_MOUSE);
 		lua_pushcfunction(L, mouse_get);
 		lua_setfield(L, -2, "__index");
diff --git a/src/lua_libs.h b/src/lua_libs.h
index b4a891edb83d68f77075e8671cc13c76b1df0de3..68e97623448584f413c4de30a0ecc541231a92c0 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -91,6 +91,7 @@ extern boolean mousegrabbedbylua;
 #define META_LUABANKS "LUABANKS[]*"
 
 #define META_KEYEVENT "KEYEVENT_T*"
+#define META_GAMEPAD "GAMEPAD_T*"
 #define META_MOUSE "MOUSE_T*"
 
 boolean luaL_checkboolean(lua_State *L, int narg);
diff --git a/src/lua_script.c b/src/lua_script.c
index f166fb4e6954310021378eb2dd3d801ccff4cacb..8d8fb295cda8cdf0df4e0bd42b31e6ae344d88cc 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -949,6 +949,7 @@ enum
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
 	ARCH_MOUSE,
+	ARCH_GAMEPAD,
 
 	ARCH_TEND=0xFF,
 };
@@ -976,6 +977,7 @@ static const struct {
 	{META_SLOPE,    ARCH_SLOPE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
+	{META_GAMEPAD,  ARCH_GAMEPAD},
 	{META_MOUSE,    ARCH_MOUSE},
 	{NULL,          ARCH_NULL}
 };
@@ -1291,6 +1293,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			WRITEUINT16(save_p, info - skincolors);
 			break;
 		}
+		case ARCH_GAMEPAD:
+		{
+			gamepad_t *gamepad = *((gamepad_t **)lua_touserdata(gL, myindex));
+			WRITEUINT8(save_p, ARCH_GAMEPAD);
+			WRITEUINT8(save_p, gamepad->num);
+			break;
+		}
 		case ARCH_MOUSE:
 		{
 			mouse_t *m = *((mouse_t **)lua_touserdata(gL, myindex));
@@ -1541,6 +1550,15 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_SKINCOLOR:
 		LUA_PushUserdata(gL, &skincolors[READUINT16(save_p)], META_SKINCOLOR);
 		break;
+	case ARCH_GAMEPAD:
+	{
+		UINT8 which = READUINT8(save_p);
+		if (which < NUM_GAMEPADS)
+			LUA_PushUserdata(gL, &gamepads[which], META_GAMEPAD);
+		else // Wait, what?
+			lua_pushnil(gL);
+		break;
+	}
 	case ARCH_MOUSE:
 		LUA_PushUserdata(gL, READUINT16(save_p) == 1 ? &mouse : &mouse2, META_MOUSE);
 		break;
diff --git a/src/lua_script.h b/src/lua_script.h
index e586b04a886d292ec65669d8f831123d0d6b57d1..080427039d240634f603f8d91dc402d1c3327ce2 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -30,6 +30,7 @@
 // TODO add some distinction between fixed numbers and integer numbers
 // for at least the purpose of printing and maybe math.
 #define luaL_checkfixed(L, i) luaL_checkinteger(L, i)
+#define luaL_optfixed(L, i, o) luaL_optinteger(L, i, o)
 #define lua_pushfixed(L, f) lua_pushinteger(L, f)
 
 // angle_t casting
diff --git a/src/sdl/i_gamepad.c b/src/sdl/i_gamepad.c
index 69f03d662e22be3dac9427e1495c342622326c34..dbafc1f63a1eb53f907457d102b2f3107165e0dc 100644
--- a/src/sdl/i_gamepad.c
+++ b/src/sdl/i_gamepad.c
@@ -233,7 +233,7 @@ static boolean Controller_OpenDevice(UINT8 which, INT32 devindex)
 		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?
+		// FIXME: Doesn't seem to work?
 		SDL_GameControllerSetPlayerIndex(controller->dev, which);
 
 		// Check if rumble is supported
@@ -248,7 +248,11 @@ static boolean Controller_OpenDevice(UINT8 which, INT32 devindex)
 			CONS_Debug(DBG_GAMELOGIC, M_GetText("    Rumble supported: No\n"));;
 		}
 
-		controller->info->connected = true;
+		if (!controller->info->connected)
+		{
+			controller->info->connected = true;
+			G_OnGamepadConnect(which);
+		}
 	}
 
 	return controller->started;