diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 87a0499b6..77b5a0e4f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -281,6 +281,7 @@ set(SRB2_LUA_SOURCES
 	lua_mobjlib.c
 	lua_playerlib.c
 	lua_polyobjlib.c
+	lua_inputlib.c
 	lua_script.c
 	lua_skinlib.c
 	lua_thinkerlib.c
diff --git a/src/blua/Makefile.cfg b/src/blua/Makefile.cfg
index 3a2962e65..4536628ba 100644
--- a/src/blua/Makefile.cfg
+++ b/src/blua/Makefile.cfg
@@ -50,4 +50,5 @@ OBJS:=$(OBJS) \
 	$(OBJDIR)/lua_taglib.o \
 	$(OBJDIR)/lua_polyobjlib.o \
 	$(OBJDIR)/lua_blockmaplib.o \
-	$(OBJDIR)/lua_hudlib.o
+	$(OBJDIR)/lua_hudlib.o \
+	$(OBJDIR)/lua_inputlib.o
diff --git a/src/console.c b/src/console.c
index 121605b10..3b29e9c4f 100644
--- a/src/console.c
+++ b/src/console.c
@@ -221,7 +221,7 @@ static void CONS_Bind_f(void)
 		for (key = 0; key < NUMINPUTS; key++)
 			if (bindtable[key])
 			{
-				CONS_Printf("%s : \"%s\"\n", G_KeynumToString(key), bindtable[key]);
+				CONS_Printf("%s : \"%s\"\n", G_KeyNumToString(key), bindtable[key]);
 				na = 1;
 			}
 		if (!na)
@@ -229,7 +229,7 @@ static void CONS_Bind_f(void)
 		return;
 	}
 
-	key = G_KeyStringtoNum(COM_Argv(1));
+	key = G_KeyStringToNum(COM_Argv(1));
 	if (key <= 0 || key >= NUMINPUTS)
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("Invalid key name\n"));
diff --git a/src/d_main.c b/src/d_main.c
index 2a4e9ab81..d60a6cf47 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -181,6 +181,8 @@ void D_ProcessEvents(void)
 
 	for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
 	{
+		boolean hooked = false;
+
 		ev = &events[eventtail];
 
 		// Screenshots over everything so that they can be taken anywhere.
@@ -193,6 +195,12 @@ void D_ProcessEvents(void)
 				continue;
 		}
 
+		if (!CON_Ready() && !menuactive) {
+			if (G_LuaResponder(ev))
+				continue;
+			hooked = true;
+		}
+
 		// Menu input
 #ifdef HAVE_THREADS
 		I_lock_mutex(&m_menu_mutex);
@@ -207,6 +215,12 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // menu ate the event
 
+		if (!hooked && !CON_Ready()) {
+			if (G_LuaResponder(ev))
+				continue;
+			hooked = true;
+		}
+
 		// console input
 #ifdef HAVE_THREADS
 		I_lock_mutex(&con_mutex);
@@ -221,6 +235,9 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // ate the event
 
+		if (!hooked && G_LuaResponder(ev))
+			continue;
+
 		G_Responder(ev);
 	}
 
diff --git a/src/deh_tables.c b/src/deh_tables.c
index dd6d7d69f..7bfe723da 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -22,6 +22,8 @@
 #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 "g_input.h" // Game controls (for lua)
 
 #include "deh_tables.h"
 
@@ -5455,6 +5457,63 @@ struct int_const_s const INT_CONST[] = {
 	{"GS_DEDICATEDSERVER",GS_DEDICATEDSERVER},
 	{"GS_WAITINGPLAYERS",GS_WAITINGPLAYERS},
 
+	// Joystick axes
+	{"JA_NONE",JA_NONE},
+	{"JA_TURN",JA_TURN},
+	{"JA_MOVE",JA_MOVE},
+	{"JA_LOOK",JA_LOOK},
+	{"JA_STRAFE",JA_STRAFE},
+	{"JA_DIGITAL",JA_DIGITAL},
+	{"JA_JUMP",JA_JUMP},
+	{"JA_SPIN",JA_SPIN},
+	{"JA_FIRE",JA_FIRE},
+	{"JA_FIRENORMAL",JA_FIRENORMAL},
+
+	// Game controls
+	{"gc_null",gc_null},
+	{"gc_forward",gc_forward},
+	{"gc_backward",gc_backward},
+	{"gc_strafeleft",gc_strafeleft},
+	{"gc_straferight",gc_straferight},
+	{"gc_turnleft",gc_turnleft},
+	{"gc_turnright",gc_turnright},
+	{"gc_weaponnext",gc_weaponnext},
+	{"gc_weaponprev",gc_weaponprev},
+	{"gc_wepslot1",gc_wepslot1},
+	{"gc_wepslot2",gc_wepslot2},
+	{"gc_wepslot3",gc_wepslot3},
+	{"gc_wepslot4",gc_wepslot4},
+	{"gc_wepslot5",gc_wepslot5},
+	{"gc_wepslot6",gc_wepslot6},
+	{"gc_wepslot7",gc_wepslot7},
+	{"gc_wepslot8",gc_wepslot8},
+	{"gc_wepslot9",gc_wepslot9},
+	{"gc_wepslot10",gc_wepslot10},
+	{"gc_fire",gc_fire},
+	{"gc_firenormal",gc_firenormal},
+	{"gc_tossflag",gc_tossflag},
+	{"gc_spin",gc_spin},
+	{"gc_camtoggle",gc_camtoggle},
+	{"gc_camreset",gc_camreset},
+	{"gc_lookup",gc_lookup},
+	{"gc_lookdown",gc_lookdown},
+	{"gc_centerview",gc_centerview},
+	{"gc_mouseaiming",gc_mouseaiming},
+	{"gc_talkkey",gc_talkkey},
+	{"gc_teamkey",gc_teamkey},
+	{"gc_scores",gc_scores},
+	{"gc_jump",gc_jump},
+	{"gc_console",gc_console},
+	{"gc_pause",gc_pause},
+	{"gc_systemmenu",gc_systemmenu},
+	{"gc_screenshot",gc_screenshot},
+	{"gc_recordgif",gc_recordgif},
+	{"gc_viewpoint",gc_viewpoint},
+	{"gc_custom1",gc_custom1},
+	{"gc_custom2",gc_custom2},
+	{"gc_custom3",gc_custom3},
+	{"num_gamecontrols",num_gamecontrols},
+
 	{NULL,0}
 };
 
diff --git a/src/g_game.c b/src/g_game.c
index e46a7f816..823ea7c56 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -406,22 +406,6 @@ consvar_t cv_cam_lockonboss[2] = {
 	CVAR_INIT ("cam2_lockaimassist", "Bosses", CV_SAVE, lockedassist_cons_t, NULL),
 };
 
-typedef enum
-{
-	AXISNONE = 0,
-	AXISTURN,
-	AXISMOVE,
-	AXISLOOK,
-	AXISSTRAFE,
-
-	AXISDIGITAL, // axes below this use digital deadzone
-
-	AXISJUMP,
-	AXISSPIN,
-	AXISFIRE,
-	AXISFIRENORMAL,
-} axis_input_e;
-
 consvar_t cv_turnaxis = CVAR_INIT ("joyaxis_turn", "X-Rudder", CV_SAVE, joyaxis_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);
@@ -841,7 +825,7 @@ INT16 G_SoftwareClipAimingPitch(INT32 *aiming)
 	return (INT16)((*aiming)>>16);
 }
 
-static INT32 JoyAxis(axis_input_e axissel)
+INT32 JoyAxis(joyaxis_e axissel)
 {
 	INT32 retaxis;
 	INT32 axisval;
@@ -850,28 +834,28 @@ static INT32 JoyAxis(axis_input_e axissel)
 	//find what axis to get
 	switch (axissel)
 	{
-		case AXISTURN:
+		case JA_TURN:
 			axisval = cv_turnaxis.value;
 			break;
-		case AXISMOVE:
+		case JA_MOVE:
 			axisval = cv_moveaxis.value;
 			break;
-		case AXISLOOK:
+		case JA_LOOK:
 			axisval = cv_lookaxis.value;
 			break;
-		case AXISSTRAFE:
+		case JA_STRAFE:
 			axisval = cv_sideaxis.value;
 			break;
-		case AXISJUMP:
+		case JA_JUMP:
 			axisval = cv_jumpaxis.value;
 			break;
-		case AXISSPIN:
+		case JA_SPIN:
 			axisval = cv_spinaxis.value;
 			break;
-		case AXISFIRE:
+		case JA_FIRE:
 			axisval = cv_fireaxis.value;
 			break;
-		case AXISFIRENORMAL:
+		case JA_FIRENORMAL:
 			axisval = cv_firenaxis.value;
 			break;
 		default:
@@ -903,7 +887,7 @@ static INT32 JoyAxis(axis_input_e axissel)
 	if (retaxis > (+JOYAXISRANGE))
 		retaxis = +JOYAXISRANGE;
 
-	if (!Joystick.bGamepadStyle && axissel > AXISDIGITAL)
+	if (!Joystick.bGamepadStyle && axissel > JA_DIGITAL)
 	{
 		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_digitaldeadzone.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
@@ -914,7 +898,7 @@ static INT32 JoyAxis(axis_input_e axissel)
 	return retaxis;
 }
 
-static INT32 Joy2Axis(axis_input_e axissel)
+INT32 Joy2Axis(joyaxis_e axissel)
 {
 	INT32 retaxis;
 	INT32 axisval;
@@ -923,28 +907,28 @@ static INT32 Joy2Axis(axis_input_e axissel)
 	//find what axis to get
 	switch (axissel)
 	{
-		case AXISTURN:
+		case JA_TURN:
 			axisval = cv_turnaxis2.value;
 			break;
-		case AXISMOVE:
+		case JA_MOVE:
 			axisval = cv_moveaxis2.value;
 			break;
-		case AXISLOOK:
+		case JA_LOOK:
 			axisval = cv_lookaxis2.value;
 			break;
-		case AXISSTRAFE:
+		case JA_STRAFE:
 			axisval = cv_sideaxis2.value;
 			break;
-		case AXISJUMP:
+		case JA_JUMP:
 			axisval = cv_jumpaxis2.value;
 			break;
-		case AXISSPIN:
+		case JA_SPIN:
 			axisval = cv_spinaxis2.value;
 			break;
-		case AXISFIRE:
+		case JA_FIRE:
 			axisval = cv_fireaxis2.value;
 			break;
-		case AXISFIRENORMAL:
+		case JA_FIRENORMAL:
 			axisval = cv_firenaxis2.value;
 			break;
 		default:
@@ -978,7 +962,7 @@ static INT32 Joy2Axis(axis_input_e axissel)
 	if (retaxis > (+JOYAXISRANGE))
 		retaxis = +JOYAXISRANGE;
 
-	if (!Joystick2.bGamepadStyle && axissel > AXISDIGITAL)
+	if (!Joystick2.bGamepadStyle && axissel > JA_DIGITAL)
 	{
 		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_digitaldeadzone2.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
@@ -1174,10 +1158,10 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		*myaiming = 0;
 	joyaiming[forplayer] = thisjoyaiming;
 
-	turnaxis = PlayerJoyAxis(ssplayer, AXISTURN);
+	turnaxis = PlayerJoyAxis(ssplayer, JA_TURN);
 	if (strafeisturn)
-		turnaxis += PlayerJoyAxis(ssplayer, AXISSTRAFE);
-	lookaxis = PlayerJoyAxis(ssplayer, AXISLOOK);
+		turnaxis += PlayerJoyAxis(ssplayer, JA_STRAFE);
+	lookaxis = PlayerJoyAxis(ssplayer, JA_LOOK);
 	lookjoystickvector.xaxis = turnaxis;
 	lookjoystickvector.yaxis = lookaxis;
 	G_HandleAxisDeadZone(forplayer, &lookjoystickvector);
@@ -1256,8 +1240,8 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			tta_factor[forplayer] = 0; // suspend turn to angle
 	}
 
-	strafeaxis = strafeisturn ? 0 : PlayerJoyAxis(ssplayer, AXISSTRAFE);
-	moveaxis = PlayerJoyAxis(ssplayer, AXISMOVE);
+	strafeaxis = strafeisturn ? 0 : PlayerJoyAxis(ssplayer, JA_STRAFE);
+	moveaxis = PlayerJoyAxis(ssplayer, JA_MOVE);
 	movejoystickvector.xaxis = strafeaxis;
 	movejoystickvector.yaxis = moveaxis;
 	G_HandleAxisDeadZone(forplayer, &movejoystickvector);
@@ -1313,12 +1297,12 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		}
 
 	// fire with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISFIRE);
+	axis = PlayerJoyAxis(ssplayer, JA_FIRE);
 	if (PLAYERINPUTDOWN(ssplayer, gc_fire) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_ATTACK;
 
 	// fire normal with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISFIRENORMAL);
+	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
 	if (PLAYERINPUTDOWN(ssplayer, gc_firenormal) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_FIRENORMAL;
 
@@ -1334,7 +1318,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		cmd->buttons |= BT_CUSTOM3;
 
 	// use with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISSPIN);
+	axis = PlayerJoyAxis(ssplayer, JA_SPIN);
 	if (PLAYERINPUTDOWN(ssplayer, gc_spin) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_SPIN;
 
@@ -1452,7 +1436,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 
 	// jump button
-	axis = PlayerJoyAxis(ssplayer, AXISJUMP);
+	axis = PlayerJoyAxis(ssplayer, JA_JUMP);
 	if (PLAYERINPUTDOWN(ssplayer, gc_jump) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_JUMP;
 
@@ -2191,6 +2175,16 @@ boolean G_Responder(event_t *ev)
 	return false;
 }
 
+//
+// G_LuaResponder
+// Let Lua handle key events.
+//
+boolean G_LuaResponder(event_t *ev)
+{
+	return (ev->type == ev_keydown && LUAh_KeyDown(ev->data1)) ||
+		(ev->type == ev_keyup && LUAh_KeyUp(ev->data1));
+}
+
 //
 // G_Ticker
 // Make ticcmd_ts for the players.
diff --git a/src/g_game.h b/src/g_game.h
index 744d6755a..0cc380294 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -85,6 +85,25 @@ typedef enum
 } lockassist_e;
 
 
+typedef enum
+{
+	JA_NONE = 0,
+	JA_TURN,
+	JA_MOVE,
+	JA_LOOK,
+	JA_STRAFE,
+
+	JA_DIGITAL, // axes below this use digital deadzone
+
+	JA_JUMP,
+	JA_SPIN,
+	JA_FIRE,
+	JA_FIRENORMAL,
+} joyaxis_e;
+
+INT32 JoyAxis(joyaxis_e axissel);
+INT32 Joy2Axis(joyaxis_e axissel);
+
 // mouseaiming (looking up/down with the mouse or keyboard)
 #define KB_LOOKSPEED (1<<25)
 #define MAXPLMOVE (50)
@@ -204,6 +223,7 @@ void G_EndGame(void); // moved from y_inter.c/h and renamed
 
 void G_Ticker(boolean run);
 boolean G_Responder(event_t *ev);
+boolean G_LuaResponder(event_t *ev);
 
 void G_AddPlayer(INT32 playernum);
 
diff --git a/src/g_input.c b/src/g_input.c
index 049a4d82c..629b389e5 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -626,7 +626,7 @@ void G_ClearAllControlKeys(void)
 // Returns the name of a key (or virtual key for mouse and joy)
 // the input value being an keynum
 //
-const char *G_KeynumToString(INT32 keynum)
+const char *G_KeyNumToString(INT32 keynum)
 {
 	static char keynamestr[8];
 
@@ -650,7 +650,7 @@ const char *G_KeynumToString(INT32 keynum)
 	return keynamestr;
 }
 
-INT32 G_KeyStringtoNum(const char *keystr)
+INT32 G_KeyStringToNum(const char *keystr)
 {
 	UINT32 j;
 
@@ -813,10 +813,10 @@ void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(fromcontrols[i][0]));
+			G_KeyNumToString(fromcontrols[i][0]));
 
 		if (fromcontrols[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(fromcontrols[i][1]));
+			fprintf(f, " \"%s\"\n", G_KeyNumToString(fromcontrols[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -824,10 +824,10 @@ void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol2 \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(fromcontrolsbis[i][0]));
+			G_KeyNumToString(fromcontrolsbis[i][0]));
 
 		if (fromcontrolsbis[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(fromcontrolsbis[i][1]));
+			fprintf(f, " \"%s\"\n", G_KeyNumToString(fromcontrolsbis[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -1003,8 +1003,8 @@ static void setcontrol(INT32 (*gc)[2])
 		CONS_Printf(M_GetText("Control '%s' unknown\n"), namectrl);
 		return;
 	}
-	keynum1 = G_KeyStringtoNum(COM_Argv(2));
-	keynum2 = G_KeyStringtoNum(COM_Argv(3));
+	keynum1 = G_KeyStringToNum(COM_Argv(2));
+	keynum2 = G_KeyStringToNum(COM_Argv(3));
 	keynum = G_FilterKeyByVersion(numctrl, 0, player, &keynum1, &keynum2, &nestedoverride);
 
 	if (keynum >= 0)
diff --git a/src/g_input.h b/src/g_input.h
index 5912bfdc7..96139e751 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -169,8 +169,8 @@ extern const INT32 gcl_jump_spin[num_gcl_jump_spin];
 void G_MapEventsToControls(event_t *ev);
 
 // returns the name of a key
-const char *G_KeynumToString(INT32 keynum);
-INT32 G_KeyStringtoNum(const char *keystr);
+const char *G_KeyNumToString(INT32 keynum);
+INT32 G_KeyStringToNum(const char *keystr);
 
 // detach any keys associated to the given game control
 void G_ClearControlKeys(INT32 (*setupcontrols)[2], INT32 control);
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index a59ba546e..71282d09c 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -212,6 +212,8 @@ static const struct {
 	{META_ACTION,       "action"},
 
 	{META_LUABANKS,     "luabanks[]"},
+	
+	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
 
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 0d631aa4e..ae1c17a4d 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -63,6 +63,8 @@ enum hook {
 	hook_MusicChange,
 	hook_PlayerHeight,
 	hook_PlayerCanEnterSpinGaps,
+	hook_KeyDown,
+	hook_KeyUp,
 
 	hook_MAX // last hook
 };
@@ -122,3 +124,5 @@ boolean LUAh_PlayerCmd(player_t *player, ticcmd_t *cmd); // Hook for building pl
 boolean LUAh_MusicChange(const char *oldname, char *newname, UINT16 *mflags, boolean *looping, UINT32 *position, UINT32 *prefadems, UINT32 *fadeinms); // Hook for music changes
 fixed_t LUAh_PlayerHeight(player_t *player);
 UINT8 LUAh_PlayerCanEnterSpinGaps(player_t *player);
+boolean LUAh_KeyDown(INT32 keycode); // Hooks for key events
+boolean LUAh_KeyUp(INT32 keycode);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 637809fd8..8a7ce2cb9 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -79,6 +79,8 @@ const char *const hookNames[hook_MAX+1] = {
 	"MusicChange",
 	"PlayerHeight",
 	"PlayerCanEnterSpinGaps",
+	"KeyDown",
+	"KeyUp",
 	NULL
 };
 
@@ -2061,3 +2063,73 @@ UINT8 LUAh_PlayerCanEnterSpinGaps(player_t *player)
 	lua_settop(gL, 0);
 	return canEnter;
 }
+
+// Hook for key press
+boolean LUAh_KeyDown(INT32 keycode)
+{
+	hook_p hookp;
+	boolean override = false;
+	if (!gL || !(hooksAvailable[hook_KeyDown/8] & (1<<(hook_KeyDown%8))))
+		return false;
+
+	lua_settop(gL, 0);
+	lua_pushcfunction(gL, LUA_GetErrorMessage);
+
+	for (hookp = roothook; hookp; hookp = hookp->next)
+	{
+		if (hookp->type != hook_KeyDown)
+			continue;
+
+		PushHook(gL, hookp);
+		lua_pushinteger(gL, keycode);
+		if (lua_pcall(gL, 1, 1, 1)) {
+			if (!hookp->error || cv_debug & DBG_LUA)
+				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
+			lua_pop(gL, 1);
+			hookp->error = true;
+			continue;
+		}
+		if (lua_toboolean(gL, -1))
+			override = true;
+		lua_pop(gL, 1);
+	}
+
+	lua_settop(gL, 0);
+
+	return override;
+}
+
+// Hook for key release
+boolean LUAh_KeyUp(INT32 keycode)
+{
+	hook_p hookp;
+	boolean override = false;
+	if (!gL || !(hooksAvailable[hook_KeyUp/8] & (1<<(hook_KeyUp%8))))
+		return false;
+
+	lua_settop(gL, 0);
+	lua_pushcfunction(gL, LUA_GetErrorMessage);
+
+	for (hookp = roothook; hookp; hookp = hookp->next)
+	{
+		if (hookp->type != hook_KeyUp)
+			continue;
+
+		PushHook(gL, hookp);
+		lua_pushinteger(gL, keycode);
+		if (lua_pcall(gL, 1, 1, 1)) {
+			if (!hookp->error || cv_debug & DBG_LUA)
+				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
+			lua_pop(gL, 1);
+			hookp->error = true;
+			continue;
+		}
+		if (lua_toboolean(gL, -1))
+			override = true;
+		lua_pop(gL, 1);
+	}
+
+	lua_settop(gL, 0);
+
+	return override;
+}
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
new file mode 100644
index 000000000..1b5991e57
--- /dev/null
+++ b/src/lua_inputlib.c
@@ -0,0 +1,214 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2021 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  lua_inputlib.c
+/// \brief input library for Lua scripting
+
+#include "doomdef.h"
+#include "fastcmp.h"
+#include "g_input.h"
+#include "g_game.h"
+#include "hu_stuff.h"
+
+#include "lua_script.h"
+#include "lua_libs.h"
+
+///////////////
+// FUNCTIONS //
+///////////////
+
+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));
+	return 1;
+}
+
+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));
+	return 1;
+}
+
+static int lib_gameControlToKeyNum(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, gamecontrol[i][0]);
+	lua_pushinteger(L, gamecontrol[i][1]);
+	return 2;
+}
+
+static int lib_gameControl2ToKeyNum(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, gamecontrolbis[i][0]);
+	lua_pushinteger(L, gamecontrolbis[i][1]);
+	return 2;
+}
+
+static int lib_joyAxis(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushinteger(L, JoyAxis(i));
+	return 1;
+}
+
+static int lib_joy2Axis(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushinteger(L, Joy2Axis(i));
+	return 1;
+}
+
+static int lib_keyNumToString(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushstring(L, G_KeyNumToString(i));
+	return 1;
+}
+
+static int lib_keyStringToNum(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);
+	lua_pushinteger(L, G_KeyStringToNum(str));
+	return 1;
+}
+
+static int lib_keyNumPrintable(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushboolean(L, i >= 32 && i <= 127);
+	return 1;
+}
+
+static int lib_shiftKeyNum(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	if (i >= 32 && i <= 127)
+		lua_pushinteger(L, shiftxform[i]);
+	return 1;
+}
+
+static luaL_Reg lib[] = {
+	{"G_GameControlDown", lib_gameControlDown},
+	{"G_GameControl2Down", lib_gameControl2Down},
+	{"G_GameControlToKeyNum", lib_gameControlToKeyNum},
+	{"G_GameControl2ToKeyNum", lib_gameControl2ToKeyNum},
+	{"G_JoyAxis", lib_joyAxis},
+	{"G_Joy2Axis", lib_joy2Axis},
+	{"G_KeyNumToString", lib_keyNumToString},
+	{"G_KeyStringToNum", lib_keyStringToNum},
+	{"HU_KeyNumPrintable", lib_keyNumPrintable},
+	{"HU_ShiftKeyNum", lib_shiftKeyNum},
+	{NULL, NULL}
+};
+
+///////////////////
+// gamekeydown[] //
+///////////////////
+
+static int lib_getGameKeyDown(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= NUMINPUTS)
+		return luaL_error(L, "gamekeydown[] index %d out of range (0 - %d)", i, NUMINPUTS-1);
+	lua_pushboolean(L, gamekeydown[i]);
+	return 1;
+}
+
+static int lib_setGameKeyDown(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 2);
+	boolean j = luaL_checkboolean(L, 3);
+	if (i < 0 || i >= NUMINPUTS)
+		return luaL_error(L, "gamekeydown[] index %d out of range (0 - %d)", i, NUMINPUTS-1);
+	gamekeydown[i] = j;
+	return 0;
+}
+
+static int lib_lenGameKeyDown(lua_State *L)
+{
+	lua_pushinteger(L, NUMINPUTS);
+	return 1;
+}
+
+///////////
+// MOUSE //
+///////////
+
+static int mouse_get(lua_State *L)
+{
+	mouse_t *m = *((mouse_t **)luaL_checkudata(L, 1, META_MOUSE));
+	const char *field = luaL_checkstring(L, 2);
+
+	I_Assert(m != NULL);
+
+	if (fastcmp(field,"dx"))
+		lua_pushinteger(L, m->dx);
+	else if (fastcmp(field,"dy"))
+		lua_pushinteger(L, m->dy);
+	else if (fastcmp(field,"mlookdy"))
+		lua_pushinteger(L, m->mlookdy);
+	else if (fastcmp(field,"rdx"))
+		lua_pushinteger(L, m->rdx);
+	else if (fastcmp(field,"rdy"))
+		lua_pushinteger(L, m->rdy);
+	else
+		return luaL_error(L, "mouse_t has no field named %s", field);
+
+	return 1;
+}
+
+// #mouse -> 1 or 2
+static int mouse_num(lua_State *L)
+{
+	mouse_t *m = *((mouse_t **)luaL_checkudata(L, 1, META_MOUSE));
+
+	I_Assert(m != NULL);
+
+	lua_pushinteger(L, m == &mouse ? 1 : 2);
+	return 1;
+}
+
+int LUA_InputLib(lua_State *L)
+{
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_getGameKeyDown);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_setGameKeyDown);
+			lua_setfield(L, -2, "__newindex");
+
+			lua_pushcfunction(L, lib_lenGameKeyDown);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "gamekeydown");
+
+	luaL_newmetatable(L, META_MOUSE);
+		lua_pushcfunction(L, mouse_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, mouse_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	// Set global functions
+	lua_pushvalue(L, LUA_GLOBALSINDEX);
+	luaL_register(L, NULL, lib);
+	return 0;
+}
diff --git a/src/lua_libs.h b/src/lua_libs.h
index fbe8d4878..d56c45ab4 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -88,6 +88,8 @@ extern lua_State *gL;
 
 #define META_LUABANKS "LUABANKS[]*"
 
+#define META_MOUSE "MOUSE_T*"
+
 boolean luaL_checkboolean(lua_State *L, int narg);
 
 int LUA_EnumLib(lua_State *L);
@@ -106,3 +108,4 @@ int LUA_TagLib(lua_State *L);
 int LUA_PolyObjLib(lua_State *L);
 int LUA_BlockmapLib(lua_State *L);
 int LUA_HudLib(lua_State *L);
+int LUA_InputLib(lua_State *L);
diff --git a/src/lua_script.c b/src/lua_script.c
index 7fd5a98e6..45c18178a 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -20,6 +20,7 @@
 #include "r_state.h"
 #include "r_sky.h"
 #include "g_game.h"
+#include "g_input.h"
 #include "f_finale.h"
 #include "byteptr.h"
 #include "p_saveg.h"
@@ -57,6 +58,7 @@ static lua_CFunction liblist[] = {
 	LUA_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
+	LUA_InputLib, // inputs
 	NULL
 };
 
@@ -380,6 +382,12 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word, "gamestate")) {
 		lua_pushinteger(L, gamestate);
 		return 1;
+	} else if (fastcmp(word, "mouse")) {
+		LUA_PushUserdata(L, &mouse, META_MOUSE);
+		return 1;
+	} else if (fastcmp(word, "mouse2")) {
+		LUA_PushUserdata(L, &mouse2, META_MOUSE);
+		return 1;
 	}
 	return 0;
 }
@@ -934,6 +942,7 @@ enum
 	ARCH_SLOPE,
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
+	ARCH_MOUSE,
 
 	ARCH_TEND=0xFF,
 };
@@ -961,6 +970,7 @@ static const struct {
 	{META_SLOPE,    ARCH_SLOPE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
+	{META_MOUSE,    ARCH_MOUSE},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1268,7 +1278,6 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
-
 		case ARCH_SKINCOLOR:
 		{
 			skincolor_t *info = *((skincolor_t **)lua_touserdata(gL, myindex));
@@ -1276,6 +1285,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			WRITEUINT16(save_p, info - skincolors);
 			break;
 		}
+		case ARCH_MOUSE:
+		{
+			mouse_t *m = *((mouse_t **)lua_touserdata(gL, myindex));
+			WRITEUINT8(save_p, ARCH_MOUSE);
+			WRITEUINT8(save_p, m == &mouse ? 1 : 2);
+			break;
+		}
 		default:
 			WRITEUINT8(save_p, ARCH_NULL);
 			return 2;
@@ -1527,6 +1543,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_SKINCOLOR:
 		LUA_PushUserdata(gL, &skincolors[READUINT16(save_p)], META_SKINCOLOR);
 		break;
+	case ARCH_MOUSE:
+		LUA_PushUserdata(gL, READUINT16(save_p) == 1 ? &mouse : &mouse2, META_MOUSE);
+		break;
 	case ARCH_TEND:
 		return 1;
 	}
diff --git a/src/m_menu.c b/src/m_menu.c
index 0fca39801..fffe08cd2 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -12683,13 +12683,13 @@ static void M_DrawControl(void)
 			else
 			{
 				if (keys[0] != KEY_NULL)
-					strcat (tmp, G_KeynumToString (keys[0]));
+					strcat (tmp, G_KeyNumToString (keys[0]));
 
 				if (keys[0] != KEY_NULL && keys[1] != KEY_NULL)
 					strcat(tmp," or ");
 
 				if (keys[1] != KEY_NULL)
-					strcat (tmp, G_KeynumToString (keys[1]));
+					strcat (tmp, G_KeyNumToString (keys[1]));
 
 
 			}
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index d46a4af2b..707b5c2d8 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -406,6 +406,7 @@
     <ClCompile Include="..\lua_hooklib.c" />
     <ClCompile Include="..\lua_hudlib.c" />
     <ClCompile Include="..\lua_infolib.c" />
+    <ClCompile Include="..\lua_inputlib.c" />
     <ClCompile Include="..\lua_maplib.c" />
     <ClCompile Include="..\lua_mathlib.c" />
     <ClCompile Include="..\lua_mobjlib.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index adae2f446..dbc32c502 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -720,6 +720,9 @@
     <ClCompile Include="..\lua_infolib.c">
       <Filter>LUA</Filter>
     </ClCompile>
+    <ClCompile Include="..\lua_inputlib.c">
+      <Filter>LUA</Filter>
+    </ClCompile>
     <ClCompile Include="..\lua_maplib.c">
       <Filter>LUA</Filter>
     </ClCompile>