diff --git a/README.md b/README.md
index 8a5ca1a1ff0193df4aeeb9d59f5b38ae65869458..49a3cc36d167169467a2d65bec7527610691694d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
 # Sonic Robo Blast 2
+[![latest release](https://badgen.net/github/release/STJr/SRB2/stable)](https://github.com/STJr/SRB2/releases/latest)
 
 [![Build status](https://ci.appveyor.com/api/projects/status/399d4hcw9yy7hg2y?svg=true)](https://ci.appveyor.com/project/STJr/srb2)
 [![Build status](https://travis-ci.org/STJr/SRB2.svg?branch=master)](https://travis-ci.org/STJr/SRB2)
diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index 5a464eb5dc008cfa9c8ddfa490df5f196914f30c..f457fe9721000f0efc7b690b13250387a42cb355 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -3589,6 +3589,7 @@ thingtypes
 			sprite = "ARCHA1";
 			width = 24;
 			height = 32;
+			flags8text = "[8] Don't jump away";
 		}
 		118
 		{
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 33898c9a170c87a24fa468f282056f81d4b44e7c..e639ae14d35af628e519d6c526dbb3a9ad181baa 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -284,6 +284,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 3a2962e659e24cbb34f9690afe52348393a31ed3..4536628ba576470a630c1f2ab24896d4b9e08c06 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 8c3703dd80bcffdf5b282b636f4794da9d97396e..b3c41384051bf3cd8a01cbd07b5a873f503551b8 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"));
@@ -1697,7 +1697,10 @@ static void CON_DrawHudlines(void)
 			{
 				charflags = (*p & 0x7f) << V_CHARCOLORSHIFT;
 				p++;
+				c++;
 			}
+			if (c >= con_width)
+				break;
 			if (*p < HU_FONTSTART)
 				;//charwidth = 4 * con_scalefactor;
 			else
@@ -1818,7 +1821,10 @@ static void CON_DrawConsole(void)
 			{
 				charflags = (*p & 0x7f) << V_CHARCOLORSHIFT;
 				p++;
+				c++;
 			}
+			if (c >= con_width)
+				break;
 			V_DrawCharacter(x, y, (INT32)(*p) | charflags | cv_constextsize.value | V_NOSCALESTART, true);
 		}
 	}
diff --git a/src/d_main.c b/src/d_main.c
index 900390864c645288369a611a24ef9d6de7770e4a..1b3449ec1cb531238feabfec25cd189c1c278c69 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -175,10 +175,53 @@ void D_ProcessEvents(void)
 
 	boolean eaten;
 
+	// Reset possibly stale mouse info
+	G_SetMouseDeltas(0, 0, 1);
+	G_SetMouseDeltas(0, 0, 2);
+	mouse.buttons &= ~(MB_SCROLLUP|MB_SCROLLDOWN);
+	mouse2.buttons &= ~(MB_SCROLLUP|MB_SCROLLDOWN);
+
 	for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
 	{
+		boolean hooked = false;
+
 		ev = &events[eventtail];
 
+		// Set mouse buttons early in case event is eaten later
+		if (ev->type == ev_keydown || ev->type == ev_keyup)
+		{
+			// Mouse buttons
+			if ((UINT32)(ev->data1 - KEY_MOUSE1) < MOUSEBUTTONS)
+			{
+				if (ev->type == ev_keydown)
+					mouse.buttons |= 1 << (ev->data1 - KEY_MOUSE1);
+				else
+					mouse.buttons &= ~(1 << (ev->data1 - KEY_MOUSE1));
+			}
+			else if ((UINT32)(ev->data1 - KEY_2MOUSE1) < MOUSEBUTTONS)
+			{
+				if (ev->type == ev_keydown)
+					mouse2.buttons |= 1 << (ev->data1 - KEY_2MOUSE1);
+				else
+					mouse2.buttons &= ~(1 << (ev->data1 - KEY_2MOUSE1));
+			}
+			// Scroll (has no keyup event)
+			else switch (ev->data1) {
+				case KEY_MOUSEWHEELUP:
+					mouse.buttons |= MB_SCROLLUP;
+					break;
+				case KEY_MOUSEWHEELDOWN:
+					mouse.buttons |= MB_SCROLLDOWN;
+					break;
+				case KEY_2MOUSEWHEELUP:
+					mouse2.buttons |= MB_SCROLLUP;
+					break;
+				case KEY_2MOUSEWHEELDOWN:
+					mouse2.buttons |= MB_SCROLLDOWN;
+					break;
+			}
+		}
+
 		// Screenshots over everything so that they can be taken anywhere.
 		if (M_ScreenshotResponder(ev))
 			continue; // ate the event
@@ -189,6 +232,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);
@@ -203,6 +252,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);
@@ -217,8 +272,16 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // ate the event
 
+		if (!hooked && G_LuaResponder(ev))
+			continue;
+
 		G_Responder(ev);
 	}
+
+	if (mouse.rdx || mouse.rdy)
+		G_SetMouseDeltas(mouse.rdx, mouse.rdy, 1);
+	if (mouse2.rdx || mouse2.rdy)
+		G_SetMouseDeltas(mouse2.rdx, mouse2.rdy, 2);
 }
 
 //
diff --git a/src/d_ticcmd.h b/src/d_ticcmd.h
index 374585edf7ba4b1568cc785bbd93883a7683cce4..182b30e6aef84b9e4148157594918be325c75095 100644
--- a/src/d_ticcmd.h
+++ b/src/d_ticcmd.h
@@ -21,6 +21,8 @@
 #pragma interface
 #endif
 
+#define MAXPREDICTTICS 12
+
 // Button/action code definitions.
 typedef enum
 {
@@ -63,6 +65,7 @@ typedef struct
 	INT16 angleturn; // <<16 for angle delta - saved as 1 byte into demos
 	INT16 aiming; // vertical aiming, see G_BuildTicCmd
 	UINT16 buttons;
+	UINT8 latency; // Netgames: how many tics ago was this ticcmd generated from this player's end?
 } ATTRPACK ticcmd_t;
 
 #if defined(_MSC_VER)
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 5a6ebb4299ac138e77989499a54001c293878c21..63e273a6c1f4bc4d605978a577db79550f8340ac 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -22,6 +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_input.h" // Game controls (for lua)
 
 #include "deh_tables.h"
 
@@ -5461,6 +5464,76 @@ 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},
+	{"JOYAXISRANGE",JOYAXISRANGE},
+
+	// 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},
+
+	// Mouse buttons
+	{"MB_BUTTON1",MB_BUTTON1},
+	{"MB_BUTTON2",MB_BUTTON2},
+	{"MB_BUTTON3",MB_BUTTON3},
+	{"MB_BUTTON4",MB_BUTTON4},
+	{"MB_BUTTON5",MB_BUTTON5},
+	{"MB_BUTTON6",MB_BUTTON6},
+	{"MB_BUTTON7",MB_BUTTON7},
+	{"MB_BUTTON8",MB_BUTTON8},
+	{"MB_SCROLLUP",MB_SCROLLUP},
+	{"MB_SCROLLDOWN",MB_SCROLLDOWN},
+
 	{NULL,0}
 };
 
diff --git a/src/f_finale.c b/src/f_finale.c
index bfba7cf9614a23e9313b86df49d35f4ab7219216..e8757c18adcaad692faac6d83542eccd00650a10 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -1063,7 +1063,7 @@ boolean F_IntroResponder(event_t *event)
 //  CREDITS
 // =========
 static const char *credits[] = {
-	"\1Sonic Robo Blast II",
+	"\1Sonic Robo Blast 2",
 	"\1Credits",
 	"",
 	"\1Game Design",
diff --git a/src/g_demo.c b/src/g_demo.c
index ea71c9bd4c6cf621b8a256f9574c4919986833ff..7793e0272178b049eab2fd57cfd8e083cdd8dd78 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -109,6 +109,7 @@ demoghost *ghosts = NULL;
 #define ZT_ANGLE   0x04
 #define ZT_BUTTONS 0x08
 #define ZT_AIMING  0x10
+#define ZT_LATENCY 0x20
 #define DEMOMARKER 0x80 // demoend
 #define METALDEATH 0x44
 #define METALSNICE 0x69
@@ -181,6 +182,8 @@ void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
 		oldcmd.buttons = (oldcmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) | (READUINT16(demo_p) & ~(BT_CAMLEFT|BT_CAMRIGHT));
 	if (ziptic & ZT_AIMING)
 		oldcmd.aiming = READINT16(demo_p);
+	if (ziptic & ZT_LATENCY)
+		oldcmd.latency = READUINT8(demo_p);
 
 	G_CopyTiccmd(cmd, &oldcmd, 1);
 	players[playernum].angleturn = cmd->angleturn;
@@ -238,6 +241,13 @@ void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
 		ziptic |= ZT_AIMING;
 	}
 
+	if (cmd->latency != oldcmd.latency)
+	{
+		WRITEUINT8(demo_p,cmd->latency);
+		oldcmd.latency = cmd->latency;
+		ziptic |= ZT_LATENCY;
+	}
+
 	*ziptic_p = ziptic;
 
 	// attention here for the ticcmd size!
@@ -679,6 +689,8 @@ void G_GhostTicker(void)
 			g->p += 2;
 		if (ziptic & ZT_AIMING)
 			g->p += 2;
+		if (ziptic & ZT_LATENCY)
+			g->p++;
 
 		// Grab ghost data.
 		ziptic = READUINT8(g->p);
diff --git a/src/g_game.c b/src/g_game.c
index 7dbbfc4b5290c954ccfc0d3595b9ac257d337461..83531bb356703d3ef445ebc12a89f1c135615cdf 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)
@@ -1094,7 +1078,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	angle_t drawangleoffset = (player->powers[pw_carry] == CR_ROLLOUT) ? ANGLE_180 : 0;
 	INT32 chasecam, chasefreelook, alwaysfreelook, usejoystick, invertmouse, turnmultiplier, mousemove;
 	controlstyle_e controlstyle = G_ControlStyle(ssplayer);
-	INT32 *mx; INT32 *my; INT32 *mly;
+	INT32 mdx, mdy, mldy;
 
 	static INT32 turnheld[2]; // for accelerative turning
 	static boolean keyboard_look[2]; // true if lookup/down using keyboard
@@ -1117,9 +1101,9 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		invertmouse = cv_invertmouse.value;
 		turnmultiplier = cv_cam_turnmultiplier.value;
 		mousemove = cv_mousemove.value;
-		mx = &mousex;
-		my = &mousey;
-		mly = &mlooky;
+		mdx = mouse.dx;
+		mdy = -mouse.dy;
+		mldy = -mouse.mlookdy;
 		G_CopyTiccmd(cmd, I_BaseTiccmd(), 1); // empty, or external driver
 	}
 	else
@@ -1131,12 +1115,15 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		invertmouse = cv_invertmouse2.value;
 		turnmultiplier = cv_cam2_turnmultiplier.value;
 		mousemove = cv_mousemove2.value;
-		mx = &mouse2x;
-		my = &mouse2y;
-		mly = &mlook2y;
+		mdx = mouse2.dx;
+		mdy = -mouse2.dy;
+		mldy = -mouse2.mlookdy;
 		G_CopyTiccmd(cmd, I_BaseTiccmd2(), 1); // empty, or external driver
 	}
 
+	if (menuactive || CON_Ready() || chat_on)
+		mdx = mdy = mldy = 0;
+
 	strafeisturn = controlstyle == CS_SIMPLE && ticcmd_centerviewdown[forplayer] &&
 		((cv_cam_lockedinput[forplayer].value && !ticcmd_ztargetfocus[forplayer]) || (player->pflags & PF_STARTDASH)) &&
 		!player->climbing && player->powers[pw_carry] != CR_MINECART;
@@ -1179,10 +1166,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);
@@ -1261,8 +1248,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);
@@ -1318,12 +1305,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;
 
@@ -1339,7 +1326,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;
 
@@ -1457,7 +1444,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;
 
@@ -1476,7 +1463,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			keyboard_look[forplayer] = false;
 
 			// looking up/down
-			*myaiming += (*mly<<19)*player_invert*screen_invert;
+			*myaiming += (mldy<<19)*player_invert*screen_invert;
 		}
 
 		if (analogjoystickmove && joyaiming[forplayer] && lookjoystickvector.yaxis != 0 && configlookaxis != 0)
@@ -1510,24 +1497,22 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	}
 
 	if (!mouseaiming && mousemove)
-		forward += *my;
+		forward += mdy;
 
 	if ((!demoplayback && (player->pflags & PF_SLIDING))) // Analog for mouse
-		side += *mx*2;
+		side += mdx*2;
 	else if (controlstyle == CS_LMAOGALOG)
 	{
-		if (*mx)
+		if (mdx)
 		{
-			if (*mx > 0)
+			if (mdx > 0)
 				cmd->buttons |= BT_CAMRIGHT;
 			else
 				cmd->buttons |= BT_CAMLEFT;
 		}
 	}
 	else
-		cmd->angleturn = (INT16)(cmd->angleturn - (*mx*8));
-
-	*mx = *my = *mly = 0;
+		cmd->angleturn = (INT16)(cmd->angleturn - (mdx*8));
 
 	if (forward > MAXPLMOVE)
 		forward = MAXPLMOVE;
@@ -1684,6 +1669,10 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		cmd->angleturn = origangle + extra;
 		*myangle += extra << 16;
 		*myaiming += (cmd->aiming - origaiming) << 16;
+
+		// Send leveltime when this tic was generated to the server for control lag calculations.
+		// Only do this when in a level. Also do this after the hook, so that it can't overwrite this.
+		cmd->latency = (leveltime & 0xFF); 
 	}
 
 	//Reset away view if a command is given.
@@ -1715,6 +1704,7 @@ ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
 		dest[i].angleturn = SHORT(src[i].angleturn);
 		dest[i].aiming = (INT16)SHORT(src[i].aiming);
 		dest[i].buttons = (UINT16)SHORT(src[i].buttons);
+		dest[i].latency = src[i].latency;
 	}
 	return dest;
 }
@@ -1864,8 +1854,8 @@ void G_DoLoadLevel(boolean resetplayer)
 		joyxmove[i] = joyymove[i] = 0;
 		joy2xmove[i] = joy2ymove[i] = 0;
 	}
-	mousex = mousey = 0;
-	mouse2x = mouse2y = 0;
+	G_SetMouseDeltas(0, 0, 1);
+	G_SetMouseDeltas(0, 0, 2);
 
 	// clear hud messages remains (usually from game startup)
 	CON_ClearHUD();
@@ -2189,6 +2179,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.
@@ -2281,16 +2281,16 @@ void G_Ticker(boolean run)
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		if (playeringame[i])
-		{ //!!!
+		{
 			INT16 received;
-			//Save last frame's button readings
+			// Save last frame's button readings
 			players[i].lastbuttons = players[i].cmd.buttons;
 
 			G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
-			//Bot ticcmd handling
-			//Yes, ordinarily this would be handled in G_BuildTiccmd...
-			//...however, bot players won't have a corresponding consoleplayer or splitscreen player 2 to send that information.
-			//Therefore, this has to be done after ticcmd sends are received.
+			// Bot ticcmd handling
+			// Yes, ordinarily this would be handled in G_BuildTiccmd...
+			// ...however, bot players won't have a corresponding consoleplayer or splitscreen player 2 to send that information.
+			// Therefore, this has to be done after ticcmd sends are received.
 			if (players[i].bot == BOT_2PAI) { // Tailsbot for P2
 				if (!players[i].powers[pw_tailsfly] && (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons))
 				{
@@ -2317,15 +2317,27 @@ void G_Ticker(boolean run)
 					P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
 				else
 					players[i].cmd.angleturn = players[i].angleturn;
-
-				players[i].cmd.angleturn &= ~TICCMD_RECEIVED;
-				players[i].cmd.angleturn |= received;
+    			players[i].angleturn += players[i].cmd.angleturn - players[i].oldrelangleturn;
+    			players[i].oldrelangleturn = players[i].cmd.angleturn;
+    			if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
+    				P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
+    			else
+    				players[i].cmd.angleturn = players[i].angleturn;
+    
+    			players[i].cmd.angleturn &= ~TICCMD_RECEIVED;
+    		
+    			// Use the leveltime sent in the player's ticcmd to determine control lag
+    			players[i].cmd.latency = min(((leveltime & 0xFF) - players[i].cmd.latency) & 0xFF, MAXPREDICTTICS-1);
 			}
 			else // Less work is required if we're building a bot ticcmd.
 			{
+    			// Since bot TicCmd is pre-determined for both the client and server, the latency and packet checks are simplified.
+    			received = 1;
+    			players[i].cmd.latency = 0;
 				players[i].angleturn = players[i].cmd.angleturn;
-				players[i].oldrelangleturn = players[i].cmd.angleturn;				
+				players[i].oldrelangleturn = players[i].cmd.angleturn;
 			}
+			players[i].cmd.angleturn |= received;
 		}
 	}
 
@@ -3140,8 +3152,8 @@ void G_DoReborn(INT32 playernum)
 				joyxmove[i] = joyymove[i] = 0;
 				joy2xmove[i] = joy2ymove[i] = 0;
 			}
-			mousex = mousey = 0;
-			mouse2x = mouse2y = 0;
+			G_SetMouseDeltas(0, 0, 1);
+			G_SetMouseDeltas(0, 0, 2);
 
 			// clear hud messages remains (usually from game startup)
 			CON_ClearHUD();
diff --git a/src/g_game.h b/src/g_game.h
index 98336ad44d66fc324edd090246d5a3e0cf3acf32..f98269fcec2280ceac908c310236c88bb01ee73f 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 henceforth use digital deadzone
+
+	JA_JUMP = JA_DIGITAL,
+	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 357081e1dd5189f86d23a418778106325c85aa37..2f7980c647d1eba7b794274200e9f9a11477c8b3 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -31,10 +31,8 @@ consvar_t cv_mouseysens = CVAR_INIT ("mouseysens", "20", CV_SAVE, mousesens_cons
 consvar_t cv_mouseysens2 = CVAR_INIT ("mouseysens2", "20", CV_SAVE, mousesens_cons_t, NULL);
 consvar_t cv_controlperkey = CVAR_INIT ("controlperkey", "One", CV_SAVE, onecontrolperkey_cons_t, NULL);
 
-INT32 mousex, mousey;
-INT32 mlooky; // like mousey but with a custom sensitivity for mlook
-
-INT32 mouse2x, mouse2y, mlook2y;
+mouse_t mouse;
+mouse_t mouse2;
 
 // joystick values are repeated
 INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
@@ -140,11 +138,8 @@ void G_MapEventsToControls(event_t *ev)
 			break;
 
 		case ev_mouse: // buttons are virtual keys
-			if (menuactive || CON_Ready() || chat_on)
-				break;
-			mousex = (INT32)(ev->data2*((cv_mousesens.value*cv_mousesens.value)/110.0f + 0.1f));
-			mousey = (INT32)(ev->data3*((cv_mousesens.value*cv_mousesens.value)/110.0f + 0.1f));
-			mlooky = (INT32)(ev->data3*((cv_mouseysens.value*cv_mousesens.value)/110.0f + 0.1f));
+			mouse.rdx = ev->data2;
+			mouse.rdy = ev->data3;
 			break;
 
 		case ev_joystick: // buttons are virtual keys
@@ -166,9 +161,8 @@ void G_MapEventsToControls(event_t *ev)
 		case ev_mouse2: // buttons are virtual keys
 			if (menuactive || CON_Ready() || chat_on)
 				break;
-			mouse2x = (INT32)(ev->data2*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
-			mouse2y = (INT32)(ev->data3*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
-			mlook2y = (INT32)(ev->data3*((cv_mouseysens2.value*cv_mousesens2.value)/110.0f + 0.1f));
+			mouse2.rdx = ev->data2;
+			mouse2.rdy = ev->data3;
 			break;
 
 		default:
@@ -630,7 +624,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];
 
@@ -654,7 +648,7 @@ const char *G_KeynumToString(INT32 keynum)
 	return keynamestr;
 }
 
-INT32 G_KeyStringtoNum(const char *keystr)
+INT32 G_KeyStringToNum(const char *keystr)
 {
 	UINT32 j;
 
@@ -817,10 +811,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");
 	}
@@ -828,10 +822,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");
 	}
@@ -1007,8 +1001,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)
@@ -1073,3 +1067,17 @@ void Command_Setcontrol2_f(void)
 
 	setcontrol(gamecontrolbis);
 }
+
+void G_SetMouseDeltas(INT32 dx, INT32 dy, UINT8 ssplayer)
+{
+	mouse_t *m = ssplayer == 1 ? &mouse : &mouse2;
+	consvar_t *cvsens, *cvysens;
+
+	cvsens = ssplayer == 1 ? &cv_mousesens : &cv_mousesens2;
+	cvysens = ssplayer == 1 ? &cv_mouseysens : &cv_mouseysens2;
+	m->rdx = dx;
+	m->rdy = dy;
+	m->dx = (INT32)(m->rdx*((cvsens->value*cvsens->value)/110.0f + 0.1f));
+	m->dy = (INT32)(m->rdy*((cvsens->value*cvsens->value)/110.0f + 0.1f));
+	m->mlookdy = (INT32)(m->rdy*((cvysens->value*cvsens->value)/110.0f + 0.1f));
+}
diff --git a/src/g_input.h b/src/g_input.h
index 609141825bc6df6699736130fd0941d959d8b929..ffd0cb560b5f0921fb3595c970f93b7e5d1ff5f0 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -116,9 +116,29 @@ extern consvar_t cv_mousesens, cv_mouseysens;
 extern consvar_t cv_mousesens2, cv_mouseysens2;
 extern consvar_t cv_controlperkey;
 
-extern INT32 mousex, mousey;
-extern INT32 mlooky; //mousey with mlookSensitivity
-extern INT32 mouse2x, mouse2y, mlook2y;
+typedef struct
+{
+	INT32 dx; // deltas with mousemove sensitivity
+	INT32 dy;
+	INT32 mlookdy; // dy with mouselook sensitivity
+	INT32 rdx; // deltas without sensitivity
+	INT32 rdy;
+	UINT16 buttons;
+} mouse_t;
+
+#define MB_BUTTON1    0x0001
+#define MB_BUTTON2    0x0002
+#define MB_BUTTON3    0x0004
+#define MB_BUTTON4    0x0008
+#define MB_BUTTON5    0x0010
+#define MB_BUTTON6    0x0020
+#define MB_BUTTON7    0x0040
+#define MB_BUTTON8    0x0080
+#define MB_SCROLLUP   0x0100
+#define MB_SCROLLDOWN 0x0200
+
+extern mouse_t mouse;
+extern mouse_t mouse2;
 
 extern INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
 
@@ -161,8 +181,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);
@@ -175,4 +195,7 @@ void G_CopyControls(INT32 (*setupcontrols)[2], INT32 (*fromcontrols)[2], const I
 void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis)[2]);
 INT32 G_CheckDoubleUsage(INT32 keynum, boolean modify);
 
+// sets the members of a mouse_t given position deltas
+void G_SetMouseDeltas(INT32 dx, INT32 dy, UINT8 ssplayer);
+
 #endif
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 1647e23f8cf39ffb32a1f7c413164ed01fe3c113..d413e3bbebff7d487f9756103631baf64d180822 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -703,13 +703,12 @@ static void HWR_RenderSkyPlane(extrasubsector_t *xsub, fixed_t fixedheight)
 
 #endif //doplanes
 
-FBITFIELD HWR_GetBlendModeFlag(INT32 ast)
+FBITFIELD HWR_GetBlendModeFlag(INT32 style)
 {
-	switch (ast)
+	switch (style)
 	{
-		case AST_COPY:
-		case AST_OVERLAY:
-			return PF_Masked;
+		case AST_TRANSLUCENT:
+			return PF_Translucent;
 		case AST_ADD:
 			return PF_Additive;
 		case AST_SUBTRACT:
@@ -719,10 +718,8 @@ FBITFIELD HWR_GetBlendModeFlag(INT32 ast)
 		case AST_MODULATE:
 			return PF_Multiplicative;
 		default:
-			return PF_Translucent;
+			return PF_Masked;
 	}
-
-	return 0;
 }
 
 UINT8 HWR_GetTranstableAlpha(INT32 transtablenum)
@@ -748,7 +745,7 @@ UINT8 HWR_GetTranstableAlpha(INT32 transtablenum)
 
 FBITFIELD HWR_SurfaceBlend(INT32 style, INT32 transtablenum, FSurfaceInfo *pSurf)
 {
-	if (!transtablenum || style == AST_COPY || style == AST_OVERLAY)
+	if (!transtablenum || style <= AST_COPY || style >= AST_OVERLAY)
 	{
 		pSurf->PolyColor.s.alpha = 0xff;
 		return PF_Masked;
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index ba6532c386ff9cf1cbed29d8ba491f221c8351ef..218650b53283a6d285f5a8e79d4fe04a8463cca7 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -69,7 +69,7 @@ void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *col
 UINT8 HWR_FogBlockAlpha(INT32 light, extracolormap_t *colormap); // Let's see if this can work
 
 UINT8 HWR_GetTranstableAlpha(INT32 transtablenum);
-FBITFIELD HWR_GetBlendModeFlag(INT32 ast);
+FBITFIELD HWR_GetBlendModeFlag(INT32 style);
 FBITFIELD HWR_SurfaceBlend(INT32 style, INT32 transtablenum, FSurfaceInfo *pSurf);
 FBITFIELD HWR_TranstableToAlpha(INT32 transtablenum, FSurfaceInfo *pSurf);
 
diff --git a/src/i_system.h b/src/i_system.h
index b88ea31772fa1b7f4e63d081faadc8512f1575b7..e046fd620114161ddf49e7bba50685547cd7d32a 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -314,4 +314,16 @@ const char *I_ClipboardPaste(void);
 
 void I_RegisterSysCommands(void);
 
+/** \brief Return the position of the cursor relative to the top-left window corner.
+*/
+void I_GetCursorPosition(INT32 *x, INT32 *y);
+
+/** \brief Returns whether the mouse is grabbed
+*/
+boolean I_GetMouseGrab(void);
+
+/** \brief Sets whether the mouse is grabbed
+*/
+void I_SetMouseGrab(boolean grab);
+
 #endif
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 926c12d0c1bc83d4c37c41ef6eab855f14a77caa..9946a9fd37ebc910cf58cd6684c4ada5913e6468 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -215,6 +215,8 @@ static const struct {
 	{META_ACTION,       "action"},
 
 	{META_LUABANKS,     "luabanks[]"},
+
+	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
 
@@ -446,7 +448,7 @@ static int lib_pAproxDistance(lua_State *L)
 	fixed_t dx = luaL_checkfixed(L, 1);
 	fixed_t dy = luaL_checkfixed(L, 2);
 	//HUDSAFE
-	lua_pushfixed(L, R_PointToDist2(0, 0, dx, dy));
+	lua_pushfixed(L, P_AproxDistance(dx, dy));
 	return 1;
 }
 
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 3a5ba3afd4af2be4a79eeed06a354a0b5c10b47a..22f81074dee0372dbc3e76c703eee314b6edb4a5 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 5dca0d7a0435f85743a9c1f1c2a2036168abb810..08226311eddf07d595ecf6ea75fae1c738f0ee9f 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
 };
 
@@ -1945,7 +1947,7 @@ boolean LUAh_MusicChange(const char *oldname, char *newname, UINT16 *mflags, boo
 			lua_pushinteger(gL, *prefadems);
 			lua_pushinteger(gL, *fadeinms);
 			if (lua_pcall(gL, 7, 6, 1)) {
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL,-1));
+				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
 				lua_pop(gL, 1);
 				continue;
 			}
@@ -2064,3 +2066,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 0000000000000000000000000000000000000000..217202222587394c94a37927e67868d1de218c95
--- /dev/null
+++ b/src/lua_inputlib.c
@@ -0,0 +1,242 @@
+// 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 "i_system.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 int lib_getMouseGrab(lua_State *L)
+{
+	lua_pushboolean(L, I_GetMouseGrab());
+	return 1;
+}
+
+static int lib_setMouseGrab(lua_State *L)
+{
+	boolean grab = luaL_checkboolean(L, 1);
+	I_SetMouseGrab(grab);
+	return 0;
+}
+
+static boolean lib_getCursorPosition(lua_State *L)
+{
+	int x, y;
+	I_GetCursorPosition(&x, &y);
+	lua_pushinteger(L, x);
+	lua_pushinteger(L, y);
+	return 2;
+}
+
+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},
+	{"I_GetMouseGrab", lib_getMouseGrab},
+	{"I_SetMouseGrab", lib_setMouseGrab},
+	{"I_GetCursorPosition", lib_getCursorPosition},
+	{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 if (fastcmp(field,"buttons"))
+		lua_pushinteger(L, m->buttons);
+	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 05061f118f29f6cdee74efb1db63bb2def90b44f..668eb99b001f4cb0607351969028392ee2e9cded 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_mobjlib.c b/src/lua_mobjlib.c
index 5d5f0e85bf9bc9b3e81f72eb30ad2e5dea1a62e7..cf8ccab2cec113df3e7038c5657a0be395b1f7ea 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -12,6 +12,7 @@
 
 #include "doomdef.h"
 #include "fastcmp.h"
+#include "r_data.h"
 #include "r_skins.h"
 #include "p_local.h"
 #include "g_game.h"
@@ -654,8 +655,13 @@ static int mobj_set(lua_State *L)
 		break;
 	}
 	case mobj_blendmode:
-		mo->blendmode = (INT32)luaL_checkinteger(L, 3);
+	{
+		INT32 blendmode = (INT32)luaL_checkinteger(L, 3);
+		if (blendmode < 0 || blendmode > AST_OVERLAY)
+			return luaL_error(L, "mobj.blendmode %d out of range (0 - %d).", blendmode, AST_OVERLAY);
+		mo->blendmode = blendmode;
 		break;
+	}
 	case mobj_bnext:
 		return NOSETPOS;
 	case mobj_bprev:
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index ec5a622a9507a2696fe16b18139243b657a04db8..1c634da45e233f5b55afdfe3320aedf508fc5cee 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -812,6 +812,7 @@ static int power_len(lua_State *L)
 }
 
 #define NOFIELD luaL_error(L, LUA_QL("ticcmd_t") " has no field named " LUA_QS, field)
+#define NOSET luaL_error(L, LUA_QL("ticcmd_t") " field " LUA_QS " should not be set directly.", field)
 
 static int ticcmd_get(lua_State *L)
 {
@@ -830,6 +831,8 @@ static int ticcmd_get(lua_State *L)
 		lua_pushinteger(L, cmd->aiming);
 	else if (fastcmp(field,"buttons"))
 		lua_pushinteger(L, cmd->buttons);
+	else if (fastcmp(field,"latency"))
+		lua_pushinteger(L, cmd->latency);
 	else
 		return NOFIELD;
 
@@ -856,6 +859,8 @@ static int ticcmd_set(lua_State *L)
 		cmd->aiming = (INT16)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"buttons"))
 		cmd->buttons = (UINT16)luaL_checkinteger(L, 3);
+	else if (fastcmp(field,"latency"))
+		return NOSET;
 	else
 		return NOFIELD;
 
@@ -863,6 +868,7 @@ static int ticcmd_set(lua_State *L)
 }
 
 #undef NOFIELD
+#undef NOSET
 
 int LUA_PlayerLib(lua_State *L)
 {
diff --git a/src/lua_script.c b/src/lua_script.c
index 6b86138129e326e2facf6c59ee63c3c8269ebc08..3074b159bf9bf8787aa59913952a1700792fc33a 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
 };
 
@@ -184,6 +186,9 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"modeattacking")) {
 		lua_pushboolean(L, modeattacking);
 		return 1;
+	} else if (fastcmp(word,"metalrecording")) {
+		lua_pushboolean(L, metalrecording);
+		return 1;
 	} else if (fastcmp(word,"splitscreen")) {
 		lua_pushboolean(L, splitscreen);
 		return 1;
@@ -382,6 +387,11 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		return 1;
 	} else if (fastcmp(word, "stagefailed")) {
 		lua_pushboolean(L, stagefailed);
+	} 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;
@@ -918,6 +928,7 @@ enum
 	ARCH_SLOPE,
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
+	ARCH_MOUSE,
 
 	ARCH_TEND=0xFF,
 };
@@ -945,6 +956,7 @@ static const struct {
 	{META_SLOPE,    ARCH_SLOPE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
+	{META_MOUSE,    ARCH_MOUSE},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1252,7 +1264,6 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
-
 		case ARCH_SKINCOLOR:
 		{
 			skincolor_t *info = *((skincolor_t **)lua_touserdata(gL, myindex));
@@ -1260,6 +1271,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;
@@ -1511,6 +1529,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 b194797978073870e5eb16151a96074c8f36ee70..525e9cbdd8768fc3849103d3332b7854ac2b237c 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -3307,7 +3307,7 @@ boolean M_Responder(event_t *ev)
 		}
 		else if (ev->type == ev_mouse && mousewait < I_GetTime())
 		{
-			pmousey += ev->data3;
+			pmousey -= ev->data3;
 			if (pmousey < lasty-30)
 			{
 				ch = KEY_DOWNARROW;
@@ -12826,13 +12826,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/p_enemy.c b/src/p_enemy.c
index d40e5e5a8bbab2483a54ec77decb9ef51e11f8a3..69ddbcee66495f263f04cb7abc8ef00b43f87fae 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -1709,7 +1709,7 @@ void A_HoodThink(mobj_t *actor)
 	dx = (actor->target->x - actor->x), dy = (actor->target->y - actor->y), dz = (actor->target->z - actor->z);
 	dm = P_AproxDistance(dx, dy);
 	// Target dangerously close to robohood, retreat then.
-	if ((dm < 256<<FRACBITS) && (abs(dz) < 128<<FRACBITS))
+	if ((dm < 256<<FRACBITS) && (abs(dz) < 128<<FRACBITS) && !(actor->flags2 & MF2_AMBUSH))
 	{
 		S_StartSound(actor, actor->info->attacksound);
 		P_SetMobjState(actor, actor->info->raisestate);
diff --git a/src/p_setup.c b/src/p_setup.c
index 51d2f474de59f6d47457dfb4b8db198ad3ea4697..db44e4be0ec8d92ffd3b74064f59b392f17916af 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -2995,7 +2995,7 @@ static void P_AddBinaryMapTags(void)
 			}
 
 			for (j = 0; j < numsectors; j++) {
-				boolean matches_target_tag = Tag_Find(&sectors[j].tags, target_tag);
+				boolean matches_target_tag = target_tag && Tag_Find(&sectors[j].tags, target_tag);
 				size_t k; for (k = 0; k < 4; k++) {
 					if (lines[i].flags & ML_EFFECT5) {
 						if (matches_target_tag || (offset_tags[k] && Tag_Find(&sectors[j].tags, offset_tags[k]))) {
diff --git a/src/p_sight.c b/src/p_sight.c
index e4a37a718cad79397611cb36aace74ce5b06f442..706745f35b45aa4f1439d8e415cc2171d32083a1 100644
--- a/src/p_sight.c
+++ b/src/p_sight.c
@@ -307,7 +307,7 @@ static boolean P_CrossSubsector(size_t num, register los_t *los)
 			for (rover = front->ffloors; rover; rover = rover->next)
 			{
 				if (!(rover->flags & FF_EXISTS)
-					|| !(rover->flags & FF_RENDERSIDES) || rover->flags & FF_TRANSLUCENT)
+					|| !(rover->flags & FF_RENDERSIDES) || (rover->flags & (FF_TRANSLUCENT|FF_FOG)))
 				{
 					continue;
 				}
@@ -323,7 +323,7 @@ static boolean P_CrossSubsector(size_t num, register los_t *los)
 			for (rover = back->ffloors; rover; rover = rover->next)
 			{
 				if (!(rover->flags & FF_EXISTS)
-					|| !(rover->flags & FF_RENDERSIDES) || rover->flags & FF_TRANSLUCENT)
+					|| !(rover->flags & FF_RENDERSIDES) || (rover->flags & (FF_TRANSLUCENT|FF_FOG)))
 				{
 					continue;
 				}
@@ -452,7 +452,7 @@ boolean P_CheckSight(mobj_t *t1, mobj_t *t2)
 			/// \todo Improve by checking fog density/translucency
 			/// and setting a sight limit.
 			if (!(rover->flags & FF_EXISTS)
-				|| !(rover->flags & FF_RENDERPLANES) || rover->flags & FF_TRANSLUCENT)
+				|| !(rover->flags & FF_RENDERPLANES) || (rover->flags & (FF_TRANSLUCENT|FF_FOG)))
 			{
 				continue;
 			}
diff --git a/src/r_draw.c b/src/r_draw.c
index d0f28b75332452dc2864a87cd68248aa016d9d7f..f0a19a462848d02c54b07a8a481f11e0969ebef0 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -342,7 +342,7 @@ UINT8 *R_GetBlendTable(int style, INT32 alphalevel)
 {
 	size_t offs;
 
-	if (style == AST_COPY || style == AST_OVERLAY)
+	if (style <= AST_COPY || style >= AST_OVERLAY)
 		return NULL;
 
 	offs = (ClipBlendLevel(style, alphalevel) << FF_TRANSSHIFT);
@@ -372,7 +372,7 @@ UINT8 *R_GetBlendTable(int style, INT32 alphalevel)
 
 boolean R_BlendLevelVisible(INT32 blendmode, INT32 alphalevel)
 {
-	if (blendmode == AST_COPY || blendmode == AST_SUBTRACT || blendmode == AST_MODULATE || blendmode == AST_OVERLAY)
+	if (blendmode <= AST_COPY || blendmode == AST_SUBTRACT || blendmode == AST_MODULATE || blendmode >= AST_OVERLAY)
 		return true;
 
 	return (alphalevel < BlendTab_Count[BlendTab_FromStyle[blendmode]]);
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index de9c4f32f9254f941b47dc0cf44646ae49d5a84d..105e1def868b96e66c05302674a9a93e4f83e159 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -407,6 +407,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 b59d9864d958bf4db53c78e3f5787ca62ec38a6c..4048903976b57317d6be3ac77ff0f87a38b17f8c 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -723,6 +723,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>
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 4ad3769ab7a146dc7c61f25603d656966b913298..d68e3e435bb6c749b8e770e0cc604aba96206185 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -1969,7 +1969,7 @@ void I_GetMouseEvents(void)
 		event.data1 = 0;
 //		event.data1 = buttons; // not needed
 		event.data2 = handlermouse2x << 1;
-		event.data3 = -handlermouse2y << 1;
+		event.data3 = handlermouse2y << 1;
 		handlermouse2x = 0;
 		handlermouse2y = 0;
 
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 5f18720f8671b4c8733e0df65358d646a8d6d225..c387e5a1855db7da429bce73b324f3ae3638114b 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -405,6 +405,19 @@ void I_UpdateMouseGrab(void)
 		SDLdoGrabMouse();
 }
 
+boolean I_GetMouseGrab(void)
+{
+	return SDL_GetWindowGrab(window);
+}
+
+void I_SetMouseGrab(boolean grab)
+{
+	if (grab)
+		SDLdoGrabMouse();
+	else
+		SDLdoUngrabMouse();
+}
+
 static void VID_Command_NumModes_f (void)
 {
 	CONS_Printf(M_GetText("%d video mode(s) available(s)\n"), VID_NumModes());
@@ -673,8 +686,8 @@ static void Impl_HandleMouseMotionEvent(SDL_MouseMotionEvent evt)
 		{
 			if (SDL_GetMouseFocus() == window && SDL_GetKeyboardFocus() == window)
 			{
-				mousemovex +=  evt.xrel;
-				mousemovey += -evt.yrel;
+				mousemovex += evt.xrel;
+				mousemovey += evt.yrel;
 				SDL_SetWindowGrab(window, SDL_TRUE);
 			}
 			firstmove = false;
@@ -1938,3 +1951,8 @@ void I_ShutdownGraphics(void)
 	framebuffer = SDL_FALSE;
 }
 #endif
+
+void I_GetCursorPosition(INT32 *x, INT32 *y)
+{
+	SDL_GetMouseState(x, y);
+}