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/Makefile b/src/Makefile
index f2d532cd5e9b75e3b559ff02747ad5b7652c5d9d..c33e42daad30003c53b223f4d44d8f81fa718312 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -64,6 +64,7 @@
 #     Compile without 3D sound support, add 'NOHS=1'
 #     Compile with GDBstubs, add 'RDB=1'
 #     Compile without PNG, add 'NOPNG=1'
+#     Compile without zlib, add 'NOZLIB=1'
 #
 # Addon for SDL:
 #     To Cross-Compile, add 'SDL_CONFIG=/usr/*/bin/sdl-config'
@@ -107,6 +108,7 @@ include Makefile.cfg
 
 ifdef DUMMY
 NOPNG=1
+NOZLIB=1
 NONET=1
 NOHW=1
 NOHS=1
@@ -269,13 +271,6 @@ LIBS+=$(PNG_LDFLAGS)
 CFLAGS+=$(PNG_CFLAGS)
 endif
 
-ZLIB_PKGCONFIG?=zlib
-ZLIB_CFLAGS?=$(shell $(PKG_CONFIG) $(ZLIB_PKGCONFIG) --cflags)
-ZLIB_LDFLAGS?=$(shell $(PKG_CONFIG) $(ZLIB_PKGCONFIG) --libs)
-
-LIBS+=$(ZLIB_LDFLAGS)
-CFLAGS+=$(ZLIB_CFLAGS)
-
 ifdef HAVE_LIBGME
 OPTS+=-DHAVE_LIBGME
 
@@ -287,6 +282,18 @@ LIBS+=$(LIBGME_LDFLAGS)
 CFLAGS+=$(LIBGME_CFLAGS)
 endif
 
+ifndef NOZLIB
+OPTS+=-DHAVE_ZLIB
+ZLIB_PKGCONFIG?=zlib
+ZLIB_CFLAGS?=$(shell $(PKG_CONFIG) $(ZLIB_PKGCONFIG) --cflags)
+ZLIB_LDFLAGS?=$(shell $(PKG_CONFIG) $(ZLIB_PKGCONFIG) --libs)
+
+LIBS+=$(ZLIB_LDFLAGS)
+CFLAGS+=$(ZLIB_CFLAGS)
+else
+NOPNG=1
+endif
+
 ifdef STATIC
 LIBS:=-static $(LIBS)
 endif
diff --git a/src/android/i_sound.c b/src/android/i_sound.c
index d7a7973d402550c6184c5b6597f70529608de58e..b5a1c364618fbde5b8142eb74f7541ff2ecd18be 100644
--- a/src/android/i_sound.c
+++ b/src/android/i_sound.c
@@ -21,13 +21,14 @@ void I_ShutdownSound(void){}
 //  SFX I/O
 //
 
-INT32 I_StartSound(sfxenum_t id, INT32 vol, INT32 sep, INT32 pitch, INT32 priority)
+INT32 I_StartSound(sfxenum_t id, INT32 vol, INT32 sep, INT32 pitch, INT32 priority, INT32 channel)
 {
         (void)id;
         (void)vol;
         (void)sep;
         (void)pitch;
         (void)priority;
+        (void)channel;
         return -1;
 }
 
diff --git a/src/byteptr.h b/src/byteptr.h
index 410d7c00442f77767a436a9f252b8082f67aa818..364e6520cf20c186f18cd47fd4e820204de0b16f 100644
--- a/src/byteptr.h
+++ b/src/byteptr.h
@@ -15,7 +15,9 @@
 #define DEALIGNED
 #endif
 
-#ifndef _BIG_ENDIAN
+#include "endian.h"
+
+#ifndef SRB2_BIG_ENDIAN
 //
 // Little-endian machines
 //
@@ -75,7 +77,7 @@
 #define READANGLE(p)        *((angle_t *)p)++
 #endif
 
-#else //_BIG_ENDIAN
+#else //SRB2_BIG_ENDIAN
 //
 // definitions for big-endian machines with alignment constraints.
 //
@@ -144,7 +146,7 @@ FUNCINLINE static ATTRINLINE UINT32 readulong(void *ptr)
 #define READCHAR(p)         ({    char *p_tmp = (   char *)p;    char b =        *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
 #define READFIXED(p)        ({ fixed_t *p_tmp = (fixed_t *)p; fixed_t b =   readlong(p); p_tmp++; p = (void *)p_tmp; b; })
 #define READANGLE(p)        ({ angle_t *p_tmp = (angle_t *)p; angle_t b =  readulong(p); p_tmp++; p = (void *)p_tmp; b; })
-#endif //_BIG_ENDIAN
+#endif //SRB2_BIG_ENDIAN
 
 #undef DEALIGNED
 
diff --git a/src/command.c b/src/command.c
index 62431b6647b5185f2d7596c93155962970e7ef65..10af3a57650e80cd8e693a60b52d8e7226e0515f 100644
--- a/src/command.c
+++ b/src/command.c
@@ -63,6 +63,7 @@ CV_PossibleValue_t CV_Unsigned[] = {{0, "MIN"}, {999999999, "MAX"}, {0, NULL}};
 CV_PossibleValue_t CV_Natural[] = {{1, "MIN"}, {999999999, "MAX"}, {0, NULL}};
 
 #define COM_BUF_SIZE 8192 // command buffer size
+#define MAX_ALIAS_RECURSION 100 // max recursion allowed for aliases
 
 static INT32 com_wait; // one command per frame (for cmd sequences)
 
@@ -485,6 +486,7 @@ static void COM_ExecuteString(char *ptext)
 {
 	xcommand_t *cmd;
 	cmdalias_t *a;
+	static INT32 recursion = 0; // detects recursion and stops it if it goes too far
 
 	COM_TokenizeString(ptext);
 
@@ -497,6 +499,7 @@ static void COM_ExecuteString(char *ptext)
 	{
 		if (!stricmp(com_argv[0], cmd->name)) //case insensitive now that we have lower and uppercase!
 		{
+			recursion = 0;
 			cmd->function();
 			return;
 		}
@@ -507,11 +510,20 @@ static void COM_ExecuteString(char *ptext)
 	{
 		if (!stricmp(com_argv[0], a->name))
 		{
+			if (recursion > MAX_ALIAS_RECURSION)
+			{
+				CONS_Alert(CONS_WARNING, M_GetText("Alias recursion cycle detected!\n"));
+				recursion = 0;
+				return;
+			}
+			recursion++;
 			COM_BufInsertText(a->value);
 			return;
 		}
 	}
 
+	recursion = 0;
+
 	// check cvars
 	// Hurdler: added at Ebola's request ;)
 	// (don't flood the console in software mode with bad gr_xxx command)
diff --git a/src/console.c b/src/console.c
index 9bc01cf19e07be4a0a0d6039ded3028318a528bb..f4234d949b879f7e0a69c9cf833900bbf74a2cbf 100644
--- a/src/console.c
+++ b/src/console.c
@@ -232,18 +232,20 @@ UINT8 *yellowmap, *magentamap, *lgreenmap, *bluemap, *graymap, *redmap, *orangem
 
 // Console BG color
 UINT8 *consolebgmap = NULL;
+UINT8 *promptbgmap = NULL;
+static UINT8 promptbgcolor = UINT8_MAX;
 
-void CON_SetupBackColormap(void)
+void CON_SetupBackColormapEx(INT32 color, boolean prompt)
 {
 	UINT16 i, palsum;
 	UINT8 j, palindex, shift;
 	UINT8 *pal = W_CacheLumpName(GetPalette(), PU_CACHE);
 
-	if (!consolebgmap)
-		consolebgmap = (UINT8 *)Z_Malloc(256, PU_STATIC, NULL);
+	if (color == INT32_MAX)
+		color = cons_backcolor.value;
 
 	shift = 6; // 12 colors -- shift of 7 means 6 colors
-	switch (cons_backcolor.value)
+	switch (color)
 	{
 		case 0:		palindex = 15; 	break; // White
 		case 1:		palindex = 31;	break; // Gray
@@ -257,20 +259,42 @@ void CON_SetupBackColormap(void)
 		case 9:		palindex = 187;	break; // Magenta
 		case 10:	palindex = 139;	break; // Aqua
 		// Default green
-		default:	palindex = 175; break;
-}
+		default:	palindex = 175; color = 11; break;
+	}
+
+	if (prompt)
+	{
+		if (!promptbgmap)
+			promptbgmap = (UINT8 *)Z_Malloc(256, PU_STATIC, NULL);
+
+		if (color == promptbgcolor)
+			return;
+		else
+			promptbgcolor = color;
+	}
+	else if (!consolebgmap)
+		consolebgmap = (UINT8 *)Z_Malloc(256, PU_STATIC, NULL);
 
 	// setup background colormap
 	for (i = 0, j = 0; i < 768; i += 3, j++)
 	{
 		palsum = (pal[i] + pal[i+1] + pal[i+2]) >> shift;
-		consolebgmap[j] = (UINT8)(palindex - palsum);
+		if (prompt)
+			promptbgmap[j] = (UINT8)(palindex - palsum);
+		else
+			consolebgmap[j] = (UINT8)(palindex - palsum);
 	}
 }
 
+void CON_SetupBackColormap(void)
+{
+	CON_SetupBackColormapEx(cons_backcolor.value, false);
+	CON_SetupBackColormapEx(1, true); // default to gray
+}
+
 static void CONS_backcolor_Change(void)
 {
-	CON_SetupBackColormap();
+	CON_SetupBackColormapEx(cons_backcolor.value, false);
 }
 
 static void CON_SetupColormaps(void)
diff --git a/src/console.h b/src/console.h
index 970f841d0e8a950cc00d91fdd5d9517f13867c98..c194f44bf05b27a238058a4fbbb37c1984c2c45e 100644
--- a/src/console.h
+++ b/src/console.h
@@ -38,7 +38,9 @@ extern UINT8 *yellowmap, *magentamap, *lgreenmap, *bluemap, *graymap, *redmap, *
 
 // Console bg color (auto updated to match)
 extern UINT8 *consolebgmap;
+extern UINT8 *promptbgmap;
 
+void CON_SetupBackColormapEx(INT32 color, boolean prompt);
 void CON_SetupBackColormap(void);
 void CON_ClearHUD(void); // clear heads up messages
 
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 74d7236a51322b8ca889cf38c912e1116393f202..282ab4ae91996e3416735007c1f16c7e8bc16dec 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"
@@ -151,7 +152,7 @@ void D_PostEvent(const event_t *ev)
 	eventhead = (eventhead+1) & (MAXEVENTS-1);
 }
 // just for lock this function
-#ifndef DOXYGEN
+#if defined (PC_DOS) && !defined (DOXYGEN)
 void D_PostEvent_end(void) {};
 #endif
 
@@ -424,6 +425,7 @@ static void D_Display(void)
 		if (gamestate == GS_LEVEL)
 		{
 			ST_Drawer();
+			F_TextPromptDrawer();
 			HU_Drawer();
 		}
 		else
@@ -733,6 +735,19 @@ void D_StartTitle(void)
 	// Reset the palette
 	if (rendermode != render_none)
 		V_SetPaletteLump("PLAYPAL");
+
+	// The title screen is obviously not a tutorial! (Unless I'm mistaken)
+	if (tutorialmode && tutorialgcs)
+	{
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gcl_tutorial_full, num_gcl_tutorial_full); // using gcs_custom as temp storage
+		CV_SetValue(&cv_usemouse, tutorialusemouse);
+		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
+		CV_SetValue(&cv_mousemove, tutorialmousemove);
+		CV_SetValue(&cv_analog, tutorialanalog);
+		M_StartMessage("Do you want to \x82save the recommended \x82movement controls?\x80\n\nPress 'Y' or 'Enter' to confirm\nPress 'N' or any key to keep \nyour current controls",
+			M_TutorialSaveControlResponse, MM_YESNO);
+	}
+	tutorialmode = false;
 }
 
 //
@@ -766,10 +781,6 @@ static inline void D_CleanFile(void)
 	}
 }
 
-#ifndef _MAX_PATH
-#define _MAX_PATH MAX_WADPATH
-#endif
-
 // ==========================================================================
 // Identify the SRB2 version, and IWAD file to use.
 // ==========================================================================
diff --git a/src/d_main.h b/src/d_main.h
index d73b19d1f6e5fd34ab5fd0470a5c762379c6fd8b..4c9c99ea5277adf90224fdf9d42a2e1b1754c73e 100644
--- a/src/d_main.h
+++ b/src/d_main.h
@@ -40,8 +40,8 @@ void D_SRB2Main(void);
 
 // Called by IO functions when input is detected.
 void D_PostEvent(const event_t *ev);
-#ifndef DOXYGEN
-FUNCMATH void D_PostEvent_end(void);    // delimiter for locking memory
+#if defined (PC_DOS) && !defined (DOXYGEN)
+void D_PostEvent_end(void);    // delimiter for locking memory
 #endif
 
 void D_ProcessEvents(void);
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 4be1da776e0a13b8b97e158c06f7ab7cbb172395..bb33eb133ed5df2f2cb16481253e78824f3f8a82 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);
@@ -719,6 +712,7 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_crosshair2);
 	CV_RegisterVar(&cv_alwaysfreelook);
 	CV_RegisterVar(&cv_alwaysfreelook2);
+	CV_RegisterVar(&cv_tutorialprompt);
 
 	// g_input.c
 	CV_RegisterVar(&cv_sideaxis);
@@ -1815,6 +1809,16 @@ static void Command_Map_f(void)
 	else
 		fromlevelselect = ((netgame || multiplayer) && ((gametype == newgametype) && (newgametype == GT_COOP)));
 
+	if (tutorialmode && tutorialgcs)
+	{
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gcl_tutorial_full, num_gcl_tutorial_full); // using gcs_custom as temp storage
+		CV_SetValue(&cv_usemouse, tutorialusemouse);
+		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
+		CV_SetValue(&cv_mousemove, tutorialmousemove);
+		CV_SetValue(&cv_analog, tutorialanalog);
+	}
+	tutorialmode = false; // warping takes us out of tutorial mode
+
 	D_MapChange(newmapnum, newgametype, false, newresetplayers, 0, false, fromlevelselect);
 }
 
@@ -3987,100 +3991,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;
-	UINT32 position = 0;
-	const size_t argc = COM_Argc();
-
-	if (argc < 2) //tunes slot ...
-	{
-		CONS_Printf("tunes <name/num> [track] [speed] [position] / <-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);
-
-	if (argc > 4)
-		position = (UINT32)atoi(COM_Argv(4));
-
-	mapmusname[6] = 0;
-	mapmusflags = (track & MUSIC_TRACKMASK);
-	mapmusposition = position;
-
-	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
-
-	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 71a36cfdf7bab3a95b65079f62b7c7c1b6efb441..7e246201d4276598916fb4582be3131f32941c19 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"
 
@@ -165,9 +166,14 @@ static char *myhashfgets(char *buf, size_t bufsize, MYFILE *f)
 		if (c == '\n') // Ensure debug line is right...
 			dbg_line++;
 		if (c == '#')
+		{
+			if (i > 0) // don't let i wrap past 0
+				i--; // don't include hash char in string
 			break;
+		}
 	}
-	i++;
+	if (buf[i] != '#') // don't include hash char in string
+		i++;
 	buf[i] = '\0';
 
 	return buf;
@@ -914,7 +920,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
@@ -1560,6 +1569,365 @@ static void readcutscene(MYFILE *f, INT32 num)
 	Z_Free(s);
 }
 
+static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
+{
+	char *s = Z_Calloc(MAXLINELEN, PU_STATIC, NULL);
+	char *word;
+	char *word2;
+	INT32 i;
+	UINT16 usi;
+	UINT8 picid;
+
+	do
+	{
+		if (myfgets(s, MAXLINELEN, f))
+		{
+			if (s[0] == '\n')
+				break;
+
+			word = strtok(s, " ");
+			if (word)
+				strupr(word);
+			else
+				break;
+
+			if (fastcmp(word, "PAGETEXT"))
+			{
+				char *pagetext = NULL;
+				char *buffer;
+				const int bufferlen = 4096;
+
+				for (i = 0; i < MAXLINELEN; i++)
+				{
+					if (s[i] == '=')
+					{
+						pagetext = &s[i+2];
+						break;
+					}
+				}
+
+				if (!pagetext)
+				{
+					Z_Free(textprompts[num]->page[pagenum].text);
+					textprompts[num]->page[pagenum].text = NULL;
+					continue;
+				}
+
+				for (i = 0; i < MAXLINELEN; i++)
+				{
+					if (s[i] == '\0')
+					{
+						s[i] = '\n';
+						s[i+1] = '\0';
+						break;
+					}
+				}
+
+				buffer = Z_Malloc(4096, PU_STATIC, NULL);
+				strcpy(buffer, pagetext);
+
+				// \todo trim trailing whitespace before the #
+				// and also support # at the end of a PAGETEXT with no line break
+
+				strcat(buffer,
+					myhashfgets(pagetext, bufferlen
+					- strlen(buffer) - 1, f));
+
+				// A text prompt overwriting another one...
+				Z_Free(textprompts[num]->page[pagenum].text);
+
+				textprompts[num]->page[pagenum].text = Z_StrDup(buffer);
+
+				Z_Free(buffer);
+
+				continue;
+			}
+
+			word2 = strtok(NULL, " = ");
+			if (word2)
+				strupr(word2);
+			else
+				break;
+
+			if (word2[strlen(word2)-1] == '\n')
+				word2[strlen(word2)-1] = '\0';
+			i = atoi(word2);
+			usi = (UINT16)i;
+
+			// copypasta from readcutscenescene
+			if (fastcmp(word, "NUMBEROFPICS"))
+			{
+				textprompts[num]->page[pagenum].numpics = (UINT8)i;
+			}
+			else if (fastcmp(word, "PICMODE"))
+			{
+				UINT8 picmode = 0; // PROMPT_PIC_PERSIST
+				if (usi == 1 || word2[0] == 'L') picmode = PROMPT_PIC_LOOP;
+				else if (usi == 2 || word2[0] == 'D' || word2[0] == 'H') picmode = PROMPT_PIC_DESTROY;
+				textprompts[num]->page[pagenum].picmode = picmode;
+			}
+			else if (fastcmp(word, "PICTOLOOP"))
+				textprompts[num]->page[pagenum].pictoloop = (UINT8)i;
+			else if (fastcmp(word, "PICTOSTART"))
+				textprompts[num]->page[pagenum].pictostart = (UINT8)i;
+			else if (fastcmp(word, "PICSMETAPAGE"))
+			{
+				if (usi && usi <= textprompts[num]->numpages)
+				{
+					UINT8 metapagenum = usi - 1;
+
+					textprompts[num]->page[pagenum].numpics = textprompts[num]->page[metapagenum].numpics;
+					textprompts[num]->page[pagenum].picmode = textprompts[num]->page[metapagenum].picmode;
+					textprompts[num]->page[pagenum].pictoloop = textprompts[num]->page[metapagenum].pictoloop;
+					textprompts[num]->page[pagenum].pictostart = textprompts[num]->page[metapagenum].pictostart;
+
+					for (picid = 0; picid < MAX_PROMPT_PICS; picid++)
+					{
+						strncpy(textprompts[num]->page[pagenum].picname[picid], textprompts[num]->page[metapagenum].picname[picid], 8);
+						textprompts[num]->page[pagenum].pichires[picid] = textprompts[num]->page[metapagenum].pichires[picid];
+						textprompts[num]->page[pagenum].picduration[picid] = textprompts[num]->page[metapagenum].picduration[picid];
+						textprompts[num]->page[pagenum].xcoord[picid] = textprompts[num]->page[metapagenum].xcoord[picid];
+						textprompts[num]->page[pagenum].ycoord[picid] = textprompts[num]->page[metapagenum].ycoord[picid];
+					}
+				}
+			}
+			else if (fastncmp(word, "PIC", 3))
+			{
+				picid = (UINT8)atoi(word + 3);
+				if (picid > MAX_PROMPT_PICS || picid == 0)
+				{
+					deh_warning("textpromptscene %d: unknown word '%s'", num, word);
+					continue;
+				}
+				--picid;
+
+				if (fastcmp(word+4, "NAME"))
+				{
+					strncpy(textprompts[num]->page[pagenum].picname[picid], word2, 8);
+				}
+				else if (fastcmp(word+4, "HIRES"))
+				{
+					textprompts[num]->page[pagenum].pichires[picid] = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
+				}
+				else if (fastcmp(word+4, "DURATION"))
+				{
+					textprompts[num]->page[pagenum].picduration[picid] = usi;
+				}
+				else if (fastcmp(word+4, "XCOORD"))
+				{
+					textprompts[num]->page[pagenum].xcoord[picid] = usi;
+				}
+				else if (fastcmp(word+4, "YCOORD"))
+				{
+					textprompts[num]->page[pagenum].ycoord[picid] = usi;
+				}
+				else
+					deh_warning("textpromptscene %d: unknown word '%s'", num, word);
+			}
+			else if (fastcmp(word, "MUSIC"))
+			{
+				strncpy(textprompts[num]->page[pagenum].musswitch, word2, 7);
+				textprompts[num]->page[pagenum].musswitch[6] = 0;
+			}
+#ifdef MUSICSLOT_COMPATIBILITY
+			else if (fastcmp(word, "MUSICSLOT"))
+			{
+				i = get_mus(word2, true);
+				if (i && i <= 1035)
+					snprintf(textprompts[num]->page[pagenum].musswitch, 7, "%sM", G_BuildMapName(i));
+				else if (i && i <= 1050)
+					strncpy(textprompts[num]->page[pagenum].musswitch, compat_special_music_slots[i - 1036], 7);
+				else
+					textprompts[num]->page[pagenum].musswitch[0] = 0; // becomes empty string
+				textprompts[num]->page[pagenum].musswitch[6] = 0;
+			}
+#endif
+			else if (fastcmp(word, "MUSICTRACK"))
+			{
+				textprompts[num]->page[pagenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
+			}
+			else if (fastcmp(word, "MUSICLOOP"))
+			{
+				textprompts[num]->page[pagenum].musicloop = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
+			}
+			// end copypasta from readcutscenescene
+			else if (fastcmp(word, "NAME"))
+			{
+				INT32 j;
+
+				// HACK: Add yellow control char now
+				// so the drawing function doesn't call it repeatedly
+				char name[34];
+				name[0] = '\x82'; // color yellow
+				name[1] = 0;
+				strncat(name, word2, 33);
+				name[33] = 0;
+
+				// Replace _ with ' '
+				for (j = 0; j < 32 && name[j]; j++)
+				{
+					if (name[j] == '_')
+						name[j] = ' ';
+				}
+
+				strncpy(textprompts[num]->page[pagenum].name, name, 32);
+			}
+			else if (fastcmp(word, "ICON"))
+				strncpy(textprompts[num]->page[pagenum].iconname, word2, 8);
+			else if (fastcmp(word, "ICONALIGN"))
+				textprompts[num]->page[pagenum].rightside = (i || word2[0] == 'R');
+			else if (fastcmp(word, "ICONFLIP"))
+				textprompts[num]->page[pagenum].iconflip = (i || word2[0] == 'T' || word2[0] == 'Y');
+			else if (fastcmp(word, "LINES"))
+				textprompts[num]->page[pagenum].lines = usi;
+			else if (fastcmp(word, "BACKCOLOR"))
+			{
+				INT32 backcolor;
+				if      (i == 0 || fastcmp(word2, "WHITE")) backcolor = 0;
+				else if (i == 1 || fastcmp(word2, "GRAY") || fastcmp(word2, "GREY") ||
+					fastcmp(word2, "BLACK")) backcolor = 1;
+				else if (i == 2 || fastcmp(word2, "BROWN")) backcolor = 2;
+				else if (i == 3 || fastcmp(word2, "RED")) backcolor = 3;
+				else if (i == 4 || fastcmp(word2, "ORANGE")) backcolor = 4;
+				else if (i == 5 || fastcmp(word2, "YELLOW")) backcolor = 5;
+				else if (i == 6 || fastcmp(word2, "GREEN")) backcolor = 6;
+				else if (i == 7 || fastcmp(word2, "BLUE")) backcolor = 7;
+				else if (i == 8 || fastcmp(word2, "PURPLE")) backcolor = 8;
+				else if (i == 9 || fastcmp(word2, "MAGENTA")) backcolor = 9;
+				else if (i == 10 || fastcmp(word2, "AQUA")) backcolor = 10;
+				else if (i < 0) backcolor = INT32_MAX; // CONS_BACKCOLOR user-configured
+				else backcolor = 1; // default gray
+				textprompts[num]->page[pagenum].backcolor = backcolor;
+			}
+			else if (fastcmp(word, "ALIGN"))
+			{
+				UINT8 align = 0; // left
+				if (usi == 1 || word2[0] == 'R') align = 1;
+				else if (usi == 2 || word2[0] == 'C' || word2[0] == 'M') align = 2;
+				textprompts[num]->page[pagenum].align = align;
+			}
+			else if (fastcmp(word, "VERTICALALIGN"))
+			{
+				UINT8 align = 0; // top
+				if (usi == 1 || word2[0] == 'B') align = 1;
+				else if (usi == 2 || word2[0] == 'C' || word2[0] == 'M') align = 2;
+				textprompts[num]->page[pagenum].verticalalign = align;
+			}
+			else if (fastcmp(word, "TEXTSPEED"))
+				textprompts[num]->page[pagenum].textspeed = get_number(word2);
+			else if (fastcmp(word, "TEXTSFX"))
+				textprompts[num]->page[pagenum].textsfx = get_number(word2);
+			else if (fastcmp(word, "HIDEHUD"))
+			{
+				UINT8 hidehud = 0;
+				if ((word2[0] == 'F' && (word2[1] == 'A' || !word2[1])) || word2[0] == 'N') hidehud = 0; // false
+				else if (usi == 1 || word2[0] == 'T' || word2[0] == 'Y') hidehud = 1; // true (hide appropriate HUD elements)
+				else if (usi == 2 || word2[0] == 'A' || (word2[0] == 'F' && word2[1] == 'O')) hidehud = 2; // force (hide all HUD elements)
+				textprompts[num]->page[pagenum].hidehud = hidehud;
+			}
+			else if (fastcmp(word, "METAPAGE"))
+			{
+				if (usi && usi <= textprompts[num]->numpages)
+				{
+					UINT8 metapagenum = usi - 1;
+
+					strncpy(textprompts[num]->page[pagenum].name, textprompts[num]->page[metapagenum].name, 32);
+					strncpy(textprompts[num]->page[pagenum].iconname, textprompts[num]->page[metapagenum].iconname, 8);
+					textprompts[num]->page[pagenum].rightside = textprompts[num]->page[metapagenum].rightside;
+					textprompts[num]->page[pagenum].iconflip = textprompts[num]->page[metapagenum].iconflip;
+					textprompts[num]->page[pagenum].lines = textprompts[num]->page[metapagenum].lines;
+					textprompts[num]->page[pagenum].backcolor = textprompts[num]->page[metapagenum].backcolor;
+					textprompts[num]->page[pagenum].align = textprompts[num]->page[metapagenum].align;
+					textprompts[num]->page[pagenum].verticalalign = textprompts[num]->page[metapagenum].verticalalign;
+					textprompts[num]->page[pagenum].textspeed = textprompts[num]->page[metapagenum].textspeed;
+					textprompts[num]->page[pagenum].textsfx = textprompts[num]->page[metapagenum].textsfx;
+					textprompts[num]->page[pagenum].hidehud = textprompts[num]->page[metapagenum].hidehud;
+
+					// music: don't copy, else each page change may reset the music
+				}
+			}
+			else if (fastcmp(word, "TAG"))
+				strncpy(textprompts[num]->page[pagenum].tag, word2, 33);
+			else if (fastcmp(word, "NEXTPROMPT"))
+				textprompts[num]->page[pagenum].nextprompt = usi;
+			else if (fastcmp(word, "NEXTPAGE"))
+				textprompts[num]->page[pagenum].nextpage = usi;
+			else if (fastcmp(word, "NEXTTAG"))
+				strncpy(textprompts[num]->page[pagenum].nexttag, word2, 33);
+			else if (fastcmp(word, "TIMETONEXT"))
+				textprompts[num]->page[pagenum].timetonext = get_number(word2);
+			else
+				deh_warning("PromptPage %d: unknown word '%s'", num, word);
+		}
+	} while (!myfeof(f)); // finish when the line is empty
+
+	Z_Free(s);
+}
+
+static void readtextprompt(MYFILE *f, INT32 num)
+{
+	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
+	char *word;
+	char *word2;
+	char *tmp;
+	INT32 value;
+
+	// Allocate memory for this prompt if we don't yet have any
+	if (!textprompts[num])
+		textprompts[num] = Z_Calloc(sizeof (textprompt_t), PU_STATIC, NULL);
+
+	do
+	{
+		if (myfgets(s, MAXLINELEN, f))
+		{
+			if (s[0] == '\n')
+				break;
+
+			tmp = strchr(s, '#');
+			if (tmp)
+				*tmp = '\0';
+			if (s == tmp)
+				continue; // Skip comment lines, but don't break.
+
+			word = strtok(s, " ");
+			if (word)
+				strupr(word);
+			else
+				break;
+
+			word2 = strtok(NULL, " ");
+			if (word2)
+				value = atoi(word2);
+			else
+			{
+				deh_warning("No value for token %s", word);
+				continue;
+			}
+
+			if (fastcmp(word, "NUMPAGES"))
+			{
+				textprompts[num]->numpages = min(max(value, 0), MAX_PAGES);
+			}
+			else if (fastcmp(word, "PAGE"))
+			{
+				if (1 <= value && value <= MAX_PAGES)
+				{
+					textprompts[num]->page[value - 1].backcolor = 1; // default to gray
+					textprompts[num]->page[value - 1].hidehud = 1; // hide appropriate HUD elements
+					readtextpromptpage(f, num, value - 1);
+				}
+				else
+					deh_warning("Page number %d out of range (1 - %d)", value, MAX_PAGES);
+
+			}
+			else
+				deh_warning("Prompt %d: unknown word '%s', Page <num> expected.", num, word);
+		}
+	} while (!myfeof(f)); // finish when the line is empty
+
+	Z_Free(s);
+}
+
 static void readhuditem(MYFILE *f, INT32 num)
 {
 	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
@@ -1588,7 +1956,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
@@ -2094,7 +2465,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
@@ -2229,7 +2603,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
@@ -2304,7 +2681,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
@@ -2591,7 +2971,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
@@ -2652,7 +3035,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
@@ -2885,6 +3271,19 @@ static void readmaincfg(MYFILE *f)
 				startchar = (INT16)value;
 				char_on = -1;
 			}
+			else if (fastcmp(word, "TUTORIALMAP"))
+			{
+				// Support using the actual map name,
+				// i.e., Level AB, Level FZ, etc.
+
+				// Convert to map number
+				if (word2[0] >= 'A' && word2[0] <= 'Z')
+					value = M_MapNumber(word2[0], word2[1]);
+				else
+					value = get_number(word2);
+
+				tutorialmap = (INT16)value;
+			}
 			else
 				deh_warning("Maincfg: unknown word '%s'", word);
 		}
@@ -2923,7 +3322,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
@@ -3229,6 +3631,16 @@ static void DEH_LoadDehackedFile(MYFILE *f)
 						ignorelines(f);
 					}
 				}
+				else if (fastcmp(word, "PROMPT"))
+				{
+					if (i > 0 && i < MAX_PROMPTS)
+						readtextprompt(f, i - 1);
+					else
+					{
+						deh_warning("Prompt number %d out of range (1 - %d)", i, MAX_PROMPTS);
+						ignorelines(f);
+					}
+				}
 				else if (fastcmp(word, "FRAME") || fastcmp(word, "STATE"))
 				{
 					if (i == 0 && word2[0] != '0') // If word2 isn't a number
@@ -6952,6 +7364,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_PULL",
 	"MT_GHOST",
 	"MT_OVERLAY",
+	"MT_ANGLEMAN",
 	"MT_POLYANCHOR",
 	"MT_POLYSPAWN",
 	"MT_POLYSPAWNCRUSH",
@@ -7763,6 +8176,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},
@@ -7895,6 +8315,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}
@@ -8283,7 +8711,7 @@ fixed_t get_number(const char *word)
 #endif
 }
 
-void FUNCMATH DEH_Check(void)
+void DEH_Check(void)
 {
 #if defined(_DEBUG) || defined(PARANOIA)
 	const size_t dehstates = sizeof(STATE_LIST)/sizeof(const char*);
@@ -8759,6 +9187,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;
@@ -8888,17 +9319,17 @@ static int lib_getActionName(lua_State *L)
 	{
 		lua_settop(L, 1); // set top of stack to 1 (removing any extra args, which there shouldn't be)
 		// get the name for this action, if possible.
-		lua_getfield(gL, LUA_REGISTRYINDEX, LREG_ACTIONS);
-		lua_pushnil(gL);
+		lua_getfield(L, LUA_REGISTRYINDEX, LREG_ACTIONS);
+		lua_pushnil(L);
 		// Lua stack at this point:
 		//  1   ...       -2              -1
 		// arg  ...   LREG_ACTIONS        nil
-		while (lua_next(gL, -2))
+		while (lua_next(L, -2))
 		{
 			// Lua stack at this point:
 			//  1   ...       -3              -2           -1
 			// arg  ...   LREG_ACTIONS    "A_ACTION"    function
-			if (lua_rawequal(gL, -1, 1)) // is this the same as the arg?
+			if (lua_rawequal(L, -1, 1)) // is this the same as the arg?
 			{
 				// make sure the key (i.e. "A_ACTION") is a string first
 				// (note: we don't use lua_isstring because it also returns true for numbers)
@@ -8907,12 +9338,12 @@ static int lib_getActionName(lua_State *L)
 					lua_pushvalue(L, -2); // push "A_ACTION" string to top of stack
 					return 1;
 				}
-				lua_pop(gL, 2); // pop the name and function
+				lua_pop(L, 2); // pop the name and function
 				break; // probably should have succeeded but we didn't, so end the loop
 			}
-			lua_pop(gL, 1);
+			lua_pop(L, 1);
 		}
-		lua_pop(gL, 1); // pop LREG_ACTIONS
+		lua_pop(L, 1); // pop LREG_ACTIONS
 		return 0; // return nothing (don't error)
 	}
 
diff --git a/src/djgppdos/i_sound.c b/src/djgppdos/i_sound.c
index 00e0f8c901b7f1df8cb5cb9a6c149316e9eb0a42..4424745c2f3d1c0f7179346c00beec022834716b 100644
--- a/src/djgppdos/i_sound.c
+++ b/src/djgppdos/i_sound.c
@@ -156,9 +156,11 @@ INT32 I_StartSound ( sfxenum_t     id,
                    INT32         vol,
                    INT32         sep,
                    INT32         pitch,
-                   INT32         priority )
+                   INT32         priority,
+				   INT32         channel)
 {
 	int voice;
+	(void)channel;
 
 	if (sound_disabled)
 	return 0;
@@ -548,6 +550,7 @@ void I_ResumeSong (INT32 handle)
 	songpaused = false;
 }
 
+
 void I_SetMusicVolume(INT32 volume)
 {
 	if (midi_disabled)
@@ -563,18 +566,6 @@ boolean I_SetSongTrack(INT32 track)
 	return false;
 }
 
-// Is the song playing?
-#if 0
-int I_QrySongPlaying(int handle)
-{
-	if (midi_disabled)
-		return 0;
-
-	//return islooping || musicdies > gametic;
-	return (midi_pos==-1);
-}
-#endif
-
 /// ------------------------
 // MUSIC FADING
 /// ------------------------
@@ -614,4 +605,14 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
         (void)ms;
         (void)looping;
         return false;
+// Is the song playing?
+#if 0
+int I_QrySongPlaying(int handle)
+{
+	if (midi_disabled)
+		return 0;
+
+	//return islooping || musicdies > gametic;
+	return (midi_pos==-1);
 }
+#endif
diff --git a/src/doomdef.h b/src/doomdef.h
index 51ff2507607540da6104354398621924d2db86ff..c067a71ee9d7d0d4f0d934b85e7569a5941cf70a 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -573,4 +573,11 @@ extern const char *compdate, *comptime, *comprevision, *compbranch;
 /// \note   Required for proper collision with moving sloped surfaces that have sector specials on them.
 #define SECTORSPECIALSAFTERTHINK
 
+/// FINALLY some real clipping that doesn't make walls dissappear AND speeds the game up
+/// (that was the original comment from SRB2CB, sadly it is a lie and actually slows game down)
+/// on the bright side it fixes some weird issues with translucent walls
+/// \note	SRB2CB port.
+///      	SRB2CB itself ported this from PrBoom+
+#define NEWCLIP
+
 #endif // __DOOMDEF__
diff --git a/src/doomstat.h b/src/doomstat.h
index 454af9a85a6d14b4a6578436116af3fde026a013..58cbaf64ca5f4ead2bbb14b8abebfca7ee526157 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -131,6 +131,14 @@ extern INT16 titlemap;
 extern boolean hidetitlepics;
 extern INT16 bootmap; //bootmap for loading a map on startup
 
+extern INT16 tutorialmap; // map to load for tutorial
+extern boolean tutorialmode; // are we in a tutorial right now?
+extern INT32 tutorialgcs; // which control scheme is loaded?
+extern INT32 tutorialusemouse; // store cv_usemouse user value
+extern INT32 tutorialfreelook; // store cv_alwaysfreelook user value
+extern INT32 tutorialmousemove; // store cv_mousemove user value
+extern INT32 tutorialanalog; // store cv_analog user value
+
 extern boolean looptitle;
 
 // CTF colors.
@@ -169,6 +177,60 @@ typedef struct
 
 extern cutscene_t *cutscenes[128];
 
+// Reserve prompt space for tutorials
+#define TUTORIAL_PROMPT 201 // one-based
+#define TUTORIAL_AREAS 6
+#define TUTORIAL_AREA_PROMPTS 5
+#define MAX_PROMPTS (TUTORIAL_PROMPT+TUTORIAL_AREAS*TUTORIAL_AREA_PROMPTS*3) // 3 control modes
+#define MAX_PAGES 128
+
+#define PROMPT_PIC_PERSIST 0
+#define PROMPT_PIC_LOOP 1
+#define PROMPT_PIC_DESTROY 2
+#define MAX_PROMPT_PICS 8
+typedef struct
+{
+	UINT8 numpics;
+	UINT8 picmode; // sequence mode after displaying last pic, 0 = persist, 1 = loop, 2 = destroy
+	UINT8 pictoloop; // if picmode == loop, which pic to loop to?
+	UINT8 pictostart; // initial pic number to show
+	char picname[MAX_PROMPT_PICS][8];
+	UINT8 pichires[MAX_PROMPT_PICS];
+	UINT16 xcoord[MAX_PROMPT_PICS]; // gfx
+	UINT16 ycoord[MAX_PROMPT_PICS]; // gfx
+	UINT16 picduration[MAX_PROMPT_PICS];
+
+	char   musswitch[7];
+	UINT16 musswitchflags;
+	UINT8 musicloop;
+
+	char tag[33]; // page tag
+	char name[34]; // narrator name, extra char for color
+	char iconname[8]; // narrator icon lump
+	boolean rightside; // narrator side, false = left, true = right
+	boolean iconflip; // narrator flip icon horizontally
+	UINT8 hidehud; // hide hud, 0 = show all, 1 = hide depending on prompt position (top/bottom), 2 = hide all
+	UINT8 lines; // # of lines to show. If name is specified, name takes one of the lines. If 0, defaults to 4.
+	INT32 backcolor; // see CON_SetupBackColormap: 0-11, INT32_MAX for user-defined (CONS_BACKCOLOR)
+	UINT8 align; // text alignment, 0 = left, 1 = right, 2 = center
+	UINT8 verticalalign; // vertical text alignment, 0 = top, 1 = bottom, 2 = middle
+	UINT8 textspeed; // text speed, delay in tics between characters.
+	sfxenum_t textsfx; // sfx_ id for printing text
+	UINT8 nextprompt; // next prompt to jump to, one-based. 0 = current prompt
+	UINT8 nextpage; // next page to jump to, one-based. 0 = next page within prompt->numpages
+	char nexttag[33]; // next tag to jump to. If set, this overrides nextprompt and nextpage.
+	INT32 timetonext; // time in tics to jump to next page automatically. 0 = don't jump automatically
+	char *text;
+} textpage_t;
+
+typedef struct
+{
+	textpage_t page[MAX_PAGES];
+	INT32 numpages; // Number of pages in this prompt
+} textprompt_t;
+
+extern textprompt_t *textprompts[MAX_PROMPTS];
+
 // For the Custom Exit linedef.
 extern INT16 nextmapoverride;
 extern boolean skipstats;
diff --git a/src/doomtype.h b/src/doomtype.h
index b0623de9ac410dae011b3ca97ad72f8eb60a8c3a..741b68c2193687c5034ce4a67e86c2289af0624f 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -40,12 +40,13 @@
 typedef long ssize_t;
 
 /* Older Visual C++ headers don't have the Win64-compatible typedefs... */
-#if ((_MSC_VER <= 1200) && (!defined(DWORD_PTR)))
-#define DWORD_PTR DWORD
-#endif
-
-#if ((_MSC_VER <= 1200) && (!defined(PDWORD_PTR)))
-#define PDWORD_PTR PDWORD
+#if (_MSC_VER <= 1200)
+	#ifndef DWORD_PTR
+		#define DWORD_PTR DWORD
+	#endif
+	#ifndef PDWORD_PTR
+		#define PDWORD_PTR PDWORD
+	#endif
 #endif
 #elif defined (__DJGPP__)
 #define UINT8 unsigned char
@@ -80,11 +81,13 @@ typedef long ssize_t;
 #define NOIPX
 #endif
 
+/* Strings and some misc platform specific stuff */
+
 #ifdef _MSC_VER
 	// Microsoft VisualC++
 #if (_MSC_VER <= 1800) // MSVC 2013 and back
 	#define snprintf                _snprintf
-#if (_MSC_VER <= 1200) // MSVC 2012 and back
+#if (_MSC_VER <= 1200) // MSVC 6.0 and back
 	#define vsnprintf               _vsnprintf
 #endif
 #endif
@@ -148,6 +151,8 @@ size_t strlcpy(char *dst, const char *src, size_t siz);
 // not the number of bytes in the buffer.
 #define STRBUFCPY(dst,src) strlcpy(dst, src, sizeof dst)
 
+/* Boolean type definition */
+
 // \note __BYTEBOOL__ used to be set above if "macintosh" was defined,
 // if macintosh's version of boolean type isn't needed anymore, then isn't this macro pointless now?
 #ifndef __BYTEBOOL__
@@ -155,7 +160,7 @@ size_t strlcpy(char *dst, const char *src, size_t siz);
 
 	//faB: clean that up !!
 	#if defined( _MSC_VER)  && (_MSC_VER >= 1800) // MSVC 2013 and forward
-	#include "stdbool.h"
+		#include "stdbool.h"
 	#elif defined (_WIN32)
 		#define false   FALSE           // use windows types
 		#define true    TRUE
@@ -205,89 +210,65 @@ size_t strlcpy(char *dst, const char *src, size_t siz);
 #define UINT64_MAX 0xffffffffffffffffULL /* 18446744073709551615ULL */
 #endif
 
-union FColorRGBA
-{
-	UINT32 rgba;
-	struct
-	{
-		UINT8 red;
-		UINT8 green;
-		UINT8 blue;
-		UINT8 alpha;
-	} s;
-} ATTRPACK;
-typedef union FColorRGBA RGBA_t;
+/* Compiler-specific attributes and other macros */
 
-typedef enum
-{
-	postimg_none,
-	postimg_water,
-	postimg_motion,
-	postimg_flip,
-	postimg_heat
-} postimg_t;
+#ifdef __GNUC__ // __attribute__ ((X))
+	#define FUNCNORETURN __attribute__ ((noreturn))
+
+	#if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)) && defined (__MINGW32__) // MinGW, >= GCC 4.1
+		#include "inttypes.h"
+		#if 0 //defined  (__USE_MINGW_ANSI_STDIO) && __USE_MINGW_ANSI_STDIO > 0
+			#define FUNCPRINTF __attribute__ ((format(gnu_printf, 1, 2)))
+			#define FUNCDEBUG  __attribute__ ((format(gnu_printf, 2, 3)))
+			#define FUNCIERROR __attribute__ ((format(gnu_printf, 1, 2),noreturn))
+		#elif (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4) // >= GCC 4.4
+			#define FUNCPRINTF __attribute__ ((format(ms_printf, 1, 2)))
+			#define FUNCDEBUG  __attribute__ ((format(ms_printf, 2, 3)))
+			#define FUNCIERROR __attribute__ ((format(ms_printf, 1, 2),noreturn))
+		#else
+			#define FUNCPRINTF __attribute__ ((format(printf, 1, 2)))
+			#define FUNCDEBUG  __attribute__ ((format(printf, 2, 3)))
+			#define FUNCIERROR __attribute__ ((format(printf, 1, 2),noreturn))
+		#endif
+	#else
+		#define FUNCPRINTF __attribute__ ((format(printf, 1, 2)))
+		#define FUNCDEBUG  __attribute__ ((format(printf, 2, 3)))
+		#define FUNCIERROR __attribute__ ((format(printf, 1, 2),noreturn))
+	#endif
 
-typedef UINT32 lumpnum_t; // 16 : 16 unsigned long (wad num: lump num)
-#define LUMPERROR UINT32_MAX
+	#ifndef FUNCIERROR
+		#define FUNCIERROR __attribute__ ((noreturn))
+	#endif
 
-typedef UINT32 tic_t;
-#define INFTICS UINT32_MAX
+	#define FUNCMATH __attribute__((const))
 
-#ifdef _BIG_ENDIAN
-#define UINT2RGBA(a) a
-#else
-#define UINT2RGBA(a) (UINT32)((a&0xff)<<24)|((a&0xff00)<<8)|((a&0xff0000)>>8)|(((UINT32)a&0xff000000)>>24)
-#endif
+	#if (__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1) // >= GCC 3.1
+		#define FUNCDEAD __attribute__ ((deprecated))
+		#define FUNCINLINE __attribute__((always_inline))
+		#define FUNCNONNULL __attribute__((nonnull))
+	#endif
 
-#ifdef __GNUC__ // __attribute__ ((X))
-#define FUNCNORETURN __attribute__ ((noreturn))
-#if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)) && defined (__MINGW32__)
-#include "inttypes.h"
-#if 0 //defined  (__USE_MINGW_ANSI_STDIO) && __USE_MINGW_ANSI_STDIO > 0
-#define FUNCPRINTF __attribute__ ((format(gnu_printf, 1, 2)))
-#define FUNCDEBUG  __attribute__ ((format(gnu_printf, 2, 3)))
-#define FUNCIERROR __attribute__ ((format(gnu_printf, 1, 2),noreturn))
-#elif (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4)
-#define FUNCPRINTF __attribute__ ((format(ms_printf, 1, 2)))
-#define FUNCDEBUG  __attribute__ ((format(ms_printf, 2, 3)))
-#define FUNCIERROR __attribute__ ((format(ms_printf, 1, 2),noreturn))
-#else
-#define FUNCPRINTF __attribute__ ((format(printf, 1, 2)))
-#define FUNCDEBUG  __attribute__ ((format(printf, 2, 3)))
-#define FUNCIERROR __attribute__ ((format(printf, 1, 2),noreturn))
-#endif
-#else
-#define FUNCPRINTF __attribute__ ((format(printf, 1, 2)))
-#define FUNCDEBUG  __attribute__ ((format(printf, 2, 3)))
-#define FUNCIERROR __attribute__ ((format(printf, 1, 2),noreturn))
-#endif
-#ifndef FUNCIERROR
-#define FUNCIERROR __attribute__ ((noreturn))
-#endif
-#define FUNCMATH __attribute__((const))
-#if (__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1)
-#define FUNCDEAD __attribute__ ((deprecated))
-#define FUNCINLINE __attribute__((always_inline))
-#define FUNCNONNULL __attribute__((nonnull))
-#endif
-#define FUNCNOINLINE __attribute__((noinline))
-#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4)
-#ifdef __i386__ // i386 only
-#define FUNCTARGET(X)  __attribute__ ((__target__ (X)))
-#endif
-#endif
-#if defined (__MINGW32__) && ((__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4))
-#define ATTRPACK __attribute__((packed, gcc_struct))
-#else
-#define ATTRPACK __attribute__((packed))
-#endif
-#define ATTRUNUSED __attribute__((unused))
+	#define FUNCNOINLINE __attribute__((noinline))
+
+	#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4) // >= GCC 4.4
+		#ifdef __i386__ // i386 only
+			#define FUNCTARGET(X)  __attribute__ ((__target__ (X)))
+		#endif
+	#endif
+
+	#if defined (__MINGW32__) && ((__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4)) // MinGW, >= GCC 3.4
+		#define ATTRPACK __attribute__((packed, gcc_struct))
+	#else
+		#define ATTRPACK __attribute__((packed))
+	#endif
+
+	#define ATTRUNUSED __attribute__((unused))
 #elif defined (_MSC_VER)
-#define ATTRNORETURN __declspec(noreturn)
-#define ATTRINLINE __forceinline
-#if _MSC_VER > 1200
-#define ATTRNOINLINE __declspec(noinline)
-#endif
+	#define ATTRNORETURN __declspec(noreturn)
+	#define ATTRINLINE __forceinline
+	#if _MSC_VER > 1200 // >= MSVC 6.0
+		#define ATTRNOINLINE __declspec(noinline)
+	#endif
 #endif
 
 #ifndef FUNCPRINTF
@@ -335,4 +316,43 @@ typedef UINT32 tic_t;
 #ifndef ATTRNOINLINE
 #define ATTRNOINLINE
 #endif
+
+/* Miscellaneous types that don't fit anywhere else (Can this be changed?) */
+
+union FColorRGBA
+{
+	UINT32 rgba;
+	struct
+	{
+		UINT8 red;
+		UINT8 green;
+		UINT8 blue;
+		UINT8 alpha;
+	} s;
+} ATTRPACK;
+typedef union FColorRGBA RGBA_t;
+
+typedef enum
+{
+	postimg_none,
+	postimg_water,
+	postimg_motion,
+	postimg_flip,
+	postimg_heat
+} postimg_t;
+
+typedef UINT32 lumpnum_t; // 16 : 16 unsigned long (wad num: lump num)
+#define LUMPERROR UINT32_MAX
+
+typedef UINT32 tic_t;
+#define INFTICS UINT32_MAX
+
+#include "endian.h" // This is needed to make sure the below macro acts correctly in big endian builds
+
+#ifdef SRB2_BIG_ENDIAN
+#define UINT2RGBA(a) a
+#else
+#define UINT2RGBA(a) (UINT32)((a&0xff)<<24)|((a&0xff00)<<8)|((a&0xff0000)>>8)|(((UINT32)a&0xff000000)>>24)
+#endif
+
 #endif //__DOOMTYPE__
diff --git a/src/dummy/i_sound.c b/src/dummy/i_sound.c
index 89c4006314eed3e6d902fdf3bd443b7cf0ecfcf7..bb209ddca189f823699688a9b0b453fa6c932e17 100644
--- a/src/dummy/i_sound.c
+++ b/src/dummy/i_sound.c
@@ -23,13 +23,14 @@ void I_UpdateSound(void){};
 //  SFX I/O
 //
 
-INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority)
+INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel)
 {
 	(void)id;
 	(void)vol;
 	(void)sep;
 	(void)pitch;
 	(void)priority;
+	(void)channel;
 	return -1;
 }
 
@@ -144,13 +145,13 @@ void I_UnloadSong(void)
 boolean I_PlaySong(boolean looping)
 {
 	(void)handle;
-	(void)looping;
-	return false;
 }
 
 void I_StopSong(void)
 {
 	(void)handle;
+	(void)looping;
+	return false;
 }
 
 void I_PauseSong(void)
@@ -180,7 +181,7 @@ boolean I_SetSongTrack(int track)
 
 void I_SetInternalMusicVolume(UINT8 volume)
 {
-	(void)volume;
+	(void)handle;
 }
 
 void I_StopFadingSong(void)
diff --git a/src/f_finale.c b/src/f_finale.c
index 755a1103a504f29fa6953905be156e7bd3d8773d..76eecb378029700f1d13fbc72bb1d2e223b1d425 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -33,6 +33,8 @@
 #include "m_cond.h"
 #include "p_local.h"
 #include "p_setup.h"
+#include "st_stuff.h" // hud hiding
+#include "fastcmp.h"
 
 #ifdef HAVE_BLUA
 #include "lua_hud.h"
@@ -48,6 +50,7 @@ static INT32 timetonext; // Delay between screen changes
 static INT32 continuetime; // Short delay when continuing
 
 static tic_t animtimer; // Used for some animation timings
+static INT16 skullAnimCounter; // Chevron animation
 static INT32 roidtics; // Asteroid spinning
 
 static INT32 deplete;
@@ -78,6 +81,18 @@ static patch_t *ttspop7;
 
 static void F_SkyScroll(INT32 scrollspeed);
 
+//
+// PROMPT STATE
+//
+static boolean promptactive = false;
+static mobj_t *promptmo;
+static INT16 promptpostexectag;
+static boolean promptblockcontrols;
+static char *promptpagetext = NULL;
+static INT32 callpromptnum = INT32_MAX;
+static INT32 callpagenum = INT32_MAX;
+static INT32 callplayer = INT32_MAX;
+
 //
 // CUTSCENE TEXT WRITING
 //
@@ -1414,6 +1429,7 @@ void F_StartGameEnd(void)
 //
 void F_GameEndDrawer(void)
 {
+	// this function does nothing
 }
 
 //
@@ -1808,7 +1824,7 @@ boolean F_ContinueResponder(event_t *event)
 //  CUSTOM CUTSCENES
 // ==================
 static INT32 scenenum, cutnum;
-static INT32 picxpos, picypos, picnum, pictime;
+static INT32 picxpos, picypos, picnum, pictime, picmode, numpics, pictoloop;
 static INT32 textxpos, textypos;
 static boolean dofadenow = false, cutsceneover = false;
 static boolean runningprecutscene = false, precutresetplayer = false;
@@ -2017,3 +2033,617 @@ boolean F_CutsceneResponder(event_t *event)
 
 	return false;
 }
+
+// ==================
+//  TEXT PROMPTS
+// ==================
+
+static void F_GetPageTextGeometry(UINT8 *pagelines, boolean *rightside, INT32 *boxh, INT32 *texth, INT32 *texty, INT32 *namey, INT32 *chevrony, INT32 *textx, INT32 *textr)
+{
+	// reuse:
+	// cutnum -> promptnum
+	// scenenum -> pagenum
+	lumpnum_t iconlump = W_CheckNumForName(textprompts[cutnum]->page[scenenum].iconname);
+
+	*pagelines = textprompts[cutnum]->page[scenenum].lines ? textprompts[cutnum]->page[scenenum].lines : 4;
+	*rightside = (iconlump != LUMPERROR && textprompts[cutnum]->page[scenenum].rightside);
+
+	// Vertical calculations
+	*boxh = *pagelines*2;
+	*texth = textprompts[cutnum]->page[scenenum].name[0] ? (*pagelines-1)*2 : *pagelines*2; // name takes up first line if it exists
+	*texty = BASEVIDHEIGHT - ((*texth * 4) + (*texth/2)*4);
+	*namey = BASEVIDHEIGHT - ((*boxh * 4) + (*boxh/2)*4);
+	*chevrony = BASEVIDHEIGHT - (((1*2) * 4) + ((1*2)/2)*4); // force on last line
+
+	// Horizontal calculations
+	// Shift text to the right if we have a character icon on the left side
+	// Add 4 margin against icon
+	*textx = (iconlump != LUMPERROR && !*rightside) ? ((*boxh * 4) + (*boxh/2)*4) + 4 : 4;
+	*textr = *rightside ? BASEVIDWIDTH - (((*boxh * 4) + (*boxh/2)*4) + 4) : BASEVIDWIDTH-4;
+}
+
+static fixed_t F_GetPromptHideHudBound(void)
+{
+	UINT8 pagelines;
+	boolean rightside;
+	INT32 boxh, texth, texty, namey, chevrony;
+	INT32 textx, textr;
+
+	if (cutnum == INT32_MAX || scenenum == INT32_MAX || !textprompts[cutnum] || scenenum >= textprompts[cutnum]->numpages ||
+		!textprompts[cutnum]->page[scenenum].hidehud ||
+		(splitscreen && textprompts[cutnum]->page[scenenum].hidehud != 2)) // don't hide on splitscreen, unless hide all is forced
+		return 0;
+	else if (textprompts[cutnum]->page[scenenum].hidehud == 2) // hide all
+		return BASEVIDHEIGHT;
+
+	F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr);
+
+	// calc boxheight (see V_DrawPromptBack)
+	boxh *= vid.dupy;
+	boxh = (boxh * 4) + (boxh/2)*5; // 4 lines of space plus gaps between and some leeway
+
+	// return a coordinate to check
+	// if negative: don't show hud elements below this coordinate (visually)
+	// if positive: don't show hud elements above this coordinate (visually)
+	return 0 - boxh; // \todo: if prompt at top of screen (someday), make this return positive
+}
+
+boolean F_GetPromptHideHudAll(void)
+{
+	if (cutnum == INT32_MAX || scenenum == INT32_MAX || !textprompts[cutnum] || scenenum >= textprompts[cutnum]->numpages ||
+		!textprompts[cutnum]->page[scenenum].hidehud ||
+		(splitscreen && textprompts[cutnum]->page[scenenum].hidehud != 2)) // don't hide on splitscreen, unless hide all is forced
+		return false;
+	else if (textprompts[cutnum]->page[scenenum].hidehud == 2) // hide all
+		return true;
+	else
+		return false;
+}
+
+boolean F_GetPromptHideHud(fixed_t y)
+{
+	INT32 ybound;
+	boolean fromtop;
+	fixed_t ytest;
+
+	if (!promptactive)
+		return false;
+
+	ybound = F_GetPromptHideHudBound();
+	fromtop = (ybound >= 0);
+	ytest = (fromtop ? ybound : BASEVIDHEIGHT + ybound);
+
+	return (fromtop ? y < ytest : y >= ytest); // true means hide
+}
+
+static void F_PreparePageText(char *pagetext)
+{
+	UINT8 pagelines;
+	boolean rightside;
+	INT32 boxh, texth, texty, namey, chevrony;
+	INT32 textx, textr;
+
+	F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr);
+
+	if (promptpagetext)
+		Z_Free(promptpagetext);
+	promptpagetext = (pagetext && pagetext[0]) ? V_WordWrap(textx, textr, 0, pagetext) : Z_StrDup("");
+
+	F_NewCutscene(promptpagetext);
+	cutscene_textspeed = textprompts[cutnum]->page[scenenum].textspeed ? textprompts[cutnum]->page[scenenum].textspeed : TICRATE/5;
+	cutscene_textcount = 0; // no delay in beginning
+	cutscene_boostspeed = 0; // don't print 8 characters to start
+
+	// \todo update control hot strings on re-config
+	// and somehow don't reset cutscene text counters
+}
+
+static void F_AdvanceToNextPage(void)
+{
+	INT32 nextprompt = textprompts[cutnum]->page[scenenum].nextprompt ? textprompts[cutnum]->page[scenenum].nextprompt - 1 : INT32_MAX,
+		nextpage = textprompts[cutnum]->page[scenenum].nextpage ? textprompts[cutnum]->page[scenenum].nextpage - 1 : INT32_MAX,
+		oldcutnum = cutnum;
+
+	if (textprompts[cutnum]->page[scenenum].nexttag[0])
+		F_GetPromptPageByNamedTag(textprompts[cutnum]->page[scenenum].nexttag, &nextprompt, &nextpage);
+
+	// determine next prompt
+	if (nextprompt != INT32_MAX)
+	{
+		if (nextprompt <= MAX_PROMPTS && textprompts[nextprompt])
+			cutnum = nextprompt;
+		else
+			cutnum = INT32_MAX;
+	}
+
+	// determine next page
+	if (nextpage != INT32_MAX)
+	{
+		if (cutnum != INT32_MAX)
+		{
+			scenenum = nextpage;
+			if (scenenum >= MAX_PAGES || scenenum > textprompts[cutnum]->numpages-1)
+				scenenum = INT32_MAX;
+		}
+	}
+	else
+	{
+		if (cutnum != oldcutnum)
+			scenenum = 0;
+		else if (scenenum + 1 < MAX_PAGES && scenenum < textprompts[cutnum]->numpages-1)
+			scenenum++;
+		else
+			scenenum = INT32_MAX;
+	}
+
+	// close the prompt if either num is invalid
+	if (cutnum == INT32_MAX || scenenum == INT32_MAX)
+		F_EndTextPrompt(false, false);
+	else
+	{
+		// on page mode, number of tics before allowing boost
+		// on timer mode, number of tics until page advances
+		timetonext = textprompts[cutnum]->page[scenenum].timetonext ? textprompts[cutnum]->page[scenenum].timetonext : TICRATE/10;
+		F_PreparePageText(textprompts[cutnum]->page[scenenum].text);
+
+		// gfx
+		picnum = textprompts[cutnum]->page[scenenum].pictostart;
+		numpics = textprompts[cutnum]->page[scenenum].numpics;
+		picmode = textprompts[cutnum]->page[scenenum].picmode;
+		pictoloop = textprompts[cutnum]->page[scenenum].pictoloop > 0 ? textprompts[cutnum]->page[scenenum].pictoloop - 1 : 0;
+		picxpos = textprompts[cutnum]->page[scenenum].xcoord[picnum];
+		picypos = textprompts[cutnum]->page[scenenum].ycoord[picnum];
+		animtimer = pictime = textprompts[cutnum]->page[scenenum].picduration[picnum];
+
+		// music change
+		if (textprompts[cutnum]->page[scenenum].musswitch[0])
+			S_ChangeMusic(textprompts[cutnum]->page[scenenum].musswitch,
+				textprompts[cutnum]->page[scenenum].musswitchflags,
+				textprompts[cutnum]->page[scenenum].musicloop);
+	}
+}
+
+void F_EndTextPrompt(boolean forceexec, boolean noexec)
+{
+	boolean promptwasactive = promptactive;
+	promptactive = false;
+	callpromptnum = callpagenum = callplayer = INT32_MAX;
+
+	if (promptwasactive)
+	{
+		if (promptmo && promptmo->player && promptblockcontrols)
+			promptmo->reactiontime = TICRATE/4; // prevent jumping right away // \todo account freeze realtime for this)
+		// \todo reset frozen realtime?
+	}
+
+	// \todo net safety, maybe loop all player thinkers?
+	if ((promptwasactive || forceexec) && !noexec && promptpostexectag)
+	{
+		if (tmthing) // edge case where starting an invalid prompt immediately on level load will make P_MapStart fail
+			P_LinedefExecute(promptpostexectag, promptmo, NULL);
+		else
+		{
+			P_MapStart();
+			P_LinedefExecute(promptpostexectag, promptmo, NULL);
+			P_MapEnd();
+		}
+	}
+}
+
+void F_StartTextPrompt(INT32 promptnum, INT32 pagenum, mobj_t *mo, UINT16 postexectag, boolean blockcontrols, boolean freezerealtime)
+{
+	INT32 i;
+
+	// if splitscreen and we already have a prompt active, ignore.
+	// \todo Proper per-player splitscreen support (individual prompts)
+	if (promptactive && splitscreen && promptnum == callpromptnum && pagenum == callpagenum)
+		return;
+
+	// \todo proper netgame support
+	if (netgame)
+	{
+		F_EndTextPrompt(true, false); // run the post-effects immediately
+		return;
+	}
+
+	// We share vars, so no starting text prompts over cutscenes or title screens!
+	keypressed = false;
+	finalecount = 0;
+	timetonext = 0;
+	animtimer = 0;
+	stoptimer = 0;
+	skullAnimCounter = 0;
+
+	// Set up state
+	promptmo = mo;
+	promptpostexectag = postexectag;
+	promptblockcontrols = blockcontrols;
+	(void)freezerealtime; // \todo freeze player->realtime, maybe this needs to cycle through player thinkers
+
+	// Initialize current prompt and scene
+	callpromptnum = promptnum;
+	callpagenum = pagenum;
+	cutnum = (promptnum < MAX_PROMPTS && textprompts[promptnum]) ? promptnum : INT32_MAX;
+	scenenum = (cutnum != INT32_MAX && pagenum < MAX_PAGES && pagenum <= textprompts[cutnum]->numpages-1) ? pagenum : INT32_MAX;
+	promptactive = (cutnum != INT32_MAX && scenenum != INT32_MAX);
+
+	if (promptactive)
+	{
+		// on page mode, number of tics before allowing boost
+		// on timer mode, number of tics until page advances
+		timetonext = textprompts[cutnum]->page[scenenum].timetonext ? textprompts[cutnum]->page[scenenum].timetonext : TICRATE/10;
+		F_PreparePageText(textprompts[cutnum]->page[scenenum].text);
+
+		// gfx
+		picnum = textprompts[cutnum]->page[scenenum].pictostart;
+		numpics = textprompts[cutnum]->page[scenenum].numpics;
+		picmode = textprompts[cutnum]->page[scenenum].picmode;
+		pictoloop = textprompts[cutnum]->page[scenenum].pictoloop > 0 ? textprompts[cutnum]->page[scenenum].pictoloop - 1 : 0;
+		picxpos = textprompts[cutnum]->page[scenenum].xcoord[picnum];
+		picypos = textprompts[cutnum]->page[scenenum].ycoord[picnum];
+		animtimer = pictime = textprompts[cutnum]->page[scenenum].picduration[picnum];
+
+		// music change
+		if (textprompts[cutnum]->page[scenenum].musswitch[0])
+			S_ChangeMusic(textprompts[cutnum]->page[scenenum].musswitch,
+				textprompts[cutnum]->page[scenenum].musswitchflags,
+				textprompts[cutnum]->page[scenenum].musicloop);
+
+		// get the calling player
+		if (promptblockcontrols && mo && mo->player)
+		{
+			for (i = 0; i < MAXPLAYERS; i++)
+			{
+				if (players[i].mo == mo)
+				{
+					callplayer = i;
+					break;
+				}
+			}
+		}
+	}
+	else
+		F_EndTextPrompt(true, false); // run the post-effects immediately
+}
+
+static boolean F_GetTextPromptTutorialTag(char *tag, INT32 length)
+{
+	INT32 gcs = gcs_custom;
+	boolean suffixed = true;
+
+	if (!tag || !tag[0] || !tutorialmode)
+		return false;
+
+	if (!strncmp(tag, "TAM", 3)) // Movement
+		gcs = G_GetControlScheme(gamecontrol, gcl_movement, num_gcl_movement);
+	else if (!strncmp(tag, "TAC", 3)) // Camera
+	{
+		// Check for gcl_movement so we can differentiate between FPS and Platform schemes.
+		gcs = G_GetControlScheme(gamecontrol, gcl_movement, num_gcl_movement);
+		if (gcs == gcs_custom) // try again, maybe we'll get a match
+			gcs = G_GetControlScheme(gamecontrol, gcl_camera, num_gcl_camera);
+		if (gcs == gcs_fps && !cv_usemouse.value)
+			gcs = gcs_platform; // Platform (arrow) scheme is stand-in for no mouse
+	}
+	else if (!strncmp(tag, "TAD", 3)) // Movement and Camera
+		gcs = G_GetControlScheme(gamecontrol, gcl_movement_camera, num_gcl_movement_camera);
+	else if (!strncmp(tag, "TAJ", 3)) // Jump
+		gcs = G_GetControlScheme(gamecontrol, gcl_jump, num_gcl_jump);
+	else if (!strncmp(tag, "TAS", 3)) // Spin
+		gcs = G_GetControlScheme(gamecontrol, gcl_use, num_gcl_use);
+	else if (!strncmp(tag, "TAA", 3)) // Char ability
+		gcs = G_GetControlScheme(gamecontrol, gcl_jump, num_gcl_jump);
+	else if (!strncmp(tag, "TAW", 3)) // Shield ability
+		gcs = G_GetControlScheme(gamecontrol, gcl_jump_use, num_gcl_jump_use);
+	else
+		gcs = G_GetControlScheme(gamecontrol, gcl_tutorial_used, num_gcl_tutorial_used);
+
+	switch (gcs)
+	{
+		case gcs_fps:
+			// strncat(tag, "FPS", length);
+			suffixed = false;
+			break;
+
+		case gcs_platform:
+			strncat(tag, "PLATFORM", length);
+			break;
+
+		default:
+			strncat(tag, "CUSTOM", length);
+			break;
+	}
+
+	return suffixed;
+}
+
+void F_GetPromptPageByNamedTag(const char *tag, INT32 *promptnum, INT32 *pagenum)
+{
+	INT32 nosuffixpromptnum = INT32_MAX, nosuffixpagenum = INT32_MAX;
+	INT32 tutorialpromptnum = (tutorialmode) ? TUTORIAL_PROMPT-1 : 0;
+	boolean suffixed = false, found = false;
+	char suffixedtag[33];
+
+	*promptnum = *pagenum = INT32_MAX;
+
+	if (!tag || !tag[0])
+		return;
+
+	strncpy(suffixedtag, tag, 33);
+	suffixedtag[32] = 0;
+
+	if (tutorialmode)
+		suffixed = F_GetTextPromptTutorialTag(suffixedtag, 33);
+
+	for (*promptnum = 0 + tutorialpromptnum; *promptnum < MAX_PROMPTS; (*promptnum)++)
+	{
+		if (!textprompts[*promptnum])
+			continue;
+
+		for (*pagenum = 0; *pagenum < textprompts[*promptnum]->numpages && *pagenum < MAX_PAGES; (*pagenum)++)
+		{
+			if (suffixed && fastcmp(suffixedtag, textprompts[*promptnum]->page[*pagenum].tag))
+			{
+				// this goes first because fastcmp ends early if first string is shorter
+				found = true;
+				break;
+			}
+			else if (nosuffixpromptnum == INT32_MAX && nosuffixpagenum == INT32_MAX && fastcmp(tag, textprompts[*promptnum]->page[*pagenum].tag))
+			{
+				if (suffixed)
+				{
+					nosuffixpromptnum = *promptnum;
+					nosuffixpagenum = *pagenum;
+					// continue searching for the suffixed tag
+				}
+				else
+				{
+					found = true;
+					break;
+				}
+			}
+		}
+
+		if (found)
+			break;
+	}
+
+	if (suffixed && !found && nosuffixpromptnum != INT32_MAX && nosuffixpagenum != INT32_MAX)
+	{
+		found = true;
+		*promptnum = nosuffixpromptnum;
+		*pagenum = nosuffixpagenum;
+	}
+
+	if (!found)
+		CONS_Debug(DBG_GAMELOGIC, "Text prompt: Can't find a page with named tag %s or suffixed tag %s\n", tag, suffixedtag);
+}
+
+void F_TextPromptDrawer(void)
+{
+	// reuse:
+	// cutnum -> promptnum
+	// scenenum -> pagenum
+	lumpnum_t iconlump;
+	UINT8 pagelines;
+	boolean rightside;
+	INT32 boxh, texth, texty, namey, chevrony;
+	INT32 textx, textr;
+
+	// Data
+	patch_t *patch;
+
+	if (!promptactive)
+		return;
+
+	iconlump = W_CheckNumForName(textprompts[cutnum]->page[scenenum].iconname);
+	F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr);
+
+	// Draw gfx first
+	if (picnum >= 0 && picnum < numpics && textprompts[cutnum]->page[scenenum].picname[picnum][0] != '\0')
+	{
+		if (textprompts[cutnum]->page[scenenum].pichires[picnum])
+			V_DrawSmallScaledPatch(picxpos, picypos, 0,
+				W_CachePatchName(textprompts[cutnum]->page[scenenum].picname[picnum], PU_CACHE));
+		else
+			V_DrawScaledPatch(picxpos,picypos, 0,
+				W_CachePatchName(textprompts[cutnum]->page[scenenum].picname[picnum], PU_CACHE));
+	}
+
+	// Draw background
+	V_DrawPromptBack(boxh, textprompts[cutnum]->page[scenenum].backcolor);
+
+	// Draw narrator icon
+	if (iconlump != LUMPERROR)
+	{
+		INT32 iconx, icony, scale, scaledsize;
+		patch = W_CachePatchName(textprompts[cutnum]->page[scenenum].iconname, PU_CACHE);
+
+		// scale and center
+		if (patch->width > patch->height)
+		{
+			scale = FixedDiv(((boxh * 4) + (boxh/2)*4) - 4, patch->width);
+			scaledsize = FixedMul(patch->height, scale);
+			iconx = (rightside ? BASEVIDWIDTH - (((boxh * 4) + (boxh/2)*4)) : 4) << FRACBITS;
+			icony = ((namey-4) << FRACBITS) + FixedDiv(BASEVIDHEIGHT - namey + 4 - scaledsize, 2); // account for 4 margin
+		}
+		else if (patch->height > patch->width)
+		{
+			scale = FixedDiv(((boxh * 4) + (boxh/2)*4) - 4, patch->height);
+			scaledsize = FixedMul(patch->width, scale);
+			iconx = (rightside ? BASEVIDWIDTH - (((boxh * 4) + (boxh/2)*4)) : 4) << FRACBITS;
+			icony = namey << FRACBITS;
+			iconx += FixedDiv(FixedMul(patch->height, scale) - scaledsize, 2);
+		}
+		else
+		{
+			scale = FixedDiv(((boxh * 4) + (boxh/2)*4) - 4, patch->width);
+			iconx = (rightside ? BASEVIDWIDTH - (((boxh * 4) + (boxh/2)*4)) : 4) << FRACBITS;
+			icony = namey << FRACBITS;
+		}
+
+		if (textprompts[cutnum]->page[scenenum].iconflip)
+			iconx += FixedMul(patch->width, scale) << FRACBITS;
+
+		V_DrawFixedPatch(iconx, icony, scale, (V_SNAPTOBOTTOM|(textprompts[cutnum]->page[scenenum].iconflip ? V_FLIP : 0)), patch, NULL);
+		W_UnlockCachedPatch(patch);
+	}
+
+	// Draw text
+	V_DrawString(textx, texty, (V_SNAPTOBOTTOM|V_ALLOWLOWERCASE), cutscene_disptext);
+
+	// Draw name
+	// Don't use V_YELLOWMAP here so that the name color can be changed with control codes
+	if (textprompts[cutnum]->page[scenenum].name[0])
+		V_DrawString(textx, namey, (V_SNAPTOBOTTOM|V_ALLOWLOWERCASE), textprompts[cutnum]->page[scenenum].name);
+
+	// Draw chevron
+	if (promptblockcontrols && !timetonext)
+		V_DrawString(textr-8, chevrony + (skullAnimCounter/5), (V_SNAPTOBOTTOM|V_YELLOWMAP), "\x1B"); // down arrow
+}
+
+void F_TextPromptTicker(void)
+{
+	INT32 i;
+
+	if (!promptactive || paused || P_AutoPause())
+		return;
+
+	// advance animation
+	finalecount++;
+	cutscene_boostspeed = 0;
+
+	// for the chevron
+	if (--skullAnimCounter <= 0)
+		skullAnimCounter = 8;
+
+	// button handling
+	if (textprompts[cutnum]->page[scenenum].timetonext)
+	{
+		if (promptblockcontrols) // same procedure as below, just without the button handling
+		{
+			for (i = 0; i < MAXPLAYERS; i++)
+			{
+				if (netgame && i != serverplayer && i != adminplayer)
+					continue;
+				else if (splitscreen) {
+					// Both players' controls are locked,
+					// But only consoleplayer can advance the prompt.
+					// \todo Proper per-player splitscreen support (individual prompts)
+					if (i == consoleplayer || i == secondarydisplayplayer)
+						players[i].powers[pw_nocontrol] = 1;
+				}
+				else if (i == consoleplayer)
+					players[i].powers[pw_nocontrol] = 1;
+
+				if (!splitscreen)
+					break;
+			}
+		}
+
+		if (timetonext >= 1)
+			timetonext--;
+
+		if (!timetonext)
+			F_AdvanceToNextPage();
+
+		F_WriteText();
+	}
+	else
+	{
+		if (promptblockcontrols)
+		{
+			for (i = 0; i < MAXPLAYERS; i++)
+			{
+				if (netgame && i != serverplayer && i != adminplayer)
+					continue;
+				else if (splitscreen) {
+					// Both players' controls are locked,
+					// But only the triggering player can advance the prompt.
+					if (i == consoleplayer || i == secondarydisplayplayer)
+					{
+						players[i].powers[pw_nocontrol] = 1;
+
+						if (callplayer == consoleplayer || callplayer == secondarydisplayplayer)
+						{
+							if (i != callplayer)
+								continue;
+						}
+						else if (i != consoleplayer)
+							continue;
+					}
+					else
+						continue;
+				}
+				else if (i == consoleplayer)
+					players[i].powers[pw_nocontrol] = 1;
+				else
+					continue;
+
+				if ((players[i].cmd.buttons & BT_USE) || (players[i].cmd.buttons & BT_JUMP))
+				{
+					if (timetonext > 1)
+						timetonext--;
+					else if (cutscene_baseptr) // don't set boost if we just reset the string
+						cutscene_boostspeed = 1; // only after a slight delay
+
+					if (keypressed)
+					{
+						if (!splitscreen)
+							break;
+						else
+							continue;
+					}
+
+					if (!timetonext) // is 0 when finished generating text
+					{
+						F_AdvanceToNextPage();
+						if (promptactive)
+							S_StartSound(NULL, sfx_menu1);
+					}
+					keypressed = true; // prevent repeat events
+				}
+				else if (!(players[i].cmd.buttons & BT_USE) && !(players[i].cmd.buttons & BT_JUMP))
+					keypressed = false;
+
+				if (!splitscreen)
+					break;
+			}
+		}
+
+		// generate letter-by-letter text
+		if (scenenum >= MAX_PAGES ||
+			!textprompts[cutnum]->page[scenenum].text ||
+			!textprompts[cutnum]->page[scenenum].text[0] ||
+			!F_WriteText())
+			timetonext = !promptblockcontrols; // never show the chevron if we can't toggle pages
+	}
+
+	// gfx
+	if (picnum >= 0 && picnum < numpics)
+	{
+		if (animtimer <= 0)
+		{
+			boolean persistanimtimer = false;
+
+			if (picnum < numpics-1 && textprompts[cutnum]->page[scenenum].picname[picnum+1][0] != '\0')
+				picnum++;
+			else if (picmode == PROMPT_PIC_LOOP)
+				picnum = pictoloop;
+			else if (picmode == PROMPT_PIC_DESTROY)
+				picnum = -1;
+			else // if (picmode == PROMPT_PIC_PERSIST)
+				persistanimtimer = true;
+
+			if (!persistanimtimer && picnum >= 0)
+			{
+				picxpos = textprompts[cutnum]->page[scenenum].xcoord[picnum];
+				picypos = textprompts[cutnum]->page[scenenum].ycoord[picnum];
+				pictime = textprompts[cutnum]->page[scenenum].picduration[picnum];
+				animtimer = pictime;
+			}
+		}
+		else
+			animtimer--;
+	}
+}
diff --git a/src/f_finale.h b/src/f_finale.h
index aadc64ad08e8abb9547155c8ada4420b49468146..8e8a06365260174e19d63fe138c48fd97e0438a1 100644
--- a/src/f_finale.h
+++ b/src/f_finale.h
@@ -17,6 +17,7 @@
 
 #include "doomtype.h"
 #include "d_event.h"
+#include "p_mobj.h"
 
 //
 // FINALE
@@ -33,9 +34,10 @@ void F_IntroTicker(void);
 void F_TitleScreenTicker(boolean run);
 void F_CutsceneTicker(void);
 void F_TitleDemoTicker(void);
+void F_TextPromptTicker(void);
 
 // Called by main loop.
-FUNCMATH void F_GameEndDrawer(void);
+void F_GameEndDrawer(void);
 void F_IntroDrawer(void);
 void F_TitleScreenDrawer(void);
 
@@ -50,6 +52,13 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset
 void F_CutsceneDrawer(void);
 void F_EndCutScene(void);
 
+void F_StartTextPrompt(INT32 promptnum, INT32 pagenum, mobj_t *mo, UINT16 postexectag, boolean blockcontrols, boolean freezerealtime);
+void F_GetPromptPageByNamedTag(const char *tag, INT32 *promptnum, INT32 *pagenum);
+void F_TextPromptDrawer(void);
+void F_EndTextPrompt(boolean forceexec, boolean noexec);
+boolean F_GetPromptHideHudAll(void);
+boolean F_GetPromptHideHud(fixed_t y);
+
 void F_StartGameEnd(void);
 void F_StartIntro(void);
 void F_StartTitleScreen(void);
diff --git a/src/g_game.c b/src/g_game.c
index 3b0656c7a398836b6f08ff8bf5589b008b60c868..67780e303722aa5a0102002dca74c8db75744c82 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -128,6 +128,14 @@ INT16 titlemap = 0;
 boolean hidetitlepics = false;
 INT16 bootmap; //bootmap for loading a map on startup
 
+INT16 tutorialmap = 0; // map to load for tutorial
+boolean tutorialmode = false; // are we in a tutorial right now?
+INT32 tutorialgcs = gcs_custom; // which control scheme is loaded?
+INT32 tutorialusemouse = 0; // store cv_usemouse user value
+INT32 tutorialfreelook = 0; // store cv_alwaysfreelook user value
+INT32 tutorialmousemove = 0; // store cv_mousemove user value
+INT32 tutorialanalog = 0; // store cv_analog user value
+
 boolean looptitle = false;
 
 UINT8 skincolor_redteam = SKINCOLOR_RED;
@@ -139,6 +147,7 @@ tic_t countdowntimer = 0;
 boolean countdowntimeup = false;
 
 cutscene_t *cutscenes[128];
+textprompt_t *textprompts[MAX_PROMPTS];
 
 INT16 nextmapoverride;
 boolean skipstats;
@@ -1934,6 +1943,7 @@ void G_Ticker(boolean run)
 				F_TitleDemoTicker();
 			P_Ticker(run); // tic the game
 			ST_Ticker();
+			F_TextPromptTicker();
 			AM_Ticker();
 			HU_Ticker();
 			break;
diff --git a/src/g_game.h b/src/g_game.h
index d6b41830e8c674e52bf0cb3374e526709df3e93b..9e8580d139c8e94b0c31183746723b62bbf32f65 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -55,6 +55,7 @@ extern tic_t timeinmap; // Ticker for time spent in level (used for levelcard di
 extern INT16 rw_maximums[NUM_WEAPONS];
 
 // used in game menu
+extern consvar_t cv_tutorialprompt;
 extern consvar_t cv_crosshair, cv_crosshair2;
 extern consvar_t cv_invertmouse, cv_alwaysfreelook, cv_mousemove;
 extern consvar_t cv_invertmouse2, cv_alwaysfreelook2, cv_mousemove2;
diff --git a/src/g_input.c b/src/g_input.c
index 67aaf41791c33d90a360a38fe78faaec960155e9..4f7296cd5a5a92d4dafe7650719b60b488b3b800 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -45,6 +45,47 @@ 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 gcl_tutorial_check[num_gcl_tutorial_check] = {
+	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
+	gc_turnleft, gc_turnright
+};
+
+const INT32 gcl_tutorial_used[num_gcl_tutorial_used] = {
+	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
+	gc_turnleft, gc_turnright,
+	gc_jump, gc_use
+};
+
+const INT32 gcl_tutorial_full[num_gcl_tutorial_full] = {
+	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 gcl_movement[num_gcl_movement] = {
+	gc_forward, gc_backward, gc_strafeleft, gc_straferight
+};
+
+const INT32 gcl_camera[num_gcl_camera] = {
+	gc_turnleft, gc_turnright
+};
+
+const INT32 gcl_movement_camera[num_gcl_movement_camera] = {
+	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
+	gc_turnleft, gc_turnright
+};
+
+const INT32 gcl_jump[num_gcl_jump] = { gc_jump };
+
+const INT32 gcl_use[num_gcl_use] = { gc_use };
+
+const INT32 gcl_jump_use[num_gcl_jump_use] = {
+	gc_jump, gc_use
+};
 
 typedef struct
 {
@@ -611,55 +652,117 @@ INT32 G_KeyStringtoNum(const char *keystr)
 	return 0;
 }
 
-void G_Controldefault(void)
+void G_DefineDefaultControls(void)
+{
+	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)
 {
-	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, 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 +770,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..4bf67ebd8cb87a4d2f2e06b331075ea97cae9eeb 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,30 @@ 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_gcl_tutorial_check 6
+#define num_gcl_tutorial_used 8
+#define num_gcl_tutorial_full 13
+#define num_gcl_movement 4
+#define num_gcl_camera 2
+#define num_gcl_movement_camera 6
+#define num_gcl_jump 1
+#define num_gcl_use 1
+#define num_gcl_jump_use 2
+
+extern const INT32 gcl_tutorial_check[num_gcl_tutorial_check];
+extern const INT32 gcl_tutorial_used[num_gcl_tutorial_used];
+extern const INT32 gcl_tutorial_full[num_gcl_tutorial_full];
+extern const INT32 gcl_movement[num_gcl_movement];
+extern const INT32 gcl_camera[num_gcl_camera];
+extern const INT32 gcl_movement_camera[num_gcl_movement_camera];
+extern const INT32 gcl_jump[num_gcl_jump];
+extern const INT32 gcl_use[num_gcl_use];
+extern const INT32 gcl_jump_use[num_gcl_jump_use];
+
 // peace to my little coder fingers!
 // check a gamecontrol being active or not
 
@@ -133,8 +162,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_clip.c b/src/hardware/hw_clip.c
index fd6ba13f331b47ee14d547c71c4b5b54cbb2b561..6d120efe72c4ca251b61dfa5c68be1b206719d31 100644
--- a/src/hardware/hw_clip.c
+++ b/src/hardware/hw_clip.c
@@ -338,7 +338,7 @@ angle_t gld_FrustumAngle(void)
 	}
 
 	// If the pitch is larger than this you can look all around at a FOV of 90
-	if (aimingangle > (ANGLE_45+ANG1) && (ANGLE_315-ANG1) > aimingangle)
+	if (abs((signed)aimingangle) > 46 * ANG1)
 		return 0xffffffff;
 
 	// ok, this is a gross hack that barely works...
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..4efd49817789fdbedf48244eaf7ac71417c1cdb7 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;
 
@@ -705,6 +716,32 @@ void HWR_DrawConsoleBack(UINT32 color, INT32 height)
 	HWD.pfnDrawPolygon(&Surf, v, 4, PF_NoTexture|PF_Modulated|PF_Translucent|PF_NoDepthTest);
 }
 
+// Very similar to HWR_DrawConsoleBack, except we draw from the middle(-ish) of the screen to the bottom.
+void HWR_DrawTutorialBack(UINT32 color, INT32 boxheight)
+{
+	FOutVector  v[4];
+	FSurfaceInfo Surf;
+	INT32 height = (boxheight * 4) + (boxheight/2)*5; // 4 lines of space plus gaps between and some leeway
+
+	// setup some neat-o translucency effect
+
+	v[0].x = v[3].x = -1.0f;
+	v[2].x = v[1].x =  1.0f;
+	v[0].y = v[1].y =  -1.0f;
+	v[2].y = v[3].y =  -1.0f+((height<<1)/(float)vid.height);
+	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
+
+	v[0].sow = v[3].sow = 0.0f;
+	v[2].sow = v[1].sow = 1.0f;
+	v[0].tow = v[1].tow = 1.0f;
+	v[2].tow = v[3].tow = 0.0f;
+
+	Surf.FlatColor.rgba = UINT2RGBA(color);
+	Surf.FlatColor.s.alpha = (color == 0 ? 0xC0 : 0x80); // make black darker, like software
+
+	HWD.pfnDrawPolygon(&Surf, v, 4, PF_NoTexture|PF_Modulated|PF_Translucent|PF_NoDepthTest);
+}
+
 
 // ==========================================================================
 //                                                             R_DRAW.C STUFF
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_main.c b/src/hardware/hw_main.c
index 50399dbd6a02be1205b5a794f21170ac60967446..349103602c460c509ab9d8934f74bc6c33a8a12b 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -2116,6 +2116,10 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 				else
 				{
 					fixed_t texturevpeg;
+					boolean attachtobottom = false;
+#ifdef ESLOPE
+					boolean slopeskew = false; // skew FOF walls with slopes?
+#endif
 
 					// Wow, how was this missing from OpenGL for so long?
 					// ...Oh well, anyway, Lower Unpegged now changes pegging of FOFs like in software
@@ -2123,24 +2127,50 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 					if (newline)
 					{
 						texturevpeg = sides[newline->sidenum[0]].rowoffset;
-						if (newline->flags & ML_DONTPEGBOTTOM)
-							texturevpeg -= *rover->topheight - *rover->bottomheight;
+						attachtobottom = !!(newline->flags & ML_DONTPEGBOTTOM);
+#ifdef ESLOPE
+						slopeskew = !!(newline->flags & ML_DONTPEGTOP);
+#endif
 					}
 					else
 					{
 						texturevpeg = sides[rover->master->sidenum[0]].rowoffset;
-						if (gr_linedef->flags & ML_DONTPEGBOTTOM)
-							texturevpeg -= *rover->topheight - *rover->bottomheight;
+						attachtobottom = !!(gr_linedef->flags & ML_DONTPEGBOTTOM);
+#ifdef ESLOPE
+						slopeskew = !!(rover->master->flags & ML_DONTPEGTOP);
+#endif
 					}
 
 					grTex = HWR_GetTexture(texnum);
 
 #ifdef ESLOPE
-					wallVerts[3].t = (*rover->topheight - h + texturevpeg) * grTex->scaleY;
-					wallVerts[2].t = (*rover->topheight - hS + texturevpeg) * grTex->scaleY;
-					wallVerts[0].t = (*rover->topheight - l + texturevpeg) * grTex->scaleY;
-					wallVerts[1].t = (*rover->topheight - lS + texturevpeg) * grTex->scaleY;
+					if (!slopeskew) // no skewing
+					{
+						if (attachtobottom)
+							texturevpeg -= *rover->topheight - *rover->bottomheight;
+						wallVerts[3].t = (*rover->topheight - h + texturevpeg) * grTex->scaleY;
+						wallVerts[2].t = (*rover->topheight - hS + texturevpeg) * grTex->scaleY;
+						wallVerts[0].t = (*rover->topheight - l + texturevpeg) * grTex->scaleY;
+						wallVerts[1].t = (*rover->topheight - lS + texturevpeg) * grTex->scaleY;
+					}
+					else
+					{
+						if (!attachtobottom) // skew by top
+						{
+							wallVerts[3].t = wallVerts[2].t = texturevpeg * grTex->scaleY;
+							wallVerts[0].t = (h - l + texturevpeg) * grTex->scaleY;
+							wallVerts[1].t = (hS - lS + texturevpeg) * grTex->scaleY;
+						}
+						else // skew by bottom
+						{
+							wallVerts[0].t = wallVerts[1].t = texturevpeg * grTex->scaleY;
+							wallVerts[3].t = wallVerts[0].t - (h - l) * grTex->scaleY;
+							wallVerts[2].t = wallVerts[1].t - (hS - lS) * grTex->scaleY;
+						}
+					}
 #else
+					if (attachtobottom)
+						texturevpeg -= *rover->topheight - *rover->bottomheight;
 					wallVerts[3].t = wallVerts[2].t = (*rover->topheight - h + texturevpeg) * grTex->scaleY;
 					wallVerts[0].t = wallVerts[1].t = (*rover->topheight - l + texturevpeg) * grTex->scaleY;
 #endif
@@ -5270,8 +5300,10 @@ static void HWR_AddSprites(sector_t *sec)
 
 			approx_dist = P_AproxDistance(viewx-thing->x, viewy-thing->y);
 
-			if (approx_dist <= limit_dist)
-				HWR_ProjectSprite(thing);
+			if (approx_dist > limit_dist)
+				continue;
+
+			HWR_ProjectSprite(thing);
 		}
 	}
 	else
@@ -5293,8 +5325,10 @@ static void HWR_AddSprites(sector_t *sec)
 
 			approx_dist = P_AproxDistance(viewx-precipthing->x, viewy-precipthing->y);
 
-			if (approx_dist <= limit_dist)
-				HWR_ProjectPrecipitationSprite(precipthing);
+			if (approx_dist > limit_dist)
+				continue;
+
+			HWR_ProjectPrecipitationSprite(precipthing);
 		}
 	}
 	else
@@ -5624,6 +5658,16 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	x1 = tr_x + x1 * rightcos;
 	x2 = tr_x - x2 * rightcos;
 
+	// okay, we can't return now... this is a hack, but weather isn't networked, so it should be ok
+	if (!(thing->precipflags & PCF_THUNK))
+	{
+		if (thing->precipflags & PCF_RAIN)
+			P_RainThinker(thing);
+		else
+			P_SnowThinker(thing);
+		thing->precipflags |= PCF_THUNK;
+	}
+
 	//
 	// store information in a vissprite
 	//
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index 1ae2d8fc39f578b7dc0a9687c18c8c90cde619b5..f25720d1eb1bed160c7efd3432e69160ecd76273 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -35,6 +35,7 @@ void HWR_clearAutomap(void);
 void HWR_drawAMline(const fline_t *fl, INT32 color);
 void HWR_FadeScreenMenuBack(UINT16 color, UINT8 strength);
 void HWR_DrawConsoleBack(UINT32 color, INT32 height);
+void HWR_DrawTutorialBack(UINT32 color, INT32 boxheight);
 void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player);
 void HWR_RenderPlayerView(INT32 viewnumber, player_t *player);
 void HWR_DrawViewBorder(INT32 clearlines);
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/hu_stuff.h b/src/hu_stuff.h
index 9e67e49b59106090b7b3e7c4e4a596169c59c280..46906dd73eb23ee35211dbc5dc77d4a28e7b427f 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -86,7 +86,7 @@ void HU_Init(void);
 void HU_LoadGraphics(void);
 
 // reset heads up when consoleplayer respawns.
-FUNCMATH void HU_Start(void);
+void HU_Start(void);
 
 boolean HU_Responder(event_t *ev);
 
diff --git a/src/i_sound.h b/src/i_sound.h
index f6e9a18afc2329bd6835470051d8946b30a67303..3dc0d0d1f0350582f71098cfab951989f59a8388 100644
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -79,7 +79,7 @@ void I_ShutdownSound(void);
 
 	\return	sfx handle
 */
-INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority);
+INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel);
 
 /**	\brief	Stops a sound channel.
 
@@ -288,4 +288,4 @@ void I_PlayCD(UINT8 track, UINT8 looping);
 */
 boolean I_SetVolumeCD(INT32 volume);
 
-#endif
+#endif
\ No newline at end of file
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/info.c b/src/info.c
index 728fb13c0a2c4402f6936a3ae1d75058c20db06d..72abcede1dad474b81a74f197dda5f3345e5836f 100644
--- a/src/info.c
+++ b/src/info.c
@@ -3458,8 +3458,8 @@ state_t states[NUMSTATES] =
 
 	{SPR_FMCE, 0, 20, {NULL}, 0, 0, S_SMASHSPIKE_EASE1}, // S_SMASHSPIKE_FLOAT
 	{SPR_FMCE, 0,  4, {A_ZThrust},  4, (1<<16)|1, S_SMASHSPIKE_EASE2}, // S_SMASHSPIKE_EASE1
-	{SPR_FMCE, 0,  4, {A_ZThrust},  0, (1<<16)|1, S_SMASHSPIKE_FALL},  // S_SMASHSPIKE_EASE1
-	{SPR_FMCE, 0,  2, {A_ZThrust}, -6, (1<<16)|1, S_SMASHSPIKE_FALL},  // S_SMASHSPIKE_FALL
+	{SPR_FMCE, 0,  4, {A_ZThrust},  0, (1<<16)|1, S_SMASHSPIKE_FALL},  // S_SMASHSPIKE_EASE2
+	{SPR_FMCE, 0,  2, {A_ZThrust}, -6,         1, S_SMASHSPIKE_FALL},  // S_SMASHSPIKE_FALL
 	{SPR_FMCE, 1,  2, {A_MultiShotDist}, (MT_DUST<<16)|10, -48, S_SMASHSPIKE_STOMP2}, // S_SMASHSPIKE_STOMP1
 	{SPR_FMCE, 2, 14, {NULL}, 0, 0, S_SMASHSPIKE_RISE1}, // S_SMASHSPIKE_STOMP2
 	{SPR_FMCE, 1,  2, {NULL}, 0, 0, S_SMASHSPIKE_RISE2}, // S_SMASHSPIKE_RISE1
@@ -17814,6 +17814,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_ANGLEMAN
+		758,            // doomednum
+		S_INVISIBLE,    // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		8,              // radius
+		8,              // height
+		0,              // display offset
+		10,             // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_NOTHINK|MF_NOBLOCKMAP|MF_NOGRAVITY, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_POLYANCHOR
 		760,            // doomednum
 		S_INVISIBLE,    // spawnstate
diff --git a/src/info.h b/src/info.h
index b757deec0353319de13d62ff7fef8104ad74c1f4..c9de82541c64f16e0b76b79b1ad43121f0750c73 100644
--- a/src/info.h
+++ b/src/info.h
@@ -4305,6 +4305,7 @@ typedef enum mobj_type
 	MT_PULL,
 	MT_GHOST,
 	MT_OVERLAY,
+	MT_ANGLEMAN,
 	MT_POLYANCHOR,
 	MT_POLYSPAWN,
 	MT_POLYSPAWNCRUSH,
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index e1908bbd7899eae0dd77157b383533bf44edfc8c..289babeba0bfaf0d767df055f185765e233042fc 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"
@@ -2009,6 +2012,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
 ////////////
 
@@ -2633,6 +2654,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 f3d1cdf664024d98410ad3053376f02cd60c7b75..fca99d27494a6f6ac2f585a769e1949a4c124a75 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[] //
 /////////////////////
@@ -1932,6 +2202,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 d5736f9e5622c059607618aad87e43a073b61b11..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
@@ -193,25 +196,27 @@ void LUA_LoadLump(UINT16 wad, UINT16 lump)
 {
 	MYFILE f;
 	char *name;
-
+	size_t len;
 	f.wad = wad;
 	f.size = W_LumpLengthPwad(wad, lump);
 	f.data = Z_Malloc(f.size, PU_LUA, NULL);
 	W_ReadLumpPwad(wad, lump, f.data);
 	f.curpos = f.data;
 
+	len = strlen(wadfiles[wad]->filename); // length of file name
+
 	if (wadfiles[wad]->type == RET_LUA)
 	{
-		name = malloc(strlen(wadfiles[wad]->filename)+1);
+		name = malloc(len+1);
 		strcpy(name, wadfiles[wad]->filename);
 	}
 	else // If it's not a .lua file, copy the lump name in too.
 	{
 		lumpinfo_t *lump_p = &wadfiles[wad]->lumpinfo[lump];
-		size_t length = strlen(wadfiles[wad]->filename) + 1 + strlen(lump_p->name2); // length of file name, '|', and lump name
-		name = malloc(length + 1);
+		len += 1 + strlen(lump_p->name2); // length of file name, '|', and lump name
+		name = malloc(len+1);
 		sprintf(name, "%s|%s", wadfiles[wad]->filename, lump_p->name2);
-		name[length] = '\0';
+		name[len] = '\0';
 	}
 
 	LUA_LoadFile(&f, name); // actually load file!
@@ -496,6 +501,9 @@ enum
 	ARCH_NODE,
 #endif
 	ARCH_FFLOOR,
+#ifdef ESLOPE
+	ARCH_SLOPE,
+#endif
 	ARCH_MAPHEADER,
 
 	ARCH_TEND=0xFF,
@@ -520,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}
 };
@@ -774,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));
@@ -995,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_anigif.c b/src/m_anigif.c
index 6ae112ea8ee86af4dcf57574f0988ecb9abc97d1..4ebf2aff3b307e79172d717f7dc7d6f8a9759b0d 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -497,7 +497,9 @@ static void GIF_framewrite(void)
 
 	// screen regions are handled in GIF_lzw
 	{
-		UINT16 delay = 3; // todo
+		int d1 = (int)((100.0/NEWTICRATE)*(gif_frames+1));
+		int d2 = (int)((100.0/NEWTICRATE)*(gif_frames));
+		UINT16 delay = d1-d2;
 		INT32 startline;
 
 		WRITEMEM(p, gifframe_gchead, 4);
diff --git a/src/m_menu.c b/src/m_menu.c
index a0f99c86a90ce44b440fb2b203bc063c8f08c158..e973cea551ab692d44f76a62ee44f9d9b4460c61 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -273,6 +273,7 @@ menu_t SP_MainDef, OP_MainDef;
 menu_t MISC_ScrambleTeamDef, MISC_ChangeTeamDef;
 
 // Single Player
+static void M_StartTutorial(INT32 choice);
 static void M_LoadGame(INT32 choice);
 static void M_TimeAttackLevelSelect(INT32 choice);
 static void M_TimeAttack(INT32 choice);
@@ -440,6 +441,9 @@ static CV_PossibleValue_t serversort_cons_t[] = {
 };
 consvar_t cv_serversort = {"serversort", "Ping", CV_HIDEN | CV_CALL, serversort_cons_t, M_SortServerList, 0, NULL, NULL, 0, 0, NULL};
 
+// first time memory
+consvar_t cv_tutorialprompt = {"tutorialprompt", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 // autorecord demos for time attack
 static consvar_t cv_autorecord = {"autorecord", "Yes", 0, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
@@ -731,6 +735,7 @@ static menuitem_t SR_EmblemHintMenu[] =
 // Single Player Main
 static menuitem_t SP_MainMenu[] =
 {
+	{IT_CALL | IT_STRING,                       NULL, "Tutorial",      M_StartTutorial,            84},
 	{IT_CALL | IT_STRING,                       NULL, "Start Game",    M_LoadGame,                 92},
 	{IT_SECRET,                                 NULL, "Record Attack", M_TimeAttack,              100},
 	{IT_SECRET,                                 NULL, "NiGHTS Mode",   M_NightsAttack,            108},
@@ -739,6 +744,7 @@ static menuitem_t SP_MainMenu[] =
 
 enum
 {
+	sptutorial,
 	sploadgame,
 	sprecordattack,
 	spnightsmode,
@@ -1554,7 +1560,18 @@ menu_t SR_EmblemHintDef =
 };
 
 // Single Player
-menu_t SP_MainDef = CENTERMENUSTYLE(NULL, SP_MainMenu, &MainDef, 72);
+menu_t SP_MainDef = //CENTERMENUSTYLE(NULL, SP_MainMenu, &MainDef, 72);
+{
+	NULL,
+	sizeof(SP_MainMenu)/sizeof(menuitem_t),
+	&MainDef,
+	SP_MainMenu,
+	M_DrawCenteredMenu,
+	BASEVIDWIDTH/2, 72,
+	1, // start at "Start Game" on first entry
+	NULL
+};
+
 menu_t SP_LoadDef =
 {
 	"M_PICKG",
@@ -6093,6 +6110,8 @@ static void M_CustomLevelSelect(INT32 choice)
 static void M_SinglePlayerMenu(INT32 choice)
 {
 	(void)choice;
+	SP_MainMenu[sptutorial].status =
+		tutorialmap ? IT_CALL|IT_STRING : IT_NOTHING|IT_DISABLED;
 	SP_MainMenu[sprecordattack].status =
 		(M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING : IT_SECRET;
 	SP_MainMenu[spnightsmode].status =
@@ -6118,6 +6137,80 @@ 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[tutorialgcs], gcl_tutorial_full, num_gcl_tutorial_full);
+		CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
+		CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
+		CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
+		CV_Set(&cv_analog, cv_analog.defaultvalue);
+		S_StartSound(NULL, sfx_itemup);
+	}
+	else
+		S_StartSound(NULL, sfx_menu1);
+}
+
+static void M_TutorialControlResponse(INT32 ch)
+{
+	if (ch != KEY_ESCAPE)
+	{
+		G_CopyControls(gamecontroldefault[gcs_custom], gamecontrol, NULL, 0); // using gcs_custom as temp storage for old controls
+		if (ch == 'y' || ch == KEY_ENTER)
+		{
+			tutorialgcs = gcs_fps;
+			tutorialusemouse = cv_usemouse.value;
+			tutorialfreelook = cv_alwaysfreelook.value;
+			tutorialmousemove = cv_mousemove.value;
+			tutorialanalog = cv_analog.value;
+
+			G_CopyControls(gamecontrol, gamecontroldefault[tutorialgcs], gcl_tutorial_full, num_gcl_tutorial_full);
+			CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
+			CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
+			CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
+			CV_Set(&cv_analog, cv_analog.defaultvalue);
+
+			//S_StartSound(NULL, sfx_itemup);
+		}
+		else
+		{
+			tutorialgcs = gcs_custom;
+			S_StartSound(NULL, sfx_menu1);
+		}
+		M_StartTutorial(INT32_MAX);
+	}
+	else
+		S_StartSound(NULL, sfx_menu1);
+
+	MessageDef.prevMenu = &SP_MainDef; // if FirstPrompt -> ControlsPrompt -> ESC, we would go to the main menu unless we force this
+}
+
+// Starts up the tutorial immediately (tbh I wasn't sure where else to put this)
+static void M_StartTutorial(INT32 choice)
+{
+	if (!tutorialmap)
+		return; // no map to go to, don't bother
+
+	if (choice != INT32_MAX && G_GetControlScheme(gamecontrol, gcl_tutorial_check, num_gcl_tutorial_check) != gcs_fps)
+	{
+		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\nPress 'N' or any key to keep \nyour current controls.\n",M_TutorialControlResponse,MM_YESNO);
+		return;
+	}
+	else if (choice != INT32_MAX)
+		tutorialgcs = gcs_custom;
+
+	CV_SetValue(&cv_tutorialprompt, 0); // first-time prompt
+
+	tutorialmode = true; // turn on tutorial mode
+
+	emeralds = 0;
+	M_ClearMenus(true);
+	gamecomplete = false;
+	cursaveslot = 0;
+	G_DeferedInitNew(false, G_BuildMapName(tutorialmap), 0, false, false);
+}
+
 // ==============
 // LOAD GAME MENU
 // ==============
@@ -6725,6 +6818,26 @@ static void M_HandleLoadSave(INT32 choice)
 	}
 }
 
+static void M_FirstTimeResponse(INT32 ch)
+{
+	S_StartSound(NULL, sfx_menu1);
+
+	if (ch == KEY_ESCAPE)
+		return;
+
+	if (ch != 'y' && ch != KEY_ENTER)
+	{
+		CV_SetValue(&cv_tutorialprompt, 0);
+		M_ReadSaveStrings();
+		MessageDef.prevMenu = &SP_LoadDef; // calls M_SetupNextMenu
+	}
+	else
+	{
+		M_StartTutorial(0);
+		MessageDef.prevMenu = &MessageDef; // otherwise, the controls prompt won't fire
+	}
+}
+
 //
 // Selected from SRB2 menu
 //
@@ -6732,6 +6845,13 @@ static void M_LoadGame(INT32 choice)
 {
 	(void)choice;
 
+	if (tutorialmap && cv_tutorialprompt.value)
+	{
+		M_StartMessage("Do you want to \x82play a brief Tutorial\x80?\n\nWe highly recommend this because \nthe controls are slightly different \nfrom other games.\n\nPress 'Y' or 'Enter' to go\nPress 'N' or any key to skip\n",
+			M_FirstTimeResponse, MM_YESNO);
+		return;
+	}
+
 	M_ReadSaveStrings();
 	M_SetupNextMenu(&SP_LoadDef);
 }
@@ -9198,9 +9318,17 @@ static void M_DrawControl(void)
 	// draw title (or big pic)
 	M_DrawMenuTitle();
 
-	M_CentreText(30,
-		 (setupcontrols_secondaryplayer ? "SET CONTROLS FOR SECONDARY PLAYER" :
-		                                  "PRESS ENTER TO CHANGE, BACKSPACE TO CLEAR"));
+	if (tutorialmode && tutorialgcs)
+	{
+		if ((gametic / TICRATE) % 2)
+			M_CentreText(30, "\202EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
+		else
+			M_CentreText(30, "EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
+	}
+	else
+		M_CentreText(30,
+		    (setupcontrols_secondaryplayer ? "SET CONTROLS FOR SECONDARY PLAYER" :
+		                                     "PRESS ENTER TO CHANGE, BACKSPACE TO CLEAR"));
 
 	if (i)
 		V_DrawString(currentMenu->x - 16, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A"); // up arrow
@@ -9335,6 +9463,9 @@ static void M_ChangeControl(INT32 choice)
 {
 	static char tmp[55];
 
+	if (tutorialmode && tutorialgcs) // don't allow control changes if temp control override is active
+		return;
+
 	controltochange = currentMenu->menuitems[choice].alphaKey;
 	sprintf(tmp, M_GetText("Hit the new key for\n%s\nESC for Cancel"),
 		currentMenu->menuitems[choice].text);
@@ -9523,7 +9654,6 @@ static void M_ToggleMIDI(INT32 choice)
 		default:
 			break;
 	}
-
 	if (midi_disabled)
 	{
 		midi_disabled = false;
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..92c2aeea9dd3458d66edc0f8495119542859998c 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));
@@ -538,8 +539,28 @@ void M_SaveConfig(const char *filename)
 
 	// FIXME: save key aliases if ever implemented..
 
-	CV_SaveVariables(f);
-	if (!dedicated) G_SaveKeySetting(f);
+	if (tutorialmode && tutorialgcs)
+	{
+		CV_SetValue(&cv_usemouse, tutorialusemouse);
+		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
+		CV_SetValue(&cv_mousemove, tutorialmousemove);
+		CV_SetValue(&cv_analog, tutorialanalog);
+		CV_SaveVariables(f);
+		CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
+		CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
+		CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
+		CV_Set(&cv_analog, cv_analog.defaultvalue);
+	}
+	else
+		CV_SaveVariables(f);
+
+	if (!dedicated)
+	{
+		if (tutorialmode && tutorialgcs)
+			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_enemy.c b/src/p_enemy.c
index 6c19ed5d36a0ed0b3cb761e2dd2f8cda5223575f..bd5b3ac19a8cb5725fe42dc1523a5a3b0d48217b 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3324,6 +3324,11 @@ void A_MonitorPop(mobj_t *actor)
 			newmobj->sprite = SPR_TV1P;
 		}
 	}
+
+	// Run a linedef executor immediately upon popping
+	// You may want to delay your effects by 18 tics to sync with the reward giving
+	if (actor->spawnpoint && actor->lastlook)
+		P_LinedefExecute(actor->lastlook, actor->target, NULL);
 }
 
 // Function: A_GoldMonitorPop
@@ -3407,6 +3412,11 @@ void A_GoldMonitorPop(mobj_t *actor)
 			newmobj->sprite = SPR_TV1P;
 		}
 	}
+
+	// Run a linedef executor immediately upon popping
+	// You may want to delay your effects by 18 tics to sync with the reward giving
+	if (actor->spawnpoint && actor->lastlook)
+		P_LinedefExecute(actor->lastlook, actor->target, NULL);
 }
 
 // Function: A_GoldMonitorRestore
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 4177160300484ba7d5d1fb36e413fd6a283af728..3c1418252839601810150fc782f5763c92021c02 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;
@@ -4059,7 +4060,8 @@ void P_RecalcPrecipInSector(sector_t *sector)
 //
 void P_NullPrecipThinker(precipmobj_t *mobj)
 {
-	(void)mobj;
+	//(void)mobj;
+	mobj->precipflags &= ~PCF_THUNK;
 }
 
 void P_SnowThinker(precipmobj_t *mobj)
@@ -4079,25 +4081,26 @@ void P_RainThinker(precipmobj_t *mobj)
 	{
 		// cycle through states,
 		// calling action functions at transitions
-		if (mobj->tics > 0 && --mobj->tics == 0)
-		{
-			// you can cycle through multiple states in a tic
-			if (!P_SetPrecipMobjState(mobj, mobj->state->nextstate))
-				return; // freed itself
-		}
+		if (mobj->tics <= 0)
+			return;
+
+		if (--mobj->tics)
+			return;
+
+		if (!P_SetPrecipMobjState(mobj, mobj->state->nextstate))
+			return;
+
+		if (mobj->state != &states[S_RAINRETURN])
+			return;
+
+		mobj->z = mobj->ceilingz;
+		P_SetPrecipMobjState(mobj, S_RAIN1);
 
-		if (mobj->state == &states[S_RAINRETURN])
-		{
-			mobj->z = mobj->ceilingz;
-			P_SetPrecipMobjState(mobj, S_RAIN1);
-		}
 		return;
 	}
 
 	// adjust height
-	mobj->z += mobj->momz;
-
-	if (mobj->z <= mobj->floorz)
+	if ((mobj->z += mobj->momz) <= mobj->floorz)
 	{
 		// no splashes on sky or bottomless pits
 		if (mobj->precipflags & PCF_PIT)
@@ -7433,6 +7436,48 @@ void P_MobjThinker(mobj_t *mobj)
 		if ((mobj->flags & MF_ENEMY) && (mobj->state->nextstate == mobj->info->spawnstate && mobj->tics == 1))
 			mobj->flags2 &= ~MF2_FRET;
 
+		// Angle-to-tracer to trigger a linedef exec
+		// See Linedef Exec 457 (Track mobj angle to point)
+		if ((mobj->eflags & MFE_TRACERANGLE) && mobj->tracer && mobj->extravalue2)
+		{
+			// mobj->lastlook - Don't disable behavior after first failure
+			// mobj->extravalue1 - Angle tolerance
+			// mobj->extravalue2 - Exec tag upon failure
+			// mobj->cvval - Allowable failure delay
+			// mobj->cvmem - Failure timer
+
+			angle_t ang = mobj->angle - R_PointToAngle2(mobj->x, mobj->y, mobj->tracer->x, mobj->tracer->y);
+
+			// \todo account for distance between mobj and tracer
+			// Because closer mobjs can be facing beyond the angle tolerance
+			// yet tracer is still in the camera view
+
+			// failure state: mobj is not facing tracer
+			// Reasaonable defaults: ANGLE_67h, ANGLE_292h
+			if (ang >= (UINT32)mobj->extravalue1 && ang <= ANGLE_MAX - (UINT32)mobj->extravalue1)
+			{
+				if (mobj->cvmem)
+					mobj->cvmem--;
+				else
+				{
+					INT32 exectag = mobj->extravalue2; // remember this before we erase the values
+
+					if (mobj->lastlook)
+						mobj->cvmem = mobj->cusval; // reset timer for next failure
+					else
+					{
+						// disable after first failure
+						mobj->eflags &= ~MFE_TRACERANGLE;
+						mobj->lastlook = mobj->extravalue1 = mobj->extravalue2 = mobj->cvmem = mobj->cusval = 0;
+					}
+
+					P_LinedefExecute(exectag, mobj, NULL);
+				}
+			}
+			else
+				mobj->cvmem = mobj->cusval; // reset failure timer
+		}
+
 		switch (mobj->type)
 		{
 			case MT_WALLSPIKEBASE:
@@ -8921,14 +8966,15 @@ static precipmobj_t *P_SpawnPrecipMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype
 static inline precipmobj_t *P_SpawnRainMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 {
 	precipmobj_t *mo = P_SpawnPrecipMobj(x,y,z,type);
-	mo->thinker.function.acp1 = (actionf_p1)P_RainThinker;
+	mo->precipflags |= PCF_RAIN;
+	//mo->thinker.function.acp1 = (actionf_p1)P_RainThinker;
 	return mo;
 }
 
 static inline precipmobj_t *P_SpawnSnowMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 {
 	precipmobj_t *mo = P_SpawnPrecipMobj(x,y,z,type);
-	mo->thinker.function.acp1 = (actionf_p1)P_SnowThinker;
+	//mo->thinker.function.acp1 = (actionf_p1)P_SnowThinker;
 	return mo;
 }
 
@@ -10891,6 +10937,16 @@ ML_EFFECT4 : Don't clip inside the ground
 		mobj->flags2 |= MF2_OBJECTFLIP;
 	}
 
+	// Extra functionality
+	if (mthing->options & MTF_EXTRA)
+	{
+		if (mobj->flags & MF_MONITOR && (mthing->angle & 16384))
+		{
+			// Store line exec tag to run upon popping
+			mobj->lastlook = (mthing->angle & 16383);
+		}
+	}
+
 	// Final set of not being able to draw nightsitems.
 	if (mobj->flags & MF_NIGHTSITEM)
 		mobj->flags2 |= MF2_DONTDRAW;
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 630600b547fd4a0aeca8c2095a24a8d79385bd08..eac5118b84c7be261e0d095db3a317094f631c37 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -239,6 +239,9 @@ typedef enum
 	MFE_SPRUNG            = 1<<8,
 	// Platform movement
 	MFE_APPLYPMOMZ        = 1<<9,
+	// Compute and trigger on mobj angle relative to tracer
+	// See Linedef Exec 457 (Track mobj angle to point)
+	MFE_TRACERANGLE       = 1<<10,
 	// free: to and including 1<<15
 } mobjeflag_t;
 
@@ -254,6 +257,10 @@ typedef enum {
 	PCF_FOF = 4,
 	// Above MOVING FOF (this means we need to keep floorz up to date...)
 	PCF_MOVINGFOF = 8,
+	// Is rain.
+	PCF_RAIN = 16,
+	// Ran the thinker this tic.
+	PCF_THUNK = 32,
 } precipflag_t;
 // Map Object definition.
 typedef struct mobj_s
@@ -449,7 +456,7 @@ boolean P_SupermanLook4Players(mobj_t *actor);
 void P_DestroyRobots(void);
 void P_SnowThinker(precipmobj_t *mobj);
 void P_RainThinker(precipmobj_t *mobj);
-FUNCMATH void P_NullPrecipThinker(precipmobj_t *mobj);
+void P_NullPrecipThinker(precipmobj_t *mobj);
 void P_RemovePrecipMobj(precipmobj_t *mobj);
 void P_SetScale(mobj_t *mobj, fixed_t newscale);
 void P_XYMovement(mobj_t *mo);
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 05aaf6f6d65e9f49e7b8a6c6d7d3395bedb0cd08..6a07e513f20d2f20407d6ad24cd0063d7de65b31 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -2121,8 +2121,7 @@ static void P_NetArchiveThinkers(void)
 	for (th = thinkercap.next; th != &thinkercap; th = th->next)
 	{
 		if (!(th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed
-		 || th->function.acp1 == (actionf_p1)P_RainThinker
-		 || th->function.acp1 == (actionf_p1)P_SnowThinker))
+		 || th->function.acp1 == (actionf_p1)P_NullPrecipThinker))
 			numsaved++;
 
 		if (th->function.acp1 == (actionf_p1)P_MobjThinker)
@@ -2131,8 +2130,7 @@ static void P_NetArchiveThinkers(void)
 			continue;
 		}
 #ifdef PARANOIA
-		else if (th->function.acp1 == (actionf_p1)P_RainThinker
-			|| th->function.acp1 == (actionf_p1)P_SnowThinker);
+		else if (th->function.acp1 == (actionf_p1)P_NullPrecipThinker);
 #endif
 		else if (th->function.acp1 == (actionf_p1)T_MoveCeiling)
 		{
diff --git a/src/p_setup.c b/src/p_setup.c
index 4926606eefc4ed0690ebf356199ca7c7d7390f50..2e86dffa133c54edc0c147192e36d4c8e841f91e 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -1546,6 +1546,7 @@ static void P_LoadRawSideDefs2(void *data)
 			}
 
 			case 443: // Calls a named Lua function
+			case 459: // Control text prompt (named tag)
 			{
 				char process[8*3+1];
 				memset(process,0,8*3+1);
@@ -2758,6 +2759,9 @@ boolean P_SetupLevel(boolean skipprecip)
 		I_UpdateNoVsync();
 	}
 
+	// Close text prompt before freeing the old level
+	F_EndTextPrompt(false, true);
+
 #ifdef HAVE_BLUA
 	LUA_InvalidateLevel();
 #endif
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_spec.c b/src/p_spec.c
index 5a53d5c86dc9eb5e0e9c21ea33f6b8bd5311970e..e5aa1a76f6d180b8ed1cc2ba9b517494c54443b5 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -35,6 +35,7 @@
 #include "m_misc.h"
 #include "m_cond.h" //unlock triggers
 #include "lua_hook.h" // LUAh_LinedefExecute
+#include "f_finale.h" // control text prompt
 
 #ifdef HW3SOUND
 #include "hardware/hw3sound.h"
@@ -2226,8 +2227,7 @@ void P_SwitchWeather(INT32 weathernum)
 
 		for (think = thinkercap.next; think != &thinkercap; think = think->next)
 		{
-			if ((think->function.acp1 != (actionf_p1)P_SnowThinker)
-				&& (think->function.acp1 != (actionf_p1)P_RainThinker))
+			if (think->function.acp1 != (actionf_p1)P_NullPrecipThinker)
 				continue; // not a precipmobj thinker
 
 			precipmobj = (precipmobj_t *)think;
@@ -2243,14 +2243,12 @@ void P_SwitchWeather(INT32 weathernum)
 
 		for (think = thinkercap.next; think != &thinkercap; think = think->next)
 		{
+			if (think->function.acp1 != (actionf_p1)P_NullPrecipThinker)
+				continue; // not a precipmobj thinker
+			precipmobj = (precipmobj_t *)think;
+
 			if (swap == PRECIP_RAIN) // Snow To Rain
 			{
-				if (!(think->function.acp1 == (actionf_p1)P_SnowThinker
-					|| think->function.acp1 == (actionf_p1)P_NullPrecipThinker))
-					continue; // not a precipmobj thinker
-
-				precipmobj = (precipmobj_t *)think;
-
 				precipmobj->flags = mobjinfo[MT_RAIN].flags;
 				st = &states[mobjinfo[MT_RAIN].spawnstate];
 				precipmobj->state = st;
@@ -2261,18 +2259,13 @@ void P_SwitchWeather(INT32 weathernum)
 
 				precipmobj->precipflags &= ~PCF_INVISIBLE;
 
-				think->function.acp1 = (actionf_p1)P_RainThinker;
+				precipmobj->precipflags |= PCF_RAIN;
+				//think->function.acp1 = (actionf_p1)P_RainThinker;
 			}
 			else if (swap == PRECIP_SNOW) // Rain To Snow
 			{
 				INT32 z;
 
-				if (!(think->function.acp1 == (actionf_p1)P_RainThinker
-					|| think->function.acp1 == (actionf_p1)P_NullPrecipThinker))
-					continue; // not a precipmobj thinker
-
-				precipmobj = (precipmobj_t *)think;
-
 				precipmobj->flags = mobjinfo[MT_SNOWFLAKE].flags;
 				z = M_RandomByte();
 
@@ -2290,19 +2283,13 @@ void P_SwitchWeather(INT32 weathernum)
 				precipmobj->frame = st->frame;
 				precipmobj->momz = mobjinfo[MT_SNOWFLAKE].speed;
 
-				precipmobj->precipflags &= ~PCF_INVISIBLE;
+				precipmobj->precipflags &= ~(PCF_INVISIBLE|PCF_RAIN);
 
-				think->function.acp1 = (actionf_p1)P_SnowThinker;
+				//think->function.acp1 = (actionf_p1)P_SnowThinker;
 			}
 			else if (swap == PRECIP_BLANK || swap == PRECIP_STORM_NORAIN) // Remove precip, but keep it around for reuse.
 			{
-				if (!(think->function.acp1 == (actionf_p1)P_RainThinker
-					|| think->function.acp1 == (actionf_p1)P_SnowThinker))
-					continue;
-
-				precipmobj = (precipmobj_t *)think;
-
-				think->function.acp1 = (actionf_p1)P_NullPrecipThinker;
+				//think->function.acp1 = (actionf_p1)P_NullPrecipThinker;
 
 				precipmobj->precipflags |= PCF_INVISIBLE;
 			}
@@ -3093,6 +3080,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				INT16 foftag = (INT16)(sides[line->sidenum[0]].rowoffset>>FRACBITS);
 				sector_t *sec; // Sector that the FOF is visible in
 				ffloor_t *rover; // FOF that we are going to crumble
+				boolean foundrover = false; // for debug, "Can't find a FOF" message
 
 				for (secnum = -1; (secnum = P_FindSectorFromTag(sectag, secnum)) >= 0 ;)
 				{
@@ -3107,16 +3095,18 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					for (rover = sec->ffloors; rover; rover = rover->next)
 					{
 						if (rover->master->frontsector->tag == foftag)
-							break;
+						{
+							foundrover = true;
+
+							EV_CrumbleChain(sec, rover);
+						}
 					}
 
-					if (!rover)
+					if (!foundrover)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 436 Executor: Can't find a FOF control sector with tag %d\n", foftag);
 						return;
 					}
-
-					EV_CrumbleChain(sec, rover);
 				}
 			}
 			break;
@@ -3277,6 +3267,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				INT16 foftag = (INT16)(sides[line->sidenum[0]].rowoffset>>FRACBITS);
 				sector_t *sec; // Sector that the FOF is visible (or not visible) in
 				ffloor_t *rover; // FOF to vanish/un-vanish
+				boolean foundrover = false; // for debug, "Can't find a FOF" message
 				ffloortype_e oldflags; // store FOF's old flags
 
 				for (secnum = -1; (secnum = P_FindSectorFromTag(sectag, secnum)) >= 0 ;)
@@ -3292,26 +3283,28 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					for (rover = sec->ffloors; rover; rover = rover->next)
 					{
 						if (rover->master->frontsector->tag == foftag)
-							break;
+						{
+							foundrover = true;
+
+							oldflags = rover->flags;
+
+							// Abracadabra!
+							if (line->flags & ML_NOCLIMB)
+								rover->flags |= FF_EXISTS;
+							else
+								rover->flags &= ~FF_EXISTS;
+
+							// if flags changed, reset sector's light list
+							if (rover->flags != oldflags)
+								sec->moved = true;
+						}
 					}
 
-					if (!rover)
+					if (!foundrover)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 445 Executor: Can't find a FOF control sector with tag %d\n", foftag);
 						return;
 					}
-
-					oldflags = rover->flags;
-
-					// Abracadabra!
-					if (line->flags & ML_NOCLIMB)
-						rover->flags |= FF_EXISTS;
-					else
-						rover->flags &= ~FF_EXISTS;
-
-					// if flags changed, reset sector's light list
-					if (rover->flags != oldflags)
-						sec->moved = true;
 				}
 			}
 			break;
@@ -3322,6 +3315,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				INT16 foftag = (INT16)(sides[line->sidenum[0]].rowoffset>>FRACBITS);
 				sector_t *sec; // Sector that the FOF is visible in
 				ffloor_t *rover; // FOF that we are going to make fall down
+				boolean foundrover = false; // for debug, "Can't find a FOF" message
 				player_t *player = NULL; // player that caused FOF to fall
 				boolean respawn = true; // should the fallen FOF respawn?
 
@@ -3344,19 +3338,21 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					for (rover = sec->ffloors; rover; rover = rover->next)
 					{
 						if (rover->master->frontsector->tag == foftag)
-							break;
+						{
+							foundrover = true;
+
+							if (line->flags & ML_BLOCKMONSTERS) // FOF flags determine respawn ability instead?
+								respawn = !(rover->flags & FF_NORETURN) ^ !!(line->flags & ML_NOCLIMB); // no climb inverts
+
+							EV_StartCrumble(rover->master->frontsector, rover, (rover->flags & FF_FLOATBOB), player, rover->alpha, respawn);
+						}
 					}
 
-					if (!rover)
+					if (!foundrover)
 					{
 						CONS_Debug(DBG_GAMELOGIC, "Line type 446 Executor: Can't find a FOF control sector with tag %d\n", foftag);
 						return;
 					}
-
-					if (line->flags & ML_BLOCKMONSTERS) // FOF flags determine respawn ability instead?
-						respawn = !(rover->flags & FF_NORETURN) ^ !!(line->flags & ML_NOCLIMB); // no climb inverts
-
-					EV_StartCrumble(rover->master->frontsector, rover, (rover->flags & FF_FLOATBOB), player, rover->alpha, respawn);
 				}
 			}
 			break;
@@ -3489,6 +3485,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			INT16 foftag = (INT16)(sides[line->sidenum[0]].rowoffset>>FRACBITS);
 			sector_t *sec; // Sector that the FOF is visible in
 			ffloor_t *rover; // FOF that we are going to operate
+			boolean foundrover = false; // for debug, "Can't find a FOF" message
 
 			for (secnum = -1; (secnum = P_FindSectorFromTag(sectag, secnum)) >= 0 ;)
 			{
@@ -3503,38 +3500,40 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				for (rover = sec->ffloors; rover; rover = rover->next)
 				{
 					if (rover->master->frontsector->tag == foftag)
-						break;
+					{
+						foundrover = true;
+
+						// If fading an invisible FOF whose render flags we did not yet set,
+						// initialize its alpha to 1
+						// for relative alpha calc
+						if (!(line->flags & ML_NOCLIMB) &&      // do translucent
+							(rover->spawnflags & FF_NOSHADE) && // do not include light blocks, which don't set FF_NOSHADE
+							!(rover->spawnflags & FF_RENDERSIDES) &&
+							!(rover->spawnflags & FF_RENDERPLANES) &&
+							!(rover->flags & FF_RENDERALL))
+							rover->alpha = 1;
+
+						P_RemoveFakeFloorFader(rover);
+						P_FadeFakeFloor(rover,
+							rover->alpha,
+							max(1, min(256, (line->flags & ML_EFFECT3) ? rover->alpha + destvalue : destvalue)),
+							0,                                  // set alpha immediately
+							false, NULL,                        // tic-based logic
+							false,                              // do not handle FF_EXISTS
+							!(line->flags & ML_NOCLIMB),        // handle FF_TRANSLUCENT
+							false,                              // do not handle lighting
+							false,                              // do not handle colormap
+							false,                              // do not handle collision
+							false,                              // do not do ghost fade (no collision during fade)
+							true);                               // use exact alpha values (for opengl)
+					}
 				}
 
-				if (!rover)
+				if (!foundrover)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 452 Executor: Can't find a FOF control sector with tag %d\n", foftag);
 					return;
 				}
-
-				// If fading an invisible FOF whose render flags we did not yet set,
-				// initialize its alpha to 1
-				// for relative alpha calc
-				if (!(line->flags & ML_NOCLIMB) &&      // do translucent
-					(rover->spawnflags & FF_NOSHADE) && // do not include light blocks, which don't set FF_NOSHADE
-					!(rover->spawnflags & FF_RENDERSIDES) &&
-					!(rover->spawnflags & FF_RENDERPLANES) &&
-					!(rover->flags & FF_RENDERALL))
-					rover->alpha = 1;
-
-				P_RemoveFakeFloorFader(rover);
-				P_FadeFakeFloor(rover,
-					rover->alpha,
-					max(1, min(256, (line->flags & ML_EFFECT3) ? rover->alpha + destvalue : destvalue)),
-					0,                                  // set alpha immediately
-					false, NULL,                        // tic-based logic
-					false,                              // do not handle FF_EXISTS
-					!(line->flags & ML_NOCLIMB),        // handle FF_TRANSLUCENT
-					false,                              // do not handle lighting
-					false,                              // do not handle colormap
-					false,                              // do not handle collision
-					false,                              // do not do ghost fade (no collision during fade)
-					true);                               // use exact alpha values (for opengl)
 			}
 			break;
 		}
@@ -3549,6 +3548,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			INT16 foftag = (INT16)(sides[line->sidenum[0]].rowoffset>>FRACBITS);
 			sector_t *sec; // Sector that the FOF is visible in
 			ffloor_t *rover; // FOF that we are going to operate
+			boolean foundrover = false; // for debug, "Can't find a FOF" message
 			size_t j = 0; // sec->ffloors is saved as ffloor #0, ss->ffloors->next is #1, etc
 
 			for (secnum = -1; (secnum = P_FindSectorFromTag(sectag, secnum)) >= 0 ;)
@@ -3564,64 +3564,66 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				for (rover = sec->ffloors; rover; rover = rover->next)
 				{
 					if (rover->master->frontsector->tag == foftag)
-						break;
+					{
+						foundrover = true;
+
+						// Prevent continuous execs from interfering on an existing fade
+						if (!(line->flags & ML_EFFECT5)
+							&& rover->fadingdata)
+							//&& ((fade_t*)rover->fadingdata)->timer > (ticbased ? 2 : speed*2))
+						{
+							CONS_Debug(DBG_GAMELOGIC, "Line type 453 Executor: Fade FOF thinker already exists, timer: %d\n", ((fade_t*)rover->fadingdata)->timer);
+							continue;
+						}
+
+						if (speed > 0)
+							P_AddFakeFloorFader(rover, secnum, j,
+								destvalue,
+								speed,
+								(line->flags & ML_EFFECT4),         // tic-based logic
+								(line->flags & ML_EFFECT3),         // Relative destvalue
+								!(line->flags & ML_BLOCKMONSTERS),  // do not handle FF_EXISTS
+								!(line->flags & ML_NOCLIMB),        // do not handle FF_TRANSLUCENT
+								!(line->flags & ML_EFFECT2),        // do not handle lighting
+								!(line->flags & ML_EFFECT2),        // do not handle colormap (ran out of flags)
+								!(line->flags & ML_BOUNCY),         // do not handle collision
+								(line->flags & ML_EFFECT1),         // do ghost fade (no collision during fade)
+								(line->flags & ML_TFERLINE));       // use exact alpha values (for opengl)
+						else
+						{
+							// If fading an invisible FOF whose render flags we did not yet set,
+							// initialize its alpha to 1
+							// for relative alpha calc
+							if (!(line->flags & ML_NOCLIMB) &&      // do translucent
+								(rover->spawnflags & FF_NOSHADE) && // do not include light blocks, which don't set FF_NOSHADE
+								!(rover->spawnflags & FF_RENDERSIDES) &&
+								!(rover->spawnflags & FF_RENDERPLANES) &&
+								!(rover->flags & FF_RENDERALL))
+								rover->alpha = 1;
+
+							P_RemoveFakeFloorFader(rover);
+							P_FadeFakeFloor(rover,
+								rover->alpha,
+								max(1, min(256, (line->flags & ML_EFFECT3) ? rover->alpha + destvalue : destvalue)),
+								0,                                  // set alpha immediately
+								false, NULL,                        // tic-based logic
+								!(line->flags & ML_BLOCKMONSTERS),  // do not handle FF_EXISTS
+								!(line->flags & ML_NOCLIMB),        // do not handle FF_TRANSLUCENT
+								!(line->flags & ML_EFFECT2),        // do not handle lighting
+								!(line->flags & ML_EFFECT2),        // do not handle colormap (ran out of flags)
+								!(line->flags & ML_BOUNCY),         // do not handle collision
+								(line->flags & ML_EFFECT1),         // do ghost fade (no collision during fade)
+								(line->flags & ML_TFERLINE));       // use exact alpha values (for opengl)
+						}
+					}
 					j++;
 				}
 
-				if (!rover)
+				if (!foundrover)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 453 Executor: Can't find a FOF control sector with tag %d\n", foftag);
 					return;
 				}
-
-				// Prevent continuous execs from interfering on an existing fade
-				if (!(line->flags & ML_EFFECT5)
-					&& rover->fadingdata)
-					//&& ((fade_t*)rover->fadingdata)->timer > (ticbased ? 2 : speed*2))
-				{
-					CONS_Debug(DBG_GAMELOGIC, "Line type 453 Executor: Fade FOF thinker already exists, timer: %d\n", ((fade_t*)rover->fadingdata)->timer);
-					continue;
-				}
-
-				if (speed > 0)
-					P_AddFakeFloorFader(rover, secnum, j,
-						destvalue,
-						speed,
-						(line->flags & ML_EFFECT4),         // tic-based logic
-						(line->flags & ML_EFFECT3),         // Relative destvalue
-						!(line->flags & ML_BLOCKMONSTERS),  // do not handle FF_EXISTS
-						!(line->flags & ML_NOCLIMB),        // do not handle FF_TRANSLUCENT
-						!(line->flags & ML_EFFECT2),        // do not handle lighting
-						!(line->flags & ML_EFFECT2),        // do not handle colormap (ran out of flags)
-						!(line->flags & ML_BOUNCY),         // do not handle collision
-						(line->flags & ML_EFFECT1),         // do ghost fade (no collision during fade)
-						(line->flags & ML_TFERLINE));       // use exact alpha values (for opengl)
-				else
-				{
-					// If fading an invisible FOF whose render flags we did not yet set,
-					// initialize its alpha to 1
-					// for relative alpha calc
-					if (!(line->flags & ML_NOCLIMB) &&      // do translucent
-						(rover->spawnflags & FF_NOSHADE) && // do not include light blocks, which don't set FF_NOSHADE
-						!(rover->spawnflags & FF_RENDERSIDES) &&
-						!(rover->spawnflags & FF_RENDERPLANES) &&
-						!(rover->flags & FF_RENDERALL))
-						rover->alpha = 1;
-
-					P_RemoveFakeFloorFader(rover);
-					P_FadeFakeFloor(rover,
-						rover->alpha,
-						max(1, min(256, (line->flags & ML_EFFECT3) ? rover->alpha + destvalue : destvalue)),
-						0,                                  // set alpha immediately
-						false, NULL,                        // tic-based logic
-						!(line->flags & ML_BLOCKMONSTERS),  // do not handle FF_EXISTS
-						!(line->flags & ML_NOCLIMB),        // do not handle FF_TRANSLUCENT
-						!(line->flags & ML_EFFECT2),        // do not handle lighting
-						!(line->flags & ML_EFFECT2),        // do not handle colormap (ran out of flags)
-						!(line->flags & ML_BOUNCY),         // do not handle collision
-						(line->flags & ML_EFFECT1),         // do ghost fade (no collision during fade)
-						(line->flags & ML_TFERLINE));       // use exact alpha values (for opengl)
-				}
 			}
 			break;
 		}
@@ -3632,6 +3634,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			INT16 foftag = (INT16)(sides[line->sidenum[0]].rowoffset>>FRACBITS);
 			sector_t *sec; // Sector that the FOF is visible in
 			ffloor_t *rover; // FOF that we are going to operate
+			boolean foundrover = false; // for debug, "Can't find a FOF" message
 
 			for (secnum = -1; (secnum = P_FindSectorFromTag(sectag, secnum)) >= 0 ;)
 			{
@@ -3646,17 +3649,19 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				for (rover = sec->ffloors; rover; rover = rover->next)
 				{
 					if (rover->master->frontsector->tag == foftag)
-						break;
+					{
+						foundrover = true;
+
+						P_ResetFakeFloorFader(rover, NULL,
+							!(line->flags & ML_BLOCKMONSTERS)); // do not finalize collision flags
+					}
 				}
 
-				if (!rover)
+				if (!foundrover)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line type 454 Executor: Can't find a FOF control sector with tag %d\n", foftag);
 					return;
 				}
-
-				P_ResetFakeFloorFader(rover, NULL,
-					!(line->flags & ML_BLOCKMONSTERS)); // do not finalize collision flags
 			}
 			break;
 		}
@@ -3755,6 +3760,68 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				P_ResetColormapFader(&sectors[secnum]);
 			break;
 
+		case 457: // Track mobj angle to point
+			if (mo)
+			{
+				INT32 failureangle = min(max(abs(sides[line->sidenum[0]].textureoffset>>FRACBITS), 0), 360) * ANG1;
+				INT32 failuredelay = abs(sides[line->sidenum[0]].rowoffset>>FRACBITS);
+				INT32 failureexectag = line->sidenum[1] != 0xffff ?
+					(INT32)(sides[line->sidenum[1]].textureoffset>>FRACBITS) : 0;
+				boolean persist = (line->flags & ML_EFFECT2);
+				mobj_t *anchormo;
+
+				if ((secnum = P_FindSectorFromLineTag(line, -1)) < 0)
+					return;
+
+				anchormo = P_GetObjectTypeInSectorNum(MT_ANGLEMAN, secnum);
+				if (!anchormo)
+					return;
+
+				mo->eflags |= MFE_TRACERANGLE;
+				P_SetTarget(&mo->tracer, anchormo);
+				mo->lastlook = persist; // don't disable behavior after first failure
+				mo->extravalue1 = failureangle; // angle to exceed for failure state
+				mo->extravalue2 = failureexectag; // exec tag for failure state (angle is not within range)
+				mo->cusval = mo->cvmem = failuredelay; // cusval = tics to allow failure before line trigger; cvmem = decrement timer
+			}
+			break;
+
+		case 458: // Stop tracking mobj angle to point
+			if (mo && (mo->eflags & MFE_TRACERANGLE))
+			{
+				mo->eflags &= ~MFE_TRACERANGLE;
+				P_SetTarget(&mo->tracer, NULL);
+				mo->lastlook = mo->cvmem = mo->cusval = mo->extravalue1 = mo->extravalue2 = 0;
+			}
+			break;
+
+		case 459: // Control Text Prompt
+			// console player only unless NOCLIMB is set
+			if (mo && mo->player && P_IsLocalPlayer(mo->player) && (!bot || bot != mo))
+			{
+				INT32 promptnum = max(0, (sides[line->sidenum[0]].textureoffset>>FRACBITS)-1);
+				INT32 pagenum = max(0, (sides[line->sidenum[0]].rowoffset>>FRACBITS)-1);
+				INT32 postexectag = abs((line->sidenum[1] != 0xFFFF) ? sides[line->sidenum[1]].textureoffset>>FRACBITS : line->tag);
+
+				boolean closetextprompt = (line->flags & ML_BLOCKMONSTERS);
+				//boolean allplayers = (line->flags & ML_NOCLIMB);
+				boolean runpostexec = (line->flags & ML_EFFECT1);
+				boolean blockcontrols = !(line->flags & ML_EFFECT2);
+				boolean freezerealtime = !(line->flags & ML_EFFECT3);
+				//boolean freezethinkers = (line->flags & ML_EFFECT4);
+				boolean callbynamedtag = (line->flags & ML_TFERLINE);
+
+				if (closetextprompt)
+					F_EndTextPrompt(false, false);
+				else
+				{
+					if (callbynamedtag && sides[line->sidenum[0]].text && sides[line->sidenum[0]].text[0])
+						F_GetPromptPageByNamedTag(sides[line->sidenum[0]].text, &promptnum, &pagenum);
+					F_StartTextPrompt(promptnum, pagenum, mo, runpostexec ? postexectag : 0, blockcontrols, freezerealtime);
+				}
+			}
+			break;
+
 #ifdef POLYOBJECTS
 		case 480: // Polyobj_DoorSlide
 		case 481: // Polyobj_DoorSwing
diff --git a/src/p_tick.c b/src/p_tick.c
index a51ce2eb6eb326248e44a5a9120a8bd53c7faf25..eb47e0c3608a7d210c37a6de3e4d1adeb80fbc2e 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -56,12 +56,12 @@ void Command_Numthinkers_f(void)
 		CONS_Printf(M_GetText("numthinkers <#>: Count number of thinkers\n"));
 		CONS_Printf(
 			"\t1: P_MobjThinker\n"
-			"\t2: P_RainThinker\n"
-			"\t3: P_SnowThinker\n"
-			"\t4: P_NullPrecipThinker\n"
-			"\t5: T_Friction\n"
-			"\t6: T_Pusher\n"
-			"\t7: P_RemoveThinkerDelayed\n");
+			/*"\t2: P_RainThinker\n"
+			"\t3: P_SnowThinker\n"*/
+			"\t2: P_NullPrecipThinker\n"
+			"\t3: T_Friction\n"
+			"\t4: T_Pusher\n"
+			"\t5: P_RemoveThinkerDelayed\n");
 		return;
 	}
 
@@ -73,27 +73,27 @@ void Command_Numthinkers_f(void)
 			action = (actionf_p1)P_MobjThinker;
 			CONS_Printf(M_GetText("Number of %s: "), "P_MobjThinker");
 			break;
-		case 2:
+		/*case 2:
 			action = (actionf_p1)P_RainThinker;
 			CONS_Printf(M_GetText("Number of %s: "), "P_RainThinker");
 			break;
 		case 3:
 			action = (actionf_p1)P_SnowThinker;
 			CONS_Printf(M_GetText("Number of %s: "), "P_SnowThinker");
-			break;
-		case 4:
+			break;*/
+		case 2:
 			action = (actionf_p1)P_NullPrecipThinker;
 			CONS_Printf(M_GetText("Number of %s: "), "P_NullPrecipThinker");
 			break;
-		case 5:
+		case 3:
 			action = (actionf_p1)T_Friction;
 			CONS_Printf(M_GetText("Number of %s: "), "T_Friction");
 			break;
-		case 6:
+		case 4:
 			action = (actionf_p1)T_Pusher;
 			CONS_Printf(M_GetText("Number of %s: "), "T_Pusher");
 			break;
-		case 7:
+		case 5:
 			action = (actionf_p1)P_RemoveThinkerDelayed;
 			CONS_Printf(M_GetText("Number of %s: "), "P_RemoveThinkerDelayed");
 			break;
diff --git a/src/p_user.c b/src/p_user.c
index 736743b3b8b7ebbaaa721ddbd68dbc63bdea71df..2639ab148503f3c586a2214a42584e01e6542ee9 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -8777,9 +8777,15 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	subsector_t *newsubsec;
 	fixed_t f1, f2;
 
-	cameranoclip = (player->powers[pw_carry] == CR_NIGHTSMODE || player->pflags & PF_NOCLIP) || (player->mo->flags & (MF_NOCLIP|MF_NOCLIPHEIGHT)); // Noclipping player camera noclips too!!
+	// We probably shouldn't move the camera if there is no player or player mobj somehow
+	if (!player || !player->mo)
+		return true;
+
+	mo = player->mo;
+
+	cameranoclip = (player->powers[pw_carry] == CR_NIGHTSMODE || player->pflags & PF_NOCLIP) || (mo->flags & (MF_NOCLIP|MF_NOCLIPHEIGHT)); // Noclipping player camera noclips too!!
 
-	if (!(player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD))
+	if (!(player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || tutorialmode))
 	{
 		if (player->spectator) // force cam off for spectators
 			return true;
@@ -8798,7 +8804,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		else if (player == &players[secondarydisplayplayer])
 			focusangle = localangle2;
 		else
-			focusangle = player->mo->angle;
+			focusangle = mo->angle;
 		if (thiscam == &camera)
 			camrotate = cv_cam_rotate.value;
 		else if (thiscam == &camera2)
@@ -8810,17 +8816,9 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		return true;
 	}
 
-	if (!player || !player->mo)
-		return true;
-
-	mo = player->mo;
-
 	thiscam->radius = FixedMul(20*FRACUNIT, mo->scale);
 	thiscam->height = FixedMul(16*FRACUNIT, mo->scale);
 
-	if (!mo)
-		return true;
-
 	// Don't run while respawning from a starpost
 	// Inu 4/8/13 Why not?!
 //	if (leveltime > 0 && timeinmap <= 0)
@@ -8828,7 +8826,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 
 	if (player->powers[pw_carry] == CR_NIGHTSMODE)
 	{
-		focusangle = player->mo->angle;
+		focusangle = mo->angle;
 		focusaiming = 0;
 	}
 	else if (player == &players[consoleplayer])
@@ -8843,14 +8841,23 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	}
 	else
 	{
-		focusangle = player->mo->angle;
+		focusangle = mo->angle;
 		focusaiming = player->aiming;
 	}
 
 	if (P_CameraThinker(player, thiscam, resetcalled))
 		return true;
 
-	if (thiscam == &camera)
+	if (tutorialmode)
+	{
+		// force defaults because we have a camera look section
+		camspeed = (INT32)(atof(cv_cam_speed.defaultvalue) * FRACUNIT);
+		camstill = (!stricmp(cv_cam_still.defaultvalue, "off")) ? false : true;
+		camrotate = atoi(cv_cam_rotate.defaultvalue);
+		camdist = FixedMul((INT32)(atof(cv_cam_dist.defaultvalue) * FRACUNIT), mo->scale);
+		camheight = FixedMul((INT32)(atof(cv_cam_height.defaultvalue) * FRACUNIT), FixedMul(player->camerascale, mo->scale));
+	}
+	else if (thiscam == &camera)
 	{
 		camspeed = cv_cam_speed.value;
 		camstill = cv_cam_still.value;
@@ -8890,12 +8897,12 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 			angle = R_PointToAngle2(player->axis1->x, player->axis1->y, player->axis2->x, player->axis2->y);
 			angle += ANGLE_90;
 		}
-		else if (player->mo->target)
+		else if (mo->target)
 		{
-			if (player->mo->target->flags2 & MF2_AMBUSH)
-				angle = R_PointToAngle2(player->mo->target->x, player->mo->target->y, player->mo->x, player->mo->y);
+			if (mo->target->flags2 & MF2_AMBUSH)
+				angle = R_PointToAngle2(mo->target->x, mo->target->y, mo->x, mo->y);
 			else
-				angle = R_PointToAngle2(player->mo->x, player->mo->y, player->mo->target->x, player->mo->target->y);
+				angle = R_PointToAngle2(mo->x, mo->y, mo->target->x, mo->target->y);
 		}
 	}
 	else if (P_AnalogMove(player)) // Analog
@@ -8984,7 +8991,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	if (twodlevel || (mo->flags2 & MF2_TWOD))
 	{
 		// Camera doesn't ALWAYS need to move, only when running...
-		if (abs(player->mo->momx) > 10)
+		if (abs(mo->momx) > 10)
 		{
 			// Move the camera all smooth-like, not jerk it around...
 			if (mo->momx > 0)
@@ -9291,24 +9298,20 @@ 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 - player->mo->x, vy - player->mo->y) < FixedMul(48*FRACUNIT, mo->scale))
-			player->mo->flags2 |= MF2_SHADOW;
+		if (P_AproxDistance(vx - mo->x, vy - mo->y) < FixedMul(48*FRACUNIT, mo->scale))
+			mo->flags2 |= MF2_SHADOW;
 		else
-			player->mo->flags2 &= ~MF2_SHADOW;
+			mo->flags2 &= ~MF2_SHADOW;
 	}
 	else
-		player->mo->flags2 &= ~MF2_SHADOW;
+		mo->flags2 &= ~MF2_SHADOW;
 
 /*	if (!resetcalled && (player->powers[pw_carry] == CR_NIGHTSMODE && player->exiting))
 	{
@@ -9623,8 +9626,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)
@@ -10597,6 +10601,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/r_draw8.c b/src/r_draw8.c
index 76eb58c0df44357b3695117745846eb20a67f9ab..bda146546457bffd158990726871236582a1625a 100644
--- a/src/r_draw8.c
+++ b/src/r_draw8.c
@@ -261,7 +261,7 @@ void R_Draw2sMultiPatchTranslucentColumn_8(void)
 				val = source[frac>>FRACBITS];
 
 				if (val != TRANSPARENTPIXEL)
-					*dest = colormap[*(transmap + (val<<8) + (*dest))];
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
 
 				dest += vid.width;
 
@@ -281,12 +281,12 @@ void R_Draw2sMultiPatchTranslucentColumn_8(void)
 			{
 				val = source[(frac>>FRACBITS) & heightmask];
 				if (val != TRANSPARENTPIXEL)
-					*dest = colormap[*(transmap + (val<<8) + (*dest))];
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
 				dest += vid.width;
 				frac += fracstep;
 				val = source[(frac>>FRACBITS) & heightmask];
 				if (val != TRANSPARENTPIXEL)
-					*dest = colormap[*(transmap + (val<<8) + (*dest))];
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
 				dest += vid.width;
 				frac += fracstep;
 			}
@@ -294,7 +294,7 @@ void R_Draw2sMultiPatchTranslucentColumn_8(void)
 			{
 				val = source[(frac>>FRACBITS) & heightmask];
 				if (val != TRANSPARENTPIXEL)
-					*dest = colormap[*(transmap + (val<<8) + (*dest))];
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
 			}
 		}
 	}
diff --git a/src/r_main.c b/src/r_main.c
index bfca180d0011ed9dc34d68bf9948b448b12827b8..98ce978a693b73acb894b46cfb9fe682990f7fe5 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -913,7 +913,7 @@ void R_SetupFrame(player_t *player, boolean skybox)
 		chasecam = (cv_chasecam.value != 0);
 	}
 
-	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN)
+	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
 		chasecam = true; // force chasecam on
 	else if (player->spectator) // no spectator chasecam
 		chasecam = false; // force chasecam off
diff --git a/src/r_plane.h b/src/r_plane.h
index 16c8c12a448e6cd5b905b066391404889b0b8eb9..dff58669a49ee14210c1aac6726e291949397649 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -87,7 +87,7 @@ extern lighttable_t **planezlight;
 extern fixed_t *yslope;
 extern fixed_t distscale[MAXVIDWIDTH];
 
-FUNCMATH void R_InitPlanes(void);
+void R_InitPlanes(void);
 void R_PortalStoreClipValues(INT32 start, INT32 end, INT16 *ceil, INT16 *floor, fixed_t *scale);
 void R_PortalRestoreClipValues(INT32 start, INT32 end, INT16 *ceil, INT16 *floor, fixed_t *scale);
 void R_ClearPlanes(void);
diff --git a/src/r_splats.h b/src/r_splats.h
index c0ba6881c7b384126796a30b9a9c8b5526419844..349d8fa7a4653cbb65874ca87950451f18ac9bbb 100644
--- a/src/r_splats.h
+++ b/src/r_splats.h
@@ -63,11 +63,7 @@ typedef struct floorsplat_s
 fixed_t P_SegLength(seg_t *seg);
 
 // call at P_SetupLevel()
-#if !(defined (WALLSPLATS) || defined (FLOORSPLATS))
-FUNCMATH void R_ClearLevelSplats(void);
-#else
 void R_ClearLevelSplats(void);
-#endif
 
 #ifdef WALLSPLATS
 void R_AddWallSplat(line_t *wallline, INT16 sectorside, const char *patchname, fixed_t top,
diff --git a/src/r_things.c b/src/r_things.c
index f4a0fd28c900df5cc5abddbbcf6c312f5425c6fe..a6a7d9877c977b60eb2008126f9d87080fd96cf6 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -1525,6 +1525,17 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 			return;
 	}
 
+	// okay, we can't return now except for vertical clipping... this is a hack, but weather isn't networked, so it should be ok
+	if (!(thing->precipflags & PCF_THUNK))
+	{
+		if (thing->precipflags & PCF_RAIN)
+			P_RainThinker(thing);
+		else
+			P_SnowThinker(thing);
+		thing->precipflags |= PCF_THUNK;
+	}
+
+
 	//SoM: 3/17/2000: Disregard sprites that are out of view..
 	gzt = thing->z + spritecachedinfo[lump].topoffset;
 	gz = gzt - spritecachedinfo[lump].height;
@@ -1642,8 +1653,10 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel)
 
 			approx_dist = P_AproxDistance(viewx-thing->x, viewy-thing->y);
 
-			if (approx_dist <= limit_dist)
-				R_ProjectSprite(thing);
+			if (approx_dist > limit_dist)
+				continue;
+
+			R_ProjectSprite(thing);
 		}
 	}
 	else
@@ -1664,8 +1677,10 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel)
 
 			approx_dist = P_AproxDistance(viewx-precipthing->x, viewy-precipthing->y);
 
-			if (approx_dist <= limit_dist)
-				R_ProjectPrecipitationSprite(precipthing);
+			if (approx_dist > limit_dist)
+				continue;
+
+			R_ProjectPrecipitationSprite(precipthing);
 		}
 	}
 	else
diff --git a/src/s_sound.c b/src/s_sound.c
index 2bf60b08792d1130b2fb0d2a35ff977aa6f06949..f25e4af13bda5a825e453365b62258f0196a5ce7 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
 
 #if defined(HAVE_BLUA) && defined(HAVE_LUA_MUSICPLUS)
 #include "lua_hook.h" // MusicChange hook
@@ -51,6 +52,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
@@ -96,6 +99,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
 
@@ -251,6 +255,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
 	{
@@ -630,7 +639,7 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 
 		// Assigns the handle to one of the channels in the
 		// mix/output buffer.
-		channels[cnum].handle = I_StartSound(sfx_id, volume, sep, pitch, priority);
+		channels[cnum].handle = I_StartSound(sfx_id, volume, sep, pitch, priority, cnum);
 	}
 
 dontplay:
@@ -683,7 +692,7 @@ dontplay:
 
 	// Assigns the handle to one of the channels in the
 	// mix/output buffer.
-	channels[cnum].handle = I_StartSound(sfx_id, volume, sep, pitch, priority);
+	channels[cnum].handle = I_StartSound(sfx_id, volume, sep, pitch, priority, cnum);
 }
 
 void S_StartSound(const void *origin, sfxenum_t sfx_id)
@@ -1747,3 +1756,88 @@ void S_Start(void)
 		S_StopMusic();
 	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 }
+
+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 7a962d11a48635397f070179940c058d9860be8e..236805ab90f141d5f0c93e30c664f741e5980085 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/screen.c b/src/screen.c
index 9bbcb995c5e84ae1a9cc22c7d8ef00675f166d3e..b3e94091fea11c820983ac2f96ac6bd366104d3b 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -69,11 +69,7 @@ consvar_t cv_scr_height = {"scr_height", "800", CV_SAVE, CV_Unsigned, NULL, 0, N
 consvar_t cv_scr_depth = {"scr_depth", "16 bits", CV_SAVE, scr_depth_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_renderview = {"renderview", "On", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 
-#ifdef DIRECTFULLSCREEN
-static FUNCMATH void SCR_ChangeFullscreen (void);
-#else
 static void SCR_ChangeFullscreen (void);
-#endif
 
 consvar_t cv_fullscreen = {"fullscreen", "Yes", CV_SAVE|CV_CALL, CV_YesNo, SCR_ChangeFullscreen, 0, NULL, NULL, 0, 0, NULL};
 
diff --git a/src/sdl/i_cdmus.c b/src/sdl/i_cdmus.c
index 3105f512278e98536de12c9f27e02c5a884e4321..5d086e73a05fdc7d28b23818a9141aa9edc650dc 100644
--- a/src/sdl/i_cdmus.c
+++ b/src/sdl/i_cdmus.c
@@ -12,19 +12,19 @@ consvar_t cd_volume = {"cd_volume","31",CV_SAVE,soundvolume_cons_t, NULL, 0, NUL
 consvar_t cdUpdate  = {"cd_update","1",CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 
-FUNCMATH void I_InitCD(void){}
+void I_InitCD(void){}
 
-FUNCMATH void I_StopCD(void){}
+void I_StopCD(void){}
 
-FUNCMATH void I_PauseCD(void){}
+void I_PauseCD(void){}
 
-FUNCMATH void I_ResumeCD(void){}
+void I_ResumeCD(void){}
 
-FUNCMATH void I_ShutdownCD(void){}
+void I_ShutdownCD(void){}
 
-FUNCMATH void I_UpdateCD(void){}
+void I_UpdateCD(void){}
 
-FUNCMATH void I_PlayCD(UINT8 track, UINT8 looping)
+void I_PlayCD(UINT8 track, UINT8 looping)
 {
 	(void)track;
 	(void)looping;
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index c8fcd080ebd42aa5f989c60875e0cd2f4bbe1aec..0963cc288dfcd3eb40ce1ca64b6a37f79f70be4a 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
@@ -124,6 +127,10 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #include "macosx/mac_resources.h"
 #endif
 
+#ifndef errno
+#include <errno.h>
+#endif
+
 // Locations for searching the srb2.pk3
 #if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
 #define DEFAULTWADLOCATION1 "/usr/local/share/games/SRB2"
@@ -1149,6 +1156,7 @@ static void I_ShutdownJoystick2(void)
 		D_PostEvent(&event);
 	}
 
+	joystick2_started = 0;
 	JoyReset(&JoyInfo2);
 	if (!joystick_started && !joystick2_started && SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
 	{
@@ -1678,7 +1686,7 @@ static void I_ShutdownMouse2(void)
 	EscapeCommFunction(mouse2filehandle, CLRRTS);
 
 	PurgeComm(mouse2filehandle, PURGE_TXABORT | PURGE_RXABORT |
-	          PURGE_TXCLEAR | PURGE_RXCLEAR);
+			  PURGE_TXCLEAR | PURGE_RXCLEAR);
 
 	CloseHandle(mouse2filehandle);
 
@@ -1871,11 +1879,11 @@ void I_StartupMouse2(void)
 	{
 		// COM file handle
 		mouse2filehandle = CreateFileA(cv_mouse2port.string, GENERIC_READ | GENERIC_WRITE,
-		                               0,                     // exclusive access
-		                               NULL,                  // no security attrs
-		                               OPEN_EXISTING,
-		                               FILE_ATTRIBUTE_NORMAL,
-		                               NULL);
+									   0,                     // exclusive access
+									   NULL,                  // no security attrs
+									   OPEN_EXISTING,
+									   FILE_ATTRIBUTE_NORMAL,
+									   NULL);
 		if (mouse2filehandle == INVALID_HANDLE_VALUE)
 		{
 			INT32 e = GetLastError();
@@ -1895,7 +1903,7 @@ void I_StartupMouse2(void)
 
 	// purge buffers
 	PurgeComm(mouse2filehandle, PURGE_TXABORT | PURGE_RXABORT
-	          | PURGE_TXCLEAR | PURGE_RXCLEAR);
+			  | PURGE_TXCLEAR | PURGE_RXCLEAR);
 
 	// setup port to 1200 7N1
 	dcb.DCBlength = sizeof (DCB);
@@ -1921,14 +1929,14 @@ void I_StartupMouse2(void)
 //
 // I_Tactile
 //
-FUNCMATH void I_Tactile(FFType pFFType, const JoyFF_t *FFEffect)
+void I_Tactile(FFType pFFType, const JoyFF_t *FFEffect)
 {
 	// UNUSED.
 	(void)pFFType;
 	(void)FFEffect;
 }
 
-FUNCMATH void I_Tactile2(FFType pFFType, const JoyFF_t *FFEffect)
+void I_Tactile2(FFType pFFType, const JoyFF_t *FFEffect)
 {
 	// UNUSED.
 	(void)pFFType;
@@ -1939,7 +1947,7 @@ FUNCMATH void I_Tactile2(FFType pFFType, const JoyFF_t *FFEffect)
 */
 static ticcmd_t emptycmd;
 
-FUNCMATH ticcmd_t *I_BaseTiccmd(void)
+ticcmd_t *I_BaseTiccmd(void)
 {
 	return &emptycmd;
 }
@@ -1948,7 +1956,7 @@ FUNCMATH ticcmd_t *I_BaseTiccmd(void)
 */
 static ticcmd_t emptycmd2;
 
-FUNCMATH ticcmd_t *I_BaseTiccmd2(void)
+ticcmd_t *I_BaseTiccmd2(void)
 {
 	return &emptycmd2;
 }
@@ -2024,7 +2032,7 @@ static void I_ShutdownTimer(void)
 tic_t I_GetTime (void)
 {
 	static Uint32 basetime = 0;
-	       Uint32 ticks = SDL_GetTicks();
+		   Uint32 ticks = SDL_GetTicks();
 
 	if (!basetime)
 		basetime = ticks;
@@ -2042,7 +2050,7 @@ tic_t I_GetTime (void)
 //
 //I_StartupTimer
 //
-FUNCMATH void I_StartupTimer(void)
+void I_StartupTimer(void)
 {
 #ifdef _WIN32
 	// for win2k time bug
@@ -2090,7 +2098,6 @@ INT32 I_StartupSystem(void)
 	return 0;
 }
 
-
 //
 // I_Quit
 //
@@ -2142,11 +2149,11 @@ void I_WaitVBL(INT32 count)
 	SDL_Delay(count);
 }
 
-FUNCMATH void I_BeginRead(void)
+void I_BeginRead(void)
 {
 }
 
-FUNCMATH void I_EndRead(void)
+void I_EndRead(void)
 {
 }
 
@@ -2369,7 +2376,7 @@ void I_GetDiskFreeSpace(INT64 *freespace)
 	{
 		DWORD SectorsPerCluster, BytesPerSector, NumberOfFreeClusters, TotalNumberOfClusters;
 		GetDiskFreeSpace(NULL, &SectorsPerCluster, &BytesPerSector,
-		                 &NumberOfFreeClusters, &TotalNumberOfClusters);
+						 &NumberOfFreeClusters, &TotalNumberOfClusters);
 		*freespace = BytesPerSector*SectorsPerCluster*NumberOfFreeClusters;
 	}
 #else // Dummy for platform independent; 1GB should be enough
@@ -2576,22 +2583,22 @@ static const char *locateWad(void)
 
 #ifdef CMAKECONFIG
 #ifndef NDEBUG
-    I_OutputMsg(","CMAKE_ASSETS_DIR);
-    strcpy(returnWadPath, CMAKE_ASSETS_DIR);
-    if (isWadPathOk(returnWadPath))
-    {
-        return returnWadPath;
-    }
+	I_OutputMsg(","CMAKE_ASSETS_DIR);
+	strcpy(returnWadPath, CMAKE_ASSETS_DIR);
+	if (isWadPathOk(returnWadPath))
+	{
+		return returnWadPath;
+	}
 #endif
 #endif
 
 #ifdef __APPLE__
-    OSX_GetResourcesPath(returnWadPath);
-    I_OutputMsg(",%s", returnWadPath);
-    if (isWadPathOk(returnWadPath))
-    {
-        return returnWadPath;
-    }
+	OSX_GetResourcesPath(returnWadPath);
+	I_OutputMsg(",%s", returnWadPath);
+	if (isWadPathOk(returnWadPath))
+	{
+		return returnWadPath;
+	}
 #endif
 
 	// examine default dirs
@@ -2696,7 +2703,30 @@ const char *I_LocateWad(void)
 #ifdef __linux__
 #define MEMINFO_FILE "/proc/meminfo"
 #define MEMTOTAL "MemTotal:"
+#define MEMAVAILABLE "MemAvailable:"
 #define MEMFREE "MemFree:"
+#define CACHED "Cached:"
+#define BUFFERS "Buffers:"
+#define SHMEM "Shmem:"
+
+/* Parse the contents of /proc/meminfo (in buf), return value of "name"
+ * (example: MemTotal) */
+static long get_entry(const char* name, const char* buf)
+{
+	long val;
+	char* hit = strstr(buf, name);
+	if (hit == NULL) {
+		return -1;
+	}
+
+	errno = 0;
+	val = strtol(hit + strlen(name), NULL, 10);
+	if (errno != 0) {
+		CONS_Alert(CONS_ERROR, M_GetText("get_entry: strtol() failed: %s\n"), strerror(errno));
+		return -1;
+	}
+	return val;
+}
 #endif
 
 // quick fix for compil
@@ -2758,6 +2788,11 @@ UINT32 I_GetFreeMem(UINT32 *total)
 	UINT32 totalKBytes;
 	INT32 n;
 	INT32 meminfo_fd = -1;
+	long Cached;
+	long MemFree;
+	long Buffers;
+	long Shmem;
+	long MemAvailable = -1;
 
 	meminfo_fd = open(MEMINFO_FILE, O_RDONLY);
 	n = read(meminfo_fd, buf, 1023);
@@ -2783,16 +2818,28 @@ UINT32 I_GetFreeMem(UINT32 *total)
 	memTag += sizeof (MEMTOTAL);
 	totalKBytes = atoi(memTag);
 
-	if ((memTag = strstr(buf, MEMFREE)) == NULL)
+	if ((memTag = strstr(buf, MEMAVAILABLE)) == NULL)
 	{
-		// Error
-		if (total)
-			*total = 0L;
-		return 0;
-	}
+		Cached = get_entry(CACHED, buf);
+		MemFree = get_entry(MEMFREE, buf);
+		Buffers = get_entry(BUFFERS, buf);
+		Shmem = get_entry(SHMEM, buf);
+		MemAvailable = Cached + MemFree + Buffers - Shmem;
 
-	memTag += sizeof (MEMFREE);
-	freeKBytes = atoi(memTag);
+		if (MemAvailable == -1)
+		{
+			// Error
+			if (total)
+				*total = 0L;
+			return 0;
+		}
+		freeKBytes = MemAvailable;
+	}
+	else
+	{
+		memTag += sizeof (MEMAVAILABLE);
+		freeKBytes = atoi(memTag);
+	}
 
 	if (total)
 		*total = totalKBytes << 10;
@@ -2869,5 +2916,5 @@ const CPUInfoFlags *I_CPUInfo(void)
 }
 
 // note CPUAFFINITY code used to reside here
-FUNCMATH void I_RegisterSysCommands(void) {}
+void I_RegisterSysCommands(void) {}
 #endif
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 48bb61649c9176f363bcef7bfff4eb739362dea2..05ed527de2b774f63e08bcf4c8003a277a9e5e80 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
@@ -1057,7 +1059,7 @@ void I_SetPalette(RGBA_t *palette)
 }
 
 // return number of fullscreen + X11 modes
-FUNCMATH INT32 VID_NumModes(void)
+INT32 VID_NumModes(void)
 {
 	if (USE_FULLSCREEN && numVidModes != -1)
 		return numVidModes - firstEntry;
@@ -1065,7 +1067,7 @@ FUNCMATH INT32 VID_NumModes(void)
 		return MAXWINMODES;
 }
 
-FUNCMATH const char *VID_GetModeName(INT32 modeNum)
+const char *VID_GetModeName(INT32 modeNum)
 {
 #if 0
 	if (USE_FULLSCREEN && numVidModes != -1) // fullscreen modes
@@ -1095,7 +1097,7 @@ FUNCMATH const char *VID_GetModeName(INT32 modeNum)
 	return &vidModeName[modeNum][0];
 }
 
-FUNCMATH INT32 VID_GetModeForSize(INT32 w, INT32 h)
+INT32 VID_GetModeForSize(INT32 w, INT32 h)
 {
 	int i;
 	for (i = 0; i < MAXWINMODES; i++)
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 6782d8ba60ff3a9e702b1933b1b1d2dd8d70f0a0..128faaab8703b5680b941f1dd6989657b148e95c 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
 
@@ -44,10 +52,8 @@
 #include "gme/gme.h"
 #define GME_TREBLE 5.0
 #define GME_BASS 1.0
-#ifdef HAVE_PNG /// TODO: compile with zlib support without libpng
-
-#define HAVE_ZLIB
 
+#ifdef HAVE_ZLIB
 #ifndef _MSC_VER
 #ifndef _LARGEFILE64_SOURCE
 #define _LARGEFILE64_SOURCE
@@ -63,8 +69,8 @@
 #endif
 
 #include "zlib.h"
-#endif
-#endif
+#endif // HAVE_ZLIB
+#endif // HAVE_LIBGME
 
 UINT8 sound_started = false;
 
@@ -128,8 +134,6 @@ void I_StartupSound(void)
 	var_cleanup();
 
 	music = NULL;
-	music_volume = sfx_volume = 0;
-
 #if SDL_MIXER_VERSION_ATLEAST(1,2,11)
 	Mix_Init(MIX_INIT_FLAC|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG);
 #endif
@@ -165,7 +169,7 @@ void I_ShutdownSound(void)
 #endif
 }
 
-FUNCMATH void I_UpdateSound(void)
+void I_UpdateSound(void)
 {
 }
 
@@ -221,7 +225,7 @@ static Mix_Chunk *ds2chunk(void *stream)
 			return NULL; // would and/or did wrap, can't store.
 		break;
 	}
-	sound = malloc(newsamples<<2); // samples * frequency shift * bytes per sample * channels
+	sound = Z_Malloc(newsamples<<2, PU_SOUND, NULL); // samples * frequency shift * bytes per sample * channels
 
 	s = (SINT8 *)stream;
 	d = (INT16 *)sound;
@@ -289,6 +293,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 {
 	void *lump;
 	Mix_Chunk *chunk;
+	SDL_RWops *rw;
 #ifdef HAVE_LIBGME
 	Music_Emu *emu;
 	gme_info_t *info;
@@ -405,7 +410,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 		}
 		Z_Free(inflatedData); // GME didn't open jack, but don't let that stop us from freeing this up
 #else
-		//CONS_Alert(CONS_ERROR,"Cannot decompress VGZ; no zlib support\n");
+		return NULL; // No zlib support
 #endif
 	}
 	// Try to read it as a GME sound
@@ -432,21 +437,43 @@ void *I_GetSfx(sfxinfo_t *sfx)
 #endif
 
 	// Try to load it as a WAVE or OGG using Mixer.
-	return Mix_LoadWAV_RW(SDL_RWFromMem(lump, sfx->length), 1);
+	rw = SDL_RWFromMem(lump, sfx->length);
+	if (rw != NULL)
+	{
+		chunk = Mix_LoadWAV_RW(rw, 1);
+		return chunk;
+	}
+
+	return NULL; // haven't been able to get anything
 }
 
 void I_FreeSfx(sfxinfo_t *sfx)
 {
 	if (sfx->data)
+	{
+		Mix_Chunk *chunk = (Mix_Chunk*)sfx->data;
+		UINT8 *abufdata = NULL;
+		if (chunk->allocated == 0)
+		{
+			// We allocated the data in this chunk, so get the abuf from mixer, then let it free the chunk, THEN we free the data
+			// I believe this should ensure the sound is not playing when we free it
+			abufdata = chunk->abuf;
+		}
 		Mix_FreeChunk(sfx->data);
+		if (abufdata)
+		{
+			// I'm going to assume we used Z_Malloc to allocate this data.
+			Z_Free(abufdata);
+		}
+	}
 	sfx->data = NULL;
 	sfx->lumpnum = LUMPERROR;
 }
 
-INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority)
+INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel)
 {
 	UINT8 volume = (((UINT16)vol + 1) * (UINT16)sfx_volume) / 62; // (256 * 31) / 62 == 127
-	INT32 handle = Mix_PlayChannel(-1, S_sfx[id].data, 0);
+	INT32 handle = Mix_PlayChannel(channel, S_sfx[id].data, 0);
 	Mix_Volume(handle, volume);
 	Mix_SetPanning(handle, min((UINT16)(0xff-sep)<<1, 0xff), min((UINT16)(sep)<<1, 0xff));
 	(void)pitch; // Mixer can't handle pitch
@@ -601,7 +628,7 @@ static void mix_gme(void *udata, Uint8 *stream, int len)
 /// Music System
 /// ------------------------
 
-FUNCMATH void I_InitMusic(void)
+void I_InitMusic(void)
 {
 }
 
@@ -883,6 +910,7 @@ boolean I_LoadSong(char *data, size_t len)
 
 	size_t wstart, wp;
 	char *p = data;
+	SDL_RWops *rw;
 
 	if (music
 #ifdef HAVE_LIBGME
@@ -978,7 +1006,8 @@ boolean I_LoadSong(char *data, size_t len)
 		}
 		Z_Free(inflatedData); // GME didn't open jack, but don't let that stop us from freeing this up
 #else
-		//CONS_Alert(CONS_ERROR,"Cannot decompress VGZ; no zlib support\n");
+		CONS_Alert(CONS_ERROR,"Cannot decompress VGZ; no zlib support\n");
+		return true;
 #endif
 	}
 	else if (!gme_open_data(data, len, &gme, 44100))
@@ -989,7 +1018,11 @@ boolean I_LoadSong(char *data, size_t len)
 	}
 #endif
 
-	music = Mix_LoadMUS_RW(SDL_RWFromMem(data, len), SDL_FALSE);
+	rw = SDL_RWFromMem(data, len);
+	if (rw != NULL)
+	{
+		music = Mix_LoadMUS_RW(rw, 1);
+	}
 	if (!music)
 	{
 		CONS_Alert(CONS_ERROR, "Mix_LoadMUS_RW: %s\n", Mix_GetError());
@@ -1033,6 +1066,7 @@ boolean I_LoadSong(char *data, size_t len)
 			song_length = (float)(atoi(p) / 1000.0L);
 		}
 		// below: search MP3 or other tags that use wide char encoding
+		// \todo this isn't actually how MP3 stores its tags. We're just using endian markers as context clues.
 		else if (!loop_point && !memcmp(p, key1w, key1len*2)) // LOOP wide char
 		{
 			p += key1len*2;
diff --git a/src/sdl/sdl_sound.c b/src/sdl/sdl_sound.c
index b62f3abb179ace242a510ed55f91fad00076ab7b..349baea32307529a02ae1ce117638b6cc5c98515 100644
--- a/src/sdl/sdl_sound.c
+++ b/src/sdl/sdl_sound.c
@@ -219,7 +219,7 @@ static void Snd_UnlockAudio(void) //Alam: Unlock audio data and reinstall audio
 #endif
 }
 
-FUNCMATH static inline Uint16 Snd_LowerRate(Uint16 sr)
+static inline Uint16 Snd_LowerRate(Uint16 sr)
 {
 	if (sr <= audio.freq) // already lowered rate?
 		return sr; // good then
@@ -604,10 +604,11 @@ void I_FreeSfx(sfxinfo_t * sfx)
 // Pitching (that is, increased speed of playback)
 //  is set, but currently not used by mixing.
 //
-INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority)
+INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel)
 {
 	(void)priority;
 	(void)pitch;
+	(void)channel;
 
 	if (sound_disabled)
 		return 0;
@@ -1350,12 +1351,12 @@ musictype_t I_SongType(void)
 
 boolean I_SongPlaying(void)
 {
-	return musicStarted;
+	return false;
 }
 
 boolean I_SongPaused(void)
 {
-	return Mix_PausedMusic();
+	return false;
 }
 
 /// ------------------------
@@ -1379,8 +1380,8 @@ UINT32 I_GetSongLength(void)
 
 boolean I_SetSongLoopPoint(UINT32 looppoint)
 {
-        (void)looppoint;
-        return false;
+    (void)looppoint;
+    return false;
 }
 
 UINT32 I_GetSongLoopPoint(void)
diff --git a/src/st_stuff.c b/src/st_stuff.c
index fa13e008a05b346766ead91d43c66a1b4af182b6..26d9678a3cd6aab92724ff1f1507ac28d4bf7a64 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -603,6 +603,9 @@ static void ST_drawDebugInfo(void)
 
 static void ST_drawScore(void)
 {
+	if (F_GetPromptHideHud(hudinfo[HUD_SCORE].y))
+		return;
+
 	// SCORE:
 	ST_DrawPatchFromHud(HUD_SCORE, sboscore, V_HUDTRANS);
 	if (objectplacing)
@@ -712,6 +715,9 @@ static void ST_drawTime(void)
 		tictrn  = G_TicsToCentiseconds(tics);
 	}
 
+	if (F_GetPromptHideHud(hudinfo[HUD_TIME].y))
+		return;
+
 	// TIME:
 	ST_DrawPatchFromHud(HUD_TIME, ((downwards && (tics < 30*TICRATE) && (leveltime/5 & 1)) ? sboredtime : sbotime), V_HUDTRANS);
 
@@ -738,6 +744,9 @@ static inline void ST_drawRings(void)
 {
 	INT32 ringnum;
 
+	if (F_GetPromptHideHud(hudinfo[HUD_RINGS].y))
+		return;
+
 	ST_DrawPatchFromHud(HUD_RINGS, ((!stplyr->spectator && stplyr->rings <= 0 && leveltime/5 & 1) ? sboredrings : sborings), ((stplyr->spectator) ? V_HUDTRANSHALF : V_HUDTRANS));
 
 	ringnum = ((objectplacing) ? op_currentdoomednum : max(stplyr->rings, 0));
@@ -756,6 +765,9 @@ static void ST_drawLivesArea(void)
 	if (!stplyr->skincolor)
 		return; // Just joined a server, skin isn't loaded yet!
 
+	if (F_GetPromptHideHud(hudinfo[HUD_LIVES].y))
+		return;
+
 	// face background
 	V_DrawSmallScaledPatch(hudinfo[HUD_LIVES].x, hudinfo[HUD_LIVES].y,
 		hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, livesback);
@@ -927,6 +939,9 @@ static void ST_drawInput(void)
 	if (stplyr->powers[pw_carry] == CR_NIGHTSMODE)
 		y -= 16;
 
+	if (F_GetPromptHideHud(y))
+		return;
+
 	// O backing
 	V_DrawFill(x, y-1, 16, 16, hudinfo[HUD_LIVES].f|20);
 	V_DrawFill(x, y+15, 16, 1, hudinfo[HUD_LIVES].f|29);
@@ -1202,6 +1217,9 @@ static void ST_drawPowerupHUD(void)
 	static INT32 flagoffs[2] = {0, 0}, shieldoffs[2] = {0, 0};
 #define ICONSEP (16+4) // matches weapon rings HUD
 
+	if (F_GetPromptHideHud(hudinfo[HUD_POWERUPS].y))
+		return;
+
 	if (stplyr->spectator || stplyr->playerstate != PST_LIVE)
 		return;
 
@@ -1364,7 +1382,7 @@ static void ST_drawFirstPersonHUD(void)
 	p = W_CachePatchNum(sprframe->lumppat[0], PU_CACHE);
 
 	// Display the countdown drown numbers!
-	if (p)
+	if (p && !F_GetPromptHideHud(60 - SHORT(p->topoffset)))
 		V_DrawScaledPatch((BASEVIDWIDTH/2) - (SHORT(p->width)/2) + SHORT(p->leftoffset), 60 - SHORT(p->topoffset),
 			V_PERPLAYER|V_PERPLAYER|V_TRANSLUCENT, p);
 }
@@ -1908,6 +1926,9 @@ static void ST_drawMatchHUD(void)
 	const INT32 y = 176; // HUD_LIVES
 	INT32 offset = (BASEVIDWIDTH / 2) - (NUM_WEAPONS * 10) - 6;
 
+	if (F_GetPromptHideHud(y))
+		return;
+
 	if (!G_RingSlingerGametype())
 		return;
 
@@ -1954,6 +1975,9 @@ static void ST_drawTextHUD(void)
 	y -= 8;\
 }
 
+	if (F_GetPromptHideHud(y))
+		return;
+
 	if ((gametype == GT_TAG || gametype == GT_HIDEANDSEEK) && (!stplyr->spectator))
 	{
 		if (leveltime < hidetime * TICRATE)
@@ -2087,6 +2111,9 @@ static void ST_drawTeamHUD(void)
 	patch_t *p;
 #define SEP 20
 
+	if (F_GetPromptHideHud(0)) // y base is 0
+		return;
+
 	if (gametype == GT_CTF)
 		p = bflagico;
 	else
@@ -2200,7 +2227,8 @@ static INT32 ST_drawEmeraldHuntIcon(mobj_t *hunt, patch_t **patches, INT32 offse
 		interval = 0;
 	}
 
-	V_DrawScaledPatch(hudinfo[HUD_HUNTPICS].x+offset, hudinfo[HUD_HUNTPICS].y, hudinfo[HUD_HUNTPICS].f|V_PERPLAYER|V_HUDTRANS, patches[i]);
+	if (!F_GetPromptHideHud(hudinfo[HUD_HUNTPICS].y))
+		V_DrawScaledPatch(hudinfo[HUD_HUNTPICS].x+offset, hudinfo[HUD_HUNTPICS].y, hudinfo[HUD_HUNTPICS].f|V_PERPLAYER|V_HUDTRANS, patches[i]);
 	return interval;
 }
 
@@ -2298,7 +2326,8 @@ static void ST_overlayDrawer(void)
 	//hu_showscores = auto hide score/time/rings when tab rankings are shown
 	if (!(hu_showscores && (netgame || multiplayer)))
 	{
-		if (maptol & TOL_NIGHTS || G_IsSpecialStage(gamemap))
+		if ((maptol & TOL_NIGHTS || G_IsSpecialStage(gamemap)) &&
+			!F_GetPromptHideHudAll())
 			ST_drawNiGHTSHUD();
 		else
 		{
diff --git a/src/st_stuff.h b/src/st_stuff.h
index df0adace3f4a451c2d2120f8410e4552edfb8b0c..3886d737cdf41c8166540b43b1ba47fb19ea0f4d 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -24,7 +24,7 @@
 //
 
 // Called by main loop.
-FUNCMATH void ST_Ticker(void);
+void ST_Ticker(void);
 
 // Called by main loop.
 void ST_Drawer(void);
diff --git a/src/v_video.c b/src/v_video.c
index 765965ffdc83f5bc3845d6a875a8184bca8ea348..ae7c08511d5c9f3d1c8f8ec701cf72824e595b6d 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;
 
@@ -1045,7 +1054,7 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 //
 void V_DrawContinueIcon(INT32 x, INT32 y, INT32 flags, INT32 skinnum, UINT8 skincolor)
 {
-	if (skins[skinnum].flags & SF_HIRES)
+	if (skinnum < 0 || skinnum >= numskins || (skins[skinnum].flags & SF_HIRES))
 		V_DrawScaledPatch(x - 10, y - 14, flags, W_CachePatchName("CONTINS", PU_CACHE));
 	else
 	{
@@ -1480,6 +1489,49 @@ void V_DrawFadeConsBack(INT32 plines)
 		*buf = consolebgmap[*buf];
 }
 
+// Very similar to F_DrawFadeConsBack, except we draw from the middle(-ish) of the screen to the bottom.
+void V_DrawPromptBack(INT32 boxheight, INT32 color)
+{
+	UINT8 *deststop, *buf;
+
+	boxheight *= vid.dupy;
+
+	if (color == INT32_MAX)
+		color = cons_backcolor.value;
+
+#ifdef HWRENDER
+	if (rendermode != render_soft && rendermode != render_none)
+	{
+		UINT32 hwcolor;
+		switch (color)
+		{
+			case 0:		hwcolor = 0xffffff00;	break; // White
+			case 1:		hwcolor = 0x00000000;	break; // Gray // Note this is different from V_DrawFadeConsBack
+			case 2:		hwcolor = 0x40201000;	break; // Brown
+			case 3:		hwcolor = 0xff000000;	break; // Red
+			case 4:		hwcolor = 0xff800000;	break; // Orange
+			case 5:		hwcolor = 0x80800000;	break; // Yellow
+			case 6:		hwcolor = 0x00800000;	break; // Green
+			case 7:		hwcolor = 0x0000ff00;	break; // Blue
+			case 8:		hwcolor = 0x4080ff00;	break; // Cyan
+			// Default green
+			default:	hwcolor = 0x00800000;	break;
+		}
+		HWR_DrawTutorialBack(hwcolor, boxheight);
+		return;
+	}
+#endif
+
+	CON_SetupBackColormapEx(color, true);
+
+	// heavily simplified -- we don't need to know x or y position,
+	// just the start and stop positions
+	deststop = screens[0] + vid.rowbytes * vid.height;
+	buf = deststop - vid.rowbytes * ((boxheight * 4) + (boxheight/2)*5); // 4 lines of space plus gaps between and some leeway
+	for (; buf < deststop; ++buf)
+		*buf = promptbgmap[*buf];
+}
+
 // Gets string colormap, used for 0x80 color codes
 //
 static const UINT8 *V_GetStringColormap(INT32 colorflags)
diff --git a/src/v_video.h b/src/v_video.h
index 6113d08ecdc9f1ad1c46868fc24149bfb80c6213..84a027963fcd0cbbabd64b5cd70f72037c7a512c 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -158,6 +158,7 @@ void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum);
 void V_DrawFadeScreen(UINT16 color, UINT8 strength);
 
 void V_DrawFadeConsBack(INT32 plines);
+void V_DrawPromptBack(INT32 boxheight, INT32 color);
 
 // draw a single character
 void V_DrawCharacter(INT32 x, INT32 y, INT32 c, boolean lowercaseallowed);
diff --git a/src/w_wad.c b/src/w_wad.c
index 7babd22ef41cadf2e35aa0e72d4e8eecd38c5e15..9056165422d044b706adeceeb31d5a77d2541e59 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -234,10 +234,10 @@ static inline void W_LoadDehackedLumps(UINT16 wadnum)
 		for (lump = 0; lump < wadfiles[wadnum]->numlumps; lump++, lump_p++)
 			if (memcmp(lump_p->name,"SOC_",4)==0) // Check for generic SOC lump
 			{	// shameless copy+paste of code from LUA_LoadLump
-				size_t length = strlen(wadfiles[wadnum]->filename) + 1 + strlen(lump_p->name2); // length of file name, '|', and lump name
-				char *name = malloc(length + 1);
+				size_t len = strlen(wadfiles[wadnum]->filename) + 1 + strlen(lump_p->name2); // length of file name, '|', and lump name
+				char *name = malloc(len+1);
 				sprintf(name, "%s|%s", wadfiles[wadnum]->filename, lump_p->name2);
-				name[length] = '\0';
+				name[len] = '\0';
 
 				CONS_Printf(M_GetText("Loading SOC from %s\n"), name);
 				DEH_LoadDehackedLumpPwad(wadnum, lump);
@@ -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;
 		}
 
diff --git a/src/win32/win_snd.c b/src/win32/win_snd.c
index 543de3231a762bffd7a69f702774a5bcd92b5107..371384328f793133022ae53bd0cf2caf3ebcb9bd 100644
--- a/src/win32/win_snd.c
+++ b/src/win32/win_snd.c
@@ -17,10 +17,8 @@
 #include "gme/gme.h"
 #define GME_TREBLE 5.0
 #define GME_BASS 1.0
-#ifdef HAVE_PNG /// TODO: compile with zlib support without libpng
-
-#define HAVE_ZLIB
 
+#ifdef HAVE_ZLIB
 #ifndef _MSC_VER
 #ifndef _LARGEFILE64_SOURCE
 #define _LARGEFILE64_SOURCE
@@ -36,8 +34,8 @@
 #endif
 
 #include "zlib.h"
-#endif
-#endif
+#endif // HAVE_ZLIB
+#endif // HAVE_LIBGME
 
 static FMOD_SYSTEM *fsys;
 static FMOD_SOUND *music_stream;
@@ -354,12 +352,13 @@ void I_FreeSfx(sfxinfo_t *sfx)
 	sfx->data = NULL;
 }
 
-INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority)
+INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel)
 {
 	FMOD_SOUND *sound;
 	FMOD_CHANNEL *chan;
 	INT32 i;
 	float frequency;
+	(void)channel;
 
 	sound = (FMOD_SOUND *)S_sfx[id].data;
 	I_Assert(sound != NULL);
@@ -927,6 +926,7 @@ boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms
 	(void)target_volume;
 	(void)source_volume;
 	(void)ms;
+	(void)callback;
 	return false;
 }
 
@@ -934,6 +934,7 @@ boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
 {
 	(void)target_volume;
 	(void)ms;
+	(void)callback;
 	return false;
 }