diff --git a/README.md b/README.md
index d16071454876bc178222e778ca7c20f63821bb97..7d92ab303fe06f475738b63b66bbb041f81bdbbb 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,6 @@
 - libupnp (Linux/OS X only)
 - libgme (Linux/OS X only)
 
-Warning: 64-bit builds are not netgame compatible with 32-bit builds. Use at your own risk.
-
 ## Compiling
 
 See [SRB2 Wiki/Source code compiling](http://wiki.srb2.org/wiki/Source_code_compiling)
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 92da2492ebdd69f0fe4b49619c61f41ef849dc9c..b720dbf16eceb09b90f5904134baf883523c3c82 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -2353,7 +2353,7 @@ void CL_ClearPlayer(INT32 playernum)
 //
 // Removes a player from the current game
 //
-static void CL_RemovePlayer(INT32 playernum)
+static void CL_RemovePlayer(INT32 playernum, INT32 reason)
 {
 	// Sanity check: exceptional cases (i.e. c-fails) can cause multiple
 	// kick commands to be issued for the same player.
@@ -2407,6 +2407,10 @@ static void CL_RemovePlayer(INT32 playernum)
 			}
 		}
 	}
+	
+#ifdef HAVE_BLUA
+	LUAh_PlayerQuit(&players[playernum], reason); // Lua hook for player quitting
+#endif
 
 	// Reset player data
 	CL_ClearPlayer(playernum);
@@ -2683,6 +2687,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	INT32 pnum, msg;
 	char buf[3 + MAX_REASONLENGTH];
 	char *reason = buf;
+	kickreason_t kickreason = KR_KICK;
 
 	pnum = READUINT8(*p);
 	msg = READUINT8(*p);
@@ -2765,14 +2770,17 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	{
 		case KICK_MSG_GO_AWAY:
 			CONS_Printf(M_GetText("has been kicked (Go away)\n"));
+			kickreason = KR_KICK;
 			break;
 #ifdef NEWPING
 		case KICK_MSG_PING_HIGH:
 			CONS_Printf(M_GetText("left the game (Broke ping limit)\n"));
+			kickreason = KR_PINGLIMIT;
 			break;
 #endif
 		case KICK_MSG_CON_FAIL:
 			CONS_Printf(M_GetText("left the game (Synch failure)\n"));
+			kickreason = KR_SYNCH;
 
 			if (M_CheckParm("-consisdump")) // Helps debugging some problems
 			{
@@ -2809,21 +2817,26 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			break;
 		case KICK_MSG_TIMEOUT:
 			CONS_Printf(M_GetText("left the game (Connection timeout)\n"));
+			kickreason = KR_TIMEOUT;
 			break;
 		case KICK_MSG_PLAYER_QUIT:
 			if (netgame) // not splitscreen/bots
 				CONS_Printf(M_GetText("left the game\n"));
+				kickreason = KR_LEAVE;
 			break;
 		case KICK_MSG_BANNED:
 			CONS_Printf(M_GetText("has been banned (Don't come back)\n"));
+			kickreason = KR_BAN;
 			break;
 		case KICK_MSG_CUSTOM_KICK:
 			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
 			CONS_Printf(M_GetText("has been kicked (%s)\n"), reason);
+			kickreason = KR_KICK;
 			break;
 		case KICK_MSG_CUSTOM_BAN:
 			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
 			CONS_Printf(M_GetText("has been banned (%s)\n"), reason);
+			kickreason = KR_BAN;
 			break;
 	}
 
@@ -2851,7 +2864,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			M_StartMessage(M_GetText("You have been kicked by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
 	}
 	else
-		CL_RemovePlayer(pnum);
+		CL_RemovePlayer(pnum, kickreason);
 }
 
 consvar_t cv_allownewplayer = {"allowjoin", "On", CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL	};
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index bdb85a76c2160c09e80224946f44ff81ac0e0c75..a0afb34c698c656c0cc84b92fcaa5e0d62aa4146 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -454,6 +454,17 @@ extern consvar_t cv_playbackspeed;
 #define KICK_MSG_CUSTOM_KICK 7
 #define KICK_MSG_CUSTOM_BAN  8
 
+typedef enum
+{
+	KR_KICK          = 1, //Kicked by server
+	KR_PINGLIMIT     = 2, //Broke Ping Limit
+	KR_SYNCH         = 3, //Synch Failure
+	KR_TIMEOUT       = 4, //Connection Timeout 
+	KR_BAN           = 5, //Banned by server
+	KR_LEAVE         = 6, //Quit the game
+	
+} kickreason_t;
+
 extern boolean server;
 #define client (!server)
 extern boolean dedicated; // For dedicated server
diff --git a/src/d_main.c b/src/d_main.c
index d2d7ff71096328d71406c3fc0f6f0356d889558b..7ffa7da7e71769b6928ff67292610aa5dc42b469 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -71,6 +71,7 @@ int	snprintf(char *str, size_t n, const char *fmt, ...);
 #include "fastcmp.h"
 #include "keys.h"
 #include "filesrch.h" // refreshdirmenu, mainwadstally
+#include "g_input.h" // tutorial mode control scheming
 
 #ifdef CMAKECONFIG
 #include "config.h"
@@ -667,6 +668,7 @@ void D_AdvanceDemo(void)
 void D_StartTitle(void)
 {
 	INT32 i;
+	boolean tutorialpostprompt = false;
 
 	S_StopMusic();
 
@@ -712,6 +714,13 @@ void D_StartTitle(void)
 	modeattacking = ATTACKING_NONE;
 
 	// The title screen is obviously not a tutorial! (Unless I'm mistaken)
+	if (tutorialmode)
+	{
+		// check if retained controls are custom
+		tutorialpostprompt = (G_GetControlScheme(gamecontroldefault[gcs_custom], gclist_tutorial, num_gclist_tutorial) == gcs_custom
+			&& G_GetControlScheme(gamecontrol, gclist_tutorial, num_gclist_tutorial) != gcs_custom);
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gclist_tutorial, num_gclist_tutorial); // using gcs_custom as temp storage
+	}
 	tutorialmode = false;
 
 	// empty maptol so mario/etc sounds don't play in sound test when they shouldn't
@@ -736,6 +745,10 @@ void D_StartTitle(void)
 	// Reset the palette
 	if (rendermode != render_none)
 		V_SetPaletteLump("PLAYPAL");
+
+	if (tutorialpostprompt)
+		M_StartMessage("Do you want to \x82save the recommended \x82movement controls?\x80\n\nPress 'Y' or 'Enter' to confirm, \nor any key to keep \nyour current controls.",
+			M_TutorialSaveControlResponse, MM_YESNO);
 }
 
 //
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index dd9ced6c4ea17d543960d8bf217ee7efdf2ef814..b584906fdb9606b7051d3ec8781359f7552afa86 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -126,8 +126,6 @@ FUNCNORETURN static ATTRNORETURN void Command_Quit_f(void);
 static void Command_Playintro_f(void);
 
 static void Command_Displayplayer_f(void);
-static void Command_Tunes_f(void);
-static void Command_RestartAudio_f(void);
 
 static void Command_ExitLevel_f(void);
 static void Command_Showmap_f(void);
@@ -315,8 +313,6 @@ consvar_t cv_timetic = {"timerres", "Classic", CV_SAVE, timetic_cons_t, NULL, 0,
 static CV_PossibleValue_t powerupdisplay_cons_t[] = {{0, "Never"}, {1, "First-person only"}, {2, "Always"}, {0, NULL}};
 consvar_t cv_powerupdisplay = {"powerupdisplay", "First-person only", CV_SAVE, powerupdisplay_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
-consvar_t cv_resetmusic = {"resetmusic", "No", CV_SAVE, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
-
 static CV_PossibleValue_t pointlimit_cons_t[] = {{0, "MIN"}, {999999990, "MAX"}, {0, NULL}};
 consvar_t cv_pointlimit = {"pointlimit", "0", CV_NETVAR|CV_CALL|CV_NOINIT, pointlimit_cons_t,
 	PointLimit_OnChange, 0, NULL, NULL, 0, 0, NULL};
@@ -685,9 +681,6 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_ghost_guest);
 
 	COM_AddCommand("displayplayer", Command_Displayplayer_f);
-	COM_AddCommand("tunes", Command_Tunes_f);
-	COM_AddCommand("restartaudio", Command_RestartAudio_f);
-	CV_RegisterVar(&cv_resetmusic);
 
 	// FIXME: not to be here.. but needs be done for config loading
 	CV_RegisterVar(&cv_globalgamma);
@@ -1816,6 +1809,8 @@ static void Command_Map_f(void)
 	else
 		fromlevelselect = ((netgame || multiplayer) && ((gametype == newgametype) && (newgametype == GT_COOP)));
 
+	if (tutorialmode)
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gclist_tutorial, num_gclist_tutorial); // using gcs_custom as temp storage
 	tutorialmode = false; // warping takes us out of tutorial mode
 
 	D_MapChange(newmapnum, newgametype, false, newresetplayers, 0, false, fromlevelselect);
@@ -3990,94 +3985,6 @@ static void Command_Displayplayer_f(void)
 	CONS_Printf(M_GetText("Displayplayer is %d\n"), displayplayer);
 }
 
-static void Command_Tunes_f(void)
-{
-	const char *tunearg;
-	UINT16 tunenum, track = 0;
-	const size_t argc = COM_Argc();
-
-	if (argc < 2) //tunes slot ...
-	{
-		CONS_Printf("tunes <name/num> [track] [speed] / <-show> / <-default> / <-none>:\n");
-		CONS_Printf(M_GetText("Play an arbitrary music lump. If a map number is used, 'MAP##M' is played.\n"));
-		CONS_Printf(M_GetText("If the format supports multiple songs, you can specify which one to play.\n\n"));
-		CONS_Printf(M_GetText("* With \"-show\", shows the currently playing tune and track.\n"));
-		CONS_Printf(M_GetText("* With \"-default\", returns to the default music for the map.\n"));
-		CONS_Printf(M_GetText("* With \"-none\", any music playing will be stopped.\n"));
-		return;
-	}
-
-	tunearg = COM_Argv(1);
-	tunenum = (UINT16)atoi(tunearg);
-	track = 0;
-
-	if (!strcasecmp(tunearg, "-show"))
-	{
-		CONS_Printf(M_GetText("The current tune is: %s [track %d]\n"),
-			mapmusname, (mapmusflags & MUSIC_TRACKMASK));
-		return;
-	}
-	if (!strcasecmp(tunearg, "-none"))
-	{
-		S_StopMusic();
-		return;
-	}
-	else if (!strcasecmp(tunearg, "-default"))
-	{
-		tunearg = mapheaderinfo[gamemap-1]->musname;
-		track = mapheaderinfo[gamemap-1]->mustrack;
-	}
-	else if (!tunearg[2] && toupper(tunearg[0]) >= 'A' && toupper(tunearg[0]) <= 'Z')
-		tunenum = (UINT16)M_MapNumber(tunearg[0], tunearg[1]);
-
-	if (tunenum && tunenum >= 1036)
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Valid music slots are 1 to 1035.\n"));
-		return;
-	}
-	if (!tunenum && strlen(tunearg) > 6) // This is automatic -- just show the error just in case
-		CONS_Alert(CONS_NOTICE, M_GetText("Music name too long - truncated to six characters.\n"));
-
-	if (argc > 2)
-		track = (UINT16)atoi(COM_Argv(2))-1;
-
-	if (tunenum)
-		snprintf(mapmusname, 7, "%sM", G_BuildMapName(tunenum));
-	else
-		strncpy(mapmusname, tunearg, 7);
-	mapmusname[6] = 0;
-	mapmusflags = (track & MUSIC_TRACKMASK);
-
-	S_ChangeMusic(mapmusname, mapmusflags, true);
-
-	if (argc > 3)
-	{
-		float speed = (float)atof(COM_Argv(3));
-		if (speed > 0.0f)
-			S_SpeedMusic(speed);
-	}
-}
-
-static void Command_RestartAudio_f(void)
-{
-	if (dedicated)  // No point in doing anything if game is a dedicated server.
-		return;
-
-	S_StopMusic();
-	S_StopSounds();
-	I_ShutdownMusic();
-	I_ShutdownSound();
-	I_StartupSound();
-	I_InitMusic();
-
-// These must be called or no sound and music until manually set.
-
-	I_SetSfxVolume(cv_soundvolume.value);
-	S_SetMusicVolume(cv_digmusicvolume.value, cv_midimusicvolume.value);
-	if (Playing()) // Gotta make sure the player is in a level
-		P_RestoreMusic(&players[consoleplayer]);
-}
-
 /** Quits a game and returns to the title screen.
   *
   */
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 57e23b0f1e0752d95576e197fe3de8aaadb1806b..435ca64a5b1f4f87cc467ddc22bfe9b083a4ee88 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -103,8 +103,6 @@ extern consvar_t cv_startinglives;
 // for F_finale.c
 extern consvar_t cv_rollingdemos;
 
-extern consvar_t cv_resetmusic;
-
 extern consvar_t cv_ringslinger, cv_soundtest;
 
 extern consvar_t cv_specialrings, cv_powerstones, cv_matchboxes, cv_competitionboxes;
@@ -202,6 +200,4 @@ void D_SetPassword(const char *pw);
 // used for the player setup menu
 UINT8 CanChangeSkin(INT32 playernum);
 
-#endif
-
-
+#endif
\ No newline at end of file
diff --git a/src/dehacked.c b/src/dehacked.c
index e33c7a6412affd64674515bc01c1bb96956b4629..11fce1002fbe93d1a71a355276ef635b0eb9b448 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -32,6 +32,7 @@
 #include "fastcmp.h"
 #include "lua_script.h"
 #include "lua_hook.h"
+#include "d_clisrv.h"
 
 #include "m_cond.h"
 
@@ -914,7 +915,10 @@ static void readlevelheader(MYFILE *f, INT32 num)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -1573,7 +1577,10 @@ static void readhuditem(MYFILE *f, INT32 num)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -2079,7 +2086,10 @@ static void reademblemdata(MYFILE *f, INT32 num)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -2214,7 +2224,10 @@ static void readextraemblemdata(MYFILE *f, INT32 num)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -2289,7 +2302,10 @@ static void readunlockable(MYFILE *f, INT32 num)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -2576,7 +2592,10 @@ static void readconditionset(MYFILE *f, UINT8 setnum)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -2637,7 +2656,10 @@ static void readmaincfg(MYFILE *f)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -2921,7 +2943,10 @@ static void readwipes(MYFILE *f)
 
 			// Get the part before the " = "
 			tmp = strchr(s, '=');
-			*(tmp-1) = '\0';
+			if (tmp)
+				*(tmp-1) = '\0';
+			else
+				break;
 			strupr(word);
 
 			// Now get the part after
@@ -7760,6 +7785,13 @@ struct {
 	// Node flags
 	{"NF_SUBSECTOR",NF_SUBSECTOR}, // Indicate a leaf.
 #endif
+#ifdef ESLOPE
+	// Slope flags
+	{"SL_NOPHYSICS",SL_NOPHYSICS},      // Don't do momentum adjustment with this slope
+	{"SL_NODYNAMIC",SL_NODYNAMIC},      // Slope will never need to move during the level, so don't fuss with recalculating it
+	{"SL_ANCHORVERTEX",SL_ANCHORVERTEX},// Slope is using a Slope Vertex Thing to anchor its position
+	{"SL_VERTEXSLOPE",SL_VERTEXSLOPE},  // Slope is built from three Slope Vertex Things
+#endif
 
 	// Angles
 	{"ANG1",ANG1},
@@ -7892,6 +7924,14 @@ struct {
 
 	{"V_CHARCOLORSHIFT",V_CHARCOLORSHIFT},
 	{"V_ALPHASHIFT",V_ALPHASHIFT},
+	
+	//Kick Reasons
+	{"KR_KICK",KR_KICK},
+	{"KR_PINGLIMIT",KR_PINGLIMIT},
+	{"KR_SYNCH",KR_SYNCH},
+	{"KR_TIMEOUT",KR_TIMEOUT},
+	{"KR_BAN",KR_BAN},
+	{"KR_LEAVE",KR_LEAVE},
 #endif
 
 	{NULL,0}
@@ -8756,6 +8796,9 @@ static inline int lib_getenum(lua_State *L)
 	} else if (fastcmp(word,"maptol")) {
 		lua_pushinteger(L, maptol);
 		return 1;
+	} else if (fastcmp(word,"ultimatemode")) {
+		lua_pushboolean(L, ultimatemode != 0);
+		return 1;
 	} else if (fastcmp(word,"mariomode")) {
 		lua_pushboolean(L, mariomode != 0);
 		return 1;
diff --git a/src/g_input.c b/src/g_input.c
index 67aaf41791c33d90a360a38fe78faaec960155e9..ebebc676ac9cec6d75152753921a802d9a8e0f34 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -45,6 +45,28 @@ UINT8 gamekeydown[NUMINPUTS];
 // two key codes (or virtual key) per game control
 INT32 gamecontrol[num_gamecontrols][2];
 INT32 gamecontrolbis[num_gamecontrols][2]; // secondary splitscreen player
+INT32 gamecontroldefault[num_gamecontrolschemes][num_gamecontrols][2]; // default control storage, use 0 (gcs_custom) for memory retention
+
+// lists of GC codes for selective operation
+const INT32 gclist_tutorial[num_gclist_tutorial] = {
+	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
+	gc_lookup, gc_lookdown, gc_turnleft, gc_turnright //, gc_centerview,
+	//gc_jump, gc_use,
+	//gc_fire, gc_firenormal
+};
+
+const INT32 gclist_movement[num_gclist_movement] = {
+	gc_forward, gc_backward, gc_strafeleft, gc_straferight
+};
+
+const INT32 gclist_camera[num_gclist_camera] = {
+	gc_turnleft, gc_turnright
+	//gc_lookup, gc_lookdown, gc_turnleft, gc_turnright
+};
+
+const INT32 gclist_jump[num_gclist_jump] = { gc_jump };
+
+const INT32 gclist_use[num_gclist_use] = { gc_use };
 
 typedef struct
 {
@@ -611,55 +633,117 @@ INT32 G_KeyStringtoNum(const char *keystr)
 	return 0;
 }
 
-void G_Controldefault(void)
+void G_DefineDefaultControls(void)
 {
-	gamecontrol[gc_forward    ][0] = 'w';
-	gamecontrol[gc_backward   ][0] = 's';
-	gamecontrol[gc_strafeleft ][0] = 'a';
-	gamecontrol[gc_straferight][0] = 'd';
-	gamecontrol[gc_turnleft   ][0] = KEY_LEFTARROW;
-	gamecontrol[gc_turnright  ][0] = KEY_RIGHTARROW;
-	gamecontrol[gc_weaponnext ][0] = 'e';
-	gamecontrol[gc_weaponprev ][0] = 'q';
-	gamecontrol[gc_wepslot1   ][0] = '1';
-	gamecontrol[gc_wepslot2   ][0] = '2';
-	gamecontrol[gc_wepslot3   ][0] = '3';
-	gamecontrol[gc_wepslot4   ][0] = '4';
-	gamecontrol[gc_wepslot5   ][0] = '5';
-	gamecontrol[gc_wepslot6   ][0] = '6';
-	gamecontrol[gc_wepslot7   ][0] = '7';
-	gamecontrol[gc_wepslot8   ][0] = '8';
-	gamecontrol[gc_wepslot9   ][0] = '9';
-	gamecontrol[gc_wepslot10  ][0] = '0';
-	gamecontrol[gc_fire       ][0] = KEY_RCTRL;
-	gamecontrol[gc_fire       ][1] = KEY_MOUSE1+0;
-	gamecontrol[gc_firenormal ][0] = 'c';
-	gamecontrol[gc_tossflag   ][0] = '\'';
-	gamecontrol[gc_use        ][0] = KEY_LSHIFT;
-	gamecontrol[gc_camtoggle  ][0] = 'v';
-	gamecontrol[gc_camreset   ][0] = 'r';
-	gamecontrol[gc_lookup     ][0] = KEY_UPARROW;
-	gamecontrol[gc_lookdown   ][0] = KEY_DOWNARROW;
-	gamecontrol[gc_centerview ][0] = KEY_END;
-	gamecontrol[gc_talkkey    ][0] = 't';
-	gamecontrol[gc_teamkey    ][0] = 'y';
-	gamecontrol[gc_scores     ][0] = KEY_TAB;
-	gamecontrol[gc_jump       ][0] = KEY_SPACE;
-	gamecontrol[gc_console    ][0] = KEY_CONSOLE;
-	gamecontrol[gc_pause      ][0] = KEY_PAUSE;
+	INT32 i;
+
+	// FPS game controls (WASD)
+	gamecontroldefault[gcs_fps][gc_forward    ][0] = 'w';
+	gamecontroldefault[gcs_fps][gc_backward   ][0] = 's';
+	gamecontroldefault[gcs_fps][gc_strafeleft ][0] = 'a';
+	gamecontroldefault[gcs_fps][gc_straferight][0] = 'd';
+	gamecontroldefault[gcs_fps][gc_lookup     ][0] = KEY_UPARROW;
+	gamecontroldefault[gcs_fps][gc_lookdown   ][0] = KEY_DOWNARROW;
+	gamecontroldefault[gcs_fps][gc_turnleft   ][0] = KEY_LEFTARROW;
+	gamecontroldefault[gcs_fps][gc_turnright  ][0] = KEY_RIGHTARROW;
+	gamecontroldefault[gcs_fps][gc_centerview ][0] = KEY_END;
+	gamecontroldefault[gcs_fps][gc_jump       ][0] = KEY_SPACE;
+	gamecontroldefault[gcs_fps][gc_use        ][0] = KEY_LSHIFT;
+	gamecontroldefault[gcs_fps][gc_fire       ][0] = KEY_RCTRL;
+	gamecontroldefault[gcs_fps][gc_fire       ][1] = KEY_MOUSE1+0;
+	gamecontroldefault[gcs_fps][gc_firenormal ][0] = 'c';
+
+	// Platform game controls (arrow keys)
+	// gamecontroldefault[gcs_platform][gc_forward    ][0] = KEY_UPARROW;
+	// gamecontroldefault[gcs_platform][gc_backward   ][0] = KEY_DOWNARROW;
+	// gamecontroldefault[gcs_platform][gc_strafeleft ][0] = 'a';
+	// gamecontroldefault[gcs_platform][gc_straferight][0] = 'd';
+	// gamecontroldefault[gcs_platform][gc_lookup     ][0] = KEY_PGUP;
+	// gamecontroldefault[gcs_platform][gc_lookdown   ][0] = KEY_PGDN;
+	// gamecontroldefault[gcs_platform][gc_turnleft   ][0] = KEY_LEFTARROW;
+	// gamecontroldefault[gcs_platform][gc_turnright  ][0] = KEY_RIGHTARROW;
+	// gamecontroldefault[gcs_platform][gc_centerview ][0] = KEY_END;
+	// gamecontroldefault[gcs_platform][gc_jump       ][0] = KEY_SPACE;
+	// gamecontroldefault[gcs_platform][gc_use        ][0] = KEY_LSHIFT;
+	// gamecontroldefault[gcs_platform][gc_fire       ][0] = 's';
+	// gamecontroldefault[gcs_platform][gc_fire       ][1] = KEY_MOUSE1+0;
+	// gamecontroldefault[gcs_platform][gc_firenormal ][0] = 'w';
+
+	for (i = 1; i < num_gamecontrolschemes; i++) // skip gcs_custom (0)
+	{
+		gamecontroldefault[i][gc_weaponnext ][0] = 'e';
+		gamecontroldefault[i][gc_weaponprev ][0] = 'q';
+		gamecontroldefault[i][gc_wepslot1   ][0] = '1';
+		gamecontroldefault[i][gc_wepslot2   ][0] = '2';
+		gamecontroldefault[i][gc_wepslot3   ][0] = '3';
+		gamecontroldefault[i][gc_wepslot4   ][0] = '4';
+		gamecontroldefault[i][gc_wepslot5   ][0] = '5';
+		gamecontroldefault[i][gc_wepslot6   ][0] = '6';
+		gamecontroldefault[i][gc_wepslot7   ][0] = '7';
+		gamecontroldefault[i][gc_wepslot8   ][0] = '8';
+		gamecontroldefault[i][gc_wepslot9   ][0] = '9';
+		gamecontroldefault[i][gc_wepslot10  ][0] = '0';
+		gamecontroldefault[i][gc_tossflag   ][0] = '\'';
+		gamecontroldefault[i][gc_camtoggle  ][0] = 'v';
+		gamecontroldefault[i][gc_camreset   ][0] = 'r';
+		gamecontroldefault[i][gc_talkkey    ][0] = 't';
+		gamecontroldefault[i][gc_teamkey    ][0] = 'y';
+		gamecontroldefault[i][gc_scores     ][0] = KEY_TAB;
+		gamecontroldefault[i][gc_console    ][0] = KEY_CONSOLE;
+		gamecontroldefault[i][gc_pause      ][0] = KEY_PAUSE;
+	}
+}
+
+INT32 G_GetControlScheme(INT32 (*fromcontrols)[2], const INT32 *gclist, INT32 gclen)
+{
+	INT32 i, j, gc;
+	boolean skipscheme;
+
+	for (i = 1; i < num_gamecontrolschemes; i++) // skip gcs_custom (0)
+	{
+		skipscheme = false;
+		for (j = 0; j < (gclist && gclen ? gclen : num_gamecontrols); j++)
+		{
+			gc = (gclist && gclen) ? gclist[j] : j;
+			if (((fromcontrols[gc][0] && gamecontroldefault[i][gc][0]) ? fromcontrols[gc][0] != gamecontroldefault[i][gc][0] : true) &&
+				((fromcontrols[gc][0] && gamecontroldefault[i][gc][1]) ? fromcontrols[gc][0] != gamecontroldefault[i][gc][1] : true) &&
+				((fromcontrols[gc][1] && gamecontroldefault[i][gc][0]) ? fromcontrols[gc][1] != gamecontroldefault[i][gc][0] : true) &&
+				((fromcontrols[gc][1] && gamecontroldefault[i][gc][1]) ? fromcontrols[gc][1] != gamecontroldefault[i][gc][1] : true))
+			{
+				skipscheme = true;
+				break;
+			}
+		}
+		if (!skipscheme)
+			return i;
+	}
+
+	return gcs_custom;
+}
+
+void G_CopyControls(INT32 (*setupcontrols)[2], INT32 (*fromcontrols)[2], const INT32 *gclist, INT32 gclen)
+{
+	INT32 i, gc;
+
+	for (i = 0; i < (gclist && gclen ? gclen : num_gamecontrols); i++)
+	{
+		gc = (gclist && gclen) ? gclist[i] : i;
+		setupcontrols[gc][0] = fromcontrols[gc][0];
+		setupcontrols[gc][1] = fromcontrols[gc][1];
+	}
 }
 
-void G_SaveKeySetting(FILE *f)
+void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis)[2])
 {
 	INT32 i;
 
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(gamecontrol[i][0]));
+			G_KeynumToString(fromcontrols[i][0]));
 
-		if (gamecontrol[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(gamecontrol[i][1]));
+		if (fromcontrols[i][1])
+			fprintf(f, " \"%s\"\n", G_KeynumToString(fromcontrols[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -667,10 +751,10 @@ void G_SaveKeySetting(FILE *f)
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol2 \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(gamecontrolbis[i][0]));
+			G_KeynumToString(fromcontrolsbis[i][0]));
 
-		if (gamecontrolbis[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(gamecontrolbis[i][1]));
+		if (fromcontrolsbis[i][1])
+			fprintf(f, " \"%s\"\n", G_KeynumToString(fromcontrolsbis[i][1]));
 		else
 			fprintf(f, "\n");
 	}
diff --git a/src/g_input.h b/src/g_input.h
index 2a447c683d35bcf083c81222185e45be8cc2c28f..10a0991e01b61af68784ee96b24fdc27bfc7168a 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -99,6 +99,14 @@ typedef enum
 	num_gamecontrols
 } gamecontrols_e;
 
+typedef enum
+{
+	gcs_custom,
+	gcs_fps,
+	//gcs_platform,
+	num_gamecontrolschemes
+} gamecontrolschemes_e;
+
 // mouse values are used once
 extern consvar_t cv_mousesens, cv_mouseysens;
 extern consvar_t cv_mousesens2, cv_mouseysens2;
@@ -116,9 +124,22 @@ extern UINT8 gamekeydown[NUMINPUTS];
 // two key codes (or virtual key) per game control
 extern INT32 gamecontrol[num_gamecontrols][2];
 extern INT32 gamecontrolbis[num_gamecontrols][2]; // secondary splitscreen player
+extern INT32 gamecontroldefault[num_gamecontrolschemes][num_gamecontrols][2]; // default control storage, use 0 (gcs_custom) for memory retention
 #define PLAYER1INPUTDOWN(gc) (gamekeydown[gamecontrol[gc][0]] || gamekeydown[gamecontrol[gc][1]])
 #define PLAYER2INPUTDOWN(gc) (gamekeydown[gamecontrolbis[gc][0]] || gamekeydown[gamecontrolbis[gc][1]])
 
+#define num_gclist_tutorial 8 // 13
+#define num_gclist_movement 4
+#define num_gclist_camera 2
+#define num_gclist_jump 1
+#define num_gclist_use 1
+
+extern const INT32 gclist_tutorial[num_gclist_tutorial];
+extern const INT32 gclist_movement[num_gclist_movement];
+extern const INT32 gclist_camera[num_gclist_camera];
+extern const INT32 gclist_jump[num_gclist_jump];
+extern const INT32 gclist_use[num_gclist_use];
+
 // peace to my little coder fingers!
 // check a gamecontrol being active or not
 
@@ -133,8 +154,10 @@ INT32 G_KeyStringtoNum(const char *keystr);
 void G_ClearControlKeys(INT32 (*setupcontrols)[2], INT32 control);
 void Command_Setcontrol_f(void);
 void Command_Setcontrol2_f(void);
-void G_Controldefault(void);
-void G_SaveKeySetting(FILE *f);
+void G_DefineDefaultControls(void);
+INT32 G_GetControlScheme(INT32 (*fromcontrols)[2], const INT32 *gclist, INT32 gclen);
+void G_CopyControls(INT32 (*setupcontrols)[2], INT32 (*fromcontrols)[2], const INT32 *gclist, INT32 gclen);
+void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis)[2]);
 void G_CheckDoubleUsage(INT32 keynum);
 
 #endif
diff --git a/src/hardware/hw_data.h b/src/hardware/hw_data.h
index f6bbf9455a2415184f2d7d69abe5b56f079d4c5e..44929dd67ba00267173afde6f4fa55a50440c107 100644
--- a/src/hardware/hw_data.h
+++ b/src/hardware/hw_data.h
@@ -26,10 +26,6 @@
 #include <windows.h>
 #endif
 
-#if defined (VID_X11) && !defined (HAVE_SDL)
-#include <GL/glx.h>
-#endif
-
 #include "../doomdef.h"
 //THIS MUST DISAPPEAR!!!
 #include "hw_glide.h"
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index 02608892e50d0c1ecf5eb0f972e47154876afbf0..294e6bcd0c5959e964839b1027a8f2b93c7b7527 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -114,10 +114,10 @@ void HWR_DrawPatch(GLPatch_t *gpatch, INT32 x, INT32 y, INT32 option)
 	if (option & V_NOSCALESTART)
 		sdupx = sdupy = 2.0f;
 
-	v[0].x = v[3].x = (x*sdupx-gpatch->leftoffset*pdupx)/vid.width - 1;
-	v[2].x = v[1].x = (x*sdupx+(gpatch->width-gpatch->leftoffset)*pdupx)/vid.width - 1;
-	v[0].y = v[1].y = 1-(y*sdupy-gpatch->topoffset*pdupy)/vid.height;
-	v[2].y = v[3].y = 1-(y*sdupy+(gpatch->height-gpatch->topoffset)*pdupy)/vid.height;
+	v[0].x = v[3].x = (x*sdupx-SHORT(gpatch->leftoffset)*pdupx)/vid.width - 1;
+	v[2].x = v[1].x = (x*sdupx+(SHORT(gpatch->width)-SHORT(gpatch->leftoffset))*pdupx)/vid.width - 1;
+	v[0].y = v[1].y = 1-(y*sdupy-SHORT(gpatch->topoffset)*pdupy)/vid.height;
+	v[2].y = v[3].y = 1-(y*sdupy+(SHORT(gpatch->height)-SHORT(gpatch->topoffset))*pdupy)/vid.height;
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
@@ -183,18 +183,29 @@ void HWR_DrawFixedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 	dupx = dupy = (dupx < dupy ? dupx : dupy);
 	fscalew = fscaleh = FIXED_TO_FLOAT(pscale);
 
-	if (option & V_OFFSET)
+	// See my comments in v_video.c's V_DrawFixedPatch
+	// -- Monster Iestyn 29/10/18
 	{
-		cx -= (float)gpatch->leftoffset * dupx * fscalew;
-		cy -= (float)gpatch->topoffset * dupy * fscaleh;
-	}
-	else
-	{
-		cy -= (float)gpatch->topoffset * fscaleh;
+		float offsetx = 0.0f, offsety = 0.0f;
+
+		// left offset
 		if (option & V_FLIP)
-			cx -= ((float)gpatch->width - (float)gpatch->leftoffset) * fscalew;
+			offsetx = (float)(SHORT(gpatch->width) - SHORT(gpatch->leftoffset)) * fscalew;
 		else
-			cx -= (float)gpatch->leftoffset * fscalew;
+			offsetx = (float)SHORT(gpatch->leftoffset) * fscalew;
+
+		// top offset
+		// TODO: make some kind of vertical version of V_FLIP, maybe by deprecating V_OFFSET in future?!?
+		offsety = (float)SHORT(gpatch->topoffset) * fscaleh;
+
+		if ((option & (V_NOSCALESTART|V_OFFSET)) == (V_NOSCALESTART|V_OFFSET)) // Multiply by dupx/dupy for crosshairs
+		{
+			offsetx *= dupx;
+			offsety *= dupy;
+		}
+
+		cx -= offsetx;
+		cy -= offsety;
 	}
 
 	if (splitscreen && (option & V_PERPLAYER))
@@ -312,13 +323,13 @@ void HWR_DrawFixedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 	if (pscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
 	{
-		fwidth = (float)gpatch->width * fscalew * dupx;
-		fheight = (float)gpatch->height * fscaleh * dupy;
+		fwidth = (float)SHORT(gpatch->width) * fscalew * dupx;
+		fheight = (float)SHORT(gpatch->height) * fscaleh * dupy;
 	}
 	else
 	{
-		fwidth = (float)gpatch->width * dupx;
-		fheight = (float)gpatch->height * dupy;
+		fwidth = (float)SHORT(gpatch->width) * dupx;
+		fheight = (float)SHORT(gpatch->height) * dupy;
 	}
 
 	// positions of the cx, cy, are between 0 and vid.width/vid.height now, we need them to be between -1 and 1
@@ -418,8 +429,8 @@ void HWR_DrawCroppedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscal
 
 	// fuck it, no GL support for croppedpatch v_perplayer right now. it's not like it's accessible to Lua or anything, and we only use it for menus...
 
-	cy -= (float)gpatch->topoffset * fscale;
-	cx -= (float)gpatch->leftoffset * fscale;
+	cy -= (float)SHORT(gpatch->topoffset) * fscale;
+	cx -= (float)SHORT(gpatch->leftoffset) * fscale;
 
 	if (!(option & V_NOSCALESTART))
 	{
@@ -461,11 +472,11 @@ void HWR_DrawCroppedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscal
 	fwidth = w;
 	fheight = h;
 
-	if (fwidth > gpatch->width)
-		fwidth = gpatch->width;
+	if (fwidth > SHORT(gpatch->width))
+		fwidth = SHORT(gpatch->width);
 
-	if (fheight > gpatch->height)
-		fheight = gpatch->height;
+	if (fheight > SHORT(gpatch->height))
+		fheight = SHORT(gpatch->height);
 
 	if (pscale != FRACUNIT)
 	{
@@ -495,17 +506,17 @@ void HWR_DrawCroppedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscal
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
-	v[0].sow = v[3].sow     = ((sx  )/(float)gpatch->width )*gpatch->max_s;
-	if (sx + w > gpatch->width)
+	v[0].sow = v[3].sow     = ((sx  )/(float)SHORT(gpatch->width) )*gpatch->max_s;
+	if (sx + w > SHORT(gpatch->width))
 		v[2].sow = v[1].sow = gpatch->max_s;
 	else
-		v[2].sow = v[1].sow = ((sx+w)/(float)gpatch->width )*gpatch->max_s;
+		v[2].sow = v[1].sow = ((sx+w)/(float)SHORT(gpatch->width) )*gpatch->max_s;
 
-	v[0].tow = v[1].tow     = ((sy  )/(float)gpatch->height)*gpatch->max_t;
-	if (sy + h > gpatch->height)
+	v[0].tow = v[1].tow     = ((sy  )/(float)SHORT(gpatch->height))*gpatch->max_t;
+	if (sy + h > SHORT(gpatch->height))
 		v[2].tow = v[3].tow = gpatch->max_t;
 	else
-		v[2].tow = v[3].tow = ((sy+h)/(float)gpatch->height)*gpatch->max_t;
+		v[2].tow = v[3].tow = ((sy+h)/(float)SHORT(gpatch->height))*gpatch->max_t;
 
 	flags = BLENDMODE|PF_Clip|PF_NoZClip|PF_NoDepthTest;
 
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index a5ac82001084f5bf755c6025b1bbcfdccb40432d..e2fa90eb035de94d0b5ad7d81de624ed0fbe4341 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -32,10 +32,6 @@
 //                                                       STANDARD DLL EXPORTS
 // ==========================================================================
 
-#ifdef HAVE_SDL
-#undef VID_X11
-#endif
-
 EXPORT boolean HWRAPI(Init) (I_Error_t ErrorFunction);
 #ifndef HAVE_SDL
 EXPORT void HWRAPI(Shutdown) (void);
@@ -43,9 +39,6 @@ EXPORT void HWRAPI(Shutdown) (void);
 #ifdef _WINDOWS
 EXPORT void HWRAPI(GetModeList) (vmode_t **pvidmodes, INT32 *numvidmodes);
 #endif
-#ifdef VID_X11
-EXPORT Window HWRAPI(HookXwin) (Display *, INT32, INT32, boolean);
-#endif
 #if defined (PURESDL) || defined (macintosh)
 EXPORT void HWRAPI(SetPalette) (INT32 *, RGBA_t *gamma);
 #else
@@ -71,10 +64,6 @@ EXPORT void HWRAPI(SetTransform) (FTransform *ptransform);
 EXPORT INT32 HWRAPI(GetTextureUsed) (void);
 EXPORT INT32 HWRAPI(GetRenderVersion) (void);
 
-#ifdef VID_X11 // ifdef to be removed as soon as windoze supports that as well
-// metzgermeister: added for Voodoo detection
-EXPORT char *HWRAPI(GetRenderer) (void);
-#endif
 #ifdef SHUFFLE
 #define SCREENVERTS 10
 EXPORT void HWRAPI(PostImgRedraw) (float points[SCREENVERTS][SCREENVERTS][2]);
@@ -115,10 +104,6 @@ struct hwdriver_s
 #ifdef _WINDOWS
 	GetModeList         pfnGetModeList;
 #endif
-#ifdef VID_X11
-	HookXwin            pfnHookXwin;
-	GetRenderer         pfnGetRenderer;
-#endif
 #ifndef HAVE_SDL
 	Shutdown            pfnShutdown;
 #endif
diff --git a/src/hardware/hw_md2.h b/src/hardware/hw_md2.h
index c7cda35af6f308c1f8fa28e0fb8358fd55542996..24a5639330c12fe59ed1aae43ace60d54139c0f3 100644
--- a/src/hardware/hw_md2.h
+++ b/src/hardware/hw_md2.h
@@ -29,6 +29,11 @@
 // model version
 #define MD2_VERSION                     8
 
+// magic number "IDP2" or 844121161
+#define MD2_IDENT                       (INT32)(('2' << 24) + ('P' << 16) + ('D' << 8) + 'I')
+// model version
+#define MD2_VERSION                     8
+
 #define MD2_MAX_TRIANGLES               8192
 #define MD2_MAX_VERTICES                4096
 #define MD2_MAX_TEXCOORDS               4096
diff --git a/src/i_tcp.c b/src/i_tcp.c
index 044bf4e4cfcde253c5474a1550d1e0e16b6b70f4..c62adab0655da42ff929178f6d4486d64fed85d5 100644
--- a/src/i_tcp.c
+++ b/src/i_tcp.c
@@ -225,6 +225,33 @@ static void wattcp_outch(char s)
 }
 #endif
 
+#ifdef USE_WINSOCK
+// stupid microsoft makes things complicated
+static char *get_WSAErrorStr(int e)
+{
+	static char buf[256]; // allow up to 255 bytes
+
+	buf[0] = '\0';
+
+	FormatMessageA(
+		FORMAT_MESSAGE_FROM_SYSTEM |
+		FORMAT_MESSAGE_IGNORE_INSERTS,
+		NULL,
+		(DWORD)e,
+		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+		(LPTSTR)buf,
+		sizeof (buf),
+		NULL);
+
+	if (!buf[0]) // provide a fallback error message if no message is available for some reason
+		sprintf(buf, "Unknown error");
+
+	return buf;
+}
+#undef strerror
+#define strerror get_WSAErrorStr
+#endif
+
 #ifdef USE_WINSOCK2
 #define inet_ntop inet_ntopA
 #define HAVE_NTOP
@@ -703,9 +730,13 @@ static void SOCK_Send(void)
 		c = SOCK_SendToAddr(nodesocket[doomcom->remotenode], &clientaddress[doomcom->remotenode]);
 	}
 
-	if (c == ERRSOCKET && errno != ECONNREFUSED && errno != EWOULDBLOCK)
-		I_Error("SOCK_Send, error sending to node %d (%s) #%u: %s", doomcom->remotenode,
-			SOCK_GetNodeAddress(doomcom->remotenode), errno, strerror(errno));
+	if (c == ERRSOCKET)
+	{
+		int e = errno; // save error code so it can't be modified later
+		if (e != ECONNREFUSED && e != EWOULDBLOCK)
+			I_Error("SOCK_Send, error sending to node %d (%s) #%u: %s", doomcom->remotenode,
+				SOCK_GetNodeAddress(doomcom->remotenode), e, strerror(e));
+	}
 }
 #endif
 
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index ce017620c5e712fd2eeb1cfaa777433e1954b139..bb9e93edf672b48f4d0ae62f55c951a84aac3e55 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -14,6 +14,9 @@
 #ifdef HAVE_BLUA
 #include "p_local.h"
 #include "p_setup.h" // So we can have P_SetupLevelSky
+#ifdef ESLOPE
+#include "p_slopes.h" // P_GetZAt
+#endif
 #include "z_zone.h"
 #include "r_main.h"
 #include "r_things.h"
@@ -2008,6 +2011,24 @@ static int lib_evStartCrumble(lua_State *L)
 	return 0;
 }
 
+#ifdef ESLOPE
+// P_SLOPES
+////////////
+
+static int lib_pGetZAt(lua_State *L)
+{
+	pslope_t *slope = *((pslope_t **)luaL_checkudata(L, 1, META_SLOPE));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	//HUDSAFE
+	if (!slope)
+		return LUA_ErrInvalid(L, "pslope_t");
+
+	lua_pushfixed(L, P_GetZAt(slope, x, y));
+	return 1;
+}
+#endif
+
 // R_DEFS
 ////////////
 
@@ -2631,6 +2652,11 @@ static luaL_Reg lib[] = {
 	{"EV_CrumbleChain",lib_evCrumbleChain},
 	{"EV_StartCrumble",lib_evStartCrumble},
 
+#ifdef ESLOPE
+	// p_slopes
+	{"P_GetZAt",lib_pGetZAt},
+#endif
+
 	// r_defs
 	{"R_PointToAngle",lib_rPointToAngle},
 	{"R_PointToAngle2",lib_rPointToAngle2},
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 822edf79fe127224e50ee10a4f7330ce68ae63e4..2b113077cd69f483cbd05a82008d964091f074f5 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -48,6 +48,7 @@ enum hook {
 	hook_MobjMoveBlocked,
 	hook_MapThingSpawn,
 	hook_FollowMobj,
+	hook_PlayerQuit,
 
 	hook_MAX // last hook
 };
@@ -87,5 +88,6 @@ boolean LUAh_HurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8
 #define LUAh_MobjMoveBlocked(mo) LUAh_MobjHook(mo, hook_MobjMoveBlocked) // Hook for P_XYMovement (when movement is blocked)
 boolean LUAh_MapThingSpawn(mobj_t *mo, mapthing_t *mthing); // Hook for P_SpawnMapThing by mobj type
 boolean LUAh_FollowMobj(player_t *player, mobj_t *mo); // Hook for P_PlayerAfterThink Smiles mobj-following
+void LUAh_PlayerQuit(player_t *plr, int reason); // Hook for player quitting
 
 #endif
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 53886f7bada25666c4d2efc6c53e8b153e9cd721..bb1f59729e90de227c87bf044e88e7fcbc749194 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -59,6 +59,7 @@ const char *const hookNames[hook_MAX+1] = {
 	"MobjMoveBlocked",
 	"MapThingSpawn",
 	"FollowMobj",
+	"PlayerQuit",
 	NULL
 };
 
@@ -1192,4 +1193,30 @@ boolean LUAh_FollowMobj(player_t *player, mobj_t *mobj)
 	return hooked;
 }
 
+void LUAh_PlayerQuit(player_t *plr, int reason)
+{
+	hook_p hookp;
+	if (!gL || !(hooksAvailable[hook_PlayerQuit/8] & (1<<(hook_PlayerQuit%8))))
+		return;
+
+	lua_settop(gL, 0);
+
+	for (hookp = roothook; hookp; hookp = hookp->next)
+		if (hookp->type == hook_PlayerQuit)
+		{
+		    if (lua_gettop(gL) == 0)
+		    {
+		        LUA_PushUserdata(gL, plr, META_PLAYER); // Player that quit
+		        lua_pushinteger(gL, reason); // Reason for quitting
+		    }
+			lua_pushfstring(gL, FMT_HOOKID, hookp->id);
+			lua_gettable(gL, LUA_REGISTRYINDEX);
+			lua_pushvalue(gL, -3);
+			lua_pushvalue(gL, -3);
+			LUA_Call(gL, 2);
+		}
+
+	lua_settop(gL, 0);
+}
+
 #endif
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index afc81c37d474092dcf1d312da504b0ae4fd64312..9c5a21ec9e8fc21628bd3c737ea2544f013268ac 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -814,6 +814,15 @@ static int libd_RandomChance(lua_State *L)
 	return 1;
 }
 
+// 30/10/18 Lat': Get cv_translucenthud's value for HUD rendering as a normal V_xxTRANS int
+// Could as well be thrown in global vars for ease of access but I guess it makes sense for it to be a HUD fn
+static int libd_getlocaltransflag(lua_State *L)
+{
+	HUDONLY
+	lua_pushinteger(L, (10-cv_translucenthud.value)*V_10TRANS);	// A bit weird that it's called "translucenthud" yet 10 is fully opaque :V
+	return 1;
+}
+
 static luaL_Reg lib_draw[] = {
 	// cache
 	{"patchExists", libd_patchExists},
@@ -844,6 +853,7 @@ static luaL_Reg lib_draw[] = {
 	{"dupx", libd_dupx},
 	{"dupy", libd_dupy},
 	{"renderer", libd_renderer},
+	{"localTransFlag", libd_getlocaltransflag},
 	{NULL, NULL}
 };
 
@@ -867,6 +877,19 @@ static int lib_huddisable(lua_State *L)
 	return 0;
 }
 
+// 30/10/18: Lat': How come this wasn't here before?
+static int lib_hudenabled(lua_State *L)
+{
+	enum hud option = luaL_checkoption(L, 1, NULL, hud_disable_options);
+	if (hud_enabled[option/8] & (1<<(option%8)))
+		lua_pushboolean(L, true);
+	else
+		lua_pushboolean(L, false);
+
+	return 1;
+}
+
+
 // add a HUD element for rendering
 static int lib_hudadd(lua_State *L)
 {
@@ -894,6 +917,7 @@ static int lib_hudadd(lua_State *L)
 static luaL_Reg lib_hud[] = {
 	{"enable", lib_hudenable},
 	{"disable", lib_huddisable},
+	{"enabled", lib_hudenabled},
 	{"add", lib_hudadd},
 	{NULL, NULL}
 };
diff --git a/src/lua_libs.h b/src/lua_libs.h
index b63a3ceed8421496335ef6f16a4bd417057914fe..3ffdd807892fcee1392de49641f5c15f08e40804 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -42,6 +42,11 @@ extern lua_State *gL;
 #define META_SEG "SEG_T*"
 #define META_NODE "NODE_T*"
 #endif
+#ifdef ESLOPE
+#define META_SLOPE "PSLOPE_T*"
+#define META_VECTOR2 "VECTOR2_T"
+#define META_VECTOR3 "VECTOR3_T"
+#endif
 #define META_MAPHEADER "MAPHEADER_T*"
 
 #define META_CVAR "CONSVAR_T*"
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 28fe8c75f7c0f955d51f18e02a60e92109a83072..66fbb22b37907df4b8cf1e990a81ed84b14abf0d 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -16,6 +16,10 @@
 #include "p_local.h"
 #include "p_setup.h"
 #include "z_zone.h"
+#ifdef ESLOPE
+#include "p_slopes.h"
+#endif
+#include "r_main.h"
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -38,7 +42,13 @@ enum sector_e {
 	sector_heightsec,
 	sector_camsec,
 	sector_lines,
+#ifdef ESLOPE
+	sector_ffloors,
+	sector_fslope,
+	sector_cslope
+#else
 	sector_ffloors
+#endif
 };
 
 static const char *const sector_opt[] = {
@@ -55,6 +65,10 @@ static const char *const sector_opt[] = {
 	"camsec",
 	"lines",
 	"ffloors",
+#ifdef ESLOPE
+	"f_slope",
+	"c_slope",
+#endif
 	NULL};
 
 enum subsector_e {
@@ -160,6 +174,10 @@ enum ffloor_e {
 	ffloor_toplightlevel,
 	ffloor_bottomheight,
 	ffloor_bottompic,
+#ifdef ESLOPE
+	ffloor_tslope,
+	ffloor_bslope,
+#endif
 	ffloor_sector,
 	ffloor_flags,
 	ffloor_master,
@@ -176,6 +194,10 @@ static const char *const ffloor_opt[] = {
 	"toplightlevel",
 	"bottomheight",
 	"bottompic",
+#ifdef ESLOPE
+	"t_slope",
+	"b_slope",
+#endif
 	"sector", // secnum pushed as control sector userdata
 	"flags",
 	"master", // control linedef
@@ -261,6 +283,47 @@ static const char *const bbox_opt[] = {
 	"right",
 	NULL};
 
+#ifdef ESLOPE
+enum slope_e {
+	slope_valid = 0,
+	slope_o,
+	slope_d,
+	slope_zdelta,
+	slope_normal,
+	slope_zangle,
+	slope_xydirection,
+	slope_sourceline,
+	slope_refpos,
+	slope_flags
+};
+
+static const char *const slope_opt[] = {
+	"valid",
+	"o",
+	"d",
+	"zdelta",
+	"normal",
+	"zangle",
+	"xydirection",
+	"sourceline",
+	"refpos",
+	"flags",
+	NULL};
+
+// shared by both vector2_t and vector3_t
+enum vector_e {
+	vector_x = 0,
+	vector_y,
+	vector_z
+};
+
+static const char *const vector_opt[] = {
+	"x",
+	"y",
+	"z",
+	NULL};
+#endif
+
 static const char *const array_opt[] ={"iterate",NULL};
 static const char *const valid_opt[] ={"valid",NULL};
 
@@ -493,6 +556,14 @@ static int sector_get(lua_State *L)
 		LUA_PushUserdata(L, sector->ffloors, META_FFLOOR);
 		lua_pushcclosure(L, sector_iterate, 2); // push lib_iterateFFloors and sector->ffloors as upvalues for the function
 		return 1;
+#ifdef ESLOPE
+	case sector_fslope: // f_slope
+		LUA_PushUserdata(L, sector->f_slope, META_SLOPE);
+		return 1;
+	case sector_cslope: // c_slope
+		LUA_PushUserdata(L, sector->c_slope, META_SLOPE);
+		return 1;
+#endif
 	}
 	return 0;
 }
@@ -515,6 +586,10 @@ static int sector_set(lua_State *L)
 	case sector_heightsec: // heightsec
 	case sector_camsec: // camsec
 	case sector_ffloors: // ffloors
+#ifdef ESLOPE
+	case sector_fslope: // f_slope
+	case sector_cslope: // c_slope
+#endif
 	default:
 		return luaL_error(L, "sector_t field " LUA_QS " cannot be set.", sector_opt[field]);
 	case sector_floorheight: { // floorheight
@@ -1602,6 +1677,14 @@ static int ffloor_get(lua_State *L)
 		lua_pushlstring(L, levelflat->name, 8);
 		return 1;
 	}
+#ifdef ESLOPE
+	case ffloor_tslope:
+		LUA_PushUserdata(L, *ffloor->t_slope, META_SLOPE);
+		return 1;
+	case ffloor_bslope:
+		LUA_PushUserdata(L, *ffloor->b_slope, META_SLOPE);
+		return 1;
+#endif
 	case ffloor_sector:
 		LUA_PushUserdata(L, &sectors[ffloor->secnum], META_SECTOR);
 		return 1;
@@ -1641,6 +1724,10 @@ static int ffloor_set(lua_State *L)
 	switch(field)
 	{
 	case ffloor_valid: // valid
+#ifdef ESLOPE
+	case ffloor_tslope: // t_slope
+	case ffloor_bslope: // b_slope
+#endif
 	case ffloor_sector: // sector
 	case ffloor_master: // master
 	case ffloor_target: // target
@@ -1701,6 +1788,189 @@ static int ffloor_set(lua_State *L)
 	return 0;
 }
 
+#ifdef ESLOPE
+//////////////
+// pslope_t //
+//////////////
+
+static int slope_get(lua_State *L)
+{
+	pslope_t *slope = *((pslope_t **)luaL_checkudata(L, 1, META_SLOPE));
+	enum slope_e field = luaL_checkoption(L, 2, slope_opt[0], slope_opt);
+
+	if (!slope)
+	{
+		if (field == slope_valid) {
+			lua_pushboolean(L, 0);
+			return 1;
+		}
+		return luaL_error(L, "accessed pslope_t doesn't exist anymore.");
+	}
+
+	switch(field)
+	{
+	case slope_valid: // valid
+		lua_pushboolean(L, 1);
+		return 1;
+	case slope_o: // o
+		LUA_PushUserdata(L, &slope->o, META_VECTOR3);
+		return 1;
+	case slope_d: // d
+		LUA_PushUserdata(L, &slope->d, META_VECTOR2);
+		return 1;
+	case slope_zdelta: // zdelta
+		lua_pushfixed(L, slope->zdelta);
+		return 1;
+	case slope_normal: // normal
+		LUA_PushUserdata(L, &slope->normal, META_VECTOR3);
+		return 1;
+	case slope_zangle: // zangle
+		lua_pushangle(L, slope->zangle);
+		return 1;
+	case slope_xydirection: // xydirection
+		lua_pushangle(L, slope->xydirection);
+		return 1;
+	case slope_sourceline: // source linedef
+		LUA_PushUserdata(L, slope->sourceline, META_LINE);
+		return 1;
+	case slope_refpos: // refpos
+		lua_pushinteger(L, slope->refpos);
+		return 1;
+	case slope_flags: // flags
+		lua_pushinteger(L, slope->flags);
+		return 1;
+	}
+	return 0;
+}
+
+static int slope_set(lua_State *L)
+{
+	pslope_t *slope = *((pslope_t **)luaL_checkudata(L, 1, META_SLOPE));
+	enum slope_e field = luaL_checkoption(L, 2, slope_opt[0], slope_opt);
+
+	if (!slope)
+		return luaL_error(L, "accessed pslope_t doesn't exist anymore.");
+
+	if (hud_running)
+		return luaL_error(L, "Do not alter pslope_t in HUD rendering code!");
+
+	switch(field) // todo: reorganize this shit
+	{
+	case slope_valid: // valid
+	case slope_sourceline: // sourceline
+	case slope_d: // d
+	case slope_flags: // flags
+	case slope_normal: // normal
+	case slope_refpos: // refpos
+	default:
+		return luaL_error(L, "pslope_t field " LUA_QS " cannot be set.", slope_opt[field]);
+	case slope_o: { // o
+		luaL_checktype(L, 3, LUA_TTABLE);
+
+		lua_getfield(L, 3, "x");
+		if (lua_isnil(L, -1))
+		{
+			lua_pop(L, 1);
+			lua_rawgeti(L, 3, 1);
+		}
+		if (!lua_isnil(L, -1))
+			slope->o.x = luaL_checkfixed(L, -1);
+		else
+			slope->o.x = 0;
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "y");
+		if (lua_isnil(L, -1))
+		{
+			lua_pop(L, 1);
+			lua_rawgeti(L, 3, 2);
+		}
+		if (!lua_isnil(L, -1))
+			slope->o.y = luaL_checkfixed(L, -1);
+		else
+			slope->o.y = 0;
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "z");
+		if (lua_isnil(L, -1))
+		{
+			lua_pop(L, 1);
+			lua_rawgeti(L, 3, 3);
+		}
+		if (!lua_isnil(L, -1))
+			slope->o.z = luaL_checkfixed(L, -1);
+		else
+			slope->o.z = 0;
+		lua_pop(L, 1);
+		break;
+	}
+	case slope_zdelta: { // zdelta, this is temp until i figure out wtf to do
+		slope->zdelta = luaL_checkfixed(L, 3);
+		slope->zangle = R_PointToAngle2(0, 0, FRACUNIT, -slope->zdelta);
+		P_CalculateSlopeNormal(slope);
+		break;
+	}
+	case slope_zangle: { // zangle
+		angle_t zangle = luaL_checkangle(L, 3);
+		if (zangle == ANGLE_90 || zangle == ANGLE_270)
+			return luaL_error(L, "invalid zangle for slope!");
+		slope->zangle = zangle;
+		slope->zdelta = -FINETANGENT(((slope->zangle+ANGLE_90)>>ANGLETOFINESHIFT) & 4095);
+		P_CalculateSlopeNormal(slope);
+		break;
+	}
+	case slope_xydirection: // xydirection
+		slope->xydirection = luaL_checkangle(L, 3);
+		slope->d.x = -FINECOSINE((slope->xydirection>>ANGLETOFINESHIFT) & FINEMASK);
+		slope->d.y = -FINESINE((slope->xydirection>>ANGLETOFINESHIFT) & FINEMASK);
+		P_CalculateSlopeNormal(slope);
+		break;
+	}
+	return 0;
+}
+
+///////////////
+// vector*_t //
+///////////////
+
+static int vector2_get(lua_State *L)
+{
+	vector2_t *vec = *((vector2_t **)luaL_checkudata(L, 1, META_VECTOR2));
+	enum vector_e field = luaL_checkoption(L, 2, vector_opt[0], vector_opt);
+
+	if (!vec)
+		return luaL_error(L, "accessed vector2_t doesn't exist anymore.");
+
+	switch(field)
+	{
+		case vector_x: lua_pushfixed(L, vec->x); return 1;
+		case vector_y: lua_pushfixed(L, vec->y); return 1;
+		default: break;
+	}
+
+	return 0;
+}
+
+static int vector3_get(lua_State *L)
+{
+	vector3_t *vec = *((vector3_t **)luaL_checkudata(L, 1, META_VECTOR3));
+	enum vector_e field = luaL_checkoption(L, 2, vector_opt[0], vector_opt);
+
+	if (!vec)
+		return luaL_error(L, "accessed vector3_t doesn't exist anymore.");
+
+	switch(field)
+	{
+		case vector_x: lua_pushfixed(L, vec->x); return 1;
+		case vector_y: lua_pushfixed(L, vec->y); return 1;
+		case vector_z: lua_pushfixed(L, vec->z); return 1;
+		default: break;
+	}
+
+	return 0;
+}
+#endif
+
 /////////////////////
 // mapheaderinfo[] //
 /////////////////////
@@ -1922,6 +2192,26 @@ int LUA_MapLib(lua_State *L)
 		lua_setfield(L, -2, "__index");
 	lua_pop(L, 1);
 
+#ifdef ESLOPE
+	luaL_newmetatable(L, META_SLOPE);
+		lua_pushcfunction(L, slope_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, slope_set);
+		lua_setfield(L, -2, "__newindex");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_VECTOR2);
+		lua_pushcfunction(L, vector2_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_VECTOR3);
+		lua_pushcfunction(L, vector3_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 1);
+#endif
+
 	luaL_newmetatable(L, META_MAPHEADER);
 		lua_pushcfunction(L, mapheaderinfo_get);
 		lua_setfield(L, -2, "__index");
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index da0e99ab216510b10d7661a2bc6fc8e03ba62a60..a9db95d4b4845a48ba0bbe0b865b57bd4d78355e 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -83,7 +83,12 @@ enum mobj_e {
 	mobj_extravalue1,
 	mobj_extravalue2,
 	mobj_cusval,
+#ifdef ESLOPE
+	mobj_cvmem,
+	mobj_standingslope
+#else
 	mobj_cvmem
+#endif
 };
 
 static const char *const mobj_opt[] = {
@@ -146,6 +151,9 @@ static const char *const mobj_opt[] = {
 	"extravalue2",
 	"cusval",
 	"cvmem",
+#ifdef ESLOPE
+	"standingslope",
+#endif
 	NULL};
 
 #define UNIMPLEMENTED luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", mobj_opt[field])
@@ -358,6 +366,11 @@ static int mobj_get(lua_State *L)
 	case mobj_cvmem:
 		lua_pushinteger(L, mo->cvmem);
 		break;
+#ifdef ESLOPE
+	case mobj_standingslope:
+		LUA_PushUserdata(L, mo->standingslope, META_SLOPE);
+		break;
+#endif
 	default: // extra custom variables in Lua memory
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
@@ -675,6 +688,10 @@ static int mobj_set(lua_State *L)
 	case mobj_cvmem:
 		mo->cvmem = luaL_checkinteger(L, 3);
 		break;
+#ifdef ESLOPE
+	case mobj_standingslope:
+		return NOSET;
+#endif
 	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
diff --git a/src/lua_script.c b/src/lua_script.c
index 3225e332a8c7d5ef47fb4d1c95bd64d5f35166a1..69e275712452e75f8dc8c5793a3620d159e58e24 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -22,6 +22,9 @@
 #include "byteptr.h"
 #include "p_saveg.h"
 #include "p_local.h"
+#ifdef ESLOPE
+#include "p_slopes.h" // for P_SlopeById
+#endif
 #ifdef LUA_ALLOW_BYTECODE
 #include "d_netfil.h" // for LUA_DumpFile
 #endif
@@ -498,6 +501,9 @@ enum
 	ARCH_NODE,
 #endif
 	ARCH_FFLOOR,
+#ifdef ESLOPE
+	ARCH_SLOPE,
+#endif
 	ARCH_MAPHEADER,
 
 	ARCH_TEND=0xFF,
@@ -522,6 +528,9 @@ static const struct {
 	{META_NODE,     ARCH_NODE},
 #endif
 	{META_FFLOOR,	ARCH_FFLOOR},
+#ifdef ESLOPE
+	{META_SLOPE,    ARCH_SLOPE},
+#endif
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{NULL,          ARCH_NULL}
 };
@@ -776,6 +785,19 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
+#ifdef ESLOPE
+		case ARCH_SLOPE:
+		{
+			pslope_t *slope = *((pslope_t **)lua_touserdata(gL, myindex));
+			if (!slope)
+				WRITEUINT8(save_p, ARCH_NULL);
+			else {
+				WRITEUINT8(save_p, ARCH_SLOPE);
+				WRITEUINT16(save_p, slope->id);
+			}
+			break;
+		}
+#endif
 		case ARCH_MAPHEADER:
 		{
 			mapheader_t *header = *((mapheader_t **)lua_touserdata(gL, myindex));
@@ -997,8 +1019,13 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 			LUA_PushUserdata(gL, rover, META_FFLOOR);
 		break;
 	}
+#ifdef ESLOPE
+	case ARCH_SLOPE:
+		LUA_PushUserdata(gL, P_SlopeById(READUINT16(save_p)), META_SLOPE);
+		break;
+#endif
 	case ARCH_MAPHEADER:
-		LUA_PushUserdata(gL, &sectors[READUINT16(save_p)], META_MAPHEADER);
+		LUA_PushUserdata(gL, mapheaderinfo[READUINT16(save_p)], META_MAPHEADER);
 		break;
 	case ARCH_TEND:
 		return 1;
diff --git a/src/m_menu.c b/src/m_menu.c
index b61c6396a008c9154d29d5d2b10c2c20fa27ba74..4738006fdc11c1d83cb5f821808b2e03a4fabd50 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -6137,13 +6137,46 @@ static void M_LoadGameLevelSelect(INT32 choice)
 	M_SetupNextMenu(&SP_LevelSelectDef);
 }
 
+void M_TutorialSaveControlResponse(INT32 ch)
+{
+	if (ch == 'y' || ch == KEY_ENTER)
+	{
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_fps], gclist_tutorial, num_gclist_tutorial);
+		S_StartSound(NULL, sfx_itemup);
+	}
+	else
+		S_StartSound(NULL, sfx_menu1);
+}
+
+static void M_TutorialControlResponse(INT32 ch)
+{
+	if (ch == 'y' || ch == KEY_ENTER)
+	{
+		G_CopyControls(gamecontroldefault[gcs_custom], gamecontrol, NULL, 0);
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_fps], gclist_tutorial, num_gclist_tutorial);
+		//S_StartSound(NULL, sfx_itemup);
+	}
+	else
+		S_StartSound(NULL, sfx_menu1);
+
+	if (ch != KEY_ESCAPE)
+		M_StartTutorial(INT32_MAX);
+}
+
 // Starts up the tutorial immediately (tbh I wasn't sure where else to put this)
 static void M_StartTutorial(INT32 choice)
 {
-	(void)choice;
 	if (!tutorialmap)
 		return; // no map to go to, don't bother
 
+	if (choice != INT32_MAX && G_GetControlScheme(gamecontrol, gclist_tutorial, num_gclist_tutorial) == gcs_custom)
+	{
+		M_StartMessage("Do you want to try the \202recommended \202movement controls\x80?\n\nWe will set them just for this tutorial.\n\nPress 'Y' or 'Enter' to confirm, \nor any key to keep \nyour current controls.\n",M_TutorialControlResponse,MM_YESNO);
+		return;
+	}
+	else if (choice != INT32_MAX)
+		G_CopyControls(gamecontroldefault[gcs_custom], gamecontrol, NULL, 0);
+
 	CV_SetValue(&cv_postfirsttime, 1);
 
 	tutorialmode = true; // turn on tutorial mode
@@ -6762,7 +6795,7 @@ static void M_HandleLoadSave(INT32 choice)
 	}
 }
 
-static void M_TutorialResponse(INT32 ch)
+static void M_FirstTimeResponse(INT32 ch)
 {
 	CV_SetValue(&cv_postfirsttime, 1);
 	if (ch != 'y' && ch != KEY_ENTER)
@@ -6785,7 +6818,7 @@ static void M_LoadGame(INT32 choice)
 
 	if (tutorialmap && !cv_postfirsttime.value)
 	{
-		M_StartMessage("Do you want to play a brief Tutorial?\n(Press 'Y' to go, or 'N' to skip)", M_TutorialResponse, MM_YESNO);
+		M_StartMessage("Do you want to play a brief Tutorial?\n(Press 'Y' to go, or 'N' to skip)", M_FirstTimeResponse, MM_YESNO);
 		return;
 	}
 
diff --git a/src/m_menu.h b/src/m_menu.h
index 9df56e897690f2044951fe85d16e96f4ee36f202..ad32de1b155120b2d82cacb74a17c28b7ee51207 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -240,6 +240,8 @@ extern INT16 char_on, startchar;
 
 #define BwehHehHe() S_StartSound(NULL, sfx_bewar1+M_RandomKey(4)) // Bweh heh he
 
+void M_TutorialSaveControlResponse(INT32 ch);
+
 void M_ForceSaveSlotSelected(INT32 sslot);
 
 void M_CheatActivationResponder(INT32 ch);
diff --git a/src/m_misc.c b/src/m_misc.c
index 50b6d7a05cb93434d5b0f679c35ce679889b461b..cc9ae2999c5ab86c08ce5c5f7088d5ef3e7898bc 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -58,7 +58,7 @@ typedef off_t off64_t;
 
 #if defined(__MINGW32__) && ((__GNUC__ > 7) || (__GNUC__ == 6 && __GNUC_MINOR__ >= 3))
 #define PRIdS "u"
-#elif defined (_WIN32) 
+#elif defined (_WIN32)
 #define PRIdS "Iu"
 #elif defined (DJGPP)
 #define PRIdS "u"
@@ -475,7 +475,8 @@ void M_FirstLoadConfig(void)
 	}
 
 	// load default control
-	G_Controldefault();
+	G_DefineDefaultControls();
+	G_CopyControls(gamecontrol, gamecontroldefault[gcs_fps], NULL, 0);
 
 	// load config, make sure those commands doesnt require the screen...
 	COM_BufInsertText(va("exec \"%s\"\n", configfile));
@@ -539,7 +540,13 @@ void M_SaveConfig(const char *filename)
 	// FIXME: save key aliases if ever implemented..
 
 	CV_SaveVariables(f);
-	if (!dedicated) G_SaveKeySetting(f);
+	if (!dedicated)
+	{
+		if (tutorialmode)
+			G_SaveKeySetting(f, gamecontroldefault[gcs_custom], gamecontrolbis); // using gcs_custom as temp storage
+		else
+			G_SaveKeySetting(f, gamecontrol, gamecontrolbis);
+	}
 
 	fclose(f);
 }
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 717bb92b615301880368c9509aa7d59ab9817325..55cb9c325bbfe19330c54130a150ba8fb5b0c27c 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -3729,14 +3729,15 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 
 	if (player->pflags & PF_FLIPCAM && !(player->powers[pw_carry] == CR_NIGHTSMODE) && player->mo->eflags & MFE_VERTICALFLIP)
 		postimg = postimg_flip;
-	else if (player->awayviewtics)
+	else if (player->awayviewtics && player->awayviewmobj != NULL)	// Camera must obviously exist
 	{
 		camera_t dummycam;
 		dummycam.subsector = player->awayviewmobj->subsector;
 		dummycam.x = player->awayviewmobj->x;
 		dummycam.y = player->awayviewmobj->y;
 		dummycam.z = player->awayviewmobj->z;
-		dummycam.height = 40*FRACUNIT; // alt view height is 20*FRACUNIT
+		//dummycam.height = 40*FRACUNIT; // alt view height is 20*FRACUNIT
+		dummycam.height = 0;			 // Why? Remote viewpoint cameras have no height.
 		// Are we in water?
 		if (P_CameraCheckWater(&dummycam))
 			postimg = postimg_water;
diff --git a/src/p_slopes.c b/src/p_slopes.c
index 7c84a2db50913732a54e103caeaa1f97c0c4c77a..b7cd597aaf28d9f3bd6122415cd773db899e3c47 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -29,7 +29,7 @@ static pslope_t *slopelist = NULL;
 static UINT16 slopecount = 0;
 
 // Calculate line normal
-static void P_CalculateSlopeNormal(pslope_t *slope) {
+void P_CalculateSlopeNormal(pslope_t *slope) {
 	slope->normal.z = FINECOSINE(slope->zangle>>ANGLETOFINESHIFT);
 	slope->normal.x = -FixedMul(FINESINE(slope->zangle>>ANGLETOFINESHIFT), slope->d.x);
 	slope->normal.y = -FixedMul(FINESINE(slope->zangle>>ANGLETOFINESHIFT), slope->d.y);
diff --git a/src/p_slopes.h b/src/p_slopes.h
index f59c5b767b4dac1762a0a458c899efce590f8531..252bbdb3cd88113e3a1717658ed73c3c942275a1 100644
--- a/src/p_slopes.h
+++ b/src/p_slopes.h
@@ -14,6 +14,7 @@
 #define P_SLOPES_H__
 
 #ifdef ESLOPE
+void P_CalculateSlopeNormal(pslope_t *slope);
 void P_ResetDynamicSlopes(void);
 void P_RunDynamicSlopes(void);
 // P_SpawnSlope_Line
diff --git a/src/p_user.c b/src/p_user.c
index dd2ddbc829477cfa628a912c14625bc7bc4bad4c..57ee9169769a782bfb751ca962284d0efc8cc32a 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -9289,16 +9289,12 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	// Make player translucent if camera is too close (only in single player).
 	if (!(multiplayer || netgame) && !splitscreen)
 	{
-		fixed_t vx = 0, vy = 0;
-		if (player->awayviewtics) {
+		fixed_t vx = thiscam->x, vy = thiscam->y;
+		if (player->awayviewtics && player->awayviewmobj != NULL)		// Camera must obviously exist
+		{
 			vx = player->awayviewmobj->x;
 			vy = player->awayviewmobj->y;
 		}
-		else
-		{
-			vx = thiscam->x;
-			vy = thiscam->y;
-		}
 
 		if (P_AproxDistance(vx - mo->x, vy - mo->y) < FixedMul(48*FRACUNIT, mo->scale))
 			mo->flags2 |= MF2_SHADOW;
@@ -9621,8 +9617,9 @@ void P_PlayerThink(player_t *player)
 	if (player->flashcount)
 		player->flashcount--;
 
-	if (player->awayviewtics)
-		player->awayviewtics--;
+	// By the time P_MoveChaseCamera is called, this might be zero. Do not do it here.
+	//if (player->awayviewtics)
+	//	player->awayviewtics--;
 
 	/// \note do this in the cheat code
 	if (player->pflags & PF_NOCLIP)
@@ -10595,6 +10592,9 @@ void P_PlayerAfterThink(player_t *player)
 		}
 	}
 
+	if (player->awayviewtics)
+		player->awayviewtics--;
+
 	// spectator invisibility and nogravity.
 	if ((netgame || multiplayer) && player->spectator)
 	{
diff --git a/src/s_sound.c b/src/s_sound.c
index 1c7b00e2c0b4d5d3b51eef7d404d27963c9576ae..ada4be6df4d8912b24194bc629a35cae5bb46eb0 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -37,6 +37,7 @@ extern INT32 msg_id;
 #include "r_sky.h" // skyflatnum
 #include "p_local.h" // camera info
 #include "fastcmp.h"
+#include "m_misc.h" // for tunes command
 
 #ifdef HW3SOUND
 // 3D Sound Interface
@@ -47,6 +48,8 @@ static INT32 S_AdjustSoundParams(const mobj_t *listener, const mobj_t *source, I
 
 CV_PossibleValue_t soundvolume_cons_t[] = {{0, "MIN"}, {31, "MAX"}, {0, NULL}};
 static void SetChannelsNum(void);
+static void Command_Tunes_f(void);
+static void Command_RestartAudio_f(void);
 
 // commands for music and sound servers
 #ifdef MUSSERV
@@ -92,6 +95,7 @@ consvar_t cv_closedcaptioning = {"closedcaptioning", "Off", CV_SAVE|CV_CALL, CV_
 consvar_t cv_numChannels = {"snd_channels", "32", CV_SAVE|CV_CALL, CV_Unsigned, SetChannelsNum, 0, NULL, NULL, 0, 0, NULL};
 
 static consvar_t surround = {"surround", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_resetmusic = {"resetmusic", "No", CV_SAVE, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 #define S_MAX_VOLUME 127
 
@@ -247,6 +251,11 @@ void S_RegisterSoundStuff(void)
 #endif
 	CV_RegisterVar(&surround);
 	CV_RegisterVar(&cv_samplerate);
+	CV_RegisterVar(&cv_resetmusic);
+
+	COM_AddCommand("tunes", Command_Tunes_f);
+	COM_AddCommand("restartaudio", Command_RestartAudio_f);
+
 
 #if defined (macintosh) && !defined (HAVE_SDL) // mp3 playlist stuff
 	{
@@ -1637,3 +1646,88 @@ void S_Start(void)
 		S_StopMusic();
 	S_ChangeMusic(mapmusname, mapmusflags, true);
 }
+
+static void Command_Tunes_f(void)
+{
+	const char *tunearg;
+	UINT16 tunenum, track = 0;
+	const size_t argc = COM_Argc();
+
+	if (argc < 2) //tunes slot ...
+	{
+		CONS_Printf("tunes <name/num> [track] [speed] / <-show> / <-default> / <-none>:\n");
+		CONS_Printf(M_GetText("Play an arbitrary music lump. If a map number is used, 'MAP##M' is played.\n"));
+		CONS_Printf(M_GetText("If the format supports multiple songs, you can specify which one to play.\n\n"));
+		CONS_Printf(M_GetText("* With \"-show\", shows the currently playing tune and track.\n"));
+		CONS_Printf(M_GetText("* With \"-default\", returns to the default music for the map.\n"));
+		CONS_Printf(M_GetText("* With \"-none\", any music playing will be stopped.\n"));
+		return;
+	}
+
+	tunearg = COM_Argv(1);
+	tunenum = (UINT16)atoi(tunearg);
+	track = 0;
+
+	if (!strcasecmp(tunearg, "-show"))
+	{
+		CONS_Printf(M_GetText("The current tune is: %s [track %d]\n"),
+			mapmusname, (mapmusflags & MUSIC_TRACKMASK));
+		return;
+	}
+	if (!strcasecmp(tunearg, "-none"))
+	{
+		S_StopMusic();
+		return;
+	}
+	else if (!strcasecmp(tunearg, "-default"))
+	{
+		tunearg = mapheaderinfo[gamemap-1]->musname;
+		track = mapheaderinfo[gamemap-1]->mustrack;
+	}
+	else if (!tunearg[2] && toupper(tunearg[0]) >= 'A' && toupper(tunearg[0]) <= 'Z')
+		tunenum = (UINT16)M_MapNumber(tunearg[0], tunearg[1]);
+
+	if (tunenum && tunenum >= 1036)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Valid music slots are 1 to 1035.\n"));
+		return;
+	}
+	if (!tunenum && strlen(tunearg) > 6) // This is automatic -- just show the error just in case
+		CONS_Alert(CONS_NOTICE, M_GetText("Music name too long - truncated to six characters.\n"));
+
+	if (argc > 2)
+		track = (UINT16)atoi(COM_Argv(2))-1;
+
+	if (tunenum)
+		snprintf(mapmusname, 7, "%sM", G_BuildMapName(tunenum));
+	else
+		strncpy(mapmusname, tunearg, 7);
+	mapmusname[6] = 0;
+	mapmusflags = (track & MUSIC_TRACKMASK);
+
+	S_ChangeMusic(mapmusname, mapmusflags, true);
+
+	if (argc > 3)
+	{
+		float speed = (float)atof(COM_Argv(3));
+		if (speed > 0.0f)
+			S_SpeedMusic(speed);
+	}
+}
+
+static void Command_RestartAudio_f(void)
+{
+	S_StopMusic();
+	S_StopSounds();
+	I_ShutdownMusic();
+	I_ShutdownSound();
+	I_StartupSound();
+	I_InitMusic();
+
+// These must be called or no sound and music until manually set.
+
+	I_SetSfxVolume(cv_soundvolume.value);
+	S_SetMusicVolume(cv_digmusicvolume.value, cv_midimusicvolume.value);
+	if (Playing()) // Gotta make sure the player is in a level
+		P_RestoreMusic(&players[consoleplayer]);
+}
diff --git a/src/s_sound.h b/src/s_sound.h
index c94ae6652ff12075e852f232f2b4baaa9b43b6ef..0fcaace5e527b99058789cc3457b92e0906f19dd 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -26,6 +26,7 @@
 extern consvar_t stereoreverse;
 extern consvar_t cv_soundvolume, cv_closedcaptioning, cv_digmusicvolume, cv_midimusicvolume;
 extern consvar_t cv_numChannels;
+extern consvar_t cv_resetmusic;
 
 #ifdef SNDSERV
 extern consvar_t sndserver_cmd, sndserver_arg;
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index eb851a619f2f3d2a8f4f67003aa0303c9d785fd8..e917515f557b5229b4efa97f717997389ccc9013 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -1,8 +1,11 @@
 // Emacs style mode select   -*- C++ -*-
+//
+// SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Portions Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 2014-2018 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 48bb61649c9176f363bcef7bfff4eb739362dea2..adffa5542f232d5ca7396c9d4ee2a921676605a7 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -1,8 +1,10 @@
 // Emacs style mode select   -*- C++ -*-
+// SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Portions Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 2014-2018 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 4d86d7a3cefb9624a96c5ef76f84bfb546138566..bbd1fcee4597be4ec8240760f2ac9d053222e95d 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -1,3 +1,11 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2014-2018 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
 /// \brief SDL Mixer interface for sound
 
diff --git a/src/v_video.c b/src/v_video.c
index 2f84d4c7e6339e9d28f3a0f564d00c6e3f80416d..c6b7de96541e1375bf8aa6507a7e206f8f924308 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -532,7 +532,6 @@ void V_DrawFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_t
 {
 	UINT8 (*patchdrawfunc)(const UINT8*, const UINT8*, fixed_t);
 	UINT32 alphalevel = 0;
-	boolean flip = false;
 
 	fixed_t col, ofs, colfrac, rowfrac, fdup;
 	INT32 dupx, dupy;
@@ -610,22 +609,32 @@ void V_DrawFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_t
 	colfrac = FixedDiv(FRACUNIT, fdup);
 	rowfrac = FixedDiv(FRACUNIT, fdup);
 
-	if (scrn & V_OFFSET) // Crosshair shit
+	// So it turns out offsets aren't scaled in V_NOSCALESTART unless V_OFFSET is applied ...poo, that's terrible
+	// For now let's just at least give V_OFFSET the ability to support V_FLIP
+	// I'll probably make a better fix for 2.2 where I don't have to worry about breaking existing support for stuff
+	// -- Monster Iestyn 29/10/18
 	{
-		y -= FixedMul((SHORT(patch->topoffset)*dupy)<<FRACBITS,  pscale);
-		x -= FixedMul((SHORT(patch->leftoffset)*dupx)<<FRACBITS, pscale);
-	}
-	else
-	{
-		y -= FixedMul(SHORT(patch->topoffset)<<FRACBITS, pscale);
+		fixed_t offsetx = 0, offsety = 0;
 
+		// left offset
 		if (scrn & V_FLIP)
+			offsetx = FixedMul((SHORT(patch->width) - SHORT(patch->leftoffset))<<FRACBITS, pscale) + 1;
+		else
+			offsetx = FixedMul(SHORT(patch->leftoffset)<<FRACBITS, pscale);
+
+		// top offset
+		// TODO: make some kind of vertical version of V_FLIP, maybe by deprecating V_OFFSET in future?!?
+		offsety = FixedMul(SHORT(patch->topoffset)<<FRACBITS, pscale);
+
+		if ((scrn & (V_NOSCALESTART|V_OFFSET)) == (V_NOSCALESTART|V_OFFSET)) // Multiply by dupx/dupy for crosshairs
 		{
-			flip = true;
-			x -= FixedMul((SHORT(patch->width) - SHORT(patch->leftoffset))<<FRACBITS, pscale) + 1;
+			offsetx = FixedMul(offsetx, dupx<<FRACBITS);
+			offsety = FixedMul(offsety, dupy<<FRACBITS);
 		}
-		else
-			x -= FixedMul(SHORT(patch->leftoffset)<<FRACBITS, pscale);
+
+		// Subtract the offsets from x/y positions
+		x -= offsetx;
+		y -= offsety;
 	}
 
 	if (splitscreen && (scrn & V_PERPLAYER))
@@ -774,7 +783,7 @@ void V_DrawFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_t
 	for (col = 0; (col>>FRACBITS) < SHORT(patch->width); col += colfrac, ++offx, desttop++)
 	{
 		INT32 topdelta, prevdelta = -1;
-		if (flip) // offx is measured from right edge instead of left
+		if (scrn & V_FLIP) // offx is measured from right edge instead of left
 		{
 			if (x+pwidth-offx < 0) // don't draw off the left of the screen (WRAP PREVENTION)
 				break;
@@ -798,7 +807,7 @@ void V_DrawFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_t
 			prevdelta = topdelta;
 			source = (const UINT8 *)(column) + 3;
 			dest = desttop;
-			if (flip)
+			if (scrn & V_FLIP)
 				dest = deststart + (destend - desttop);
 			dest += FixedInt(FixedMul(topdelta<<FRACBITS,fdup))*vid.width;
 
diff --git a/src/w_wad.c b/src/w_wad.c
index 3112e02a0f182e4cebc67122790163812267bbaf..9056165422d044b706adeceeb31d5a77d2541e59 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -389,6 +389,8 @@ UINT16 W_InitFile(const char *filename)
 		if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), filename);
+			if (handle)
+				fclose(handle);
 			return INT16_MAX;
 		}
 	}
@@ -634,6 +636,8 @@ UINT16 W_InitFile(const char *filename)
 		if (fread(&header, 1, sizeof header, handle) < sizeof header)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("Can't read wad header from %s because %s\n"), filename, strerror(ferror(handle)));
+			if (handle)
+				fclose(handle);
 			return INT16_MAX;
 		}
 
@@ -644,6 +648,8 @@ UINT16 W_InitFile(const char *filename)
 			&& memcmp(header.identification, "SDLL", 4) != 0)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("%s does not have a valid WAD header\n"), filename);
+			if (handle)
+				fclose(handle);
 			return INT16_MAX;
 		}
 
@@ -658,6 +664,8 @@ UINT16 W_InitFile(const char *filename)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("Wadfile directory in %s is corrupted (%s)\n"), filename, strerror(ferror(handle)));
 			free(fileinfov);
+			if (handle)
+				fclose(handle);
 			return INT16_MAX;
 		}