diff --git a/debian-template/rules b/debian-template/rules
index 0a77624cb490639564b0212abc902dbfdda88be5..12ceaf98b97a09926f41b502c0ae88ec8f63e4a8 100644
--- a/debian-template/rules
+++ b/debian-template/rules
@@ -78,7 +78,7 @@ NONX86	= $(shell test "`echo $(CROSS_COMPILE_HOST) | grep 'i[3-6]86'`" || echo "
 MAKEARGS = $(OS) $(NONX86) $(PREFIX) EXENAME=$(EXENAME) DBGNAME=$(DBGNAME) NOOBJDUMP=1 # SDL_PKGCONFIG=sdl2 PNG_PKGCONFIG=libpng
 MENUFILE1 = ?package($(PACKAGE)):needs="X11" section="$(SECTION)"
 MENUFILE2 = title="$(TITLE)" command="/$(PKGDIR)/$(PACKAGE)"
-BINDIR :=  $(DIR)/bin/Linux/Release
+BINDIR :=  $(DIR)/bin/
 
 # FIXME pkg-config dir hacks
 # Launchpad doesn't need this; it actually makes i386 builds fail due to cross-compile
diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index de46118b47e2165e9f9acd6e8c3a5c2a41bccf78..7d82af8c7e9b48cfdf256c3679cf1eda157bb7ed 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -776,7 +776,6 @@ linedeftypes
 		{
 			title = "Make FOF Bouncy";
 			prefix = "(76)";
-			flags16384text = "[14] Dampen";
 		}
 	}
 
@@ -1999,7 +1998,7 @@ linedeftypes
 			title = "Set Tagged Sector's Floor Height/Texture";
 			prefix = "(400)";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Keep floor flat";
+			flags64text = "[6] Don't change floor texture";
 		}
 
 		401
@@ -2007,7 +2006,7 @@ linedeftypes
 			title = "Set Tagged Sector's Ceiling Height/Texture";
 			prefix = "(401)";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Keep floor flat";
+			flags64text = "[6] Don't change ceiling texture";
 		}
 
 		402
@@ -2106,7 +2105,7 @@ linedeftypes
 			prefix = "(403)";
 			flags2text = "[1] Trigger linedef executor";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Change floor flat";
+			flags64text = "[6] Change floor texture";
 		}
 
 		404
@@ -2115,7 +2114,7 @@ linedeftypes
 			prefix = "(404)";
 			flags2text = "[1] Trigger linedef executor";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Change ceiling flat";
+			flags64text = "[6] Change ceiling texture";
 		}
 
 		405
diff --git a/extras/conf/udb/Includes/SRB222_linedefs.cfg b/extras/conf/udb/Includes/SRB222_linedefs.cfg
index abe50c1b52e1164177aa7951980b5636b841a528..68893f3de897e0562eab88d1d26b4028fb58e464 100644
--- a/extras/conf/udb/Includes/SRB222_linedefs.cfg
+++ b/extras/conf/udb/Includes/SRB222_linedefs.cfg
@@ -1909,12 +1909,6 @@ udmf
 			{
 				title = "Bounce strength";
 			}
-			arg2
-			{
-				title = "Dampen?";
-				type = 11;
-				enum = "noyes";
-			}
 		}
 	}
 
diff --git a/src/Makefile.d/versions.mk b/src/Makefile.d/versions.mk
index d7d0c3dd1ef59f5ca3d9b6b6d7e6191af9f671d8..f0b59658ee741e7d709b4d4037b1745a1e3bfefb 100644
--- a/src/Makefile.d/versions.mk
+++ b/src/Makefile.d/versions.mk
@@ -35,8 +35,6 @@ ifndef GCC295
  WFLAGS+=-Wendif-labels
 endif
 ifdef GCC41
- WFLAGS+=-Wdeclaration-after-statement
- WFLAGS+=-Wno-error=declaration-after-statement
  WFLAGS+=-Wshadow
 endif
 #WFLAGS+=-Wlarger-than-%len%
diff --git a/src/Makefile.d/win32.mk b/src/Makefile.d/win32.mk
index 0c671b26876e1740c1ab101a711b561f9ad2dd7a..768133c151c7a597871ff605fa0eb045cfc7df05 100644
--- a/src/Makefile.d/win32.mk
+++ b/src/Makefile.d/win32.mk
@@ -8,6 +8,11 @@ else
 EXENAME?=srb2win64.exe
 endif
 
+# disable dynamicbase if under msys2
+ifdef MSYSTEM
+libs+=-Wl,--disable-dynamicbase
+endif
+
 sources+=win32/Srb2win.rc
 opts+=-DSTDC_HEADERS
 libs+=-ladvapi32 -lkernel32 -lmsvcrt -luser32
diff --git a/src/console.c b/src/console.c
index f01e1824c97a8df1041ab42fe829ad588e058877..6f21aeb3dd4d022c51e37c1781ec28aa029a652c 100644
--- a/src/console.c
+++ b/src/console.c
@@ -484,6 +484,19 @@ void CON_Init(void)
 		Unlock_state();
 	}
 }
+
+void CON_StartRefresh(void)
+{
+	if (con_startup)
+		con_refresh = true;
+}
+
+void CON_StopRefresh(void)
+{
+	if (con_startup)
+		con_refresh = false;
+}
+
 // Console input initialization
 //
 static void CON_InputInit(void)
diff --git a/src/console.h b/src/console.h
index 28f40d308270cc279bc70b5995b6f3fdf378536c..accf89d960faf6975ecd02345bba66d6ef3fd264 100644
--- a/src/console.h
+++ b/src/console.h
@@ -16,6 +16,9 @@
 
 void CON_Init(void);
 
+void CON_StartRefresh(void);
+void CON_StopRefresh(void);
+
 boolean CON_Responder(event_t *ev);
 
 #ifdef HAVE_THREADS
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index b7071320c80d4f0917040e95a68d7870a21755d0..78a3ebe6cb961cff2fb22e5e0f4520559078865a 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -128,10 +128,14 @@ static UINT8 localtextcmd[MAXTEXTCMD];
 static UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
 static tic_t neededtic;
 SINT8 servernode = 0; // the number of the server node
+
 /// \brief do we accept new players?
 /// \todo WORK!
 boolean acceptnewnode = true;
 
+static boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
+static tic_t firstconnectattempttime = 0;
+
 // engine
 
 // Must be a power of two
@@ -511,18 +515,24 @@ static INT16 Consistancy(void);
 typedef enum
 {
 	CL_SEARCHING,
+	CL_CHECKFILES,
 	CL_DOWNLOADFILES,
 	CL_ASKJOIN,
+	CL_LOADFILES,
 	CL_WAITJOINRESPONSE,
 	CL_DOWNLOADSAVEGAME,
 	CL_CONNECTED,
-	CL_ABORTED
+	CL_ABORTED,
+	CL_ASKFULLFILELIST,
+	CL_CONFIRMCONNECT
 } cl_mode_t;
 
 static void GetPackets(void);
 
 static cl_mode_t cl_mode = CL_SEARCHING;
 
+static UINT16 cl_lastcheckedfilecount = 0;	// used for full file list
+
 #ifndef NONET
 #define SNAKE_SPEED 5
 
@@ -920,6 +930,8 @@ static void Snake_Draw(void)
 	INT16 i;
 
 	// Background
+	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+
 	V_DrawFlatFill(
 		SNAKE_LEFT_X + SNAKE_BORDER_SIZE,
 		SNAKE_TOP_Y  + SNAKE_BORDER_SIZE,
@@ -1021,6 +1033,13 @@ static void Snake_Draw(void)
 		);
 }
 
+static void CL_DrawConnectionStatusBox(void)
+{
+	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+	if (cl_mode != CL_CONFIRMCONNECT)
+		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+}
+
 //
 // CL_DrawConnectionStatus
 //
@@ -1031,28 +1050,32 @@ static inline void CL_DrawConnectionStatus(void)
 	INT32 ccstime = I_GetTime();
 
 	// Draw background fade
-	if (!menuactive) // menu already draws its own fade
-		V_DrawFadeScreen(0xFF00, 16); // force default
-
-	// Draw the bottom box.
-	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+	V_DrawFadeScreen(0xFF00, 16); // force default
 
-	if (cl_mode != CL_DOWNLOADFILES)
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
 	{
 		INT32 i, animtime = ((ccstime / 4) & 15) + 16;
-		UINT8 palstart = (cl_mode == CL_SEARCHING) ? 32 : 96;
-		// 15 pal entries total.
+		UINT8 palstart;
 		const char *cltext;
 
+		// Draw the bottom box.
+		CL_DrawConnectionStatusBox();
+
+		if (cl_mode == CL_SEARCHING)
+			palstart = 32; // Red
+		else if (cl_mode == CL_CONFIRMCONNECT)
+			palstart = 48; // Orange
+		else
+			palstart = 96; // Green
+
 		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
-			for (i = 0; i < 16; ++i)
+			for (i = 0; i < 16; ++i) // 15 pal entries total.
 				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
 
 		switch (cl_mode)
 		{
 			case CL_DOWNLOADSAVEGAME:
-				if (lastfilenum != -1)
+				if (fileneeded && lastfilenum != -1)
 				{
 					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
 					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
@@ -1076,9 +1099,22 @@ static inline void CL_DrawConnectionStatus(void)
 				else
 					cltext = M_GetText("Waiting to download game state...");
 				break;
+			case CL_ASKFULLFILELIST:
+			case CL_CHECKFILES:
+				cltext = M_GetText("Checking server addon list...");
+				break;
+			case CL_CONFIRMCONNECT:
+				cltext = "";
+				break;
+			case CL_LOADFILES:
+				cltext = M_GetText("Loading server addons...");
+				break;
 			case CL_ASKJOIN:
 			case CL_WAITJOINRESPONSE:
-				cltext = M_GetText("Requesting to join...");
+				if (serverisfull)
+					cltext = M_GetText("Server full, waiting for a slot...");
+				else
+					cltext = M_GetText("Requesting to join...");
 				break;
 			default:
 				cltext = M_GetText("Connecting to server...");
@@ -1088,14 +1124,51 @@ static inline void CL_DrawConnectionStatus(void)
 	}
 	else
 	{
-		if (lastfilenum != -1)
+		if (cl_mode == CL_LOADFILES)
+		{
+			INT32 totalfileslength;
+			INT32 loadcompletednum = 0;
+			INT32 i;
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+
+			//ima just count files here
+			if (fileneeded)
+			{
+				for (i = 0; i < fileneedednum; i++)
+					if (fileneeded[i].status == FS_OPEN)
+						loadcompletednum++;
+			}
+
+			// Loading progress
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
+			totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
+			M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
+		}
+		else if (lastfilenum != -1)
 		{
 			INT32 dldlength;
 			static char tempname[28];
-			fileneeded_t *file = &fileneeded[lastfilenum];
-			char *filename = file->filename;
+			fileneeded_t *file;
+			char *filename;
+
+			if (snake)
+				Snake_Draw();
 
-			Snake_Draw();
+			// Draw the bottom box.
+			CL_DrawConnectionStatusBox();
+
+			if (fileneeded)
+			{
+				file = &fileneeded[lastfilenum];
+				filename = file->filename;
+			}
+			else
+				return;
 
 			Net_GetNetStat();
 			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
@@ -1128,20 +1201,32 @@ static inline void CL_DrawConnectionStatus(void)
 				va("%3.1fK/s ", ((double)getbps)/1024));
 		}
 		else
+		{
+			if (snake)
+				Snake_Draw();
+
+			CL_DrawConnectionStatusBox();
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
 				M_GetText("Waiting to download files..."));
+		}
 	}
 }
 #endif
 
+static boolean CL_AskFileList(INT32 firstfile)
+{
+	netbuffer->packettype = PT_TELLFILESNEEDED;
+	netbuffer->u.filesneedednum = firstfile;
+
+	return HSendPacket(servernode, false, 0, sizeof (INT32));
+}
+
 /** Sends a special packet to declare how many players in local
   * Used only in arbitratrenetstart()
   * Sends a PT_CLIENTJOIN packet to the server
   *
   * \return True if the packet was successfully sent
   * \todo Improve the description...
-  *       Because to be honest, I have no idea what arbitratrenetstart is...
-  *       Is it even used...?
   *
   */
 static boolean CL_SendJoin(void)
@@ -1240,7 +1325,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 			sizeof netbuffer->u.serverinfo.gametypename);
 	netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame;
 	netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled();
-	netbuffer->u.serverinfo.isdedicated = (UINT8)dedicated;
+	netbuffer->u.serverinfo.flags = (dedicated ? SV_DEDICATED : 0);
 	strncpy(netbuffer->u.serverinfo.servername, cv_servername.string,
 		MAXSERVERNAME);
 	strncpy(netbuffer->u.serverinfo.mapname, G_BuildMapName(gamemap), 7);
@@ -1275,7 +1360,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 	if (mapheaderinfo[gamemap-1])
 		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
 
-	p = PutFileNeeded();
+	p = PutFileNeeded(0);
 
 	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
 }
@@ -1526,6 +1611,8 @@ static void CL_LoadReceivedSavegame(boolean reloading)
 	size_t length, decompressedlen;
 	char tmpsave[256];
 
+	FreeFileNeeded();
+
 	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
 
 	length = FIL_ReadFile(tmpsave, &savebuffer);
@@ -1838,11 +1925,161 @@ void CL_UpdateServerList(boolean internetsearch, INT32 room)
 
 #endif // ifndef NONET
 
-static const char * InvalidServerReason (INT32 i)
+static void M_ConfirmConnect(event_t *ev)
 {
-#define EOT "\nPress ESC\n"
+#ifndef NONET
+	if (ev->type == ev_keydown)
+	{
+		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER)
+		{
+			if (totalfilesrequestednum > 0)
+			{
+				if (CL_SendFileRequest())
+				{
+					cl_mode = CL_DOWNLOADFILES;
+					Snake_Initialise();
+				}
+			}
+			else
+				cl_mode = CL_LOADFILES;
+
+			M_ClearMenus(true);
+		}
+		else if (ev->key == 'n' || ev->key == KEY_ESCAPE)
+		{
+			cl_mode = CL_ABORTED;
+			M_ClearMenus(true);
+		}
+	}
+#else
+	(void)ev;
+#endif
+}
+
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i;
+	char *downloadsize = NULL;
+	//CONS_Printf(M_GetText("Checking files...\n"));
+	i = CL_CheckFiles();
+	if (i == 4) // still checking ...
+	{
+		return true;
+	}
+	else if (i == 3) // too many files
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have too many WAD files loaded\n"
+			"to add ones the server is using.\n"
+			"Please restart SRB2 before connecting.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 2) // cannot join for some reason
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have the wrong addons loaded.\n\n"
+			"To play on this server, restart\n"
+			"the game and don't load any addons.\n"
+			"SRB2 will automatically add\n"
+			"everything you need when you join.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 1)
+	{
+		if (serverisfull)
+		{
+			M_StartMessage(M_GetText(
+				"This server is full!\n"
+				"\n"
+				"You may load server addons (if any), and wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n\n"
+			), M_ConfirmConnect, MM_EVENTHANDLER);
+			cl_mode = CL_CONFIRMCONNECT;
+			curfadevalue = 0;
+		}
+		else
+			cl_mode = CL_LOADFILES;
+	}
+	else
+	{
+		// must download something
+		// can we, though?
+		if (!CL_CheckDownloadable()) // nope!
+		{
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			M_StartMessage(M_GetText(
+				"An error occured when trying to\n"
+				"download missing addons.\n"
+				"(This is almost always a problem\n"
+				"with the server, not your game.)\n\n"
+				"See the console or log file\n"
+				"for additional details.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+			return false;
+		}
+
+#ifndef NONET
+		downloadcompletednum = 0;
+		downloadcompletedsize = 0;
+		totalfilesrequestednum = 0;
+		totalfilesrequestedsize = 0;
+
+		if (fileneeded == NULL)
+			I_Error("CL_FinishedFileList: fileneeded == NULL");
 
-	serverinfo_pak *info = &serverlist[i].info;
+		for (i = 0; i < fileneedednum; i++)
+			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+			{
+				totalfilesrequestednum++;
+				totalfilesrequestedsize += fileneeded[i].totalsize;
+			}
+
+		if (totalfilesrequestedsize>>20 >= 100)
+			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
+		else
+			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
+#endif
+
+		if (serverisfull)
+			M_StartMessage(va(M_GetText(
+				"This server is full!\n"
+				"Download of %s additional content\nis required to join.\n"
+				"\n"
+				"You may download, load server addons,\nand wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n"
+			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+		else
+			M_StartMessage(va(M_GetText(
+				"Download of %s additional content\nis required to join.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n"
+			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+
+		Z_Free(downloadsize);
+		cl_mode = CL_CONFIRMCONNECT;
+		curfadevalue = 0;
+	}
+	return true;
+}
+
+static const char * InvalidServerReason (serverinfo_pak *info)
+{
+#define EOT "\nPress ESC\n"
 
 	/* magic number for new packet format */
 	if (info->_255 != 255)
@@ -1936,86 +2173,44 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 
 		if (client)
 		{
-			const char *reason = InvalidServerReason(i);
-
-			// Quit here rather than downloading files
-			// and being refused later.
-			if (reason)
-			{
-				char *message = Z_StrDup(reason);
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(message, NULL, MM_NOTHING);
-				Z_Free(message);
-				return false;
-			}
+			serverinfo_pak *info = &serverlist[i].info;
 
-			D_ParseFileneeded(serverlist[i].info.fileneedednum,
-				serverlist[i].info.fileneeded);
-			CONS_Printf(M_GetText("Checking files...\n"));
-			i = CL_CheckFiles();
-			if (i == 3) // too many files
-			{
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"You have too many WAD files loaded\n"
-					"to add ones the server is using.\n"
-					"Please restart SRB2 before connecting.\n\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-			else if (i == 2) // cannot join for some reason
-			{
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"You have the wrong addons loaded.\n\n"
-					"To play on this server, restart\n"
-					"the game and don't load any addons.\n"
-					"SRB2 will automatically add\n"
-					"everything you need when you join.\n\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-			else if (i == 1)
-				cl_mode = CL_ASKJOIN;
+			if (info->refusereason == REFUSE_SLOTS_FULL)
+				serverisfull = true;
 			else
 			{
-				// must download something
-				// can we, though?
-				if (!CL_CheckDownloadable()) // nope!
+				const char *reason = InvalidServerReason(info);
+
+				// Quit here rather than downloading files
+				// and being refused later.
+				if (reason)
 				{
+					char *message = Z_StrDup(reason);
 					D_QuitNetGame();
 					CL_Reset();
 					D_StartTitle();
-					M_StartMessage(M_GetText(
-						"You cannot connect to this server\n"
-						"because you cannot download the files\n"
-						"that you are missing from the server.\n\n"
-						"See the console or log file for\n"
-						"more details.\n\n"
-						"Press ESC\n"
-					), NULL, MM_NOTHING);
+					M_StartMessage(message, NULL, MM_NOTHING);
+					Z_Free(message);
 					return false;
 				}
-				// no problem if can't send packet, we will retry later
-				if (CL_SendFileRequest())
-				{
-					cl_mode = CL_DOWNLOADFILES;
-#ifndef NONET
-					Snake_Initialise();
-#endif
-				}
 			}
+
+			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
+
+			if (info->flags & SV_LOTSOFADDONS)
+			{
+				cl_mode = CL_ASKFULLFILELIST;
+				cl_lastcheckedfilecount = 0;
+				return true;
+			}
+
+			cl_mode = CL_CHECKFILES;
 		}
 		else
+		{
 			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
+			*asksent = 0;
+		}
 
 		return true;
 	}
@@ -2061,6 +2256,22 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 				return false;
 			break;
 
+		case CL_ASKFULLFILELIST:
+			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
+				cl_mode = CL_CHECKFILES;
+			else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
+			{
+				if (CL_AskFileList(fileneedednum))
+				{
+					cl_lastcheckedfilecount = fileneedednum;
+					*asksent = I_GetTime() + NEWTICRATE;
+				}
+			}
+			break;
+		case CL_CHECKFILES:
+			if (!CL_FinishedFileList())
+				return false;
+			break;
 		case CL_DOWNLOADFILES:
 			waitmore = false;
 			for (i = 0; i < fileneedednum; i++)
@@ -2081,21 +2292,51 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			}
 #endif
 
-			cl_mode = CL_ASKJOIN; // don't break case continue to cljoin request now
-			/* FALLTHRU */
-
+			cl_mode = CL_LOADFILES;
+			break;
+		case CL_LOADFILES:
+			if (CL_LoadServerFiles())
+			{
+				FreeFileNeeded();
+				*asksent = 0; //This ensure the first join ask is right away
+				firstconnectattempttime = I_GetTime();
+				cl_mode = CL_ASKJOIN;
+			}
+			break;
 		case CL_ASKJOIN:
-			CL_LoadServerFiles();
+			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
+			{
+				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
+				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
+				D_QuitNetGame();
+				CL_Reset();
+				D_StartTitle();
+				M_StartMessage(M_GetText(
+					"5 minute wait time exceeded.\n"
+					"You may retry connection.\n"
+					"\n"
+					"Press ESC\n"
+				), NULL, MM_NOTHING);
+				return false;
+			}
 #ifndef NONET
 			// prepare structures to save the file
 			// WARNING: this can be useless in case of server not in GS_LEVEL
 			// but since the network layer doesn't provide ordered packets...
 			CL_PrepareDownloadSaveGame(tmpsave);
 #endif
-			if (CL_SendJoin())
+			if (I_GetTime() >= *asksent && CL_SendJoin())
+			{
+				*asksent = I_GetTime() + NEWTICRATE*3;
 				cl_mode = CL_WAITJOINRESPONSE;
+			}
+			break;
+		case CL_WAITJOINRESPONSE:
+			if (I_GetTime() >= *asksent)
+			{
+				cl_mode = CL_ASKJOIN;
+			}
 			break;
-
 #ifndef NONET
 		case CL_DOWNLOADSAVEGAME:
 			// At this state, the first (and only) needed file is the gamestate
@@ -2109,8 +2350,8 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 				break;
 #endif
 
-		case CL_WAITJOINRESPONSE:
 		case CL_CONNECTED:
+		case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
 		default:
 			break;
 
@@ -2118,7 +2359,6 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 		case CL_ABORTED:
 			cl_mode = CL_SEARCHING;
 			return false;
-
 	}
 
 	GetPackets();
@@ -2128,13 +2368,19 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 	if (*oldtic != I_GetTime())
 	{
 		I_OsPolling();
-		for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
-			G_MapEventsToControls(&events[eventtail]);
 
-		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1])
+		if (cl_mode == CL_CONFIRMCONNECT)
+			D_ProcessEvents(); //needed for menu system to receive inputs
+		else
+		{
+			for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
+				G_MapEventsToControls(&events[eventtail]);
+		}
+
+		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
 		{
 			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-//				M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
+			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
 
 #ifndef NONET
 			if (snake)
@@ -2167,13 +2413,20 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 #ifndef NONET
 		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
 		{
-			if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADSAVEGAME)
+			if (!snake)
 			{
 				F_MenuPresTicker(true); // title sky
 				F_TitleScreenTicker(true);
 				F_TitleScreenDrawer();
 			}
 			CL_DrawConnectionStatus();
+#ifdef HAVE_THREADS
+			I_lock_mutex(&m_menu_mutex);
+#endif
+			M_Drawer(); //Needed for drawing messageboxes on the connection screen
+#ifdef HAVE_THREADS
+			I_unlock_mutex(m_menu_mutex);
+#endif
 			I_UpdateNoVsync(); // page flip or blit buffer
 			if (moviemode)
 				M_SaveFrame();
@@ -2235,8 +2488,10 @@ static void CL_ConnectToServer(void)
 	ClearAdminPlayers();
 	pnumnodes = 1;
 	oldtic = I_GetTime() - 1;
+
 #ifndef NONET
 	asksent = (tic_t) - TICRATE;
+	firstconnectattempttime = I_GetTime();
 
 	i = SL_SearchServer(servernode);
 
@@ -2676,8 +2931,16 @@ void CL_Reset(void)
 	SV_ResetServer();
 
 	// make sure we don't leave any fileneeded gunk over from a failed join
+	FreeFileNeeded();
 	fileneedednum = 0;
-	memset(fileneeded, 0, sizeof(fileneeded));
+
+#ifndef NONET
+	totalfilesrequestednum = 0;
+	totalfilesrequestedsize = 0;
+#endif
+	firstconnectattempttime = 0;
+	serverisfull = false;
+	connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack
 
 	// D_StartTitle should get done now, but the calling function will handle it
 }
@@ -2932,6 +3195,34 @@ static void Command_Kick(void)
 	else
 		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
 }
+
+static void Command_ResendGamestate(void)
+{
+	SINT8 playernum;
+
+	if (COM_Argc() == 1)
+	{
+		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
+		return;
+	}
+	else if (client)
+	{
+		CONS_Printf(M_GetText("Only the server can use this.\n"));
+		return;
+	}
+
+	playernum = nametonum(COM_Argv(1));
+	if (playernum == -1 || playernum == 0)
+		return;
+
+	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
+	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
+	if (!HSendPacket(playernode[playernum], true, 0, 0))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n"));
+		return;
+	}
+}
 #endif
 
 static void Got_KickCmd(UINT8 **p, INT32 playernum)
@@ -3139,34 +3430,6 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 		CL_RemovePlayer(pnum, kickreason);
 }
 
-static void Command_ResendGamestate(void)
-{
-	SINT8 playernum;
-
-	if (COM_Argc() == 1)
-	{
-		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
-		return;
-	}
-	else if (client)
-	{
-		CONS_Printf(M_GetText("Only the server can use this.\n"));
-		return;
-	}
-
-	playernum = nametonum(COM_Argv(1));
-	if (playernum == -1 || playernum == 0)
-		return;
-
-	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
-	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
-	if (!HSendPacket(playernode[playernum], true, 0, 0))
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n"));
-		return;
-	}
-}
-
 static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
 consvar_t cv_netticbuffer = CVAR_INIT ("netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL);
 
@@ -3189,7 +3452,7 @@ consvar_t cv_maxsend = CVAR_INIT ("maxsend", "4096", CV_SAVE|CV_NETVAR, maxsend_
 consvar_t cv_noticedownload = CVAR_INIT ("noticedownload", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
 
 // Speed of file downloading (in packets per tic)
-static CV_PossibleValue_t downloadspeed_cons_t[] = {{0, "MIN"}, {32, "MAX"}, {0, NULL}};
+static CV_PossibleValue_t downloadspeed_cons_t[] = {{1, "MIN"}, {300, "MAX"}, {0, NULL}};
 consvar_t cv_downloadspeed = CVAR_INIT ("downloadspeed", "16", CV_SAVE|CV_NETVAR, downloadspeed_cons_t, NULL);
 
 static void Got_AddPlayer(UINT8 **p, INT32 playernum);
@@ -3903,6 +4166,7 @@ static void HandleServerInfo(SINT8 node)
 
 static void PT_WillResendGamestate(void)
 {
+#ifndef NONET
 	char tmpsave[256];
 
 	if (server || cl_redownloadinggamestate)
@@ -3925,10 +4189,12 @@ static void PT_WillResendGamestate(void)
 	CL_PrepareDownloadSaveGame(tmpsave);
 
 	cl_redownloadinggamestate = true;
+#endif
 }
 
 static void PT_CanReceiveGamestate(SINT8 node)
 {
+#ifndef NONET
 	if (client || sendingsavegame[node])
 		return;
 
@@ -3936,6 +4202,9 @@ static void PT_CanReceiveGamestate(SINT8 node)
 
 	SV_SendSaveGame(node, true); // Resend a complete game state
 	resendingsavegame[node] = true;
+#else
+	(void)node;
+#endif
 }
 
 /** Handles a packet received from a node that isn't in game
@@ -3962,31 +4231,40 @@ static void HandlePacketFromAwayNode(SINT8 node)
 	switch (netbuffer->packettype)
 	{
 		case PT_ASKINFOVIAMS:
-#if 0
+			Net_CloseConnection(node);
+			break;
+
+		case PT_TELLFILESNEEDED:
 			if (server && serverrunning)
 			{
-				INT32 clientnode;
-				if (ms_RoomId < 0) // ignore if we're not actually on the MS right now
-				{
-					Net_CloseConnection(node); // and yes, close connection
-					return;
-				}
-				clientnode = I_NetMakeNode(netbuffer->u.msaskinfo.clientaddr);
-				if (clientnode != -1)
-				{
-					SV_SendServerInfo(clientnode, (tic_t)LONG(netbuffer->u.msaskinfo.time));
-					SV_SendPlayerInfo(clientnode); // Send extra info
-					Net_CloseConnection(clientnode);
-					// Don't close connection to MS...
-				}
-				else
-					Net_CloseConnection(node); // ...unless the IP address is not valid
+				UINT8 *p;
+				INT32 firstfile = netbuffer->u.filesneedednum;
+
+				netbuffer->packettype = PT_MOREFILESNEEDED;
+				netbuffer->u.filesneededcfg.first = firstfile;
+				netbuffer->u.filesneededcfg.more = 0;
+
+				p = PutFileNeeded(firstfile);
+
+				HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
+			}
+			else // Shouldn't get this if you aren't the server...?
+				Net_CloseConnection(node);
+			break;
+
+		case PT_MOREFILESNEEDED:
+			if (server && serverrunning)
+			{ // But wait I thought I'm the server?
+				Net_CloseConnection(node);
+				break;
+			}
+			SERVERONLY
+			if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
+			{
+				D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
+				if (!netbuffer->u.filesneededcfg.more)
+					cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
 			}
-			else
-				Net_CloseConnection(node); // you're not supposed to get it, so ignore it
-#else
-			Net_CloseConnection(node);
-#endif
 			break;
 
 		case PT_ASKINFO:
@@ -4012,13 +4290,24 @@ static void HandlePacketFromAwayNode(SINT8 node)
 				if (!reason)
 					I_Error("Out of memory!\n");
 
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
+				if (strstr(reason, "Maximum players reached"))
+				{
+					serverisfull = true;
+					//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
+					//We set it back to the value of cv_nettimeout.value in CL_Reset
+					connectiontimeout = NEWTICRATE*7;
+					cl_mode = CL_ASKJOIN;
+					free(reason);
+					break;
+				}
 
 				M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
 					reason), NULL, MM_NOTHING);
 
+				D_QuitNetGame();
+				CL_Reset();
+				D_StartTitle();
+
 				free(reason);
 
 				// Will be reset by caller. Signals refusal.
@@ -4222,8 +4511,10 @@ static void HandlePacketFromPlayer(SINT8 node)
 			// Check player consistancy during the level
 			if (realstart <= gametic && realstart + BACKUPTICS - 1 > gametic && gamestate == GS_LEVEL
 				&& consistancy[realstart%BACKUPTICS] != SHORT(netbuffer->u.clientpak.consistancy)
-				&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime()
-				&& !SV_ResendingSavegameToAnyone())
+#ifndef NONET
+				&& !SV_ResendingSavegameToAnyone()
+#endif
+				&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime())
 			{
 				if (cv_resynchattempts.value)
 				{
@@ -4974,16 +5265,23 @@ void TryRunTics(tic_t realtics)
 			// run the count * tics
 			while (neededtic > gametic)
 			{
+				boolean update_stats = !(paused || P_AutoPause());
+
 				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
 
-				ps_tictime = I_GetPreciseTime();
+				if (update_stats)
+					PS_START_TIMING(ps_tictime);
 
 				G_Ticker((gametic % NEWTICRATERATIO) == 0);
 				ExtraDataTicker();
 				gametic++;
 				consistancy[gametic%BACKUPTICS] = Consistancy();
 
-				ps_tictime = I_GetPreciseTime() - ps_tictime;
+				if (update_stats)
+				{
+					PS_STOP_TIMING(ps_tictime);
+					PS_UpdateTickStats();
+				}
 
 				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
 				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
@@ -5127,9 +5425,11 @@ void NetUpdate(void)
 
 	if (client)
 	{
+#ifndef NONET
 		// If the client just finished redownloading the game state, load it
 		if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND)
 			CL_ReloadReceivedSavegame();
+#endif
 
 		CL_SendClientCmd(); // Send tic cmd
 		hu_redownloadinggamestate = cl_redownloadinggamestate;
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index a89c054e1f0556fc7089d32dfdcdbaa4035fa2a4..8e75fb963c860d64e91557bc47b17daea22af7f4 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -94,6 +94,9 @@ typedef enum
 
 	PT_LOGIN,         // Login attempt from the client.
 
+	PT_TELLFILESNEEDED, // Client, to server: "what other files do I need starting from this number?"
+	PT_MOREFILESNEEDED, // Server, to client: "you need these (+ more on top of those)"
+
 	PT_PING,          // Packet sent to tell clients the other client's latency to server.
 	NUMPACKETTYPE
 } packettype_t;
@@ -198,6 +201,9 @@ typedef struct
 	char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME];
 } ATTRPACK clientconfig_pak;
 
+#define SV_DEDICATED    0x40 // server is dedicated
+#define SV_LOTSOFADDONS 0x20 // flag used to ask for full file list in d_netfil
+
 enum {
 	REFUSE_JOINS_DISABLED = 1,
 	REFUSE_SLOTS_FULL,
@@ -225,7 +231,7 @@ typedef struct
 	char gametypename[24];
 	UINT8 modifiedgame;
 	UINT8 cheatsenabled;
-	UINT8 isdedicated;
+	UINT8 flags;
 	UINT8 fileneedednum;
 	tic_t time;
 	tic_t leveltime;
@@ -279,6 +285,14 @@ typedef struct
 	UINT8 ctfteam;
 } ATTRPACK plrconfig;
 
+typedef struct
+{
+	INT32 first;
+	UINT8 num;
+	UINT8 more;
+	UINT8 files[MAXFILENEEDED]; // is filled with writexxx (byteptr.h)
+} ATTRPACK filesneededconfig_pak;
+
 //
 // Network packet data
 //
@@ -308,6 +322,8 @@ typedef struct
 		msaskinfo_pak msaskinfo;            //          22 bytes
 		plrinfo playerinfo[MAXPLAYERS];     //         576 bytes(?)
 		plrconfig playerconfig[MAXPLAYERS]; // (up to) 528 bytes(?)
+		INT32 filesneedednum;               //           4 bytes
+		filesneededconfig_pak filesneededcfg; //       ??? bytes
 		UINT32 pingtable[MAXPLAYERS+1];     //          68 bytes
 	} u; // This is needed to pack diff packet types data together
 } ATTRPACK doomdata_t;
diff --git a/src/d_main.c b/src/d_main.c
index b4b668f4bc0c8b984800cae319862f7b2aa2fe7c..83419d266c84703694d06644a28a953e3550f6b1 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -65,7 +65,7 @@
 #include "m_cond.h" // condition initialization
 #include "fastcmp.h"
 #include "keys.h"
-#include "filesrch.h" // refreshdirmenu, mainwadstally
+#include "filesrch.h" // refreshdirmenu
 #include "g_input.h" // tutorial mode control scheming
 #include "m_perfstats.h"
 
@@ -96,11 +96,8 @@ int SUBVERSION;
 // platform independant focus loss
 UINT8 window_notinfocus = false;
 
-//
-// DEMO LOOP
-//
-static char *startupwadfiles[MAX_WADFILES];
-static char *startuppwads[MAX_WADFILES];
+static addfilelist_t startupwadfiles;
+static addfilelist_t startuppwads;
 
 boolean devparm = false; // started game with -devparm
 
@@ -119,6 +116,9 @@ boolean midi_disabled = false;
 boolean sound_disabled = false;
 boolean digital_disabled = false;
 
+//
+// DEMO LOOP
+//
 boolean advancedemo;
 #ifdef DEBUGFILE
 INT32 debugload = 0;
@@ -272,7 +272,7 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // ate the event
 
-		if (!hooked && G_LuaResponder(ev))
+		if (!hooked && !CON_Ready() && G_LuaResponder(ev))
 			continue;
 
 		G_Responder(ev);
@@ -476,7 +476,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
-				ps_rendercalltime = I_GetPreciseTime();
+				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
 					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
@@ -523,7 +523,7 @@ static void D_Display(void)
 					if (postimgtype2)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
-				ps_rendercalltime = I_GetPreciseTime() - ps_rendercalltime;
+				PS_STOP_TIMING(ps_rendercalltime);
 			}
 
 			if (lastdraw)
@@ -537,7 +537,7 @@ static void D_Display(void)
 				lastdraw = false;
 			}
 
-			ps_uitime = I_GetPreciseTime();
+			PS_START_TIMING(ps_uitime);
 
 			if (gamestate == GS_LEVEL)
 			{
@@ -550,7 +550,7 @@ static void D_Display(void)
 		}
 		else
 		{
-			ps_uitime = I_GetPreciseTime();
+			PS_START_TIMING(ps_uitime);
 		}
 	}
 
@@ -592,7 +592,7 @@ static void D_Display(void)
 
 	CON_Drawer();
 
-	ps_uitime = I_GetPreciseTime() - ps_uitime;
+	PS_STOP_TIMING(ps_uitime);
 
 	//
 	// wipe update
@@ -678,9 +678,9 @@ static void D_Display(void)
 			M_DrawPerfStats();
 		}
 
-		ps_swaptime = I_GetPreciseTime();
+		PS_START_TIMING(ps_swaptime);
 		I_FinishUpdate(); // page flip or blit buffer
-		ps_swaptime = I_GetPreciseTime() - ps_swaptime;
+		PS_STOP_TIMING(ps_swaptime);
 	}
 }
 
@@ -923,51 +923,68 @@ void D_StartTitle(void)
 	tutorialmode = false;
 }
 
-//
-// D_AddFile
-//
-static void D_AddFile(char **list, const char *file)
+#define REALLOC_FILE_LIST \
+	if (list->files == NULL) \
+	{ \
+		list->files = calloc(sizeof(list->files), 2); \
+		list->numfiles = 1; \
+	} \
+	else \
+	{ \
+		index = list->numfiles; \
+		list->files = realloc(list->files, sizeof(list->files) * ((++list->numfiles) + 1)); \
+		if (list->files == NULL) \
+			I_Error("%s: No more free memory to add file %s", __FUNCTION__, file); \
+	}
+
+static void D_AddFile(addfilelist_t *list, const char *file)
 {
-	size_t pnumwadfiles;
 	char *newfile;
+	size_t index = 0;
 
-	for (pnumwadfiles = 0; list[pnumwadfiles]; pnumwadfiles++)
-		;
+	REALLOC_FILE_LIST
 
 	newfile = malloc(strlen(file) + 1);
 	if (!newfile)
-		I_Error("No more free memory to AddFile %s",file);
+		I_Error("D_AddFile: No more free memory to add file %s", file);
 
 	strcpy(newfile, file);
-	list[pnumwadfiles] = newfile;
+	list->files[index] = newfile;
 }
 
-static void D_AddFolder(char **list, const char *file)
+static void D_AddFolder(addfilelist_t *list, const char *file)
 {
-	size_t pnumwadfiles;
 	char *newfile;
+	size_t index = 0;
 
-	for (pnumwadfiles = 0; list[pnumwadfiles]; pnumwadfiles++)
-		;
+	REALLOC_FILE_LIST
 
 	newfile = malloc(strlen(file) + 2); // Path delimiter + NULL terminator
 	if (!newfile)
-		I_Error("No more free memory to AddFolder %s",file);
+		I_Error("D_AddFolder: No more free memory to add folder %s", file);
 
 	strcpy(newfile, file);
 	strcat(newfile, PATHSEP);
 
-	list[pnumwadfiles] = newfile;
+	list->files[index] = newfile;
 }
 
-static inline void D_CleanFile(char **list)
+#undef REALLOC_FILE_LIST
+
+static inline void D_CleanFile(addfilelist_t *list)
 {
-	size_t pnumwadfiles;
-	for (pnumwadfiles = 0; list[pnumwadfiles]; pnumwadfiles++)
+	if (list->files)
 	{
-		free(list[pnumwadfiles]);
-		list[pnumwadfiles] = NULL;
+		size_t pnumwadfiles = 0;
+
+		for (; pnumwadfiles < list->numfiles; pnumwadfiles++)
+			free(list->files[pnumwadfiles]);
+
+		free(list->files);
+		list->files = NULL;
 	}
+
+	list->numfiles = 0;
 }
 
 ///\brief Checks if a netgame URL is being handled, and changes working directory to the EXE's if so.
@@ -1051,7 +1068,7 @@ static void IdentifyVersion(void)
 
 	// Load the IWAD
 	if (srb2wad != NULL && FIL_ReadFileOK(srb2wad))
-		D_AddFile(startupwadfiles, srb2wad);
+		D_AddFile(&startupwadfiles, srb2wad);
 	else
 		I_Error("srb2.pk3 not found! Expected in %s, ss file: %s\n", srb2waddir, srb2wad);
 
@@ -1062,14 +1079,14 @@ static void IdentifyVersion(void)
 	// checking in D_SRB2Main
 
 	// Add the maps
-	D_AddFile(startupwadfiles, va(pandf,srb2waddir,"zones.pk3"));
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "zones.pk3"));
 
 	// Add the players
-	D_AddFile(startupwadfiles, va(pandf,srb2waddir, "player.dta"));
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "player.dta"));
 
 #ifdef USE_PATCH_DTA
 	// Add our crappy patches to fix our bugs
-	D_AddFile(startupwadfiles, va(pandf,srb2waddir,"patch.pk3"));
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "patch.pk3"));
 #endif
 
 #if !defined (HAVE_SDL) || defined (HAVE_MIXER)
@@ -1079,7 +1096,7 @@ static void IdentifyVersion(void)
 			const char *musicpath = va(pandf,srb2waddir,str);\
 			int ms = W_VerifyNMUSlumps(musicpath, false); \
 			if (ms == 1) \
-				D_AddFile(startupwadfiles, musicpath); \
+				D_AddFile(&startupwadfiles, musicpath); \
 			else if (ms == 0) \
 				I_Error("File "str" has been modified with non-music/sound lumps"); \
 		}
@@ -1269,9 +1286,9 @@ void D_SRB2Main(void)
 			else if (myargv[i][0] == '-' || myargv[i][0] == '+')
 				addontype = 0;
 			else if (addontype == 1)
-				D_AddFile(startuppwads, myargv[i]);
+				D_AddFile(&startuppwads, myargv[i]);
 			else if (addontype == 2)
-				D_AddFolder(startuppwads, myargv[i]);
+				D_AddFolder(&startuppwads, myargv[i]);
 		}
 	}
 
@@ -1310,8 +1327,8 @@ void D_SRB2Main(void)
 
 	// load wad, including the main wad file
 	CONS_Printf("W_InitMultipleFiles(): Adding IWAD and main PWADs.\n");
-	W_InitMultipleFiles(startupwadfiles);
-	D_CleanFile(startupwadfiles);
+	W_InitMultipleFiles(&startupwadfiles);
+	D_CleanFile(&startupwadfiles);
 
 #ifndef DEVELOP // md5s last updated 22/02/20 (ddmmyy)
 
@@ -1326,8 +1343,6 @@ void D_SRB2Main(void)
 	// ...except it does if they slip maps in there, and that's what W_VerifyNMUSlumps is for.
 #endif //ifndef DEVELOP
 
-	mainwadstally = packetsizetally; // technically not accurate atm, remember to port the two-stage -file process from kart in 2.2.x
-
 	cht_Init();
 
 	//---------------------------------------------------- READY SCREEN
@@ -1358,9 +1373,16 @@ void D_SRB2Main(void)
 
 	I_RegisterSysCommands();
 
-	CONS_Printf("W_InitMultipleFiles(): Adding extra PWADs.\n");
-	W_InitMultipleFiles(startuppwads);
-	D_CleanFile(startuppwads);
+	CON_StopRefresh(); // Temporarily stop refreshing the screen for wad loading
+
+	if (startuppwads.numfiles)
+	{
+		CONS_Printf("W_InitMultipleFiles(): Adding extra PWADs.\n");
+		W_InitMultipleFiles(&startuppwads);
+		D_CleanFile(&startuppwads);
+	}
+
+	CON_StartRefresh(); // Restart the refresh!
 
 	CONS_Printf("HU_LoadGraphics()...\n");
 	HU_LoadGraphics();
diff --git a/src/d_net.c b/src/d_net.c
index 9e5abe24a0adbd1b81d5ab67e102b7689bca53b3..3a4746002eb87efe8dd57e45729cefc96943bdca 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -815,6 +815,8 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"CLIENTJOIN",
 	"NODETIMEOUT",
 	"LOGIN",
+	"TELLFILESNEEDED",
+	"MOREFILESNEEDED",
 	"PING"
 };
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 5eb360befe41f54a2a87d56bbfa706032c76ab4d..cca3102d085aac427b5a3469dd00f3085a3fee3d 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -47,6 +47,7 @@
 #include "m_cond.h"
 #include "m_anigif.h"
 #include "md5.h"
+#include "m_perfstats.h"
 
 #ifdef NETGAME_DEVMODE
 #define CV_RESTRICT CV_NETVAR
@@ -374,7 +375,14 @@ consvar_t cv_sleep = CVAR_INIT ("cpusleep", "1", CV_SAVE, sleeping_cons_t, NULL)
 
 static CV_PossibleValue_t perfstats_cons_t[] = {
 	{0, "Off"}, {1, "Rendering"}, {2, "Logic"}, {3, "ThinkFrame"}, {0, NULL}};
-consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", 0, perfstats_cons_t, NULL);
+consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", CV_CALL, perfstats_cons_t, PS_PerfStats_OnChange);
+static CV_PossibleValue_t ps_samplesize_cons_t[] = {
+	{1, "MIN"}, {1000, "MAX"}, {0, NULL}};
+consvar_t cv_ps_samplesize = CVAR_INIT ("ps_samplesize", "1", CV_CALL, ps_samplesize_cons_t, PS_SampleSize_OnChange);
+static CV_PossibleValue_t ps_descriptor_cons_t[] = {
+	{1, "Average"}, {2, "SD"}, {3, "Minimum"}, {4, "Maximum"}, {0, NULL}};
+consvar_t cv_ps_descriptor = CVAR_INIT ("ps_descriptor", "Average", 0, ps_descriptor_cons_t, NULL);
+
 consvar_t cv_freedemocamera = CVAR_INIT("freedemocamera", "Off", CV_SAVE, CV_OnOff, NULL);
 
 char timedemo_name[256];
@@ -867,6 +875,8 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_soundtest);
 
 	CV_RegisterVar(&cv_perfstats);
+	CV_RegisterVar(&cv_ps_samplesize);
+	CV_RegisterVar(&cv_ps_descriptor);
 
 	// ingame object placing
 	COM_AddCommand("objectplace", Command_ObjectPlace_f);
@@ -3223,7 +3233,7 @@ static void Command_RunSOC(void)
 static void Got_RunSOCcmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[256];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 
 	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
 	{
@@ -3347,10 +3357,9 @@ static void Command_Addfile(void)
 				break;
 		++p;
 
-		// check total packet size and no of files currently loaded
-		// See W_InitFile in w_wad.c
-		if ((numwadfiles >= MAX_WADFILES)
-		|| ((packetsizetally + nameonlylength(fn) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
+		// check no of files currently loaded
+		// See W_LoadWadFile in w_wad.c
+		if (numwadfiles >= MAX_WADFILES)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
 			return;
@@ -3379,6 +3388,9 @@ static void Command_Addfile(void)
 
 			for (i = 0; i < numwadfiles; i++)
 			{
+				if (wadfiles[i]->type == RET_FOLDER)
+					continue;
+
 				if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
 				{
 					CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), fn);
@@ -3469,10 +3481,9 @@ static void Command_Addfolder(void)
 			continue;
 		}
 
-		// check total packet size and no of files currently loaded
-		// See W_InitFile in w_wad.c
-		if ((numwadfiles >= MAX_WADFILES)
-		|| ((packetsizetally + strlen(fn) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
+		// check no of files currently loaded
+		// See W_LoadWadFile in w_wad.c
+		if (numwadfiles >= MAX_WADFILES)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
 			return;
@@ -3534,7 +3545,7 @@ static void Command_Addfolder(void)
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 	UINT8 md5sum[16];
 	boolean kick = false;
 	boolean toomany = false;
@@ -3559,9 +3570,7 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 		return;
 	}
 
-	// See W_InitFile in w_wad.c
-	if ((numwadfiles >= MAX_WADFILES)
-	|| ((packetsizetally + nameonlylength(filename) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
+	if (numwadfiles >= MAX_WADFILES)
 		toomany = true;
 	else
 		ncs = findfile(filename,md5sum,true);
@@ -3594,7 +3603,7 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum)
 {
 	char path[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 	boolean kick = false;
 	boolean toomany = false;
 	INT32 i,j;
@@ -3619,9 +3628,7 @@ static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum)
 		return;
 	}
 
-	// See W_InitFile in w_wad.c
-	if ((numwadfiles >= MAX_WADFILES)
-	|| ((packetsizetally + strlen(path) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
+	if (numwadfiles >= MAX_WADFILES)
 		toomany = true;
 	else
 		ncs = findfolder(path);
@@ -3652,7 +3659,7 @@ static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum)
 static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 	UINT8 md5sum[16];
 
 	READSTRINGN(*cp, filename, 240);
@@ -3700,7 +3707,7 @@ static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum)
 {
 	char path[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 
 	READSTRINGN(*cp, path, 240);
 
@@ -3744,7 +3751,13 @@ static void Command_ListWADS_f(void)
 {
 	INT32 i = numwadfiles;
 	char *tempname;
-	CONS_Printf(M_GetText("There are %d wads loaded:\n"),numwadfiles);
+
+#ifdef ENFORCE_WAD_LIMIT
+	CONS_Printf(M_GetText("There are %d/%d files loaded:\n"),numwadfiles,MAX_WADFILES);
+#else
+	CONS_Printf(M_GetText("There are %d files loaded:\n"),numwadfiles);
+#endif
+
 	for (i--; i >= 0; i--)
 	{
 		nameonly(tempname = va("%s", wadfiles[i]->filename));
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index cae32643e5dc25e5947e812e8e7c88836cb9aea0..7bb7eab03a1345190c670b3d9309c159cc3d3c07 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -73,6 +73,7 @@ extern consvar_t cv_teamscramble;
 extern consvar_t cv_scrambleonchange;
 
 extern consvar_t cv_netstat;
+extern consvar_t cv_nettimeout;
 
 extern consvar_t cv_countdowntime;
 extern consvar_t cv_runscripts;
@@ -110,6 +111,8 @@ extern consvar_t cv_skipmapcheck;
 extern consvar_t cv_sleep;
 
 extern consvar_t cv_perfstats;
+extern consvar_t cv_ps_samplesize;
+extern consvar_t cv_ps_descriptor;
 
 extern char timedemo_name[256];
 extern boolean timedemo_csv;
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 12c5ee6a211640046b74bd1b7ade6fcb811e1ff0..fdc0026a8bbf2a87b12fa0444fbfa2156e40c5c1 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -52,7 +52,7 @@
 #include <errno.h>
 
 // Prototypes
-static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid);
+static boolean AddFileToSendQueue(INT32 node, UINT8 fileid);
 
 // Sender structure
 typedef struct filetx_s
@@ -87,7 +87,7 @@ static filetran_t transfer[MAXNETNODES];
 
 // Receiver structure
 INT32 fileneedednum; // Number of files needed to join the server
-fileneeded_t fileneeded[MAX_WADFILES]; // List of needed files
+fileneeded_t *fileneeded; // List of needed files
 static tic_t lasttimeackpacketsent = 0;
 char downloaddir[512] = "DOWNLOAD";
 
@@ -105,6 +105,10 @@ static pauseddownload_t *pauseddownload = NULL;
 #ifndef NONET
 // for cl loading screen
 INT32 lastfilenum = -1;
+INT32 downloadcompletednum = 0;
+UINT32 downloadcompletedsize = 0;
+INT32 totalfilesrequestednum = 0;
+UINT32 totalfilesrequestedsize = 0;
 #endif
 
 luafiletransfer_t *luafiletransfers = NULL;
@@ -113,25 +117,62 @@ boolean waitingforluafilecommand = false;
 char luafiledir[256 + 16] = "luafiles";
 
 
+static UINT16 GetWadNumFromFileNeededId(UINT8 id)
+{
+	UINT16 wadnum;
+
+	for (wadnum = mainwads; wadnum < numwadfiles; wadnum++)
+	{
+		if (!wadfiles[wadnum]->important)
+			continue;
+		if (id == 0)
+			return wadnum;
+		id--;
+	}
+
+	return UINT16_MAX;
+}
+
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
   * Used to have size limiting built in - now handled via W_InitFile in w_wad.c
   *
   */
-UINT8 *PutFileNeeded(void)
+UINT8 *PutFileNeeded(UINT16 firstfile)
 {
-	size_t i, count = 0;
-	UINT8 *p = netbuffer->u.serverinfo.fileneeded;
+	size_t i;
+	UINT8 count = 0;
+	UINT8 *p_start = netbuffer->packettype == PT_MOREFILESNEEDED ? netbuffer->u.filesneededcfg.files : netbuffer->u.serverinfo.fileneeded;
+	UINT8 *p = p_start;
 	char wadfilename[MAX_WADPATH] = "";
 	UINT8 filestatus, folder;
 
-	for (i = 0; i < numwadfiles; i++)
+	for (i = mainwads; i < numwadfiles; i++) //mainwads, otherwise we start on the first mainwad
 	{
 		// If it has only music/sound lumps, don't put it in the list
 		if (!wadfiles[i]->important)
 			continue;
 
+		if (firstfile)
+		{ // Skip files until we reach the first file.
+			firstfile--;
+			continue;
+		}
+
+		nameonly(strcpy(wadfilename, wadfiles[i]->filename));
+
+		// Look below at the WRITE macros to understand what these numbers mean.
+		if (p + 1 + 4 + min(strlen(wadfilename) + 1, MAX_WADPATH) + 16 > p_start + MAXFILENEEDED)
+		{
+			// Too many files to send all at once
+			if (netbuffer->packettype == PT_MOREFILESNEEDED)
+				netbuffer->u.filesneededcfg.more = 1;
+			else
+				netbuffer->u.serverinfo.flags |= SV_LOTSOFADDONS;
+			break;
+		}
+
 		filestatus = 1; // Importance - not really used any more, holds 1 by default for backwards compat with MS
 		folder = (wadfiles[i]->type == RET_FOLDER);
 
@@ -148,32 +189,53 @@ UINT8 *PutFileNeeded(void)
 
 		count++;
 		WRITEUINT32(p, wadfiles[i]->filesize);
-		nameonly(strcpy(wadfilename, wadfiles[i]->filename));
 		WRITESTRINGN(p, wadfilename, MAX_WADPATH);
 		WRITEMEM(p, wadfiles[i]->md5sum, 16);
 	}
-	netbuffer->u.serverinfo.fileneedednum = (UINT8)count;
+
+	if (netbuffer->packettype == PT_MOREFILESNEEDED)
+		netbuffer->u.filesneededcfg.num = count;
+	else
+		netbuffer->u.serverinfo.fileneedednum = count;
 
 	return p;
 }
 
+void AllocFileNeeded(INT32 size)
+{
+	if (fileneeded == NULL)
+		fileneeded = Z_Calloc(sizeof(fileneeded_t) * size, PU_STATIC, NULL);
+	else
+		fileneeded = Z_Realloc(fileneeded, sizeof(fileneeded_t) * size, PU_STATIC, NULL);
+}
+
+void FreeFileNeeded(void)
+{
+	Z_Free(fileneeded);
+	fileneeded = NULL;
+}
+
 /** Parses the serverinfo packet and fills the fileneeded table on client
   *
   * \param fileneedednum_parm The number of files needed to join the server
   * \param fileneededstr The memory block containing the list of needed files
   *
   */
-void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
+void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile)
 {
 	INT32 i;
 	UINT8 *p;
 	UINT8 filestatus;
 
-	fileneedednum = fileneedednum_parm;
+	fileneedednum = firstfile + fileneedednum_parm;
 	p = (UINT8 *)fileneededstr;
-	for (i = 0; i < fileneedednum; i++)
+
+	AllocFileNeeded(fileneedednum);
+
+	for (i = firstfile; i < fileneedednum; i++)
 	{
-		fileneeded[i].status = FS_NOTFOUND; // We haven't even started looking for the file yet
+		fileneeded[i].type = FILENEEDED_WAD;
+		fileneeded[i].status = FS_NOTCHECKED; // We haven't even started looking for the file yet
 		fileneeded[i].justdownloaded = false;
 		filestatus = READUINT8(p); // The first byte is the file status
 		fileneeded[i].folder = READUINT8(p); // The second byte is the folder flag
@@ -191,7 +253,11 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
 	lastfilenum = -1;
 #endif
 
+	FreeFileNeeded();
+	AllocFileNeeded(1);
+
 	fileneedednum = 1;
+	fileneeded[0].type = FILENEEDED_SAVEGAME;
 	fileneeded[0].status = FS_REQUESTED;
 	fileneeded[0].justdownloaded = false;
 	fileneeded[0].totalsize = UINT32_MAX;
@@ -322,14 +388,18 @@ boolean CL_SendFileRequest(void)
 		if ((fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD))
 		{
 			totalfreespaceneeded += fileneeded[i].totalsize;
-			nameonly(fileneeded[i].filename);
+
 			WRITEUINT8(p, i); // fileid
-			WRITESTRINGN(p, fileneeded[i].filename, MAX_WADPATH);
+
 			// put it in download dir
+			nameonly(fileneeded[i].filename);
 			strcatbf(fileneeded[i].filename, downloaddir, "/");
+
 			fileneeded[i].status = FS_REQUESTED;
 		}
+
 	WRITEUINT8(p, 0xFF);
+
 	I_GetDiskFreeSpace(&availablefreespace);
 	if (totalfreespaceneeded > availablefreespace)
 		I_Error("To play on this server you must download %s KB,\n"
@@ -345,21 +415,22 @@ boolean CL_SendFileRequest(void)
 // returns false if a requested file was not found or cannot be sent
 boolean PT_RequestFile(INT32 node)
 {
-	char wad[MAX_WADPATH+1];
 	UINT8 *p = netbuffer->u.textcmd;
 	UINT8 id;
+
 	while (p < netbuffer->u.textcmd + MAXTEXTCMD-1) // Don't allow hacked client to overflow
 	{
 		id = READUINT8(p);
 		if (id == 0xFF)
 			break;
-		READSTRINGN(p, wad, MAX_WADPATH);
-		if (!AddFileToSendQueue(node, wad, id))
+
+		if (!AddFileToSendQueue(node, id))
 		{
 			SV_AbortSendFiles(node);
 			return false; // don't read the rest of the files
 		}
 	}
+
 	return true; // no problems with any files
 }
 
@@ -368,23 +439,16 @@ boolean PT_RequestFile(INT32 node)
   * \return 0 if some files are missing
   *         1 if all files exist
   *         2 if some already loaded files are not requested or are in a different order
+  *         3 too many files, over WADLIMIT
+  *         4 still checking, continuing next tic
   *
   */
 INT32 CL_CheckFiles(void)
 {
 	INT32 i, j;
 	char wadfilename[MAX_WADPATH];
-	INT32 ret = 1;
-	size_t packetsize = 0;
-	size_t filestoget = 0;
-
-//	if (M_CheckParm("-nofiles"))
-//		return 1;
-
-	// the first is the iwad (the main wad file)
-	// we don't care if it's called srb2.pk3 or not.
-	// Never download the IWAD, just assume it's there and identical
-	fileneeded[0].status = FS_OPEN;
+	size_t filestoload = 0;
+	boolean downloadrequired = false;
 
 	// Modified game handling -- check for an identical file list
 	// must be identical in files loaded AND in order
@@ -392,7 +456,7 @@ INT32 CL_CheckFiles(void)
 	if (modifiedgame)
 	{
 		CONS_Debug(DBG_NETPLAY, "game is modified; only doing basic checks\n");
-		for (i = 1, j = 1; i < fileneedednum || j < numwadfiles;)
+		for (i = 0, j = mainwads; i < fileneedednum || j < numwadfiles;)
 		{
 			if (j < numwadfiles && !wadfiles[j]->important)
 			{
@@ -419,15 +483,21 @@ INT32 CL_CheckFiles(void)
 		return 1;
 	}
 
-	// See W_InitFile in w_wad.c
-	packetsize = packetsizetally;
-
-	for (i = 1; i < fileneedednum; i++)
+	for (i = 0; i < fileneedednum; i++)
 	{
+		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+			downloadrequired = true;
+
+		if (fileneeded[i].status != FS_OPEN)
+			filestoload++;
+
+		if (fileneeded[i].status != FS_NOTCHECKED) //since we're running this over multiple tics now, its possible for us to come across files checked in previous tics
+			continue;
+
 		CONS_Debug(DBG_NETPLAY, "searching for '%s' ", fileneeded[i].filename);
 
 		// Check in already loaded files
-		for (j = 1; wadfiles[j]; j++)
+		for (j = mainwads; wadfiles[j]; j++)
 		{
 			nameonly(strcpy(wadfilename, wadfiles[j]->filename));
 			if (!stricmp(wadfilename, fileneeded[i].filename) &&
@@ -435,43 +505,34 @@ INT32 CL_CheckFiles(void)
 			{
 				CONS_Debug(DBG_NETPLAY, "already loaded\n");
 				fileneeded[i].status = FS_OPEN;
-				break;
+				return 4;
 			}
 		}
-		if (fileneeded[i].status != FS_NOTFOUND)
-			continue;
-
-		if (fileneeded[i].folder)
-			packetsize += strlen(fileneeded[i].filename) + FILENEEDEDSIZE;
-		else
-			packetsize += nameonlylength(fileneeded[i].filename) + FILENEEDEDSIZE;
-
-		if ((numwadfiles+filestoget >= MAX_WADFILES)
-		|| (packetsize > MAXFILENEEDED*sizeof(UINT8)))
-			return 3;
-
-		filestoget++;
 
 		if (fileneeded[i].folder)
 			fileneeded[i].status = findfolder(fileneeded[i].filename);
 		else
 			fileneeded[i].status = findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
+
 		CONS_Debug(DBG_NETPLAY, "found %d\n", fileneeded[i].status);
-		if (fileneeded[i].status != FS_FOUND)
-			ret = 0;
+		return 4;
 	}
-	return ret;
+
+	//now making it here means we've checked the entire list and no FS_NOTCHECKED files remain
+	if (numwadfiles+filestoload > MAX_WADFILES)
+		return 3;
+	else if (downloadrequired)
+		return 0; //some stuff is FS_NOTFOUND, needs download
+	else
+		return 1; //everything is FS_OPEN or FS_FOUND, proceed to loading
 }
 
 // Load it now
-void CL_LoadServerFiles(void)
+boolean CL_LoadServerFiles(void)
 {
 	INT32 i;
 
-//	if (M_CheckParm("-nofiles"))
-//		return;
-
-	for (i = 1; i < fileneedednum; i++)
+	for (i = 0; i < fileneedednum; i++)
 	{
 		if (fileneeded[i].status == FS_OPEN)
 			continue; // Already loaded
@@ -483,6 +544,7 @@ void CL_LoadServerFiles(void)
 				P_AddWadFile(fileneeded[i].filename);
 			G_SetGameModified(true);
 			fileneeded[i].status = FS_OPEN;
+			return false;
 		}
 		else if (fileneeded[i].status == FS_MD5SUMBAD)
 			I_Error("Wrong version of file %s", fileneeded[i].filename);
@@ -508,6 +570,7 @@ void CL_LoadServerFiles(void)
 				fileneeded[i].status, s);
 		}
 	}
+	return true;
 }
 
 void AddLuaFileTransfer(const char *filename, const char *mode)
@@ -689,7 +752,11 @@ void CL_PrepareDownloadLuaFile(void)
 	netbuffer->packettype = PT_ASKLUAFILE;
 	HSendPacket(servernode, true, 0, 0);
 
+	FreeFileNeeded();
+	AllocFileNeeded(1);
+
 	fileneedednum = 1;
+	fileneeded[0].type = FILENEEDED_LUAFILE;
 	fileneeded[0].status = FS_REQUESTED;
 	fileneeded[0].justdownloaded = false;
 	fileneeded[0].totalsize = UINT32_MAX;
@@ -716,15 +783,11 @@ static INT32 filestosend = 0;
   * \sa AddLuaFileToSendQueue
   *
   */
-static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid)
+static boolean AddFileToSendQueue(INT32 node, UINT8 fileid)
 {
 	filetx_t **q; // A pointer to the "next" field of the last file in the list
 	filetx_t *p; // The new file request
-	INT32 i;
-	char wadfilename[MAX_WADPATH];
-
-	if (cv_noticedownload.value)
-		CONS_Printf("Sending file \"%s\" to node %d (%s)\n", filename, node, I_GetNodeAddress(node));
+	UINT16 wadnum;
 
 	// Find the last file in the list and set a pointer to its "next" field
 	q = &transfer[node].txlist;
@@ -744,51 +807,43 @@ static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid
 	if (!p->id.filename)
 		I_Error("AddFileToSendQueue: No more memory\n");
 
-	// Set the file name and get rid of the path
-	strlcpy(p->id.filename, filename, MAX_WADPATH);
-	nameonly(p->id.filename);
-
-	// Look for the requested file through all loaded files
-	for (i = 0; wadfiles[i]; i++)
-	{
-		strlcpy(wadfilename, wadfiles[i]->filename, MAX_WADPATH);
-		nameonly(wadfilename);
-		if (!stricmp(wadfilename, p->id.filename))
-		{
-			// Copy file name with full path
-			strlcpy(p->id.filename, wadfiles[i]->filename, MAX_WADPATH);
-			break;
-		}
-	}
+	// Find the wad the ID refers to
+	wadnum = GetWadNumFromFileNeededId(fileid);
 
 	// Handle non-loaded file requests
-	if (!wadfiles[i])
+	if (wadnum == UINT16_MAX)
 	{
-		DEBFILE(va("%s not found in wadfiles\n", filename));
+		DEBFILE(va("fileneeded %d not found in wadfiles\n", fileid));
 		// This formerly checked if (!findfile(p->id.filename, NULL, true))
 
 		// Not found
 		// Don't inform client
-		DEBFILE(va("Client %d request %s: not found\n", node, filename));
+		DEBFILE(va("Client %d request fileneeded %d: not found\n", node, fileid));
 		free(p->id.filename);
 		free(p);
 		*q = NULL;
 		return false; // cancel the rest of the requests
 	}
 
+	// Set the file name and get rid of the path
+	strlcpy(p->id.filename, wadfiles[wadnum]->filename, MAX_WADPATH);
+
 	// Handle huge file requests (i.e. bigger than cv_maxsend.value KB)
-	if (wadfiles[i]->filesize > (UINT32)cv_maxsend.value * 1024)
+	if (wadfiles[wadnum]->filesize > (UINT32)cv_maxsend.value * 1024)
 	{
 		// Too big
 		// Don't inform client (client sucks, man)
-		DEBFILE(va("Client %d request %s: file too big, not sending\n", node, filename));
+		DEBFILE(va("Client %d request %s: file too big, not sending\n", node, p->id.filename));
 		free(p->id.filename);
 		free(p);
 		*q = NULL;
 		return false; // cancel the rest of the requests
 	}
 
-	DEBFILE(va("Sending file %s (id=%d) to %d\n", filename, fileid, node));
+	if (cv_noticedownload.value)
+		CONS_Printf("Sending file \"%s\" to node %d (%s)\n", p->id.filename, node, I_GetNodeAddress(node));
+
+	DEBFILE(va("Sending file %s (id=%d) to %d\n", p->id.filename, fileid, node));
 	p->ram = SF_FILE; // It's a file, we need to close it and free its name once we're done sending it
 	p->fileid = fileid;
 	p->next = NULL; // End of list
@@ -925,7 +980,6 @@ static void SV_EndFileSend(INT32 node)
 	filestosend--;
 }
 
-#define PACKETPERTIC net_bandwidth/(TICRATE*software_MAXPACKETLENGTH)
 #define FILEFRAGMENTSIZE (software_MAXPACKETLENGTH - (FILETXHEADER + BASEPACKETSIZE))
 
 /** Handles file transmission
@@ -958,14 +1012,7 @@ void FileSendTicker(void)
 	if (!filestosend) // No file to send
 		return;
 
-	if (cv_downloadspeed.value) // New behavior
-		packetsent = cv_downloadspeed.value;
-	else // Old behavior
-	{
-		packetsent = PACKETPERTIC;
-		if (!packetsent)
-			packetsent = 1;
-	}
+	packetsent = cv_downloadspeed.value;
 
 	netbuffer->packettype = PT_FILEFRAGMENT;
 
@@ -1242,6 +1289,9 @@ void PT_FileFragment(void)
 	UINT16 boundedfragmentsize = doomcom->datalength - BASEPACKETSIZE - sizeof(netbuffer->u.filetxpak);
 	char *filename;
 
+	if (!file)
+		return;
+
 	filename = va("%s", file->filename);
 	nameonly(filename);
 
@@ -1353,6 +1403,7 @@ void PT_FileFragment(void)
 					// Tell the server we have received the file
 					netbuffer->packettype = PT_HASLUAFILE;
 					HSendPacket(servernode, true, 0, 0);
+					FreeFileNeeded();
 				}
 			}
 		}
@@ -1423,32 +1474,37 @@ void CloseNetFile(void)
 		SV_AbortSendFiles(i);
 
 	// Receiving a file?
-	for (i = 0; i < MAX_WADFILES; i++)
-		if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
-		{
-			fclose(fileneeded[i].file);
-			free(fileneeded[i].ackpacket);
-
-			if (!pauseddownload && i != 0) // 0 is either srb2.srb or the gamestate...
-			{
-				// Don't remove the file, save it for later in case we resume the download
-				pauseddownload = malloc(sizeof(*pauseddownload));
-				if (!pauseddownload)
-					I_Error("CloseNetFile: No more memory\n");
-
-				strcpy(pauseddownload->filename, fileneeded[i].filename);
-				memcpy(pauseddownload->md5sum, fileneeded[i].md5sum, 16);
-				pauseddownload->currentsize = fileneeded[i].currentsize;
-				pauseddownload->receivedfragments = fileneeded[i].receivedfragments;
-				pauseddownload->fragmentsize = fileneeded[i].fragmentsize;
-			}
-			else
+	if (fileneeded)
+	{
+		for (i = 0; i < fileneedednum; i++)
+			if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
 			{
-				free(fileneeded[i].receivedfragments);
-				// File is not complete delete it
-				remove(fileneeded[i].filename);
+				fclose(fileneeded[i].file);
+				free(fileneeded[i].ackpacket);
+
+				if (!pauseddownload && (fileneeded[i].type == FILENEEDED_WAD || i != 0)) // 0 is the gamestate...
+				{
+					// Don't remove the file, save it for later in case we resume the download
+					pauseddownload = malloc(sizeof(*pauseddownload));
+					if (!pauseddownload)
+						I_Error("CloseNetFile: No more memory\n");
+
+					strcpy(pauseddownload->filename, fileneeded[i].filename);
+					memcpy(pauseddownload->md5sum, fileneeded[i].md5sum, 16);
+					pauseddownload->currentsize = fileneeded[i].currentsize;
+					pauseddownload->receivedfragments = fileneeded[i].receivedfragments;
+					pauseddownload->fragmentsize = fileneeded[i].fragmentsize;
+				}
+				else
+				{
+					// File is not complete, delete it.
+					free(fileneeded[i].receivedfragments);
+					remove(fileneeded[i].filename);
+				}
 			}
-		}
+	}
+
+	FreeFileNeeded();
 }
 
 void Command_Downloads_f(void)
diff --git a/src/d_netfil.h b/src/d_netfil.h
index 70b721bf75191ca5b127157d1a6f79697da672e0..3d713c150fad6f520a618d8e158f96599f081323 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -27,6 +27,7 @@ typedef enum
 
 typedef enum
 {
+	FS_NOTCHECKED,
 	FS_NOTFOUND,
 	FS_FOUND,
 	FS_REQUESTED,
@@ -35,13 +36,21 @@ typedef enum
 	FS_MD5SUMBAD
 } filestatus_t;
 
+typedef enum
+{
+	FILENEEDED_WAD,
+	FILENEEDED_SAVEGAME,
+	FILENEEDED_LUAFILE
+} fileneededtype_t;
+
 typedef struct
 {
-	UINT8 willsend; // Is the server willing to send it?
-	UINT8 folder; // File is a folder
 	char filename[MAX_WADPATH];
 	UINT8 md5sum[16];
 	filestatus_t status; // The value returned by recsearch
+	UINT8 willsend; // Is the server willing to send it?
+	UINT8 folder; // File is a folder
+	fileneededtype_t type;
 	boolean justdownloaded; // To prevent late fragments from causing an I_Error
 
 	// Used only for download
@@ -58,19 +67,25 @@ typedef struct
 #define FILENEEDEDSIZE 23
 
 extern INT32 fileneedednum;
-extern fileneeded_t fileneeded[MAX_WADFILES];
+extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
 
 #ifndef NONET
 extern INT32 lastfilenum;
+extern INT32 downloadcompletednum;
+extern UINT32 downloadcompletedsize;
+extern INT32 totalfilesrequestednum;
+extern UINT32 totalfilesrequestedsize;
 #endif
 
-UINT8 *PutFileNeeded(void);
-void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr);
+void AllocFileNeeded(INT32 size);
+void FreeFileNeeded(void);
+UINT8 *PutFileNeeded(UINT16 firstfile);
+void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile);
 void CL_PrepareDownloadSaveGame(const char *tmpsave);
 
 INT32 CL_CheckFiles(void);
-void CL_LoadServerFiles(void);
+boolean CL_LoadServerFiles(void);
 void AddRamToSendQueue(INT32 node, void *data, size_t size, freemethod_t freemethod,
 	UINT8 fileid);
 
diff --git a/src/deh_tables.c b/src/deh_tables.c
index f2fd380592f6c648dc28a8ff34be7fce7b14dd5f..9a0d919911312fc57759f0621f2dccd46a4c9391 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -5236,12 +5236,10 @@ struct int_const_s const INT_CONST[] = {
 	{"FF_BOUNCY",FF_BOUNCY},                   ///< Bounces players
 	{"FF_SPLAT",FF_SPLAT},                     ///< Use splat flat renderer (treat cyan pixels as invisible)
 
-	// FOF special flags
-	{"FS_PUSHABLES",FS_PUSHABLES},
-	{"FS_EXECUTOR",FS_EXECUTOR},
-	{"FS_ONLYBOTTOM",FS_ONLYBOTTOM},
-	{"FS_BUSTMASK",FS_BUSTMASK},
-	{"FS_DAMPEN",FS_DAMPEN},
+	// FOF bustable flags
+	{"FB_PUSHABLES",FB_PUSHABLES},
+	{"FB_EXECUTOR",FB_EXECUTOR},
+	{"FB_ONLYBOTTOM",FB_ONLYBOTTOM},
 
 	// Bustable FOF type
 	{"BT_TOUCH",BT_TOUCH},
diff --git a/src/doomdef.h b/src/doomdef.h
index 37edca896337a3b349c54b28345978d1bf0f74c0..7e7e355990d422a8a6ecc86f54f49a17902ea66b 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -152,6 +152,9 @@ extern char logfilename[1024];
 // Comment or uncomment this as necessary.
 #define USE_PATCH_DTA
 
+// Enforce a limit of loaded WAD files.
+//#define ENFORCE_WAD_LIMIT
+
 // Use .kart extension addons
 //#define USE_KART
 
diff --git a/src/f_finale.c b/src/f_finale.c
index aecf79fc22d1674479ad77d322cb664c0a60a7ff..8dd03d44f5d068f2b5762dbaea7e51dbd408e5b7 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -1091,11 +1091,11 @@ static const char *credits[] = {
 	"Kepa \"Nev3r\" Iceta",
 	"Thomas \"Shadow Hog\" Igoe",
 	"Iestyn \"Monster Iestyn\" Jealous",
-	"\"Jimita\"",
 	"\"Kaito Sinclaire\"",
 	"\"Kalaron\"", // Coded some of Sryder13's collection of OpenGL fixes, especially fog
 	"Ronald \"Furyhunter\" Kinard", // The SDL2 port
 	"\"Lat'\"", // SRB2-CHAT, the chat window from Kart
+	"\"LZA\"",
 	"Matthew \"Shuffle\" Marsalko",
 	"Steven \"StroggOnMeth\" McGranahan",
 	"\"Morph\"", // For SRB2Morphed stuff
@@ -1167,8 +1167,8 @@ static const char *credits[] = {
 	"Alexander \"DrTapeworm\" Moench-Ford",
 	"Stefan \"Stuf\" Rimalia",
 	"Shane Mychal Sexton",
-	"David \"Big Wave Dave\" Spencer Sr.",
-	"David \"Instant Sonic\" Spencer Jr.",
+	"Dave \"Big Wave Dave\" Spencer",
+	"David \"instantSonic\" Spencer",
 	"\"SSNTails\"",
 	"",
 	"\1Level Design",
@@ -1219,7 +1219,7 @@ static const char *credits[] = {
 	"Bill \"Tets\" Reed",
 	"",
 	"\1Special Thanks",
-	"iD Software",
+	"id Software",
 	"Doom Legacy Project",
 	"FreeDoom Project", // Used some of the mancubus and rocket launcher sprites for Brak
 	"Kart Krew",
diff --git a/src/filesrch.c b/src/filesrch.c
index b4039e5263c1d8b7fcecef4ca45cf7353a7ca08c..ec095518e824d540675750c1b70c56fad9065b96 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -338,9 +338,6 @@ size_t dir_on[menudepth];
 UINT8 refreshdirmenu = 0;
 char *refreshdirname = NULL;
 
-size_t packetsizetally = 0;
-size_t mainwadstally = 0;
-
 #define dirpathlen 1024
 #define maxdirdepth 48
 
@@ -830,7 +827,7 @@ char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) pl
 #endif
 	"\5.pk3", "\5.soc", "\5.lua"}; // addfile
 
-char filenamebuf[MAX_WADFILES][MAX_WADPATH];
+static char (*filenamebuf)[MAX_WADPATH];
 
 static boolean filemenucmp(char *haystack, char *needle)
 {
@@ -1102,6 +1099,10 @@ boolean preparefilemenu(boolean samedepth)
 				if (ext >= EXT_LOADSTART)
 				{
 					size_t i;
+
+					if (filenamebuf == NULL)
+						filenamebuf = calloc(sizeof(char) * MAX_WADPATH, numwadfiles);
+
 					for (i = 0; i < numwadfiles; i++)
 					{
 						if (!filenamebuf[i][0])
@@ -1151,6 +1152,12 @@ boolean preparefilemenu(boolean samedepth)
 		}
 	}
 
+	if (filenamebuf)
+	{
+		free(filenamebuf);
+		filenamebuf = NULL;
+	}
+
 	closedir(dirhandle);
 
 	if ((menudepthleft != menudepth-1) // now for UP... entry
diff --git a/src/filesrch.h b/src/filesrch.h
index 9d5f31bbb8b980606e4ec37ae8a7685189dd756d..59ef5269b194f0918a14927a6fc1eaf003e1a40b 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -53,9 +53,6 @@ extern size_t dir_on[menudepth];
 extern UINT8 refreshdirmenu;
 extern char *refreshdirname;
 
-extern size_t packetsizetally;
-extern size_t mainwadstally;
-
 typedef enum
 {
 	EXT_FOLDER = 0,
diff --git a/src/g_demo.c b/src/g_demo.c
index 211239bfe003f94861e14a16908ee47e2584238f..c97dbcf9ee0559e23e6635e4a62f2de1cb843af2 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -2433,12 +2433,13 @@ ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill)
 	{
 		WRITEUINT8(demo_p, (kill) ? METALDEATH : DEMOMARKER); // add the demo end (or metal death) marker
 		WriteDemoChecksum();
-		saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuffer, demo_p - demobuffer); // finally output the file.
+		sprintf(demoname, "%sMS.LMP", G_BuildMapName(gamemap));
+		saved = FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer); // finally output the file.
 	}
 	free(demobuffer);
 	metalrecording = false;
 	if (saved)
-		I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
+		I_Error("Saved to %s", demoname);
 	I_Error("Failed to save demo!");
 }
 
diff --git a/src/g_game.c b/src/g_game.c
index de1a774f409f2449a75d5616cffdac047cd49045..ce2aa41f52da22da2e0d3d856426c9d7ce6fdf1c 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1549,8 +1549,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	// Note: Majority of botstuffs are handled in G_Ticker now.
 	if (player->bot == BOT_2PHUMAN) //Player-controlled bot
 	{
-		G_CopyTiccmd(cmd,  I_BaseTiccmd2(), 1); // empty, or external driver
-		// Fix offset angle for P2-controlled Tailsbot when P2's controls are set to non-Legacy
+		// Fix offset angle for P2-controlled Tailsbot when P2's controls are set to non-Strafe
 		cmd->angleturn = (INT16)((localangle - *myangle) >> 16);
 	}	
 	
diff --git a/src/g_input.c b/src/g_input.c
index cd4536bbad289e9c239f6fbb736cc8c4cdd645ee..6383c3f0068a3c47007f6fc50422f46b9e4bbb2c 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -233,323 +233,323 @@ typedef struct
 
 static keyname_t keynames[] =
 {
-	{KEY_SPACE, "SPACE"},
-	{KEY_CAPSLOCK, "CAPS LOCK"},
-	{KEY_ENTER, "ENTER"},
-	{KEY_TAB, "TAB"},
-	{KEY_ESCAPE, "ESCAPE"},
-	{KEY_BACKSPACE, "BACKSPACE"},
+	{KEY_SPACE, "space"},
+	{KEY_CAPSLOCK, "caps lock"},
+	{KEY_ENTER, "enter"},
+	{KEY_TAB, "tab"},
+	{KEY_ESCAPE, "escape"},
+	{KEY_BACKSPACE, "backspace"},
 
-	{KEY_NUMLOCK, "NUMLOCK"},
-	{KEY_SCROLLLOCK, "SCROLLLOCK"},
+	{KEY_NUMLOCK, "numlock"},
+	{KEY_SCROLLLOCK, "scrolllock"},
 
 	// bill gates keys
-	{KEY_LEFTWIN, "LEFTWIN"},
-	{KEY_RIGHTWIN, "RIGHTWIN"},
-	{KEY_MENU, "MENU"},
-
-	{KEY_LSHIFT, "LSHIFT"},
-	{KEY_RSHIFT, "RSHIFT"},
-	{KEY_LSHIFT, "SHIFT"},
-	{KEY_LCTRL, "LCTRL"},
-	{KEY_RCTRL, "RCTRL"},
-	{KEY_LCTRL, "CTRL"},
-	{KEY_LALT, "LALT"},
-	{KEY_RALT, "RALT"},
-	{KEY_LALT, "ALT"},
+	{KEY_LEFTWIN, "leftwin"},
+	{KEY_RIGHTWIN, "rightwin"},
+	{KEY_MENU, "menu"},
+
+	{KEY_LSHIFT, "lshift"},
+	{KEY_RSHIFT, "rshift"},
+	{KEY_LSHIFT, "shift"},
+	{KEY_LCTRL, "lctrl"},
+	{KEY_RCTRL, "rctrl"},
+	{KEY_LCTRL, "ctrl"},
+	{KEY_LALT, "lalt"},
+	{KEY_RALT, "ralt"},
+	{KEY_LALT, "alt"},
 
 	// keypad keys
-	{KEY_KPADSLASH, "KEYPAD /"},
-	{KEY_KEYPAD7, "KEYPAD 7"},
-	{KEY_KEYPAD8, "KEYPAD 8"},
-	{KEY_KEYPAD9, "KEYPAD 9"},
-	{KEY_MINUSPAD, "KEYPAD -"},
-	{KEY_KEYPAD4, "KEYPAD 4"},
-	{KEY_KEYPAD5, "KEYPAD 5"},
-	{KEY_KEYPAD6, "KEYPAD 6"},
-	{KEY_PLUSPAD, "KEYPAD +"},
-	{KEY_KEYPAD1, "KEYPAD 1"},
-	{KEY_KEYPAD2, "KEYPAD 2"},
-	{KEY_KEYPAD3, "KEYPAD 3"},
-	{KEY_KEYPAD0, "KEYPAD 0"},
-	{KEY_KPADDEL, "KEYPAD ."},
+	{KEY_KPADSLASH, "keypad /"},
+	{KEY_KEYPAD7, "keypad 7"},
+	{KEY_KEYPAD8, "keypad 8"},
+	{KEY_KEYPAD9, "keypad 9"},
+	{KEY_MINUSPAD, "keypad -"},
+	{KEY_KEYPAD4, "keypad 4"},
+	{KEY_KEYPAD5, "keypad 5"},
+	{KEY_KEYPAD6, "keypad 6"},
+	{KEY_PLUSPAD, "keypad +"},
+	{KEY_KEYPAD1, "keypad 1"},
+	{KEY_KEYPAD2, "keypad 2"},
+	{KEY_KEYPAD3, "keypad 3"},
+	{KEY_KEYPAD0, "keypad 0"},
+	{KEY_KPADDEL, "keypad ."},
 
 	// extended keys (not keypad)
-	{KEY_HOME, "HOME"},
-	{KEY_UPARROW, "UP ARROW"},
-	{KEY_PGUP, "PGUP"},
-	{KEY_LEFTARROW, "LEFT ARROW"},
-	{KEY_RIGHTARROW, "RIGHT ARROW"},
-	{KEY_END, "END"},
-	{KEY_DOWNARROW, "DOWN ARROW"},
-	{KEY_PGDN, "PGDN"},
-	{KEY_INS, "INS"},
-	{KEY_DEL, "DEL"},
+	{KEY_HOME, "home"},
+	{KEY_UPARROW, "up arrow"},
+	{KEY_PGUP, "pgup"},
+	{KEY_LEFTARROW, "left arrow"},
+	{KEY_RIGHTARROW, "right arrow"},
+	{KEY_END, "end"},
+	{KEY_DOWNARROW, "down arrow"},
+	{KEY_PGDN, "pgdn"},
+	{KEY_INS, "ins"},
+	{KEY_DEL, "del"},
 
 	// other keys
-	{KEY_F1, "F1"},
-	{KEY_F2, "F2"},
-	{KEY_F3, "F3"},
-	{KEY_F4, "F4"},
-	{KEY_F5, "F5"},
-	{KEY_F6, "F6"},
-	{KEY_F7, "F7"},
-	{KEY_F8, "F8"},
-	{KEY_F9, "F9"},
-	{KEY_F10, "F10"},
-	{KEY_F11, "F11"},
-	{KEY_F12, "F12"},
+	{KEY_F1, "f1"},
+	{KEY_F2, "f2"},
+	{KEY_F3, "f3"},
+	{KEY_F4, "f4"},
+	{KEY_F5, "f5"},
+	{KEY_F6, "f6"},
+	{KEY_F7, "f7"},
+	{KEY_F8, "f8"},
+	{KEY_F9, "f9"},
+	{KEY_F10, "f10"},
+	{KEY_F11, "f11"},
+	{KEY_F12, "f12"},
 
 	// KEY_CONSOLE has an exception in the keyname code
 	{'`', "TILDE"},
-	{KEY_PAUSE, "PAUSE/BREAK"},
+	{KEY_PAUSE, "pause/break"},
 
 	// virtual keys for mouse buttons and joystick buttons
-	{KEY_MOUSE1+0,"MOUSE1"},
-	{KEY_MOUSE1+1,"MOUSE2"},
-	{KEY_MOUSE1+2,"MOUSE3"},
-	{KEY_MOUSE1+3,"MOUSE4"},
-	{KEY_MOUSE1+4,"MOUSE5"},
-	{KEY_MOUSE1+5,"MOUSE6"},
-	{KEY_MOUSE1+6,"MOUSE7"},
-	{KEY_MOUSE1+7,"MOUSE8"},
-	{KEY_2MOUSE1+0,"SEC_MOUSE2"}, // BP: sorry my mouse handler swap button 1 and 2
-	{KEY_2MOUSE1+1,"SEC_MOUSE1"},
-	{KEY_2MOUSE1+2,"SEC_MOUSE3"},
-	{KEY_2MOUSE1+3,"SEC_MOUSE4"},
-	{KEY_2MOUSE1+4,"SEC_MOUSE5"},
-	{KEY_2MOUSE1+5,"SEC_MOUSE6"},
-	{KEY_2MOUSE1+6,"SEC_MOUSE7"},
-	{KEY_2MOUSE1+7,"SEC_MOUSE8"},
-	{KEY_MOUSEWHEELUP, "Wheel 1 UP"},
-	{KEY_MOUSEWHEELDOWN, "Wheel 1 Down"},
-	{KEY_2MOUSEWHEELUP, "Wheel 2 UP"},
-	{KEY_2MOUSEWHEELDOWN, "Wheel 2 Down"},
-
-	{KEY_JOY1+0, "JOY1"},
-	{KEY_JOY1+1, "JOY2"},
-	{KEY_JOY1+2, "JOY3"},
-	{KEY_JOY1+3, "JOY4"},
-	{KEY_JOY1+4, "JOY5"},
-	{KEY_JOY1+5, "JOY6"},
-	{KEY_JOY1+6, "JOY7"},
-	{KEY_JOY1+7, "JOY8"},
-	{KEY_JOY1+8, "JOY9"},
+	{KEY_MOUSE1+0,"mouse1"},
+	{KEY_MOUSE1+1,"mouse2"},
+	{KEY_MOUSE1+2,"mouse3"},
+	{KEY_MOUSE1+3,"mouse4"},
+	{KEY_MOUSE1+4,"mouse5"},
+	{KEY_MOUSE1+5,"mouse6"},
+	{KEY_MOUSE1+6,"mouse7"},
+	{KEY_MOUSE1+7,"mouse8"},
+	{KEY_2MOUSE1+0,"sec_mouse2"}, // BP: sorry my mouse handler swap button 1 and 2
+	{KEY_2MOUSE1+1,"sec_mouse1"},
+	{KEY_2MOUSE1+2,"sec_mouse3"},
+	{KEY_2MOUSE1+3,"sec_mouse4"},
+	{KEY_2MOUSE1+4,"sec_mouse5"},
+	{KEY_2MOUSE1+5,"sec_mouse6"},
+	{KEY_2MOUSE1+6,"sec_mouse7"},
+	{KEY_2MOUSE1+7,"sec_mouse8"},
+	{KEY_MOUSEWHEELUP, "wheel 1 up"},
+	{KEY_MOUSEWHEELDOWN, "wheel 1 down"},
+	{KEY_2MOUSEWHEELUP, "wheel 2 up"},
+	{KEY_2MOUSEWHEELDOWN, "wheel 2 down"},
+
+	{KEY_JOY1+0, "joy1"},
+	{KEY_JOY1+1, "joy2"},
+	{KEY_JOY1+2, "joy3"},
+	{KEY_JOY1+3, "joy4"},
+	{KEY_JOY1+4, "joy5"},
+	{KEY_JOY1+5, "joy6"},
+	{KEY_JOY1+6, "joy7"},
+	{KEY_JOY1+7, "joy8"},
+	{KEY_JOY1+8, "joy9"},
 #if !defined (NOMOREJOYBTN_1S)
 	// we use up to 32 buttons in DirectInput
-	{KEY_JOY1+9, "JOY10"},
-	{KEY_JOY1+10, "JOY11"},
-	{KEY_JOY1+11, "JOY12"},
-	{KEY_JOY1+12, "JOY13"},
-	{KEY_JOY1+13, "JOY14"},
-	{KEY_JOY1+14, "JOY15"},
-	{KEY_JOY1+15, "JOY16"},
-	{KEY_JOY1+16, "JOY17"},
-	{KEY_JOY1+17, "JOY18"},
-	{KEY_JOY1+18, "JOY19"},
-	{KEY_JOY1+19, "JOY20"},
-	{KEY_JOY1+20, "JOY21"},
-	{KEY_JOY1+21, "JOY22"},
-	{KEY_JOY1+22, "JOY23"},
-	{KEY_JOY1+23, "JOY24"},
-	{KEY_JOY1+24, "JOY25"},
-	{KEY_JOY1+25, "JOY26"},
-	{KEY_JOY1+26, "JOY27"},
-	{KEY_JOY1+27, "JOY28"},
-	{KEY_JOY1+28, "JOY29"},
-	{KEY_JOY1+29, "JOY30"},
-	{KEY_JOY1+30, "JOY31"},
-	{KEY_JOY1+31, "JOY32"},
+	{KEY_JOY1+9, "joy10"},
+	{KEY_JOY1+10, "joy11"},
+	{KEY_JOY1+11, "joy12"},
+	{KEY_JOY1+12, "joy13"},
+	{KEY_JOY1+13, "joy14"},
+	{KEY_JOY1+14, "joy15"},
+	{KEY_JOY1+15, "joy16"},
+	{KEY_JOY1+16, "joy17"},
+	{KEY_JOY1+17, "joy18"},
+	{KEY_JOY1+18, "joy19"},
+	{KEY_JOY1+19, "joy20"},
+	{KEY_JOY1+20, "joy21"},
+	{KEY_JOY1+21, "joy22"},
+	{KEY_JOY1+22, "joy23"},
+	{KEY_JOY1+23, "joy24"},
+	{KEY_JOY1+24, "joy25"},
+	{KEY_JOY1+25, "joy26"},
+	{KEY_JOY1+26, "joy27"},
+	{KEY_JOY1+27, "joy28"},
+	{KEY_JOY1+28, "joy29"},
+	{KEY_JOY1+29, "joy30"},
+	{KEY_JOY1+30, "joy31"},
+	{KEY_JOY1+31, "joy32"},
 #endif
 	// the DOS version uses Allegro's joystick support
-	{KEY_HAT1+0, "HATUP"},
-	{KEY_HAT1+1, "HATDOWN"},
-	{KEY_HAT1+2, "HATLEFT"},
-	{KEY_HAT1+3, "HATRIGHT"},
-	{KEY_HAT1+4, "HATUP2"},
-	{KEY_HAT1+5, "HATDOWN2"},
-	{KEY_HAT1+6, "HATLEFT2"},
-	{KEY_HAT1+7, "HATRIGHT2"},
-	{KEY_HAT1+8, "HATUP3"},
-	{KEY_HAT1+9, "HATDOWN3"},
-	{KEY_HAT1+10, "HATLEFT3"},
-	{KEY_HAT1+11, "HATRIGHT3"},
-	{KEY_HAT1+12, "HATUP4"},
-	{KEY_HAT1+13, "HATDOWN4"},
-	{KEY_HAT1+14, "HATLEFT4"},
-	{KEY_HAT1+15, "HATRIGHT4"},
-
-	{KEY_DBLMOUSE1+0, "DBLMOUSE1"},
-	{KEY_DBLMOUSE1+1, "DBLMOUSE2"},
-	{KEY_DBLMOUSE1+2, "DBLMOUSE3"},
-	{KEY_DBLMOUSE1+3, "DBLMOUSE4"},
-	{KEY_DBLMOUSE1+4, "DBLMOUSE5"},
-	{KEY_DBLMOUSE1+5, "DBLMOUSE6"},
-	{KEY_DBLMOUSE1+6, "DBLMOUSE7"},
-	{KEY_DBLMOUSE1+7, "DBLMOUSE8"},
-	{KEY_DBL2MOUSE1+0, "DBLSEC_MOUSE2"}, // BP: sorry my mouse handler swap button 1 and 2
-	{KEY_DBL2MOUSE1+1, "DBLSEC_MOUSE1"},
-	{KEY_DBL2MOUSE1+2, "DBLSEC_MOUSE3"},
-	{KEY_DBL2MOUSE1+3, "DBLSEC_MOUSE4"},
-	{KEY_DBL2MOUSE1+4, "DBLSEC_MOUSE5"},
-	{KEY_DBL2MOUSE1+5, "DBLSEC_MOUSE6"},
-	{KEY_DBL2MOUSE1+6, "DBLSEC_MOUSE7"},
-	{KEY_DBL2MOUSE1+7, "DBLSEC_MOUSE8"},
-
-	{KEY_DBLJOY1+0, "DBLJOY1"},
-	{KEY_DBLJOY1+1, "DBLJOY2"},
-	{KEY_DBLJOY1+2, "DBLJOY3"},
-	{KEY_DBLJOY1+3, "DBLJOY4"},
-	{KEY_DBLJOY1+4, "DBLJOY5"},
-	{KEY_DBLJOY1+5, "DBLJOY6"},
-	{KEY_DBLJOY1+6, "DBLJOY7"},
-	{KEY_DBLJOY1+7, "DBLJOY8"},
+	{KEY_HAT1+0, "hatup"},
+	{KEY_HAT1+1, "hatdown"},
+	{KEY_HAT1+2, "hatleft"},
+	{KEY_HAT1+3, "hatright"},
+	{KEY_HAT1+4, "hatup2"},
+	{KEY_HAT1+5, "hatdown2"},
+	{KEY_HAT1+6, "hatleft2"},
+	{KEY_HAT1+7, "hatright2"},
+	{KEY_HAT1+8, "hatup3"},
+	{KEY_HAT1+9, "hatdown3"},
+	{KEY_HAT1+10, "hatleft3"},
+	{KEY_HAT1+11, "hatright3"},
+	{KEY_HAT1+12, "hatup4"},
+	{KEY_HAT1+13, "hatdown4"},
+	{KEY_HAT1+14, "hatleft4"},
+	{KEY_HAT1+15, "hatright4"},
+
+	{KEY_DBLMOUSE1+0, "dblmouse1"},
+	{KEY_DBLMOUSE1+1, "dblmouse2"},
+	{KEY_DBLMOUSE1+2, "dblmouse3"},
+	{KEY_DBLMOUSE1+3, "dblmouse4"},
+	{KEY_DBLMOUSE1+4, "dblmouse5"},
+	{KEY_DBLMOUSE1+5, "dblmouse6"},
+	{KEY_DBLMOUSE1+6, "dblmouse7"},
+	{KEY_DBLMOUSE1+7, "dblmouse8"},
+	{KEY_DBL2MOUSE1+0, "dblsec_mouse2"}, // BP: sorry my mouse handler swap button 1 and 2
+	{KEY_DBL2MOUSE1+1, "dblsec_mouse1"},
+	{KEY_DBL2MOUSE1+2, "dblsec_mouse3"},
+	{KEY_DBL2MOUSE1+3, "dblsec_mouse4"},
+	{KEY_DBL2MOUSE1+4, "dblsec_mouse5"},
+	{KEY_DBL2MOUSE1+5, "dblsec_mouse6"},
+	{KEY_DBL2MOUSE1+6, "dblsec_mouse7"},
+	{KEY_DBL2MOUSE1+7, "dblsec_mouse8"},
+
+	{KEY_DBLJOY1+0, "dbljoy1"},
+	{KEY_DBLJOY1+1, "dbljoy2"},
+	{KEY_DBLJOY1+2, "dbljoy3"},
+	{KEY_DBLJOY1+3, "dbljoy4"},
+	{KEY_DBLJOY1+4, "dbljoy5"},
+	{KEY_DBLJOY1+5, "dbljoy6"},
+	{KEY_DBLJOY1+6, "dbljoy7"},
+	{KEY_DBLJOY1+7, "dbljoy8"},
 #if !defined (NOMOREJOYBTN_1DBL)
-	{KEY_DBLJOY1+8, "DBLJOY9"},
-	{KEY_DBLJOY1+9, "DBLJOY10"},
-	{KEY_DBLJOY1+10, "DBLJOY11"},
-	{KEY_DBLJOY1+11, "DBLJOY12"},
-	{KEY_DBLJOY1+12, "DBLJOY13"},
-	{KEY_DBLJOY1+13, "DBLJOY14"},
-	{KEY_DBLJOY1+14, "DBLJOY15"},
-	{KEY_DBLJOY1+15, "DBLJOY16"},
-	{KEY_DBLJOY1+16, "DBLJOY17"},
-	{KEY_DBLJOY1+17, "DBLJOY18"},
-	{KEY_DBLJOY1+18, "DBLJOY19"},
-	{KEY_DBLJOY1+19, "DBLJOY20"},
-	{KEY_DBLJOY1+20, "DBLJOY21"},
-	{KEY_DBLJOY1+21, "DBLJOY22"},
-	{KEY_DBLJOY1+22, "DBLJOY23"},
-	{KEY_DBLJOY1+23, "DBLJOY24"},
-	{KEY_DBLJOY1+24, "DBLJOY25"},
-	{KEY_DBLJOY1+25, "DBLJOY26"},
-	{KEY_DBLJOY1+26, "DBLJOY27"},
-	{KEY_DBLJOY1+27, "DBLJOY28"},
-	{KEY_DBLJOY1+28, "DBLJOY29"},
-	{KEY_DBLJOY1+29, "DBLJOY30"},
-	{KEY_DBLJOY1+30, "DBLJOY31"},
-	{KEY_DBLJOY1+31, "DBLJOY32"},
+	{KEY_DBLJOY1+8, "dbljoy9"},
+	{KEY_DBLJOY1+9, "dbljoy10"},
+	{KEY_DBLJOY1+10, "dbljoy11"},
+	{KEY_DBLJOY1+11, "dbljoy12"},
+	{KEY_DBLJOY1+12, "dbljoy13"},
+	{KEY_DBLJOY1+13, "dbljoy14"},
+	{KEY_DBLJOY1+14, "dbljoy15"},
+	{KEY_DBLJOY1+15, "dbljoy16"},
+	{KEY_DBLJOY1+16, "dbljoy17"},
+	{KEY_DBLJOY1+17, "dbljoy18"},
+	{KEY_DBLJOY1+18, "dbljoy19"},
+	{KEY_DBLJOY1+19, "dbljoy20"},
+	{KEY_DBLJOY1+20, "dbljoy21"},
+	{KEY_DBLJOY1+21, "dbljoy22"},
+	{KEY_DBLJOY1+22, "dbljoy23"},
+	{KEY_DBLJOY1+23, "dbljoy24"},
+	{KEY_DBLJOY1+24, "dbljoy25"},
+	{KEY_DBLJOY1+25, "dbljoy26"},
+	{KEY_DBLJOY1+26, "dbljoy27"},
+	{KEY_DBLJOY1+27, "dbljoy28"},
+	{KEY_DBLJOY1+28, "dbljoy29"},
+	{KEY_DBLJOY1+29, "dbljoy30"},
+	{KEY_DBLJOY1+30, "dbljoy31"},
+	{KEY_DBLJOY1+31, "dbljoy32"},
 #endif
-	{KEY_DBLHAT1+0, "DBLHATUP"},
-	{KEY_DBLHAT1+1, "DBLHATDOWN"},
-	{KEY_DBLHAT1+2, "DBLHATLEFT"},
-	{KEY_DBLHAT1+3, "DBLHATRIGHT"},
-	{KEY_DBLHAT1+4, "DBLHATUP2"},
-	{KEY_DBLHAT1+5, "DBLHATDOWN2"},
-	{KEY_DBLHAT1+6, "DBLHATLEFT2"},
-	{KEY_DBLHAT1+7, "DBLHATRIGHT2"},
-	{KEY_DBLHAT1+8, "DBLHATUP3"},
-	{KEY_DBLHAT1+9, "DBLHATDOWN3"},
-	{KEY_DBLHAT1+10, "DBLHATLEFT3"},
-	{KEY_DBLHAT1+11, "DBLHATRIGHT3"},
-	{KEY_DBLHAT1+12, "DBLHATUP4"},
-	{KEY_DBLHAT1+13, "DBLHATDOWN4"},
-	{KEY_DBLHAT1+14, "DBLHATLEFT4"},
-	{KEY_DBLHAT1+15, "DBLHATRIGHT4"},
-
-	{KEY_2JOY1+0, "SEC_JOY1"},
-	{KEY_2JOY1+1, "SEC_JOY2"},
-	{KEY_2JOY1+2, "SEC_JOY3"},
-	{KEY_2JOY1+3, "SEC_JOY4"},
-	{KEY_2JOY1+4, "SEC_JOY5"},
-	{KEY_2JOY1+5, "SEC_JOY6"},
-	{KEY_2JOY1+6, "SEC_JOY7"},
-	{KEY_2JOY1+7, "SEC_JOY8"},
+	{KEY_DBLHAT1+0, "dblhatup"},
+	{KEY_DBLHAT1+1, "dblhatdown"},
+	{KEY_DBLHAT1+2, "dblhatleft"},
+	{KEY_DBLHAT1+3, "dblhatright"},
+	{KEY_DBLHAT1+4, "dblhatup2"},
+	{KEY_DBLHAT1+5, "dblhatdown2"},
+	{KEY_DBLHAT1+6, "dblhatleft2"},
+	{KEY_DBLHAT1+7, "dblhatright2"},
+	{KEY_DBLHAT1+8, "dblhatup3"},
+	{KEY_DBLHAT1+9, "dblhatdown3"},
+	{KEY_DBLHAT1+10, "dblhatleft3"},
+	{KEY_DBLHAT1+11, "dblhatright3"},
+	{KEY_DBLHAT1+12, "dblhatup4"},
+	{KEY_DBLHAT1+13, "dblhatdown4"},
+	{KEY_DBLHAT1+14, "dblhatleft4"},
+	{KEY_DBLHAT1+15, "dblhatright4"},
+
+	{KEY_2JOY1+0, "sec_joy1"},
+	{KEY_2JOY1+1, "sec_joy2"},
+	{KEY_2JOY1+2, "sec_joy3"},
+	{KEY_2JOY1+3, "sec_joy4"},
+	{KEY_2JOY1+4, "sec_joy5"},
+	{KEY_2JOY1+5, "sec_joy6"},
+	{KEY_2JOY1+6, "sec_joy7"},
+	{KEY_2JOY1+7, "sec_joy8"},
 #if !defined (NOMOREJOYBTN_2S)
 	// we use up to 32 buttons in DirectInput
-	{KEY_2JOY1+8, "SEC_JOY9"},
-	{KEY_2JOY1+9, "SEC_JOY10"},
-	{KEY_2JOY1+10, "SEC_JOY11"},
-	{KEY_2JOY1+11, "SEC_JOY12"},
-	{KEY_2JOY1+12, "SEC_JOY13"},
-	{KEY_2JOY1+13, "SEC_JOY14"},
-	{KEY_2JOY1+14, "SEC_JOY15"},
-	{KEY_2JOY1+15, "SEC_JOY16"},
-	{KEY_2JOY1+16, "SEC_JOY17"},
-	{KEY_2JOY1+17, "SEC_JOY18"},
-	{KEY_2JOY1+18, "SEC_JOY19"},
-	{KEY_2JOY1+19, "SEC_JOY20"},
-	{KEY_2JOY1+20, "SEC_JOY21"},
-	{KEY_2JOY1+21, "SEC_JOY22"},
-	{KEY_2JOY1+22, "SEC_JOY23"},
-	{KEY_2JOY1+23, "SEC_JOY24"},
-	{KEY_2JOY1+24, "SEC_JOY25"},
-	{KEY_2JOY1+25, "SEC_JOY26"},
-	{KEY_2JOY1+26, "SEC_JOY27"},
-	{KEY_2JOY1+27, "SEC_JOY28"},
-	{KEY_2JOY1+28, "SEC_JOY29"},
-	{KEY_2JOY1+29, "SEC_JOY30"},
-	{KEY_2JOY1+30, "SEC_JOY31"},
-	{KEY_2JOY1+31, "SEC_JOY32"},
+	{KEY_2JOY1+8, "sec_joy9"},
+	{KEY_2JOY1+9, "sec_joy10"},
+	{KEY_2JOY1+10, "sec_joy11"},
+	{KEY_2JOY1+11, "sec_joy12"},
+	{KEY_2JOY1+12, "sec_joy13"},
+	{KEY_2JOY1+13, "sec_joy14"},
+	{KEY_2JOY1+14, "sec_joy15"},
+	{KEY_2JOY1+15, "sec_joy16"},
+	{KEY_2JOY1+16, "sec_joy17"},
+	{KEY_2JOY1+17, "sec_joy18"},
+	{KEY_2JOY1+18, "sec_joy19"},
+	{KEY_2JOY1+19, "sec_joy20"},
+	{KEY_2JOY1+20, "sec_joy21"},
+	{KEY_2JOY1+21, "sec_joy22"},
+	{KEY_2JOY1+22, "sec_joy23"},
+	{KEY_2JOY1+23, "sec_joy24"},
+	{KEY_2JOY1+24, "sec_joy25"},
+	{KEY_2JOY1+25, "sec_joy26"},
+	{KEY_2JOY1+26, "sec_joy27"},
+	{KEY_2JOY1+27, "sec_joy28"},
+	{KEY_2JOY1+28, "sec_joy29"},
+	{KEY_2JOY1+29, "sec_joy30"},
+	{KEY_2JOY1+30, "sec_joy31"},
+	{KEY_2JOY1+31, "sec_joy32"},
 #endif
 	// the DOS version uses Allegro's joystick support
-	{KEY_2HAT1+0,  "SEC_HATUP"},
-	{KEY_2HAT1+1,  "SEC_HATDOWN"},
-	{KEY_2HAT1+2,  "SEC_HATLEFT"},
-	{KEY_2HAT1+3,  "SEC_HATRIGHT"},
-	{KEY_2HAT1+4, "SEC_HATUP2"},
-	{KEY_2HAT1+5, "SEC_HATDOWN2"},
-	{KEY_2HAT1+6, "SEC_HATLEFT2"},
-	{KEY_2HAT1+7, "SEC_HATRIGHT2"},
-	{KEY_2HAT1+8, "SEC_HATUP3"},
-	{KEY_2HAT1+9, "SEC_HATDOWN3"},
-	{KEY_2HAT1+10, "SEC_HATLEFT3"},
-	{KEY_2HAT1+11, "SEC_HATRIGHT3"},
-	{KEY_2HAT1+12, "SEC_HATUP4"},
-	{KEY_2HAT1+13, "SEC_HATDOWN4"},
-	{KEY_2HAT1+14, "SEC_HATLEFT4"},
-	{KEY_2HAT1+15, "SEC_HATRIGHT4"},
-
-	{KEY_DBL2JOY1+0, "DBLSEC_JOY1"},
-	{KEY_DBL2JOY1+1, "DBLSEC_JOY2"},
-	{KEY_DBL2JOY1+2, "DBLSEC_JOY3"},
-	{KEY_DBL2JOY1+3, "DBLSEC_JOY4"},
-	{KEY_DBL2JOY1+4, "DBLSEC_JOY5"},
-	{KEY_DBL2JOY1+5, "DBLSEC_JOY6"},
-	{KEY_DBL2JOY1+6, "DBLSEC_JOY7"},
-	{KEY_DBL2JOY1+7, "DBLSEC_JOY8"},
+	{KEY_2HAT1+0,  "sec_hatup"},
+	{KEY_2HAT1+1,  "sec_hatdown"},
+	{KEY_2HAT1+2,  "sec_hatleft"},
+	{KEY_2HAT1+3,  "sec_hatright"},
+	{KEY_2HAT1+4, "sec_hatup2"},
+	{KEY_2HAT1+5, "sec_hatdown2"},
+	{KEY_2HAT1+6, "sec_hatleft2"},
+	{KEY_2HAT1+7, "sec_hatright2"},
+	{KEY_2HAT1+8, "sec_hatup3"},
+	{KEY_2HAT1+9, "sec_hatdown3"},
+	{KEY_2HAT1+10, "sec_hatleft3"},
+	{KEY_2HAT1+11, "sec_hatright3"},
+	{KEY_2HAT1+12, "sec_hatup4"},
+	{KEY_2HAT1+13, "sec_hatdown4"},
+	{KEY_2HAT1+14, "sec_hatleft4"},
+	{KEY_2HAT1+15, "sec_hatright4"},
+
+	{KEY_DBL2JOY1+0, "dblsec_joy1"},
+	{KEY_DBL2JOY1+1, "dblsec_joy2"},
+	{KEY_DBL2JOY1+2, "dblsec_joy3"},
+	{KEY_DBL2JOY1+3, "dblsec_joy4"},
+	{KEY_DBL2JOY1+4, "dblsec_joy5"},
+	{KEY_DBL2JOY1+5, "dblsec_joy6"},
+	{KEY_DBL2JOY1+6, "dblsec_joy7"},
+	{KEY_DBL2JOY1+7, "dblsec_joy8"},
 #if !defined (NOMOREJOYBTN_2DBL)
-	{KEY_DBL2JOY1+8, "DBLSEC_JOY9"},
-	{KEY_DBL2JOY1+9, "DBLSEC_JOY10"},
-	{KEY_DBL2JOY1+10, "DBLSEC_JOY11"},
-	{KEY_DBL2JOY1+11, "DBLSEC_JOY12"},
-	{KEY_DBL2JOY1+12, "DBLSEC_JOY13"},
-	{KEY_DBL2JOY1+13, "DBLSEC_JOY14"},
-	{KEY_DBL2JOY1+14, "DBLSEC_JOY15"},
-	{KEY_DBL2JOY1+15, "DBLSEC_JOY16"},
-	{KEY_DBL2JOY1+16, "DBLSEC_JOY17"},
-	{KEY_DBL2JOY1+17, "DBLSEC_JOY18"},
-	{KEY_DBL2JOY1+18, "DBLSEC_JOY19"},
-	{KEY_DBL2JOY1+19, "DBLSEC_JOY20"},
-	{KEY_DBL2JOY1+20, "DBLSEC_JOY21"},
-	{KEY_DBL2JOY1+21, "DBLSEC_JOY22"},
-	{KEY_DBL2JOY1+22, "DBLSEC_JOY23"},
-	{KEY_DBL2JOY1+23, "DBLSEC_JOY24"},
-	{KEY_DBL2JOY1+24, "DBLSEC_JOY25"},
-	{KEY_DBL2JOY1+25, "DBLSEC_JOY26"},
-	{KEY_DBL2JOY1+26, "DBLSEC_JOY27"},
-	{KEY_DBL2JOY1+27, "DBLSEC_JOY28"},
-	{KEY_DBL2JOY1+28, "DBLSEC_JOY29"},
-	{KEY_DBL2JOY1+29, "DBLSEC_JOY30"},
-	{KEY_DBL2JOY1+30, "DBLSEC_JOY31"},
-	{KEY_DBL2JOY1+31, "DBLSEC_JOY32"},
+	{KEY_DBL2JOY1+8, "dblsec_joy9"},
+	{KEY_DBL2JOY1+9, "dblsec_joy10"},
+	{KEY_DBL2JOY1+10, "dblsec_joy11"},
+	{KEY_DBL2JOY1+11, "dblsec_joy12"},
+	{KEY_DBL2JOY1+12, "dblsec_joy13"},
+	{KEY_DBL2JOY1+13, "dblsec_joy14"},
+	{KEY_DBL2JOY1+14, "dblsec_joy15"},
+	{KEY_DBL2JOY1+15, "dblsec_joy16"},
+	{KEY_DBL2JOY1+16, "dblsec_joy17"},
+	{KEY_DBL2JOY1+17, "dblsec_joy18"},
+	{KEY_DBL2JOY1+18, "dblsec_joy19"},
+	{KEY_DBL2JOY1+19, "dblsec_joy20"},
+	{KEY_DBL2JOY1+20, "dblsec_joy21"},
+	{KEY_DBL2JOY1+21, "dblsec_joy22"},
+	{KEY_DBL2JOY1+22, "dblsec_joy23"},
+	{KEY_DBL2JOY1+23, "dblsec_joy24"},
+	{KEY_DBL2JOY1+24, "dblsec_joy25"},
+	{KEY_DBL2JOY1+25, "dblsec_joy26"},
+	{KEY_DBL2JOY1+26, "dblsec_joy27"},
+	{KEY_DBL2JOY1+27, "dblsec_joy28"},
+	{KEY_DBL2JOY1+28, "dblsec_joy29"},
+	{KEY_DBL2JOY1+29, "dblsec_joy30"},
+	{KEY_DBL2JOY1+30, "dblsec_joy31"},
+	{KEY_DBL2JOY1+31, "dblsec_joy32"},
 #endif
-	{KEY_DBL2HAT1+0, "DBLSEC_HATUP"},
-	{KEY_DBL2HAT1+1, "DBLSEC_HATDOWN"},
-	{KEY_DBL2HAT1+2, "DBLSEC_HATLEFT"},
-	{KEY_DBL2HAT1+3, "DBLSEC_HATRIGHT"},
-	{KEY_DBL2HAT1+4, "DBLSEC_HATUP2"},
-	{KEY_DBL2HAT1+5, "DBLSEC_HATDOWN2"},
-	{KEY_DBL2HAT1+6, "DBLSEC_HATLEFT2"},
-	{KEY_DBL2HAT1+7, "DBLSEC_HATRIGHT2"},
-	{KEY_DBL2HAT1+8, "DBLSEC_HATUP3"},
-	{KEY_DBL2HAT1+9, "DBLSEC_HATDOWN3"},
-	{KEY_DBL2HAT1+10, "DBLSEC_HATLEFT3"},
-	{KEY_DBL2HAT1+11, "DBLSEC_HATRIGHT3"},
-	{KEY_DBL2HAT1+12, "DBLSEC_HATUP4"},
-	{KEY_DBL2HAT1+13, "DBLSEC_HATDOWN4"},
-	{KEY_DBL2HAT1+14, "DBLSEC_HATLEFT4"},
-	{KEY_DBL2HAT1+15, "DBLSEC_HATRIGHT4"},
+	{KEY_DBL2HAT1+0, "dblsec_hatup"},
+	{KEY_DBL2HAT1+1, "dblsec_hatdown"},
+	{KEY_DBL2HAT1+2, "dblsec_hatleft"},
+	{KEY_DBL2HAT1+3, "dblsec_hatright"},
+	{KEY_DBL2HAT1+4, "dblsec_hatup2"},
+	{KEY_DBL2HAT1+5, "dblsec_hatdown2"},
+	{KEY_DBL2HAT1+6, "dblsec_hatleft2"},
+	{KEY_DBL2HAT1+7, "dblsec_hatright2"},
+	{KEY_DBL2HAT1+8, "dblsec_hatup3"},
+	{KEY_DBL2HAT1+9, "dblsec_hatdown3"},
+	{KEY_DBL2HAT1+10, "dblsec_hatleft3"},
+	{KEY_DBL2HAT1+11, "dblsec_hatright3"},
+	{KEY_DBL2HAT1+12, "dblsec_hatup4"},
+	{KEY_DBL2HAT1+13, "dblsec_hatdown4"},
+	{KEY_DBL2HAT1+14, "dblsec_hatleft4"},
+	{KEY_DBL2HAT1+15, "dblsec_hatright4"},
 
 };
 
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index 0ac33d1361d2a4bf92070a9d944c1bfeab9cd980..da0319bccfecbd70901fe05b1658a49c54c24998 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -245,13 +245,16 @@ void HWR_RenderBatches(void)
 	currently_batching = false;// no longer collecting batches
 	if (!polygonArraySize)
 	{
-		ps_hw_numpolys = ps_hw_numcalls = ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 0;
+		ps_hw_numpolys.value.i = ps_hw_numcalls.value.i = ps_hw_numshaders.value.i
+			= ps_hw_numtextures.value.i = ps_hw_numpolyflags.value.i
+			= ps_hw_numcolors.value.i = 0;
 		return;// nothing to draw
 	}
 	// init stats vars
-	ps_hw_numpolys = polygonArraySize;
-	ps_hw_numcalls = ps_hw_numverts = 0;
-	ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 1;
+	ps_hw_numpolys.value.i = polygonArraySize;
+	ps_hw_numcalls.value.i = ps_hw_numverts.value.i = 0;
+	ps_hw_numshaders.value.i = ps_hw_numtextures.value.i
+		= ps_hw_numpolyflags.value.i = ps_hw_numcolors.value.i = 1;
 	// init polygonIndexArray
 	for (i = 0; i < polygonArraySize; i++)
 	{
@@ -259,12 +262,12 @@ void HWR_RenderBatches(void)
 	}
 
 	// sort polygons
-	ps_hw_batchsorttime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_batchsorttime);
 	if (cv_glshaders.value && gl_shadersavailable)
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygons);
 	else
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygonsNoShaders);
-	ps_hw_batchsorttime = I_GetPreciseTime() - ps_hw_batchsorttime;
+	PS_STOP_TIMING(ps_hw_batchsorttime);
 	// sort order
 	// 1. shader
 	// 2. texture
@@ -272,7 +275,7 @@ void HWR_RenderBatches(void)
 	// 4. colors + light level
 	// not sure about what order of the last 2 should be, or if it even matters
 
-	ps_hw_batchdrawtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_batchdrawtime);
 
 	currentShader = polygonArray[polygonIndexArray[0]].shader;
 	currentTexture = polygonArray[polygonIndexArray[0]].texture;
@@ -408,8 +411,8 @@ void HWR_RenderBatches(void)
 			// execute draw call
             HWD.pfnDrawIndexedTriangles(&currentSurfaceInfo, finalVertexArray, finalIndexWritePos, currentPolyFlags, finalVertexIndexArray);
 			// update stats
-			ps_hw_numcalls++;
-			ps_hw_numverts += finalIndexWritePos;
+			ps_hw_numcalls.value.i++;
+			ps_hw_numverts.value.i += finalIndexWritePos;
 			// reset write positions
 			finalVertexWritePos = 0;
 			finalIndexWritePos = 0;
@@ -426,7 +429,7 @@ void HWR_RenderBatches(void)
 			currentShader = nextShader;
 			changeShader = false;
 
-			ps_hw_numshaders++;
+			ps_hw_numshaders.value.i++;
 		}
 		if (changeTexture)
 		{
@@ -435,21 +438,21 @@ void HWR_RenderBatches(void)
 			currentTexture = nextTexture;
 			changeTexture = false;
 
-			ps_hw_numtextures++;
+			ps_hw_numtextures.value.i++;
 		}
 		if (changePolyFlags)
 		{
 			currentPolyFlags = nextPolyFlags;
 			changePolyFlags = false;
 
-			ps_hw_numpolyflags++;
+			ps_hw_numpolyflags.value.i++;
 		}
 		if (changeSurfaceInfo)
 		{
 			currentSurfaceInfo = nextSurfaceInfo;
 			changeSurfaceInfo = false;
 
-			ps_hw_numcolors++;
+			ps_hw_numcolors.value.i++;
 		}
 		// and that should be it?
 	}
@@ -457,7 +460,7 @@ void HWR_RenderBatches(void)
 	polygonArraySize = 0;
 	unsortedVertexArraySize = 0;
 
-	ps_hw_batchdrawtime = I_GetPreciseTime() - ps_hw_batchdrawtime;
+	PS_STOP_TIMING(ps_hw_batchdrawtime);
 }
 
 
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index e0851af8518e5146e8fac17af3f56a102799b7e9..9bade3d6fb19676cd988bc53fbce8ff8cd2a705e 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -147,22 +147,22 @@ static angle_t gl_aimingangle;
 static void HWR_SetTransformAiming(FTransform *trans, player_t *player, boolean skybox);
 
 // Render stats
-precise_t ps_hw_skyboxtime = 0;
-precise_t ps_hw_nodesorttime = 0;
-precise_t ps_hw_nodedrawtime = 0;
-precise_t ps_hw_spritesorttime = 0;
-precise_t ps_hw_spritedrawtime = 0;
+ps_metric_t ps_hw_skyboxtime = {0};
+ps_metric_t ps_hw_nodesorttime = {0};
+ps_metric_t ps_hw_nodedrawtime = {0};
+ps_metric_t ps_hw_spritesorttime = {0};
+ps_metric_t ps_hw_spritedrawtime = {0};
 
 // Render stats for batching
-int ps_hw_numpolys = 0;
-int ps_hw_numverts = 0;
-int ps_hw_numcalls = 0;
-int ps_hw_numshaders = 0;
-int ps_hw_numtextures = 0;
-int ps_hw_numpolyflags = 0;
-int ps_hw_numcolors = 0;
-precise_t ps_hw_batchsorttime = 0;
-precise_t ps_hw_batchdrawtime = 0;
+ps_metric_t ps_hw_numpolys = {0};
+ps_metric_t ps_hw_numverts = {0};
+ps_metric_t ps_hw_numcalls = {0};
+ps_metric_t ps_hw_numshaders = {0};
+ps_metric_t ps_hw_numtextures = {0};
+ps_metric_t ps_hw_numpolyflags = {0};
+ps_metric_t ps_hw_numcolors = {0};
+ps_metric_t ps_hw_batchsorttime = {0};
+ps_metric_t ps_hw_batchdrawtime = {0};
 
 boolean gl_init = false;
 boolean gl_maploaded = false;
@@ -3235,7 +3235,7 @@ static void HWR_Subsector(size_t num)
 		}
 
 		// for render stats
-		ps_numpolyobjects += numpolys;
+		ps_numpolyobjects.value.i += numpolys;
 
 		// Sort polyobjects
 		R_SortPolyObjects(sub);
@@ -3343,7 +3343,7 @@ static void HWR_RenderBSPNode(INT32 bspnum)
 	// Decide which side the view point is on
 	INT32 side;
 
-	ps_numbspcalls++;
+	ps_numbspcalls.value.i++;
 
 	// Found a subsector?
 	if (bspnum & NF_SUBSECTOR)
@@ -4718,7 +4718,7 @@ static void HWR_CreateDrawNodes(void)
 	// that is already lying around. This should all be in some sort of linked list or lists.
 	sortindex = Z_Calloc(sizeof(size_t) * (numplanes + numpolyplanes + numwalls), PU_STATIC, NULL);
 
-	ps_hw_nodesorttime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_nodesorttime);
 
 	for (i = 0; i < numplanes; i++, p++)
 	{
@@ -4738,7 +4738,7 @@ static void HWR_CreateDrawNodes(void)
 		sortindex[p] = p;
 	}
 
-	ps_numdrawnodes = p;
+	ps_numdrawnodes.value.i = p;
 
 	// p is the number of stuff to sort
 
@@ -4773,9 +4773,9 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodesorttime = I_GetPreciseTime() - ps_hw_nodesorttime;
+	PS_STOP_TIMING(ps_hw_nodesorttime);
 
-	ps_hw_nodedrawtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_nodedrawtime);
 
 	// Okay! Let's draw it all! Woo!
 	HWD.pfnSetTransform(&atransform);
@@ -4812,7 +4812,7 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodedrawtime = I_GetPreciseTime() - ps_hw_nodedrawtime;
+	PS_STOP_TIMING(ps_hw_nodedrawtime);
 
 	numwalls = 0;
 	numplanes = 0;
@@ -6095,10 +6095,10 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	if (viewnumber == 0) // Only do it if it's the first screen being rendered
 		HWD.pfnClearBuffer(true, false, &ClearColor); // Clear the Color Buffer, stops HOMs. Also seems to fix the skybox issue on Intel GPUs.
 
-	ps_hw_skyboxtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_skyboxtime);
 	if (skybox && drawsky) // If there's a skybox and we should be drawing the sky, draw the skybox
 		HWR_RenderSkyboxView(viewnumber, player); // This is drawn before everything else so it is placed behind
-	ps_hw_skyboxtime = I_GetPreciseTime() - ps_hw_skyboxtime;
+	PS_STOP_TIMING(ps_hw_skyboxtime);
 
 	{
 		// do we really need to save player (is it not the same)?
@@ -6208,9 +6208,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	// Reset the shader state.
 	HWR_SetShaderState();
 
-	ps_numbspcalls = 0;
-	ps_numpolyobjects = 0;
-	ps_bsptime = I_GetPreciseTime();
+	ps_numbspcalls.value.i = 0;
+	ps_numpolyobjects.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 
 	validcount++;
 
@@ -6248,7 +6248,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	}
 #endif
 
-	ps_bsptime = I_GetPreciseTime() - ps_bsptime;
+	PS_STOP_TIMING(ps_bsptime);
 
 	if (cv_glbatching.value)
 		HWR_RenderBatches();
@@ -6263,22 +6263,22 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 #endif
 
 	// Draw MD2 and sprites
-	ps_numsprites = gl_visspritecount;
-	ps_hw_spritesorttime = I_GetPreciseTime();
+	ps_numsprites.value.i = gl_visspritecount;
+	PS_START_TIMING(ps_hw_spritesorttime);
 	HWR_SortVisSprites();
-	ps_hw_spritesorttime = I_GetPreciseTime() - ps_hw_spritesorttime;
-	ps_hw_spritedrawtime = I_GetPreciseTime();
+	PS_STOP_TIMING(ps_hw_spritesorttime);
+	PS_START_TIMING(ps_hw_spritedrawtime);
 	HWR_DrawSprites();
-	ps_hw_spritedrawtime = I_GetPreciseTime() - ps_hw_spritedrawtime;
+	PS_STOP_TIMING(ps_hw_spritedrawtime);
 
 #ifdef NEWCORONAS
 	//Hurdler: they must be drawn before translucent planes, what about gl fog?
 	HWR_DrawCoronas();
 #endif
 
-	ps_numdrawnodes = 0;
-	ps_hw_nodesorttime = 0;
-	ps_hw_nodedrawtime = 0;
+	ps_numdrawnodes.value.i = 0;
+	ps_hw_nodesorttime.value.p = 0;
+	ps_hw_nodedrawtime.value.p = 0;
 	if (numplanes || numpolyplanes || numwalls) //Hurdler: render 3D water and transparent walls after everything
 	{
 		HWR_CreateDrawNodes();
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index b751b2a6e1c7698663e353c56d49e778e81fb7f0..3f90f0ae17a58070948148f168c48f248510ce61 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -20,6 +20,8 @@
 #include "../d_player.h"
 #include "../r_defs.h"
 
+#include "../m_perfstats.h"
+
 // Startup & Shutdown the hardware mode renderer
 void HWR_Startup(void);
 void HWR_Switch(void);
@@ -116,22 +118,22 @@ extern FTransform atransform;
 
 
 // Render stats
-extern precise_t ps_hw_skyboxtime;
-extern precise_t ps_hw_nodesorttime;
-extern precise_t ps_hw_nodedrawtime;
-extern precise_t ps_hw_spritesorttime;
-extern precise_t ps_hw_spritedrawtime;
+extern ps_metric_t ps_hw_skyboxtime;
+extern ps_metric_t ps_hw_nodesorttime;
+extern ps_metric_t ps_hw_nodedrawtime;
+extern ps_metric_t ps_hw_spritesorttime;
+extern ps_metric_t ps_hw_spritedrawtime;
 
 // Render stats for batching
-extern int ps_hw_numpolys;
-extern int ps_hw_numverts;
-extern int ps_hw_numcalls;
-extern int ps_hw_numshaders;
-extern int ps_hw_numtextures;
-extern int ps_hw_numpolyflags;
-extern int ps_hw_numcolors;
-extern precise_t ps_hw_batchsorttime;
-extern precise_t ps_hw_batchdrawtime;
+extern ps_metric_t ps_hw_numpolys;
+extern ps_metric_t ps_hw_numverts;
+extern ps_metric_t ps_hw_numcalls;
+extern ps_metric_t ps_hw_numshaders;
+extern ps_metric_t ps_hw_numtextures;
+extern ps_metric_t ps_hw_numpolyflags;
+extern ps_metric_t ps_hw_numcolors;
+extern ps_metric_t ps_hw_batchsorttime;
+extern ps_metric_t ps_hw_batchdrawtime;
 
 extern boolean gl_init;
 extern boolean gl_maploaded;
diff --git a/src/i_system.h b/src/i_system.h
index e046fd620114161ddf49e7bba50685547cd7d32a..a2dd81cca3ef815ec6121d09b64308f54c6adc3e 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -50,7 +50,7 @@ tic_t I_GetTime(void);
   */
 precise_t I_GetPreciseTime(void);
 
-/**	\brief	Returns the difference between precise times as microseconds.
+/**	\brief	Converts a precise_t to microseconds and casts it to a 32 bit integer.
   */
 int I_PreciseToMicros(precise_t);
 
@@ -318,10 +318,6 @@ void I_RegisterSysCommands(void);
 */
 void I_GetCursorPosition(INT32 *x, INT32 *y);
 
-/** \brief Returns whether the mouse is grabbed
-*/
-boolean I_GetMouseGrab(void);
-
 /** \brief Sets whether the mouse is grabbed
 */
 void I_SetMouseGrab(boolean grab);
diff --git a/src/info.c b/src/info.c
index efcf1c044141225bf9fd64e1af619548df2c7298..f56e5d78e3e786b33806a1a596f0051a182144fd 100644
--- a/src/info.c
+++ b/src/info.c
@@ -2069,7 +2069,7 @@ state_t states[NUMSTATES] =
 	{SPR_TVFL, 2, 18, {A_GiveShield}, SH_FLAMEAURA, 0, S_NULL}, // S_FLAMEAURA_ICON2
 
 	{SPR_TVBB, FF_ANIMATE|2, 18, {NULL}, 3, 4, S_BUBBLEWRAP_ICON2}, // S_BUBBLEWRAP_ICON1
-	{SPR_TVBB, 2, 18, {A_GiveShield}, SH_BUBBLEWRAP, 0, S_NULL}, // S_BUBBLERWAP_ICON2
+	{SPR_TVBB, 2, 18, {A_GiveShield}, SH_BUBBLEWRAP, 0, S_NULL}, // S_BUBBLEWRAP_ICON2
 
 	{SPR_TVZP, FF_ANIMATE|2, 18, {NULL}, 3, 4, S_THUNDERCOIN_ICON2}, // S_THUNDERCOIN_ICON1
 	{SPR_TVZP, 2, 18, {A_GiveShield}, SH_THUNDERCOIN, 0, S_NULL}, // S_THUNDERCOIN_ICON2
@@ -5199,7 +5199,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		24*FRACUNIT,    // radius
 		34*FRACUNIT,    // height
 		0,              // display offset
-		100,            // mass
+		DMG_FIRE,       // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOGRAVITY|MF_NOBLOCKMAP|MF_FIRE|MF_PAIN, // flags
@@ -7974,7 +7974,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		8*FRACUNIT,     // radius
 		32*FRACUNIT,    // height
 		0,              // display offset
-		4,              // mass
+		DMG_SPIKE,      // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOBLOCKMAP|MF_SCENERY|MF_NOCLIPHEIGHT,  // flags
@@ -8001,7 +8001,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		16*FRACUNIT,    // radius
 		14*FRACUNIT,    // height
 		0,              // display offset
-		4,              // mass
+		DMG_SPIKE,      // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOBLOCKMAP|MF_NOGRAVITY|MF_SCENERY|MF_NOCLIPHEIGHT|MF_PAPERCOLLISION,  // flags
@@ -11430,7 +11430,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		17*FRACUNIT,    // radius
 		34*FRACUNIT,    // height
 		1,              // display offset
-		0,              // mass
+		DMG_SPIKE,      // mass
 		1,              // damage
 		sfx_s3kc9s, //sfx_mswing, -- activesound
 		MF_SCENERY|MF_PAIN|MF_NOGRAVITY, // flags
@@ -11457,7 +11457,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		34*FRACUNIT,    // radius
 		68*FRACUNIT,    // height
 		1,              // display offset
-		0,              // mass
+		DMG_SPIKE,      // mass
 		1,              // damage
 		sfx_s3kc9s, //sfx_mswing, -- activesound
 		MF_SCENERY|MF_PAIN|MF_NOGRAVITY, // flags
@@ -13401,7 +13401,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		30*FRACUNIT,    // radius
 		48*FRACUNIT,    // height
 		0,              // display offset
-		100,            // mass
+		DMG_FIRE,       // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_SPECIAL|MF_PAIN|MF_NOGRAVITY|MF_FIRE, // flags
@@ -13806,7 +13806,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		8*FRACUNIT,     // radius
 		32*FRACUNIT,    // height
 		0,              // display offset
-		0,       // mass
+		0,              // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOGRAVITY|MF_PAIN, // flags
@@ -20380,7 +20380,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		18*FRACUNIT,    // radius
 		28*FRACUNIT,    // height
 		0,              // display offset
-		0,              // mass
+		DMG_SPIKE,      // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOGRAVITY|MF_PAIN, // flags
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 350c1585fab2478bf4dfa438306215a510b330b2..12ad4fee0549bbaad9d5ea2dd3106c9bd9b8f955 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -31,6 +31,7 @@
 #include "m_misc.h" // M_MapNumber
 #include "b_bot.h" // B_UpdateBotleader
 #include "d_clisrv.h" // CL_RemovePlayer
+#include "i_system.h" // I_GetPreciseTime, I_PreciseToMicros
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -187,6 +188,8 @@ static const struct {
 	{META_MAPHEADER,    "mapheader_t"},
 
 	{META_POLYOBJ,      "polyobj_t"},
+	{META_POLYOBJVERTICES, "polyobj_t.vertices"},
+	{META_POLYOBJLINES, "polyobj_t.lines"},
 
 	{META_CVAR,         "consvar_t"},
 
@@ -216,6 +219,7 @@ static const struct {
 
 	{META_LUABANKS,     "luabanks[]"},
 
+	{META_KEYEVENT,     "keyevent_t"},
 	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
@@ -3876,6 +3880,12 @@ static int lib_gTicsToMilliseconds(lua_State *L)
 	return 1;
 }
 
+static int lib_getTimeMicros(lua_State *L)
+{
+	lua_pushinteger(L, I_PreciseToMicros(I_GetPreciseTime()));
+	return 1;
+}
+
 static luaL_Reg lib[] = {
 	{"print", lib_print},
 	{"chatprint", lib_chatprint},
@@ -4150,6 +4160,8 @@ static luaL_Reg lib[] = {
 	{"G_TicsToCentiseconds",lib_gTicsToCentiseconds},
 	{"G_TicsToMilliseconds",lib_gTicsToMilliseconds},
 
+	{"getTimeMicros",lib_getTimeMicros},
+
 	{NULL, NULL}
 };
 
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 32b5e52fb6875cbbd10aa6b10011c654c60dab5d..a72b22b5a62b953bbccde13f271c76fb6e24cad1 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -505,7 +505,7 @@ static int call_hooks
 		calls += call_mobj_type_hooks(hook, MT_NULL);
 		calls += call_mobj_type_hooks(hook, hook->mobj_type);
 
-		ps_lua_mobjhooks += calls;
+		ps_lua_mobjhooks.value.i += calls;
 	}
 	else
 		calls += call_mapped(hook, &hookIds[hook->hook_type]);
@@ -868,7 +868,7 @@ void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 		LUA_PushUserdata(gL, line, META_LINE);
 		LUA_PushUserdata(gL, mo, META_MOBJ);
 		LUA_PushUserdata(gL, sector, META_SECTOR);
-		ps_lua_mobjhooks += call_hooks(&hook, 0, res_none);
+		ps_lua_mobjhooks.value.i += call_hooks(&hook, 0, res_none);
 	}
 }
 
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index fca832053b2740e51ba2d4c303aa88fc698ccc49..0dd951efd964e28a04074d311215d059564d88a7 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -265,7 +265,7 @@ static int hudinfo_num(lua_State *L)
 
 static int colormap_get(lua_State *L)
 {
-	UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
+	const UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
 	UINT32 i = luaL_checkinteger(L, 2);
 	if (i >= 256)
 		return luaL_error(L, "colormap index %d out of range (0 - %d)", i, 255);
@@ -273,23 +273,6 @@ static int colormap_get(lua_State *L)
 	return 1;
 }
 
-static int colormap_set(lua_State *L)
-{
-	UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
-	UINT32 i = luaL_checkinteger(L, 2);
-	if (i >= 256)
-		return luaL_error(L, "colormap index %d out of range (0 - %d)", i, 255);
-	colormap[i] = (UINT8)luaL_checkinteger(L, 3);
-	return 0;
-}
-
-static int colormap_free(lua_State *L)
-{
-	UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
-	Z_Free(colormap);
-	return 0;
-}
-
 static int patch_get(lua_State *L)
 {
 	patch_t *patch = *((patch_t **)luaL_checkudata(L, 1, META_PATCH));
@@ -1057,7 +1040,7 @@ static int libd_getColormap(lua_State *L)
 
 	// all was successful above, now we generate the colormap at last!
 
-	colormap = R_GetTranslationColormap(skinnum, color, 0);
+	colormap = R_GetTranslationColormap(skinnum, color, GTC_CACHE);
 	LUA_PushUserdata(L, colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
 	return 1;
 }
@@ -1066,14 +1049,10 @@ static int libd_getStringColormap(lua_State *L)
 {
 	INT32 flags = luaL_checkinteger(L, 1);
 	UINT8* colormap = NULL;
-	UINT8* lua_colormap = NULL;
 	HUDONLY
 	colormap = V_GetStringColormap(flags & V_CHARCOLORMASK);
 	if (colormap) {
-		lua_colormap = Z_Malloc(256 * sizeof(UINT8), PU_LUA, NULL);
-		memcpy(lua_colormap, colormap, 256 * sizeof(UINT8));
-
-		LUA_PushUserdata(L, lua_colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
+		LUA_PushUserdata(L, colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
 		return 1;
 	}
 	return 0;
@@ -1348,12 +1327,6 @@ int LUA_HudLib(lua_State *L)
 	luaL_newmetatable(L, META_COLORMAP);
 		lua_pushcfunction(L, colormap_get);
 		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, colormap_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, colormap_free);
-		lua_setfield(L, -2, "__gc");
 	lua_pop(L,1);
 
 	luaL_newmetatable(L, META_PATCH);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 534ef9a071a1b31f28b9dde034bf55398fc7b0b7..b54924cdfe8100640719aff4ee39cfeeabfd0724 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -196,7 +196,7 @@ enum ffloor_e {
 	ffloor_next,
 	ffloor_prev,
 	ffloor_alpha,
-	ffloor_specialflags,
+	ffloor_bustflags,
 	ffloor_busttype,
 	ffloor_busttag,
 	ffloor_sinkspeed,
@@ -220,7 +220,7 @@ static const char *const ffloor_opt[] = {
 	"next",
 	"prev",
 	"alpha",
-	"specialflags",
+	"bustflags",
 	"busttype",
 	"busttag",
 	"sinkspeed",
@@ -1819,8 +1819,8 @@ static int ffloor_get(lua_State *L)
 	case ffloor_alpha:
 		lua_pushinteger(L, ffloor->alpha);
 		return 1;
-	case ffloor_specialflags:
-		lua_pushinteger(L, ffloor->specialflags);
+	case ffloor_bustflags:
+		lua_pushinteger(L, ffloor->bustflags);
 		return 1;
 	case ffloor_busttype:
 		lua_pushinteger(L, ffloor->busttype);
diff --git a/src/m_menu.c b/src/m_menu.c
index 7a82fd8fb1a81009590b8495e90d6a8f41102fa7..fc1e33b67dc16c6e8898a1df754119ecf8cf86ed 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -3215,7 +3215,7 @@ boolean M_Responder(event_t *ev)
 	if (gamestate == GS_TITLESCREEN && finalecount < TICRATE)
 		return false;
 
-	if (CON_Ready())
+	if (CON_Ready() && gamestate != GS_WAITINGPLAYERS)
 		return false;
 
 	if (noFurtherInput)
@@ -4062,14 +4062,6 @@ static void M_DrawSlider(INT32 x, INT32 y, const consvar_t *cv, boolean ontop)
 	for (i = 1; i < SLIDER_RANGE; i++)
 		V_DrawScaledPatch (x+i*8, y, 0,p);
 
-	if (ontop)
-	{
-		V_DrawCharacter(x - 6 - (skullAnimCounter/5), y,
-			'\x1C' | V_YELLOWMAP, false);
-		V_DrawCharacter(x+i*8 + 8 + (skullAnimCounter/5), y,
-			'\x1D' | V_YELLOWMAP, false);
-	}
-
 	p = W_CachePatchName("M_SLIDER", PU_PATCH);
 	V_DrawScaledPatch(x+i*8, y, 0, p);
 
@@ -4105,6 +4097,16 @@ static void M_DrawSlider(INT32 x, INT32 y, const consvar_t *cv, boolean ontop)
 		range = 100;
 
 	V_DrawMappedPatch(x + 2 + (SLIDER_RANGE*8*range)/100, y, 0, p, yellowmap);
+
+	if (ontop)
+	{
+		V_DrawCharacter(x - 6 - (skullAnimCounter/5), y,
+			'\x1C' | V_YELLOWMAP, false);
+		V_DrawCharacter(x + 80 + (skullAnimCounter/5), y,
+			'\x1D' | V_YELLOWMAP, false);
+		V_DrawCenteredString(x + 40, y, V_30TRANS,
+			(cv->flags & CV_FLOAT) ? va("%.2f", FIXED_TO_FLOAT(cv->value)) : va("%d", cv->value));
+	}
 }
 
 //
@@ -6410,6 +6412,7 @@ static void M_Addons(INT32 choice)
 	M_SetupNextMenu(&MISC_AddonsDef);
 }
 
+#ifdef ENFORCE_WAD_LIMIT
 #define width 4
 #define vpadding 27
 #define h (BASEVIDHEIGHT-(2*vpadding))
@@ -6457,6 +6460,7 @@ static void M_DrawTemperature(INT32 x, fixed_t t)
 #undef vpadding
 #undef h
 #undef NUMCOLOURS
+#endif
 
 static char *M_AddonsHeaderPath(void)
 {
@@ -6550,21 +6554,20 @@ static void M_DrawAddons(void)
 		V_DrawCenteredString(BASEVIDWIDTH/2, 5, 0, LOCATIONSTRING1);
 			// (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)
 
+#ifdef ENFORCE_WAD_LIMIT
 	if (numwadfiles <= mainwads+1)
 		y = 0;
 	else if (numwadfiles >= MAX_WADFILES)
 		y = FRACUNIT;
 	else
 	{
-		x = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
-		y = FixedDiv((((ssize_t)packetsizetally-(ssize_t)mainwadstally)<<FRACBITS), ((((ssize_t)MAXFILENEEDED*sizeof(UINT8)-(ssize_t)mainwadstally)-(5+22))<<FRACBITS)); // 5+22 = (a.ext + checksum length) is minimum addition to packet size tally
-		if (x > y)
-			y = x;
+		y = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
 		if (y > FRACUNIT) // happens because of how we're shrinkin' it a little
 			y = FRACUNIT;
 	}
 
 	M_DrawTemperature(BASEVIDWIDTH - 19 - 5, y);
+#endif
 
 	// DRAW MENU
 	x = currentMenu->x;
@@ -7187,13 +7190,20 @@ static void M_HandleChecklist(INT32 choice)
 
 static void M_DrawChecklist(void)
 {
-	INT32 i = check_on, j = 0, y = currentMenu->y;
+	INT32 i = check_on, j = 0, y = currentMenu->y, emblems = numemblems+numextraemblems;
 	UINT32 condnum, previd, maxcond;
 	condition_t *cond;
 
 	// draw title (or big pic)
 	M_DrawMenuTitle();
 
+	// draw emblem counter
+	if (emblems > 0)
+	{
+		V_DrawString(42, 20, (emblems == M_CountEmblems()) ? V_GREENMAP : 0, va("%d/%d", M_CountEmblems(), emblems));
+		V_DrawSmallScaledPatch(28, 20, 0, W_CachePatchName("EMBLICON", PU_PATCH));
+	}
+
 	if (check_on)
 		V_DrawString(10, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A");
 
@@ -8652,6 +8662,12 @@ static void M_DrawLoad(void)
 		loadgameoffset = 0;
 
 	M_DrawLoadGameData();
+
+	if (modifiedgame && !savemoddata)
+	{
+		V_DrawCenteredThinString(BASEVIDWIDTH/2, 184, 0, "\x85WARNING: \x80The game is modified.");
+		V_DrawCenteredThinString(BASEVIDWIDTH/2, 192, 0, "Progress will not be saved.");
+	}
 }
 
 //
@@ -8953,7 +8969,7 @@ static void M_HandleLoadSave(INT32 choice)
 			break;
 
 		case KEY_ENTER:
-			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT)
+			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT && !savemoddata && !modifiedgame)
 			{
 				loadgamescroll = 0;
 				S_StartSound(NULL, sfx_skid);
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 8a99312e6a3d987de4ef6ab7965e31cbdc220458..439a9da1cd009106bc80fa4f3c81e3337a0acbdf 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -22,560 +22,802 @@
 #include "hardware/hw_main.h"
 #endif
 
-struct perfstatcol;
 struct perfstatrow;
 
-typedef struct perfstatcol perfstatcol_t;
 typedef struct perfstatrow perfstatrow_t;
 
-struct perfstatcol {
-	INT32 lores_x;
-	INT32 hires_x;
-	INT32 color;
-	perfstatrow_t * rows;
+struct perfstatrow {
+	const char  * lores_label;
+	const char  * hires_label;
+	ps_metric_t * metric;
+	UINT8         flags;
 };
 
-struct perfstatrow {
-	const char * lores_label;
-	const char * hires_label;
-	void       * value;
+// perfstatrow_t flags
+
+#define PS_TIME      1  // metric measures time (uses precise_t instead of INT32)
+#define PS_LEVEL     2  // metric is valid only when a level is active
+#define PS_SW        4  // metric is valid only in software mode
+#define PS_HW        8  // metric is valid only in opengl mode
+#define PS_BATCHING  16 // metric is valid only when opengl batching is active
+#define PS_HIDE_ZERO 32 // hide metric if its value is zero
+
+static ps_metric_t ps_frametime = {0};
+
+ps_metric_t ps_tictime = {0};
+
+ps_metric_t ps_playerthink_time = {0};
+ps_metric_t ps_thinkertime = {0};
+
+ps_metric_t ps_thlist_times[NUM_THINKERLISTS];
+
+static ps_metric_t ps_thinkercount = {0};
+static ps_metric_t ps_polythcount = {0};
+static ps_metric_t ps_mainthcount = {0};
+static ps_metric_t ps_mobjcount = {0};
+static ps_metric_t ps_regularcount = {0};
+static ps_metric_t ps_scenerycount = {0};
+static ps_metric_t ps_nothinkcount = {0};
+static ps_metric_t ps_dynslopethcount = {0};
+static ps_metric_t ps_precipcount = {0};
+static ps_metric_t ps_removecount = {0};
+
+ps_metric_t ps_checkposition_calls = {0};
+
+ps_metric_t ps_lua_thinkframe_time = {0};
+ps_metric_t ps_lua_mobjhooks = {0};
+
+ps_metric_t ps_otherlogictime = {0};
+
+// Columns for perfstats pages.
+
+// Position on screen is determined separately in the drawing functions.
+
+// New columns must also be added to the drawing and update functions.
+// Drawing functions: PS_DrawRenderStats, PS_DrawGameLogicStats, etc.
+// Update functions:
+//  - PS_UpdateFrameStats for frame-dependent values
+//  - PS_UpdateTickStats for tick-dependent values
+
+// Rendering stats columns
+
+perfstatrow_t rendertime_rows[] = {
+	{"frmtime", "Frame time:    ", &ps_frametime, PS_TIME},
+	{"drwtime", "3d rendering:  ", &ps_rendercalltime, PS_TIME|PS_LEVEL},
+
+#ifdef HWRENDER
+	{" skybox ", " Skybox render: ", &ps_hw_skyboxtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_HW},
+	{" batsort", " Batch sort:    ", &ps_hw_batchsorttime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
+	{" batdraw", " Batch render:  ", &ps_hw_batchdrawtime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
+	{" sprsort", " Sprite sort:   ", &ps_hw_spritesorttime, PS_TIME|PS_LEVEL|PS_HW},
+	{" sprdraw", " Sprite render: ", &ps_hw_spritedrawtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" nodesrt", " Drwnode sort:  ", &ps_hw_nodesorttime, PS_TIME|PS_LEVEL|PS_HW},
+	{" nodedrw", " Drwnode render:", &ps_hw_nodedrawtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_HW},
+#endif
+
+	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_SW},
+	{" sprclip", " R_ClipSprites: ", &ps_sw_spritecliptime, PS_TIME|PS_LEVEL|PS_SW},
+	{" portals", " Portals+Skybox:", &ps_sw_portaltime, PS_TIME|PS_LEVEL|PS_SW},
+	{" planes ", " R_DrawPlanes:  ", &ps_sw_planetime, PS_TIME|PS_LEVEL|PS_SW},
+	{" masked ", " R_DrawMasked:  ", &ps_sw_maskedtime, PS_TIME|PS_LEVEL|PS_SW},
+	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_SW},
+
+	{"ui     ", "UI render:     ", &ps_uitime, PS_TIME},
+	{"finupdt", "I_FinishUpdate:", &ps_swaptime, PS_TIME},
+	{0}
 };
 
-static precise_t ps_frametime = 0;
+perfstatrow_t gamelogicbrief_row[] = {
+	{"logic  ", "Game logic:    ", &ps_tictime, PS_TIME},
+	{0}
+};
 
-precise_t ps_tictime = 0;
+perfstatrow_t commoncounter_rows[] = {
+	{"bspcall", "BSP calls:   ", &ps_numbspcalls, 0},
+	{"sprites", "Sprites:     ", &ps_numsprites, 0},
+	{"drwnode", "Drawnodes:   ", &ps_numdrawnodes, 0},
+	{"plyobjs", "Polyobjects: ", &ps_numpolyobjects, 0},
+	{0}
+};
 
-precise_t ps_playerthink_time = 0;
-precise_t ps_thinkertime = 0;
+#ifdef HWRENDER
+perfstatrow_t batchcount_rows[] = {
+	{"polygon", "Polygons:  ", &ps_hw_numpolys, 0},
+	{"vertex ", "Vertices:  ", &ps_hw_numverts, 0},
+	{0}
+};
 
-precise_t ps_thlist_times[NUM_THINKERLISTS];
+perfstatrow_t batchcalls_rows[] = {
+	{"drwcall", "Draw calls:", &ps_hw_numcalls, 0},
+	{"shaders", "Shaders:   ", &ps_hw_numshaders, 0},
+	{"texture", "Textures:  ", &ps_hw_numtextures, 0},
+	{"polyflg", "Polyflags: ", &ps_hw_numpolyflags, 0},
+	{"colors ", "Colors:    ", &ps_hw_numcolors, 0},
+	{0}
+};
+#endif
 
-int ps_checkposition_calls = 0;
+// Game logic stats columns
+
+perfstatrow_t gamelogic_rows[] = {
+	{"logic  ", "Game logic:     ", &ps_tictime, PS_TIME},
+	{" plrthnk", " P_PlayerThink:  ", &ps_playerthink_time, PS_TIME|PS_LEVEL},
+	{" thnkers", " P_RunThinkers:  ", &ps_thinkertime, PS_TIME|PS_LEVEL},
+	{"  plyobjs", "  Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ], PS_TIME|PS_LEVEL},
+	{"  main   ", "  Main:           ", &ps_thlist_times[THINK_MAIN], PS_TIME|PS_LEVEL},
+	{"  mobjs  ", "  Mobjs:          ", &ps_thlist_times[THINK_MOBJ], PS_TIME|PS_LEVEL},
+	{"  dynslop", "  Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE], PS_TIME|PS_LEVEL},
+	{"  precip ", "  Precipitation:  ", &ps_thlist_times[THINK_PRECIP], PS_TIME|PS_LEVEL},
+	{" lthinkf", " LUAh_ThinkFrame:", &ps_lua_thinkframe_time, PS_TIME|PS_LEVEL},
+	{" other  ", " Other:          ", &ps_otherlogictime, PS_TIME|PS_LEVEL},
+	{0}
+};
 
-precise_t ps_lua_thinkframe_time = 0;
-int ps_lua_mobjhooks = 0;
+perfstatrow_t thinkercount_rows[] = {
+	{"thnkers", "Thinkers:       ", &ps_thinkercount, PS_LEVEL},
+	{" plyobjs", " Polyobjects:    ", &ps_polythcount, PS_LEVEL},
+	{" main   ", " Main:           ", &ps_mainthcount, PS_LEVEL},
+	{" mobjs  ", " Mobjs:          ", &ps_mobjcount, PS_LEVEL},
+	{"  regular", "  Regular:        ", &ps_regularcount, PS_LEVEL},
+	{"  scenery", "  Scenery:        ", &ps_scenerycount, PS_LEVEL},
+	{"  nothink", "  Nothink:        ", &ps_nothinkcount, PS_HIDE_ZERO|PS_LEVEL},
+	{" dynslop", " Dynamic slopes: ", &ps_dynslopethcount, PS_LEVEL},
+	{" precip ", " Precipitation:  ", &ps_precipcount, PS_LEVEL},
+	{" remove ", " Pending removal:", &ps_removecount, PS_LEVEL},
+	{0}
+};
+
+perfstatrow_t misc_calls_rows[] = {
+	{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks, PS_LEVEL},
+	{"chkpos", "P_CheckPosition:", &ps_checkposition_calls, PS_LEVEL},
+	{0}
+};
+
+// Sample collection status for averaging.
+// Maximum of these two is shown to user if nonzero to tell that
+// the reported averages are not correct yet.
+int ps_frame_samples_left = 0;
+int ps_tick_samples_left = 0;
+// History writing positions for frame and tick based metrics
+int ps_frame_index = 0;
+int ps_tick_index = 0;
 
 // dynamically allocated resizeable array for thinkframe hook stats
 ps_hookinfo_t *thinkframe_hooks = NULL;
 int thinkframe_hooks_length = 0;
 int thinkframe_hooks_capacity = 16;
 
-static INT32 draw_row;
-
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
 {
 	if (!thinkframe_hooks)
 	{
 		// array needs to be initialized
-		thinkframe_hooks = Z_Malloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
+		thinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
 	}
 	if (index >= thinkframe_hooks_capacity)
 	{
 		// array needs more space, realloc with double size
-		thinkframe_hooks_capacity *= 2;
+		int new_capacity = thinkframe_hooks_capacity * 2;
 		thinkframe_hooks = Z_Realloc(thinkframe_hooks,
-			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
+			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
+		// initialize new memory with zeros so the pointers in the structs are null
+		memset(&thinkframe_hooks[thinkframe_hooks_capacity], 0,
+			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity);
+		thinkframe_hooks_capacity = new_capacity;
 	}
-	thinkframe_hooks[index].time_taken = time_taken;
+	thinkframe_hooks[index].time_taken.value.p = time_taken;
 	memcpy(thinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
 	// since the values are set sequentially from begin to end, the last call should leave
 	// the correct value to this variable
 	thinkframe_hooks_length = index + 1;
 }
 
-static void PS_SetFrameTime(void)
+static boolean PS_HighResolution(void)
 {
-	precise_t currenttime = I_GetPreciseTime();
-	ps_frametime = currenttime - ps_prevframetime;
-	ps_prevframetime = currenttime;
+	return (vid.width >= 640 && vid.height >= 400);
 }
 
-static boolean M_HighResolution(void)
+static boolean PS_IsLevelActive(void)
 {
-	return (vid.width >= 640 && vid.height >= 400);
+	return gamestate == GS_LEVEL ||
+			(gamestate == GS_TITLESCREEN && titlemapinaction);
 }
 
-enum {
-	PERF_TIME,
-	PERF_COUNT,
-};
+// Is the row valid in the current context?
+static boolean PS_IsRowValid(perfstatrow_t *row)
+{
+	return !((row->flags & PS_LEVEL && !PS_IsLevelActive()) ||
+		(row->flags & PS_SW && rendermode != render_soft) ||
+		(row->flags & PS_HW && rendermode != render_opengl) ||
+		(row->flags & PS_BATCHING && !cv_glbatching.value));
+}
 
-static void M_DrawPerfString(perfstatcol_t *col, int type)
+// Should the row be visible on the screen?
+static boolean PS_IsRowVisible(perfstatrow_t *row)
 {
-	const boolean hires = M_HighResolution();
+	boolean value_is_zero;
 
-	INT32 draw_flags = V_MONOSPACE | col->color;
+	if (row->flags & PS_TIME)
+		value_is_zero = row->metric->value.p == 0;
+	else
+		value_is_zero = row->metric->value.i == 0;
 
-	perfstatrow_t * row;
+	return !(!PS_IsRowValid(row) ||
+		(row->flags & PS_HIDE_ZERO && value_is_zero));
+}
+
+static INT32 PS_GetMetricAverage(ps_metric_t *metric, boolean time_metric)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT64 sum = 0;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		if (time_metric)
+			sum += I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			sum += *((INT32*)history_read_pos);
+		history_read_pos += value_size;
+	}
+
+	return sum / cv_ps_samplesize.value;
+}
+
+static INT32 PS_GetMetricMinOrMax(ps_metric_t *metric, boolean time_metric, boolean get_max)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT32 found_value = get_max ? INT32_MIN : INT32_MAX;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		INT32 value;
+		if (time_metric)
+			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			value = *((INT32*)history_read_pos);
+
+		if ((get_max && value > found_value) ||
+			(!get_max && value < found_value))
+		{
+			found_value = value;
+		}
+		history_read_pos += value_size;
+	}
+
+	return found_value;
+}
+
+// Calculates the standard deviation for metric.
+static INT32 PS_GetMetricSD(ps_metric_t *metric, boolean time_metric)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT64 sum = 0;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+	INT32 avg = PS_GetMetricAverage(metric, time_metric);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		INT64 value;
+		if (time_metric)
+			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			value = *((INT32*)history_read_pos);
+
+		value -= avg;
+		sum += value * value;
+
+		history_read_pos += value_size;
+	}
+
+	return round(sqrt(sum / cv_ps_samplesize.value));
+}
 
-	int value;
+// Returns the value to show on screen for metric.
+static INT32 PS_GetMetricScreenValue(ps_metric_t *metric, boolean time_metric)
+{
+	if (cv_ps_samplesize.value > 1 && metric->history)
+	{
+		if (cv_ps_descriptor.value == 1)
+			return PS_GetMetricAverage(metric, time_metric);
+		else if (cv_ps_descriptor.value == 2)
+			return PS_GetMetricSD(metric, time_metric);
+		else if (cv_ps_descriptor.value == 3)
+			return PS_GetMetricMinOrMax(metric, time_metric, false);
+		else
+			return PS_GetMetricMinOrMax(metric, time_metric, true);
+	}
+	else
+	{
+		if (time_metric)
+			return I_PreciseToMicros(metric->value.p);
+		else
+			return metric->value.i;
+	}
+}
+
+static int PS_DrawPerfRows(int x, int y, int color, perfstatrow_t *rows)
+{
+	const boolean hires = PS_HighResolution();
+	INT32 draw_flags = V_MONOSPACE | color;
+	perfstatrow_t * row;
+	int draw_y = y;
 
 	if (hires)
 		draw_flags |= V_ALLOWLOWERCASE;
 
-	for (row = col->rows; row->lores_label; ++row)
+	for (row = rows; row->lores_label; ++row)
 	{
-		if (type == PERF_TIME)
-			value = I_PreciseToMicros(*(precise_t *)row->value);
-		else
-			value = *(int *)row->value;
+		const char *label;
+		INT32 value;
+		char *final_str;
+
+		if (!PS_IsRowVisible(row))
+			continue;
+
+		label = hires ? row->hires_label : row->lores_label;
+		value = PS_GetMetricScreenValue(row->metric, !!(row->flags & PS_TIME));
+		final_str = va("%s %d", label, value);
 
 		if (hires)
 		{
-			V_DrawSmallString(col->hires_x, draw_row, draw_flags,
-					va("%s %d", row->hires_label, value));
-
-			draw_row += 5;
+			V_DrawSmallString(x, draw_y, draw_flags, final_str);
+			draw_y += 5;
 		}
 		else
 		{
-			V_DrawThinString(col->lores_x, draw_row, draw_flags,
-					va("%s %d", row->lores_label, value));
-
-			draw_row += 8;
+			V_DrawThinString(x, draw_y, draw_flags, final_str);
+			draw_y += 8;
 		}
 	}
+
+	return draw_y;
 }
 
-static void M_DrawPerfTiming(perfstatcol_t *col)
+static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric, boolean set_user)
 {
-	M_DrawPerfString(col, PERF_TIME);
+	int index = frame_metric ? ps_frame_index : ps_tick_index;
+
+	if (!metric->history)
+	{
+		// allocate history table
+		int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+		void** memory_user = set_user ? &metric->history : NULL;
+
+		metric->history = Z_Calloc(value_size * cv_ps_samplesize.value, PU_PERFSTATS,
+				memory_user);
+
+		// reset "samples left" counter since this history table needs to be filled
+		if (frame_metric)
+			ps_frame_samples_left = cv_ps_samplesize.value;
+		else
+			ps_tick_samples_left = cv_ps_samplesize.value;
+	}
+
+	if (time_metric)
+	{
+		precise_t *history = (precise_t*)metric->history;
+		history[index] = metric->value.p;
+	}
+	else
+	{
+		INT32 *history = (INT32*)metric->history;
+		history[index] = metric->value.i;
+	}
 }
 
-static void M_DrawPerfCount(perfstatcol_t *col)
+static void PS_UpdateRowHistories(perfstatrow_t *rows, boolean frame_metric)
 {
-	M_DrawPerfString(col, PERF_COUNT);
+	perfstatrow_t *row;
+	for (row = rows; row->lores_label; row++)
+	{
+		if (PS_IsRowValid(row))
+			PS_UpdateMetricHistory(row->metric, !!(row->flags & PS_TIME), frame_metric, true);
+	}
 }
 
-static void M_DrawRenderStats(void)
+// Update all metrics that are calculated on every frame.
+static void PS_UpdateFrameStats(void)
 {
-	const boolean hires = M_HighResolution();
-
-	const int half_row = hires ? 5 : 4;
+	// update frame time
+	precise_t currenttime = I_GetPreciseTime();
+	ps_frametime.value.p = currenttime - ps_prevframetime;
+	ps_prevframetime = currenttime;
 
-	precise_t extrarendertime;
-
-	perfstatrow_t frametime_row[] = {
-		{"frmtime", "Frame time:    ", &ps_frametime},
-		{0}
-	};
-
-	perfstatrow_t rendercalltime_row[] = {
-		{"drwtime", "3d rendering:  ", &ps_rendercalltime},
-		{0}
-	};
-
-	perfstatrow_t opengltime_row[] = {
-		{"skybox ", "Skybox render: ", &ps_hw_skyboxtime},
-		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
-		{"nodesrt", "Drwnode sort:  ", &ps_hw_nodesorttime},
-		{"nodedrw", "Drwnode render:", &ps_hw_nodedrawtime},
-		{"sprsort", "Sprite sort:   ", &ps_hw_spritesorttime},
-		{"sprdraw", "Sprite render: ", &ps_hw_spritedrawtime},
-		{"other  ", "Other:         ", &extrarendertime},
-		{0}
-	};
-
-	perfstatrow_t softwaretime_row[] = {
-		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
-		{"sprclip", "R_ClipSprites: ", &ps_sw_spritecliptime},
-		{"portals", "Portals+Skybox:", &ps_sw_portaltime},
-		{"planes ", "R_DrawPlanes:  ", &ps_sw_planetime},
-		{"masked ", "R_DrawMasked:  ", &ps_sw_maskedtime},
-		{"other  ", "Other:         ", &extrarendertime},
-		{0}
-	};
-
-	perfstatrow_t uiswaptime_row[] = {
-		{"ui     ", "UI render:     ", &ps_uitime},
-		{"finupdt", "I_FinishUpdate:", &ps_swaptime},
-		{0}
-	};
-
-	perfstatrow_t tictime_row[] = {
-		{"logic  ", "Game logic:    ", &ps_tictime},
-		{0}
-	};
-
-	perfstatrow_t rendercalls_row[] = {
-		{"bspcall", "BSP calls:   ", &ps_numbspcalls},
-		{"sprites", "Sprites:     ", &ps_numsprites},
-		{"drwnode", "Drawnodes:   ", &ps_numdrawnodes},
-		{"plyobjs", "Polyobjects: ", &ps_numpolyobjects},
-		{0}
-	};
-
-	perfstatrow_t batchtime_row[] = {
-		{"batsort", "Batch sort:  ", &ps_hw_batchsorttime},
-		{"batdraw", "Batch render:", &ps_hw_batchdrawtime},
-		{0}
-	};
-
-	perfstatrow_t batchcount_row[] = {
-		{"polygon", "Polygons:  ", &ps_hw_numpolys},
-		{"vertex ", "Vertices:  ", &ps_hw_numverts},
-		{0}
-	};
-
-	perfstatrow_t batchcalls_row[] = {
-		{"drwcall", "Draw calls:", &ps_hw_numcalls},
-		{"shaders", "Shaders:   ", &ps_hw_numshaders},
-		{"texture", "Textures:  ", &ps_hw_numtextures},
-		{"polyflg", "Polyflags: ", &ps_hw_numpolyflags},
-		{"colors ", "Colors:    ", &ps_hw_numcolors},
-		{0}
-	};
-
-	perfstatcol_t      frametime_col =  {20,  20, V_YELLOWMAP,      frametime_row};
-	perfstatcol_t rendercalltime_col =  {20,  20, V_YELLOWMAP, rendercalltime_row};
-
-	perfstatcol_t     opengltime_col =  {24,  24, V_YELLOWMAP,     opengltime_row};
-	perfstatcol_t   softwaretime_col =  {24,  24, V_YELLOWMAP,   softwaretime_row};
-
-	perfstatcol_t     uiswaptime_col =  {20,  20, V_YELLOWMAP,     uiswaptime_row};
-	perfstatcol_t        tictime_col =  {20,  20, V_GRAYMAP,          tictime_row};
-
-	perfstatcol_t    rendercalls_col =  {90, 115, V_BLUEMAP,      rendercalls_row};
-
-	perfstatcol_t      batchtime_col =  {90, 115, V_REDMAP,         batchtime_row};
-
-	perfstatcol_t     batchcount_col = {155, 200, V_PURPLEMAP,     batchcount_row};
-	perfstatcol_t     batchcalls_col = {220, 200, V_PURPLEMAP,     batchcalls_row};
-
-
-	boolean rendering = (
-			gamestate == GS_LEVEL ||
-			(gamestate == GS_TITLESCREEN && titlemapinaction)
-	);
-
-	draw_row = 10;
-	M_DrawPerfTiming(&frametime_col);
-
-	if (rendering)
+	// update 3d rendering stats
+	if (PS_IsLevelActive())
 	{
-		M_DrawPerfTiming(&rendercalltime_col);
-
 		// Remember to update this calculation when adding more 3d rendering stats!
-		extrarendertime = ps_rendercalltime - ps_bsptime;
+		ps_otherrendertime.value.p = ps_rendercalltime.value.p - ps_bsptime.value.p;
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl)
 		{
-			extrarendertime -=
-				ps_hw_skyboxtime +
-				ps_hw_nodesorttime +
-				ps_hw_nodedrawtime +
-				ps_hw_spritesorttime +
-				ps_hw_spritedrawtime;
+			ps_otherrendertime.value.p -=
+				ps_hw_skyboxtime.value.p +
+				ps_hw_nodesorttime.value.p +
+				ps_hw_nodedrawtime.value.p +
+				ps_hw_spritesorttime.value.p +
+				ps_hw_spritedrawtime.value.p;
 
 			if (cv_glbatching.value)
 			{
-				extrarendertime -=
-					ps_hw_batchsorttime +
-					ps_hw_batchdrawtime;
+				ps_otherrendertime.value.p -=
+					ps_hw_batchsorttime.value.p +
+					ps_hw_batchdrawtime.value.p;
 			}
-
-			M_DrawPerfTiming(&opengltime_col);
 		}
 		else
 #endif
 		{
-			extrarendertime -=
-				ps_sw_spritecliptime +
-				ps_sw_portaltime +
-				ps_sw_planetime +
-				ps_sw_maskedtime;
-
-			M_DrawPerfTiming(&softwaretime_col);
+			ps_otherrendertime.value.p -=
+				ps_sw_spritecliptime.value.p +
+				ps_sw_portaltime.value.p +
+				ps_sw_planetime.value.p +
+				ps_sw_maskedtime.value.p;
 		}
 	}
 
-	M_DrawPerfTiming(&uiswaptime_col);
-
-	draw_row += half_row;
-	M_DrawPerfTiming(&tictime_col);
-
-	if (rendering)
+	if (cv_ps_samplesize.value > 1)
 	{
-		draw_row = 10;
-		M_DrawPerfCount(&rendercalls_col);
+		PS_UpdateRowHistories(rendertime_rows, true);
+		if (PS_IsLevelActive())
+			PS_UpdateRowHistories(commoncounter_rows, true);
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl && cv_glbatching.value)
 		{
-			draw_row += half_row;
-			M_DrawPerfTiming(&batchtime_col);
-
-			draw_row = 10;
-			M_DrawPerfCount(&batchcount_col);
-
-			if (hires)
-				draw_row += half_row;
-			else
-				draw_row  = 10;
-
-			M_DrawPerfCount(&batchcalls_col);
+			PS_UpdateRowHistories(batchcount_rows, true);
+			PS_UpdateRowHistories(batchcalls_rows, true);
 		}
 #endif
+
+		ps_frame_index++;
+		if (ps_frame_index >= cv_ps_samplesize.value)
+			ps_frame_index = 0;
+		if (ps_frame_samples_left)
+			ps_frame_samples_left--;
 	}
 }
 
-static void M_DrawTickStats(void)
+// Update thinker counters by iterating the thinker lists.
+static void PS_CountThinkers(void)
 {
-	int i = 0;
+	int i;
 	thinker_t *thinker;
-	int thinkercount = 0;
-	int polythcount = 0;
-	int mainthcount = 0;
-	int mobjcount = 0;
-	int nothinkcount = 0;
-	int scenerycount = 0;
-	int regularcount = 0;
-	int dynslopethcount = 0;
-	int precipcount = 0;
-	int removecount = 0;
-
-	precise_t extratime =
-		ps_tictime -
-		ps_playerthink_time -
-		ps_thinkertime -
-		ps_lua_thinkframe_time;
-
-	perfstatrow_t tictime_row[] = {
-		{"logic  ", "Game logic:     ", &ps_tictime},
-		{0}
-	};
-
-	perfstatrow_t thinker_time_row[] = {
-		{"plrthnk", "P_PlayerThink:  ", &ps_playerthink_time},
-		{"thnkers", "P_RunThinkers:  ", &ps_thinkertime},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinker_time_row[] = {
-		{"plyobjs", "Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ]},
-		{"main   ", "Main:           ", &ps_thlist_times[THINK_MAIN]},
-		{"mobjs  ", "Mobjs:          ", &ps_thlist_times[THINK_MOBJ]},
-		{"dynslop", "Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE]},
-		{"precip ", "Precipitation:  ", &ps_thlist_times[THINK_PRECIP]},
-		{0}
-	};
-
-	perfstatrow_t extra_thinker_time_row[] = {
-		{"lthinkf", "LUAh_ThinkFrame:", &ps_lua_thinkframe_time},
-		{"other  ", "Other:          ", &extratime},
-		{0}
-	};
-
-	perfstatrow_t thinkercount_row[] = {
-		{"thnkers", "Thinkers:       ", &thinkercount},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinkercount_row[] = {
-		{"plyobjs", "Polyobjects:    ", &polythcount},
-		{"main   ", "Main:           ", &mainthcount},
-		{"mobjs  ", "Mobjs:          ", &mobjcount},
-		{0}
-	};
-
-	perfstatrow_t mobjthinkercount_row[] = {
-		{"regular", "Regular:        ", &regularcount},
-		{"scenery", "Scenery:        ", &scenerycount},
-		{0}
-	};
-
-	perfstatrow_t nothinkcount_row[] = {
-		{"nothink", "Nothink:        ", &nothinkcount},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinkercount_row2[] = {
-		{"dynslop", "Dynamic slopes: ", &dynslopethcount},
-		{"precip ", "Precipitation:  ", &precipcount},
-		{"remove ", "Pending removal:", &removecount},
-		{0}
-	};
-
-	perfstatrow_t misc_calls_row[] = {
-		{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks},
-		{"chkpos", "P_CheckPosition:", &ps_checkposition_calls},
-		{0}
-	};
-
-	perfstatcol_t               tictime_col  =  {20,  20, V_YELLOWMAP,               tictime_row};
-	perfstatcol_t          thinker_time_col  =  {24,  24, V_YELLOWMAP,          thinker_time_row};
-	perfstatcol_t detailed_thinker_time_col  =  {28,  28, V_YELLOWMAP, detailed_thinker_time_row};
-	perfstatcol_t    extra_thinker_time_col  =  {24,  24, V_YELLOWMAP,    extra_thinker_time_row};
-
-	perfstatcol_t          thinkercount_col  =  {90, 115, V_BLUEMAP,            thinkercount_row};
-	perfstatcol_t detailed_thinkercount_col  =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row};
-	perfstatcol_t      mobjthinkercount_col  =  {98, 123, V_BLUEMAP,        mobjthinkercount_row};
-	perfstatcol_t          nothinkcount_col  =  {98, 123, V_BLUEMAP,            nothinkcount_row};
-	perfstatcol_t detailed_thinkercount_col2 =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row2};
-	perfstatcol_t            misc_calls_col  = {170, 216, V_PURPLEMAP,            misc_calls_row};
+
+	ps_thinkercount.value.i = 0;
+	ps_polythcount.value.i = 0;
+	ps_mainthcount.value.i = 0;
+	ps_mobjcount.value.i = 0;
+	ps_regularcount.value.i = 0;
+	ps_scenerycount.value.i = 0;
+	ps_nothinkcount.value.i = 0;
+	ps_dynslopethcount.value.i = 0;
+	ps_precipcount.value.i = 0;
+	ps_removecount.value.i = 0;
 
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
 		for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
 		{
-			thinkercount++;
+			ps_thinkercount.value.i++;
 			if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-				removecount++;
+				ps_removecount.value.i++;
 			else if (i == THINK_POLYOBJ)
-				polythcount++;
+				ps_polythcount.value.i++;
 			else if (i == THINK_MAIN)
-				mainthcount++;
+				ps_mainthcount.value.i++;
 			else if (i == THINK_MOBJ)
 			{
 				if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
 				{
 					mobj_t *mobj = (mobj_t*)thinker;
-					mobjcount++;
+					ps_mobjcount.value.i++;
 					if (mobj->flags & MF_NOTHINK)
-						nothinkcount++;
+						ps_nothinkcount.value.i++;
 					else if (mobj->flags & MF_SCENERY)
-						scenerycount++;
+						ps_scenerycount.value.i++;
 					else
-						regularcount++;
+						ps_regularcount.value.i++;
 				}
 			}
 			else if (i == THINK_DYNSLOPE)
-				dynslopethcount++;
+				ps_dynslopethcount.value.i++;
 			else if (i == THINK_PRECIP)
-				precipcount++;
+				ps_precipcount.value.i++;
 		}
 	}
+}
 
-	draw_row = 10;
-	M_DrawPerfTiming(&tictime_col);
-	M_DrawPerfTiming(&thinker_time_col);
-	M_DrawPerfTiming(&detailed_thinker_time_col);
-	M_DrawPerfTiming(&extra_thinker_time_col);
-
-	draw_row = 10;
-	M_DrawPerfCount(&thinkercount_col);
-	M_DrawPerfCount(&detailed_thinkercount_col);
-	M_DrawPerfCount(&mobjthinkercount_col);
+// Update all metrics that are calculated on every tick.
+void PS_UpdateTickStats(void)
+{
+	if (cv_perfstats.value == 1 && cv_ps_samplesize.value > 1)
+	{
+		PS_UpdateRowHistories(gamelogicbrief_row, false);
+	}
+	if (cv_perfstats.value == 2)
+	{
+		if (PS_IsLevelActive())
+		{
+			ps_otherlogictime.value.p =
+				ps_tictime.value.p -
+				ps_playerthink_time.value.p -
+				ps_thinkertime.value.p -
+				ps_lua_thinkframe_time.value.p;
 
-	if (nothinkcount)
-		M_DrawPerfCount(&nothinkcount_col);
+			PS_CountThinkers();
+		}
 
-	M_DrawPerfCount(&detailed_thinkercount_col2);
+		if (cv_ps_samplesize.value > 1)
+		{
+			PS_UpdateRowHistories(gamelogic_rows, false);
+			PS_UpdateRowHistories(thinkercount_rows, false);
+			PS_UpdateRowHistories(misc_calls_rows, false);
+		}
+	}
+	if (cv_perfstats.value == 3 && cv_ps_samplesize.value > 1 && PS_IsLevelActive())
+	{
+		int i;
+		for (i = 0; i < thinkframe_hooks_length; i++)
+		{
+			PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false, false);
+		}
+	}
+	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
+	{
+		ps_tick_index++;
+		if (ps_tick_index >= cv_ps_samplesize.value)
+			ps_tick_index = 0;
+		if (ps_tick_samples_left)
+			ps_tick_samples_left--;
+	}
+}
 
-	if (M_HighResolution())
+static void PS_DrawDescriptorHeader(void)
+{
+	if (cv_ps_samplesize.value > 1)
 	{
-		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");
+		const char* descriptor_names[] = {
+			"average",
+			"standard deviation",
+			"minimum",
+			"maximum"
+		};
+		const boolean hires = PS_HighResolution();
+		char* str;
+		INT32 flags = V_MONOSPACE | V_ALLOWLOWERCASE;
+		int samples_left = max(ps_frame_samples_left, ps_tick_samples_left);
+		int x, y;
+
+		if (cv_perfstats.value == 3)
+		{
+			x = 2;
+			y = 0;
+		}
+		else
+		{
+			x = 20;
+			y = hires ? 5 : 2;
+		}
+
+		if (samples_left)
+		{
+			str = va("Samples needed for correct results: %d", samples_left);
+			flags |= V_REDMAP;
+		}
+		else
+		{
+			str = va("Showing the %s of %d samples.",
+					descriptor_names[cv_ps_descriptor.value - 1], cv_ps_samplesize.value);
+			flags |= V_GREENMAP;
+		}
 
-		draw_row = 15;
+		if (hires)
+			V_DrawSmallString(x, y, flags, str);
+		else
+			V_DrawThinString(x, y, flags, str);
 	}
-	else
+}
+
+static void PS_DrawRenderStats(void)
+{
+	const boolean hires = PS_HighResolution();
+	const int half_row = hires ? 5 : 4;
+	int x, y;
+
+	PS_DrawDescriptorHeader();
+
+	y = PS_DrawPerfRows(20, 10, V_YELLOWMAP, rendertime_rows);
+
+	PS_DrawPerfRows(20, y + half_row, V_GRAYMAP, gamelogicbrief_row);
+
+	if (PS_IsLevelActive())
 	{
-		draw_row = 10;
+		x = hires ? 115 : 90;
+		PS_DrawPerfRows(x, 10, V_BLUEMAP, commoncounter_rows);
+
+#ifdef HWRENDER
+		if (rendermode == render_opengl && cv_glbatching.value)
+		{
+			x = hires ? 200 : 155;
+			y = PS_DrawPerfRows(x, 10, V_PURPLEMAP, batchcount_rows);
+
+			x = hires ? 200 : 220;
+			y = hires ? y + half_row : 10;
+			PS_DrawPerfRows(x, y, V_PURPLEMAP, batchcalls_rows);
+		}
+#endif
 	}
+}
+
+static void PS_DrawGameLogicStats(void)
+{
+	const boolean hires = PS_HighResolution();
+	int x, y;
+
+	PS_DrawDescriptorHeader();
+
+	PS_DrawPerfRows(20, 10, V_YELLOWMAP, gamelogic_rows);
 
-	M_DrawPerfCount(&misc_calls_col);
+	x = hires ? 115 : 90;
+	PS_DrawPerfRows(x, 10, V_BLUEMAP, thinkercount_rows);
+
+	if (hires)
+		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");
+
+	x = hires ? 216 : 170;
+	y = hires ? 15 : 10;
+	PS_DrawPerfRows(x, y, V_PURPLEMAP, misc_calls_rows);
 }
 
-void M_DrawPerfStats(void)
+static void PS_DrawThinkFrameStats(void)
 {
 	char s[100];
+	int i;
+	// text writing position
+	int x = 2;
+	int y = 4;
+	UINT32 text_color;
+	char tempbuffer[LUA_IDSIZE];
+	char last_mod_name[LUA_IDSIZE];
+	last_mod_name[0] = '\0';
+
+	PS_DrawDescriptorHeader();
+
+	for (i = 0; i < thinkframe_hooks_length; i++)
+	{
 
-	PS_SetFrameTime();
+#define NEXT_ROW() \
+y += 4; \
+if (y > 192) \
+{ \
+	y = 4; \
+	x += 106; \
+	if (x > 214) \
+		break; \
+}
+
+		char* str = thinkframe_hooks[i].short_src;
+		char* tempstr = tempbuffer;
+		int len = (int)strlen(str);
+		char* str_ptr;
+		if (strcmp(".lua", str + len - 4) == 0)
+		{
+			str[len-4] = '\0'; // remove .lua at end
+			len -= 4;
+		}
+		// we locate the wad/pk3 name in the string and compare it to
+		// what we found on the previous iteration.
+		// if the name has changed, print it out on the screen
+		strcpy(tempstr, str);
+		str_ptr = strrchr(tempstr, '|');
+		if (str_ptr)
+		{
+			*str_ptr = '\0';
+			str = str_ptr + 1; // this is the name of the hook without the mod file name
+			str_ptr = strrchr(tempstr, PATHSEP[0]);
+			if (str_ptr)
+				tempstr = str_ptr + 1;
+			// tempstr should now point to the mod name, (wad/pk3) possibly truncated
+			if (strcmp(tempstr, last_mod_name) != 0)
+			{
+				strcpy(last_mod_name, tempstr);
+				len = (int)strlen(tempstr);
+				if (len > 25)
+					tempstr += len - 25;
+				snprintf(s, sizeof s - 1, "%s", tempstr);
+				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
+				NEXT_ROW()
+			}
+			text_color = V_YELLOWMAP;
+		}
+		else
+		{
+			// probably a standalone lua file
+			// cut off the folder if it's there
+			str_ptr = strrchr(tempstr, PATHSEP[0]);
+			if (str_ptr)
+				str = str_ptr + 1;
+			text_color = 0; // white
+		}
+		len = (int)strlen(str);
+		if (len > 20)
+			str += len - 20;
+		snprintf(s, sizeof s - 1, "%20s: %d", str,
+				PS_GetMetricScreenValue(&thinkframe_hooks[i].time_taken, true));
+		V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
+		NEXT_ROW()
+
+#undef NEXT_ROW
+
+	}
+}
 
+void M_DrawPerfStats(void)
+{
 	if (cv_perfstats.value == 1) // rendering
 	{
-		M_DrawRenderStats();
+		PS_UpdateFrameStats();
+		PS_DrawRenderStats();
 	}
 	else if (cv_perfstats.value == 2) // logic
 	{
-		M_DrawTickStats();
+		// PS_UpdateTickStats is called in TryRunTics, since otherwise it would miss
+		// tics when frame skips happen
+		PS_DrawGameLogicStats();
 	}
 	else if (cv_perfstats.value == 3) // lua thinkframe
 	{
-		if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+		if (!PS_IsLevelActive())
 			return;
-		if (vid.width < 640 || vid.height < 400) // low resolution
+		if (!PS_HighResolution())
 		{
-			// it's not gonna fit very well..
-			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not available for resolutions below 640x400");
+			// Low resolutions can't really use V_DrawSmallString that is used by thinkframe stats.
+			// A low-res version using V_DrawThinString could be implemented,
+			// but it would have much less space for information.
+			V_DrawThinString(80, 92, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Perfstats 3 is not available");
+			V_DrawThinString(80, 100, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "for resolutions below 640x400.");
 		}
-		else // high resolution
+		else
 		{
-			int i;
-			// text writing position
-			int x = 2;
-			int y = 4;
-			UINT32 text_color;
-			char tempbuffer[LUA_IDSIZE];
-			char last_mod_name[LUA_IDSIZE];
-			last_mod_name[0] = '\0';
-			for (i = 0; i < thinkframe_hooks_length; i++)
-			{
-				char* str = thinkframe_hooks[i].short_src;
-				char* tempstr = tempbuffer;
-				int len = (int)strlen(str);
-				char* str_ptr;
-				if (strcmp(".lua", str + len - 4) == 0)
-				{
-					str[len-4] = '\0'; // remove .lua at end
-					len -= 4;
-				}
-				// we locate the wad/pk3 name in the string and compare it to
-				// what we found on the previous iteration.
-				// if the name has changed, print it out on the screen
-				strcpy(tempstr, str);
-				str_ptr = strrchr(tempstr, '|');
-				if (str_ptr)
-				{
-					*str_ptr = '\0';
-					str = str_ptr + 1; // this is the name of the hook without the mod file name
-					str_ptr = strrchr(tempstr, PATHSEP[0]);
-					if (str_ptr)
-						tempstr = str_ptr + 1;
-					// tempstr should now point to the mod name, (wad/pk3) possibly truncated
-					if (strcmp(tempstr, last_mod_name) != 0)
-					{
-						strcpy(last_mod_name, tempstr);
-						len = (int)strlen(tempstr);
-						if (len > 25)
-							tempstr += len - 25;
-						snprintf(s, sizeof s - 1, "%s", tempstr);
-						V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
-						y += 4; // repeated code!
-						if (y > 192)
-						{
-							y = 4;
-							x += 106;
-							if (x > 214)
-								break;
-						}
-					}
-					text_color = V_YELLOWMAP;
-				}
-				else
-				{
-					// probably a standalone lua file
-					// cut off the folder if it's there
-					str_ptr = strrchr(tempstr, PATHSEP[0]);
-					if (str_ptr)
-						str = str_ptr + 1;
-					text_color = 0; // white
-				}
-				len = (int)strlen(str);
-				if (len > 20)
-					str += len - 20;
-				snprintf(s, sizeof s - 1, "%20s: %d", str, I_PreciseToMicros(thinkframe_hooks[i].time_taken));
-				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
-				y += 4; // repeated code!
-				if (y > 192)
-				{
-					y = 4;
-					x += 106;
-					if (x > 214)
-						break;
-				}
-			}
+			PS_DrawThinkFrameStats();
 		}
 	}
 }
+
+// remove and unallocate history from all metrics
+static void PS_ClearHistory(void)
+{
+	int i;
+
+	Z_FreeTag(PU_PERFSTATS);
+	// thinkframe hook metric history pointers need to be cleared manually
+	for (i = 0; i < thinkframe_hooks_length; i++)
+	{
+		thinkframe_hooks[i].time_taken.history = NULL;
+	}
+
+	ps_frame_index = ps_tick_index = 0;
+	// PS_UpdateMetricHistory will set these correctly when it runs
+	ps_frame_samples_left = ps_tick_samples_left = 0;
+}
+
+void PS_PerfStats_OnChange(void)
+{
+	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
+		PS_ClearHistory();
+}
+
+void PS_SampleSize_OnChange(void)
+{
+	if (cv_ps_samplesize.value > 1)
+		PS_ClearHistory();
+}
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
index 71208fbc19f95d886e7cce17c1982ff61de0089f..3ff0e6c6b014b4c24099be602e507d3c07cfcc43 100644
--- a/src/m_perfstats.h
+++ b/src/m_perfstats.h
@@ -16,26 +16,45 @@
 #include "lua_script.h"
 #include "p_local.h"
 
-extern precise_t ps_tictime;
-
-extern precise_t ps_playerthink_time;
-extern precise_t ps_thinkertime;
-
-extern precise_t ps_thlist_times[];
-
-extern int       ps_checkposition_calls;
-
-extern precise_t ps_lua_thinkframe_time;
-extern int       ps_lua_mobjhooks;
+typedef struct
+{
+	union {
+		precise_t p;
+		INT32 i;
+	} value;
+	void *history;
+} ps_metric_t;
 
 typedef struct
 {
-	precise_t time_taken;
+	ps_metric_t time_taken;
 	char short_src[LUA_IDSIZE];
 } ps_hookinfo_t;
 
+#define PS_START_TIMING(metric) metric.value.p = I_GetPreciseTime()
+#define PS_STOP_TIMING(metric) metric.value.p = I_GetPreciseTime() - metric.value.p
+
+extern ps_metric_t ps_tictime;
+
+extern ps_metric_t ps_playerthink_time;
+extern ps_metric_t ps_thinkertime;
+
+extern ps_metric_t ps_thlist_times[];
+
+extern ps_metric_t ps_checkposition_calls;
+
+extern ps_metric_t ps_lua_thinkframe_time;
+extern ps_metric_t ps_lua_mobjhooks;
+
+extern ps_metric_t ps_otherlogictime;
+
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
 
+void PS_UpdateTickStats(void);
+
 void M_DrawPerfStats(void);
 
+void PS_PerfStats_OnChange(void);
+void PS_SampleSize_OnChange(void);
+
 #endif
diff --git a/src/p_enemy.c b/src/p_enemy.c
index e79baa67987905da9d55ef8a190ab4a1eff82667..1216bb09569ad812f2a7a3030109b84ba2c3f7db 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3518,9 +3518,7 @@ void A_Scream(mobj_t *actor)
 	if (LUA_CallAction(A_SCREAM, actor))
 		return;
 
-	if (actor->tracer && (actor->tracer->type == MT_SHELL || actor->tracer->type == MT_FIREBALL))
-		S_StartScreamSound(actor, sfx_mario2);
-	else if (actor->info->deathsound)
+	if (actor->info->deathsound && !S_SoundPlaying(actor, sfx_mario2))
 		S_StartScreamSound(actor, actor->info->deathsound);
 }
 
@@ -5284,7 +5282,7 @@ void A_OverlayThink(mobj_t *actor)
 		actor->z = actor->target->z + actor->target->height - mobjinfo[actor->type].height  - ((var2>>16) ? -1 : 1)*(var2&0xFFFF)*FRACUNIT;
 	else
 		actor->z = actor->target->z + ((var2>>16) ? -1 : 1)*(var2&0xFFFF)*FRACUNIT;
-	actor->angle = actor->target->angle + actor->movedir;
+	actor->angle = (actor->target->player ? actor->target->player->drawangle : actor->target->angle) + actor->movedir;
 	actor->eflags = actor->target->eflags;
 
 	actor->momx = actor->target->momx;
diff --git a/src/p_floor.c b/src/p_floor.c
index f421b550f2039d7e547b1f63fb48c80f564e6978..7f1a7c2e0024fbbf16488b37e2d8b099b3abdf44 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -988,6 +988,7 @@ static mobj_t *SearchMarioNode(msecnode_t *node)
 		case MT_THUNDERCOIN_ORB:
 		case MT_IVSP:
 		case MT_SUPERSPARK:
+		case MT_BOXSPARKLE:
 		case MT_RAIN:
 		case MT_SNOWFLAKE:
 		case MT_SPLISH:
diff --git a/src/p_inter.c b/src/p_inter.c
index d7f0fb8a2818fcf5c15e9fb9d8053a08e229d645..d84cb8ce57f846f9495fa90f3657e9a4eb8fd921 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2385,7 +2385,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 	mobj_t *mo;
 
 	if (inflictor && (inflictor->type == MT_SHELL || inflictor->type == MT_FIREBALL))
-		P_SetTarget(&target->tracer, inflictor);
+		S_StartScreamSound(target, sfx_mario2);
 
 	if (!(maptol & TOL_NIGHTS) && G_IsSpecialStage(gamemap) && target->player && target->player->nightstime > 6)
 		target->player->nightstime = 6; // Just let P_Ticker take care of the rest.
@@ -3645,7 +3645,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			return true;
 		}
 
-		if (!force && inflictor && inflictor->flags & MF_FIRE)
+		if (!force && inflictor && inflictor->flags & MF_FIRE && !(damagetype && damagetype != DMG_FIRE))
 		{
 			if (player->powers[pw_shield] & SH_PROTECTFIRE)
 				return false; // Invincible to fire objects
diff --git a/src/p_map.c b/src/p_map.c
index e55bebb9a74d9a3428b270398570373b593d8fc8..836e75c4e8bb08937e79fadf5f572125c9841b90 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -1156,7 +1156,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		else
 			thing->z = tmthing->z + tmthing->height + FixedMul(FRACUNIT, tmthing->scale);
 		if (thing->flags & MF_SHOOTABLE)
-			P_DamageMobj(thing, tmthing, tmthing, 1, 0);
+			P_DamageMobj(thing, tmthing, tmthing, 1, DMG_SPIKE);
 		return true;
 	}
 
@@ -2029,7 +2029,7 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 	subsector_t *newsubsec;
 	boolean blockval = true;
 
-	ps_checkposition_calls++;
+	ps_checkposition_calls.value.i++;
 
 	I_Assert(thing != NULL);
 #ifdef PARANOIA
diff --git a/src/p_mobj.c b/src/p_mobj.c
index a0a48ab6d4193903cfd36accee61b3d69dadc9e9..da0bde867e6ddb3efe40c311e5a2e9fe1c7bec7b 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -78,7 +78,7 @@ void P_AddCachedAction(mobj_t *mobj, INT32 statenum)
 //
 // P_SetupStateAnimation
 //
-FUNCINLINE static ATTRINLINE void P_SetupStateAnimation(mobj_t *mobj, state_t *st)
+static void P_SetupStateAnimation(mobj_t *mobj, state_t *st)
 {
 	INT32 animlength = (mobj->sprite == SPR_PLAY && mobj->skin)
 		? (INT32)(((skin_t *)mobj->skin)->sprites[mobj->sprite2].numframes) - 1
@@ -1688,7 +1688,7 @@ static void P_PushableCheckBustables(mobj_t *mo)
 			if (!(rover->flags & FF_BUSTUP))
 				continue;
 
-			if (!(rover->specialflags & FS_PUSHABLES))
+			if (!(rover->bustflags & FB_PUSHABLES))
 				continue;
 
 			if (rover->master->frontsector->crumblestate != CRUMBLE_NONE)
@@ -1698,7 +1698,7 @@ static void P_PushableCheckBustables(mobj_t *mo)
 			bottomheight = P_GetFOFBottomZ(mo, node->m_sector, rover, mo->x, mo->y, NULL);
 
 			// Height checks
-			if (rover->specialflags & FS_ONLYBOTTOM)
+			if (rover->bustflags & FB_ONLYBOTTOM)
 			{
 				if (mo->z + mo->momz + mo->height < bottomheight)
 					continue;
@@ -1740,7 +1740,7 @@ static void P_PushableCheckBustables(mobj_t *mo)
 			EV_CrumbleChain(NULL, rover); // node->m_sector
 
 			// Run a linedef executor??
-			if (rover->specialflags & FS_EXECUTOR)
+			if (rover->bustflags & FB_EXECUTOR)
 				P_LinedefExecute(rover->busttag, mo, node->m_sector);
 
 			goto bustupdone;
@@ -3241,8 +3241,8 @@ void P_MobjCheckWater(mobj_t *mobj)
 		 || ((rover->flags & FF_BLOCKOTHERS) && !mobj->player)))
 			continue;
 
-		topheight    = P_GetFFloorTopZAt   (rover, mobj->x, mobj->y);
-		bottomheight = P_GetFFloorBottomZAt(rover, mobj->x, mobj->y);
+		topheight = P_GetSpecialTopZ(mobj, sectors + rover->secnum, sector);
+		bottomheight = P_GetSpecialBottomZ(mobj, sectors + rover->secnum, sector);
 
 		if (mobj->eflags & MFE_VERTICALFLIP)
 		{
@@ -6842,7 +6842,7 @@ void P_RunOverlays(void)
 
 		mo->eflags = (mo->eflags & ~MFE_VERTICALFLIP) | (mo->target->eflags & MFE_VERTICALFLIP);
 		mo->scale = mo->destscale = mo->target->scale;
-		mo->angle = mo->target->angle + mo->movedir;
+		mo->angle = (mo->target->player ? mo->target->player->drawangle : mo->target->angle) + mo->movedir;
 
 		if (!(mo->state->frame & FF_ANIMATE))
 			zoffs = FixedMul(((signed)mo->state->var2)*FRACUNIT, mo->scale);
diff --git a/src/p_setup.c b/src/p_setup.c
index addd7d07052eeb0dfe0ef2f8f9384ffa3e9ebff9..017a8e18baf81adb0e47f5d110097bd39bd27840 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -3220,8 +3220,6 @@ static void P_ConvertBinaryMap(void)
 		case 76: //Make FOF bouncy
 			lines[i].args[0] = tag;
 			lines[i].args[1] = P_AproxDistance(lines[i].dx, lines[i].dy) >> FRACBITS;
-			if (lines[i].flags & ML_BOUNCY)
-				lines[i].args[2] = 1;
 			break;
 		case 100: //FOF: solid, opaque, shadowcasting
 		case 101: //FOF: solid, opaque, non-shadowcasting
diff --git a/src/p_spec.c b/src/p_spec.c
index 76404af3e50ddef16c4ab3024300fd9da0b6e121..cfb68aaf7a4206081850c5e8f61c0752e211d9ee 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2041,7 +2041,7 @@ void P_SwitchWeather(INT32 weathernum)
 				continue; // not a precipmobj thinker
 			precipmobj = (precipmobj_t *)think;
 
-			if (weathernum == (PRECIP_RAIN || PRECIP_STORM || PRECIP_STORM_NOSTRIKES)) // Snow To Rain
+			if (weathernum == PRECIP_RAIN || weathernum == PRECIP_STORM || weathernum == PRECIP_STORM_NOSTRIKES) // Snow To Rain
 			{
 				precipmobj->flags = mobjinfo[MT_RAIN].flags;
 				st = &states[mobjinfo[MT_RAIN].spawnstate];
@@ -6030,10 +6030,6 @@ static void P_MakeFOFBouncy(line_t *paramline, line_t *masterline)
 			rover->flags |= FF_BOUNCY;
 			rover->spawnflags |= FF_BOUNCY;
 			rover->bouncestrength = (paramline->args[1]<< FRACBITS)/100;
-			if (paramline->args[2])
-				rover->specialflags |= FS_DAMPEN;
-			else
-				rover->specialflags &= ~FS_DAMPEN;
 			CheckForBouncySector = true;
 			break;
 		}
@@ -6659,7 +6655,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 			case 254: // Bustable block
 			{
 				UINT8 busttype = BT_REGULAR;
-				ffloorspecialflags_e bustflags = 0;
+				ffloorbustflags_e bustflags = 0;
 
 				ffloorflags = FF_EXISTS|FF_BLOCKOTHERS|FF_RENDERALL|FF_BUSTUP;
 
@@ -6682,15 +6678,15 @@ void P_SpawnSpecials(boolean fromnetsave)
 
 				//Flags
 				if (lines[i].args[3] & TMFB_PUSHABLES)
-					bustflags |= FS_PUSHABLES;
+					bustflags |= FB_PUSHABLES;
 				if (lines[i].args[3] & TMFB_EXECUTOR)
-					bustflags |= FS_EXECUTOR;
+					bustflags |= FB_EXECUTOR;
 				if (lines[i].args[3] & TMFB_ONLYBOTTOM)
-					bustflags |= FS_ONLYBOTTOM;
+					bustflags |= FB_ONLYBOTTOM;
 				if (lines[i].args[3] & TMFB_SPLAT)
 					ffloorflags |= FF_SPLAT;
 
-				if (busttype != BT_TOUCH || bustflags & FS_ONLYBOTTOM)
+				if (busttype != BT_TOUCH || bustflags & FB_ONLYBOTTOM)
 					ffloorflags |= FF_BLOCKPLAYER;
 
 				TAG_ITER_SECTORS(lines[i].args[0], s)
@@ -6698,8 +6694,8 @@ void P_SpawnSpecials(boolean fromnetsave)
 					ffloor_t *fflr = P_AddFakeFloor(&sectors[s], lines[i].frontsector, lines + i, lines[i].args[1], ffloorflags, secthinkers);
 					if (!fflr)
 						continue;
+					fflr->bustflags = bustflags;
 					fflr->busttype = busttype;
-					fflr->specialflags = bustflags;
 					fflr->busttag = lines[i].args[4];
 				}
 				break;
@@ -6759,12 +6755,12 @@ void P_SpawnSpecials(boolean fromnetsave)
 							}
 
 							if (lines[i].args[3] & TMFB_ONLYBOTTOM)
-								fflr->specialflags |= FS_ONLYBOTTOM;
+								fflr->bustflags |= FB_ONLYBOTTOM;
 							if (lines[i].flags & ML_EFFECT4)
-								fflr->specialflags |= FS_PUSHABLES;
+								fflr->bustflags |= FB_PUSHABLES;
 							if (lines[i].flags & ML_EFFECT5)
 							{
-								fflr->specialflags |= FS_EXECUTOR;
+								fflr->bustflags |= FB_EXECUTOR;
 								fflr->busttag = P_AproxDistance(lines[i].dx, lines[i].dy) >> FRACBITS;
 							}
 						}
@@ -7096,7 +7092,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 			case 74: // Make FOF bustable
 			{
 				UINT8 busttype = BT_REGULAR;
-				ffloorspecialflags_e bustflags = 0;
+				ffloorbustflags_e bustflags = 0;
 
 				if (!udmf)
 					break;
@@ -7118,11 +7114,11 @@ void P_SpawnSpecials(boolean fromnetsave)
 				}
 
 				if (lines[i].args[2] & TMFB_PUSHABLES)
-					bustflags |= FS_PUSHABLES;
+					bustflags |= FB_PUSHABLES;
 				if (lines[i].args[2] & TMFB_EXECUTOR)
-					bustflags |= FS_EXECUTOR;
+					bustflags |= FB_EXECUTOR;
 				if (lines[i].args[2] & TMFB_ONLYBOTTOM)
-					bustflags |= FS_ONLYBOTTOM;
+					bustflags |= FB_ONLYBOTTOM;
 
 				TAG_ITER_LINES(lines[i].args[0], l)
 				{
@@ -7140,9 +7136,8 @@ void P_SpawnSpecials(boolean fromnetsave)
 
 							rover->flags |= FF_BUSTUP;
 							rover->spawnflags |= FF_BUSTUP;
+							rover->bustflags = bustflags;
 							rover->busttype = busttype;
-							rover->specialflags &= ~FS_BUSTMASK;
-							rover->specialflags |= bustflags;
 							rover->busttag = lines[i].args[3];
 							CheckForBustableBlocks = true;
 							break;
diff --git a/src/p_tick.c b/src/p_tick.c
index d7357eb828508b49cc60b9e6d80beedab282982c..55a16fd81cb8b582fd8ab53c2d64c4f18809b8e2 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -323,7 +323,7 @@ static inline void P_RunThinkers(void)
 	size_t i;
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
-		ps_thlist_times[i] = I_GetPreciseTime();
+		PS_START_TIMING(ps_thlist_times[i]);
 		for (currentthinker = thlist[i].next; currentthinker != &thlist[i]; currentthinker = currentthinker->next)
 		{
 #ifdef PARANOIA
@@ -331,7 +331,7 @@ static inline void P_RunThinkers(void)
 #endif
 			currentthinker->function.acp1(currentthinker);
 		}
-		ps_thlist_times[i] = I_GetPreciseTime() - ps_thlist_times[i];
+		PS_STOP_TIMING(ps_thlist_times[i]);
 	}
 
 }
@@ -487,7 +487,7 @@ static inline void P_DoSpecialStageStuff(void)
 					continue;
 
 				// If in water, deplete timer 6x as fast.
-				if (players[i].mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(players[i].powers[pw_shield] & SH_PROTECTWATER))
+				if (players[i].mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(players[i].powers[pw_shield] & ((players[i].mo->eflags & MFE_TOUCHLAVA) ? SH_PROTECTFIRE : SH_PROTECTWATER)))
 					players[i].nightstime -= 5;
 				if (--players[i].nightstime > 6)
 				{
@@ -653,16 +653,16 @@ void P_Ticker(boolean run)
 			}
 		}
 
-		ps_lua_mobjhooks = 0;
-		ps_checkposition_calls = 0;
+		ps_lua_mobjhooks.value.i = 0;
+		ps_checkposition_calls.value.i = 0;
 
 		LUA_HOOK(PreThinkFrame);
 
-		ps_playerthink_time = I_GetPreciseTime();
+		PS_START_TIMING(ps_playerthink_time);
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerThink(&players[i]);
-		ps_playerthink_time = I_GetPreciseTime() - ps_playerthink_time;
+		PS_STOP_TIMING(ps_playerthink_time);
 	}
 
 	// Keep track of how long they've been playing!
@@ -677,18 +677,18 @@ void P_Ticker(boolean run)
 
 	if (run)
 	{
-		ps_thinkertime = I_GetPreciseTime();
+		PS_START_TIMING(ps_thinkertime);
 		P_RunThinkers();
-		ps_thinkertime = I_GetPreciseTime() - ps_thinkertime;
+		PS_STOP_TIMING(ps_thinkertime);
 
 		// Run any "after all the other thinkers" stuff
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerAfterThink(&players[i]);
 
-		ps_lua_thinkframe_time = I_GetPreciseTime();
+		PS_START_TIMING(ps_lua_thinkframe_time);
 		LUA_HookThinkFrame();
-		ps_lua_thinkframe_time = I_GetPreciseTime() - ps_lua_thinkframe_time;
+		PS_STOP_TIMING(ps_lua_thinkframe_time);
 	}
 
 	// Run shield positioning
diff --git a/src/p_user.c b/src/p_user.c
index 9f373750152a412d61405b3b0f98e18ab29475e6..9921ed078742bd3850f76ef4fb104d066efd3fa0 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -777,7 +777,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	UINT8 oldmare, oldmarelap, oldmarebonuslap;
 
 	// Bots can't be NiGHTSerized, silly!1 :P
-	if (player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN)
+	if (player->bot)
 		return;
 
 	if (player->powers[pw_carry] != CR_NIGHTSMODE)
@@ -969,9 +969,6 @@ pflags_t P_GetJumpFlags(player_t *player)
 //
 boolean P_PlayerInPain(player_t *player)
 {
-	// If the player doesn't have a mobj, it can't be in pain.
-	if (!player->mo)
-		return false;
 	// no silly, sliding isn't pain
 	if (!(player->pflags & PF_SLIDING) && player->mo->state == &states[player->mo->info->painstate] && player->powers[pw_flashing])
 		return true;
@@ -1191,9 +1188,9 @@ void P_GivePlayerRings(player_t *player, INT32 num_rings)
 {
 	if (!player)
 		return;
-	
-	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
-		player = player->botleader;
+
+	if (player->bot)
+		player = &players[consoleplayer];
 
 	if (!player->mo)
 		return;
@@ -1237,8 +1234,8 @@ void P_GivePlayerSpheres(player_t *player, INT32 num_spheres)
 	if (!player)
 		return;
 
-	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
-		player = player->botleader;
+	if (player->bot)
+		player = &players[consoleplayer];
 
 	if (!player->mo)
 		return;
@@ -1264,8 +1261,8 @@ void P_GivePlayerLives(player_t *player, INT32 numlives)
 	if (!player)
 		return;
 
-	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
-		player = player->botleader;
+	if (player->bot)
+		player = &players[consoleplayer];
 
 	if (gamestate == GS_LEVEL)
 	{
@@ -2659,7 +2656,7 @@ static void P_CheckBustableBlocks(player_t *player)
 			}
 
 			// Height checks
-			if (rover->specialflags & FS_ONLYBOTTOM)
+			if (rover->bustflags & FB_ONLYBOTTOM)
 			{
 				if (player->mo->z + player->mo->momz + player->mo->height < bottomheight)
 					continue;
@@ -2713,7 +2710,7 @@ static void P_CheckBustableBlocks(player_t *player)
 			EV_CrumbleChain(NULL, rover); // node->m_sector
 
 			// Run a linedef executor??
-			if (rover->specialflags & FS_EXECUTOR)
+			if (rover->bustflags & FB_EXECUTOR)
 				P_LinedefExecute(rover->busttag, player->mo, node->m_sector);
 
 			goto bustupdone;
@@ -2769,10 +2766,6 @@ static void P_CheckBouncySectors(player_t *player)
 			{
 				rover->flags |= FF_BOUNCY;
 				rover->bouncestrength = P_AproxDistance(rover->master->dx, rover->master->dy)/100;
-				if (rover->master->flags & ML_BOUNCY)
-					rover->specialflags |= FS_DAMPEN;
-				else
-					rover->specialflags &= ~FS_DAMPEN;
 			}
 
 			if (!(rover->flags & FF_BOUNCY))
@@ -2792,17 +2785,9 @@ static void P_CheckBouncySectors(player_t *player)
 			{
 				player->mo->momx = -FixedMul(player->mo->momx,rover->bouncestrength);
 				player->mo->momy = -FixedMul(player->mo->momy,rover->bouncestrength);
-
-				if (player->pflags & PF_SPINNING)
-				{
-					player->pflags &= ~PF_SPINNING;
-					player->pflags |= P_GetJumpFlags(player);
-					player->pflags |= PF_THOKKED;
-				}
 			}
 			else
 			{
-				fixed_t newmom;
 				pslope_t *slope = (abs(oldz - topheight) < abs(oldz + player->mo->height - bottomheight)) ? *rover->t_slope : *rover->b_slope;
 
 				momentum.x = player->mo->momx;
@@ -2812,53 +2797,28 @@ static void P_CheckBouncySectors(player_t *player)
 				if (slope)
 					P_ReverseQuantizeMomentumToSlope(&momentum, slope);
 
-				newmom = momentum.z = -FixedMul(momentum.z,rover->bouncestrength)/2;
+				momentum.z = -FixedMul(momentum.z,rover->bouncestrength)/2;
 
-				if (abs(newmom) < (rover->bouncestrength*2))
+				if (abs(momentum.z) < (rover->bouncestrength*2))
 					goto bouncydone;
 
-				if (!(rover->specialflags & FS_DAMPEN))
-				{
-					if (newmom > 0)
-					{
-						if (newmom < 8*FRACUNIT)
-							newmom = 8*FRACUNIT;
-					}
-					else if (newmom < 0)
-					{
-						if (newmom > -8*FRACUNIT)
-							newmom = -8*FRACUNIT;
-					}
-				}
-
-				if (newmom > P_GetPlayerHeight(player)/2)
-					newmom = P_GetPlayerHeight(player)/2;
-				else if (newmom < -P_GetPlayerHeight(player)/2)
-					newmom = -P_GetPlayerHeight(player)/2;
-
-				momentum.z = newmom*2;
+				if (momentum.z > FixedMul(24*FRACUNIT, player->mo->scale)) //half of the default player height
+					momentum.z = FixedMul(24*FRACUNIT, player->mo->scale);
+				else if (momentum.z < -FixedMul(24*FRACUNIT, player->mo->scale))
+					momentum.z = -FixedMul(24*FRACUNIT, player->mo->scale);
 
 				if (slope)
 					P_QuantizeMomentumToSlope(&momentum, slope);
 
 				player->mo->momx = momentum.x;
 				player->mo->momy = momentum.y;
-				player->mo->momz = momentum.z/2;
+				player->mo->momz = momentum.z;
 
 				if (player->pflags & PF_SPINNING)
 				{
-					player->pflags &= ~PF_SPINNING;
-					player->pflags |= P_GetJumpFlags(player);
 					player->pflags |= PF_THOKKED;
 				}
 			}
-
-			if ((player->pflags & PF_SPINNING) && player->speed < FixedMul(1<<FRACBITS, player->mo->scale) && player->mo->momz)
-			{
-				player->pflags &= ~PF_SPINNING;
-				player->pflags |= P_GetJumpFlags(player);
-			}
-
 			goto bouncydone;
 		}
 	}
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 5acd4e70c5fa083eaf675055be9d623e15f014e3..b8559d39e54cb5debb00878487bae7b9b7a5a29c 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -804,7 +804,7 @@ static void R_AddPolyObjects(subsector_t *sub)
 	}
 
 	// for render stats
-	ps_numpolyobjects += numpolys;
+	ps_numpolyobjects.value.i += numpolys;
 
 	// sort polyobjects
 	R_SortPolyObjects(sub);
@@ -1239,7 +1239,7 @@ void R_RenderBSPNode(INT32 bspnum)
 	node_t *bsp;
 	INT32 side;
 
-	ps_numbspcalls++;
+	ps_numbspcalls.value.i++;
 
 	while (!(bspnum & NF_SUBSECTOR))  // Found a subsector?
 	{
diff --git a/src/r_defs.h b/src/r_defs.h
index cb21d50430bc2dff87f756529b65fef4d99ea523..4cc7d2e96c0d7aaa04134b240b1500e68cbb96fd 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -154,12 +154,10 @@ typedef enum
 
 typedef enum
 {
-	FS_PUSHABLES   = 0x1, // FF_BUSTABLE: Bustable by pushables
-	FS_EXECUTOR    = 0x2, // FF_BUSTABLE: Trigger linedef executor
-	FS_ONLYBOTTOM  = 0x4, // FF_BUSTABLE: Only bustable from below
-	FS_BUSTMASK    = 0x7,
-	FS_DAMPEN      = 0x8, // FF_BOUNCY:   Dampen bounce
-} ffloorspecialflags_e;
+	FB_PUSHABLES   = 0x1, // Bustable by pushables
+	FB_EXECUTOR    = 0x2, // Trigger linedef executor
+	FB_ONLYBOTTOM  = 0x4, // Only bustable from below
+} ffloorbustflags_e;
 
 typedef enum
 {
@@ -201,10 +199,8 @@ typedef struct ffloor_s
 	INT32 alpha;
 	tic_t norender; // for culling
 
-	// Flags that are only relevant for special ffloor types
-	ffloorspecialflags_e specialflags;
-
 	// Only relevant for FF_BUSTUP
+	ffloorbustflags_e bustflags;
 	UINT8 busttype;
 	INT16 busttag;
 
diff --git a/src/r_draw8_npo2.c b/src/r_draw8_npo2.c
index 71ec999486497c562214620fcd0510219daf2ce0..2433cb4024295401017fae6b08394a7db7d0d0df 100644
--- a/src/r_draw8_npo2.c
+++ b/src/r_draw8_npo2.c
@@ -666,7 +666,6 @@ void R_DrawTiltedSplat_NPO2_8(void)
 			for (; width != 0; width--)
 			{
 				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
-				val = source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)];
 				// Lactozilla: Non-powers-of-two
 				{
 					fixed_t x = (((fixed_t)u) >> FRACBITS);
diff --git a/src/r_main.c b/src/r_main.c
index 17e124cb9271c714a115e37705ee6ec11ed60faa..8729b5dcb36ccedb8aed999dbf3afcb60e0faf04 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -101,21 +101,22 @@ extracolormap_t *extra_colormaps = NULL;
 
 // Render stats
 precise_t ps_prevframetime = 0;
-precise_t ps_rendercalltime = 0;
-precise_t ps_uitime = 0;
-precise_t ps_swaptime = 0;
+ps_metric_t ps_rendercalltime = {0};
+ps_metric_t ps_otherrendertime = {0};
+ps_metric_t ps_uitime = {0};
+ps_metric_t ps_swaptime = {0};
 
-precise_t ps_bsptime = 0;
+ps_metric_t ps_bsptime = {0};
 
-precise_t ps_sw_spritecliptime = 0;
-precise_t ps_sw_portaltime = 0;
-precise_t ps_sw_planetime = 0;
-precise_t ps_sw_maskedtime = 0;
+ps_metric_t ps_sw_spritecliptime = {0};
+ps_metric_t ps_sw_portaltime = {0};
+ps_metric_t ps_sw_planetime = {0};
+ps_metric_t ps_sw_maskedtime = {0};
 
-int ps_numbspcalls = 0;
-int ps_numsprites = 0;
-int ps_numdrawnodes = 0;
-int ps_numpolyobjects = 0;
+ps_metric_t ps_numbspcalls = {0};
+ps_metric_t ps_numsprites = {0};
+ps_metric_t ps_numdrawnodes = {0};
+ps_metric_t ps_numpolyobjects = {0};
 
 static CV_PossibleValue_t drawdist_cons_t[] = {
 	{256, "256"},	{512, "512"},	{768, "768"},
@@ -1496,11 +1497,11 @@ void R_RenderPlayerView(player_t *player)
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
-	ps_numbspcalls = ps_numpolyobjects = ps_numdrawnodes = 0;
-	ps_bsptime = I_GetPreciseTime();
+	ps_numbspcalls.value.i = ps_numpolyobjects.value.i = ps_numdrawnodes.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 	R_RenderBSPNode((INT32)numnodes - 1);
-	ps_bsptime = I_GetPreciseTime() - ps_bsptime;
-	ps_numsprites = visspritecount;
+	PS_STOP_TIMING(ps_bsptime);
+	ps_numsprites.value.i = visspritecount;
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
 	mytotal += mycount; // 64bit add
@@ -1510,9 +1511,9 @@ void R_RenderPlayerView(player_t *player)
 //profile stuff ---------------------------------------------------------
 	Mask_Post(&masks[nummasks - 1]);
 
-	ps_sw_spritecliptime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_spritecliptime);
 	R_ClipSprites(drawsegs, NULL);
-	ps_sw_spritecliptime = I_GetPreciseTime() - ps_sw_spritecliptime;
+	PS_STOP_TIMING(ps_sw_spritecliptime);
 
 
 	// Add skybox portals caused by sky visplanes.
@@ -1520,7 +1521,7 @@ void R_RenderPlayerView(player_t *player)
 		Portal_AddSkyboxPortals();
 
 	// Portal rendering. Hijacks the BSP traversal.
-	ps_sw_portaltime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_portaltime);
 	if (portal_base)
 	{
 		portal_t *portal;
@@ -1560,17 +1561,17 @@ void R_RenderPlayerView(player_t *player)
 			Portal_Remove(portal);
 		}
 	}
-	ps_sw_portaltime = I_GetPreciseTime() - ps_sw_portaltime;
+	PS_STOP_TIMING(ps_sw_portaltime);
 
-	ps_sw_planetime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_planetime);
 	R_DrawPlanes();
-	ps_sw_planetime = I_GetPreciseTime() - ps_sw_planetime;
+	PS_STOP_TIMING(ps_sw_planetime);
 
 	// draw mid texture and sprite
 	// And now 3D floors/sides!
-	ps_sw_maskedtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_maskedtime);
 	R_DrawMasked(masks, nummasks);
-	ps_sw_maskedtime = I_GetPreciseTime() - ps_sw_maskedtime;
+	PS_STOP_TIMING(ps_sw_maskedtime);
 
 	free(masks);
 }
diff --git a/src/r_main.h b/src/r_main.h
index f81447c456b8866cf9a0c1347c32a20ca7b7f057..5f3bed9803102cb7592ac755ef09f30870cc3f9f 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -17,6 +17,7 @@
 #include "d_player.h"
 #include "r_data.h"
 #include "r_textures.h"
+#include "m_perfstats.h" // ps_metric_t
 
 //
 // POV related.
@@ -79,21 +80,22 @@ boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixe
 // Render stats
 
 extern precise_t ps_prevframetime;// time when previous frame was rendered
-extern precise_t ps_rendercalltime;
-extern precise_t ps_uitime;
-extern precise_t ps_swaptime;
-
-extern precise_t ps_bsptime;
-
-extern precise_t ps_sw_spritecliptime;
-extern precise_t ps_sw_portaltime;
-extern precise_t ps_sw_planetime;
-extern precise_t ps_sw_maskedtime;
-
-extern int ps_numbspcalls;
-extern int ps_numsprites;
-extern int ps_numdrawnodes;
-extern int ps_numpolyobjects;
+extern ps_metric_t ps_rendercalltime;
+extern ps_metric_t ps_otherrendertime;
+extern ps_metric_t ps_uitime;
+extern ps_metric_t ps_swaptime;
+
+extern ps_metric_t ps_bsptime;
+
+extern ps_metric_t ps_sw_spritecliptime;
+extern ps_metric_t ps_sw_portaltime;
+extern ps_metric_t ps_sw_planetime;
+extern ps_metric_t ps_sw_maskedtime;
+
+extern ps_metric_t ps_numbspcalls;
+extern ps_metric_t ps_numsprites;
+extern ps_metric_t ps_numdrawnodes;
+extern ps_metric_t ps_numpolyobjects;
 
 //
 // REFRESH - the actual rendering functions.
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index a9b4a2b8fb701d45f7f21deefee1f75c768ee797..dae3a7b53a6cab88c151e7d1605d99fc5972c321 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -227,8 +227,8 @@ void RotatedPatch_DoRotation(rotsprite_t *rotsprite, patch_t *patch, INT32 angle
 
 	ox = (newwidth / 2) + (leftoffset - xpivot);
 	oy = (newheight / 2) + (patch->topoffset - ypivot);
-	width = (maxx - minx);
-	height = (maxy - miny);
+	width = (maxx+1 - minx);
+	height = (maxy+1 - miny);
 
 	if ((unsigned)(width * height) != size)
 	{
diff --git a/src/r_plane.c b/src/r_plane.c
index 50b76abdef98842b07691b53fc3a62c1c57049de..88abed44a48c53a34fa1e42bfdfc44628f7d846b 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -318,7 +318,7 @@ static visplane_t *new_visplane(unsigned hash)
 	visplane_t *check = freetail;
 	if (!check)
 	{
-		check = calloc(2, sizeof (*check));
+		check = malloc(sizeof (*check));
 		if (check == NULL) I_Error("%s: Out of memory", "new_visplane"); // FIXME: ugly
 	}
 	else
diff --git a/src/r_things.c b/src/r_things.c
index ea57e4086015cf3a9e8254eac0fe633766dda3f0..bed71a6d791f1c5dbc41a0f517560dc5387c1b08 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -2745,7 +2745,7 @@ static drawnode_t *R_CreateDrawNode(drawnode_t *link)
 	node->ffloor = NULL;
 	node->sprite = NULL;
 
-	ps_numdrawnodes++;
+	ps_numdrawnodes.value.i++;
 	return node;
 }
 
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 2ec28ebc824c561eb14547757a916e8a60eb39a7..ccec37093698edbcc7865702fea0138db5996749 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -2163,7 +2163,13 @@ precise_t I_GetPreciseTime(void)
 
 int I_PreciseToMicros(precise_t d)
 {
-	return (int)(d / (timer_frequency / 1000000.0));
+	// d is going to be converted into a double. So remove the highest bits
+	// to avoid loss of precision in the lower bits, for the (probably rare) case
+	// that the higher bits are actually used.
+	d &= ((precise_t)1 << 53) - 1; // The mantissa of a double can handle 53 bits at most.
+	// The resulting double from the calculation is converted first to UINT64 to avoid overflow,
+	// which is undefined behaviour when converting floating point values to integers.
+	return (int)(UINT64)(d / (timer_frequency / 1000000.0));
 }
 
 //
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 97e4a7214b420a2b4a3764eb5b505f500f161799..ed766ff23dfb79395bbb5648211be73c45fc34b6 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -409,11 +409,6 @@ void I_UpdateMouseGrab(void)
 		SDLdoGrabMouse();
 }
 
-boolean I_GetMouseGrab(void)
-{
-	return (boolean)SDL_GetWindowGrab(window);
-}
-
 void I_SetMouseGrab(boolean grab)
 {
 	if (grab)
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 784666ad49e1db0492e4fa98d72dbba739893574..a328d669e51169ba18d8ae8d046774d3b298eed3 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -2037,9 +2037,8 @@ static void ST_drawNiGHTSHUD(void)
 		else
 			numbersize = 48/2;
 
-		if ((oldspecialstage && leveltime & 2)
-			&& (stplyr->mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER))
-			&& !(stplyr->powers[pw_shield] & SH_PROTECTWATER))
+		if ((oldspecialstage && leveltime & 2) &&
+			(stplyr->mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(stplyr->powers[pw_shield] & ((stplyr->mo->eflags & MFE_TOUCHLAVA) ? SH_PROTECTFIRE : SH_PROTECTWATER))))
 			col = SKINCOLOR_ORANGE;
 
 		ST_DrawNightsOverlayNum((160 + numbersize)<<FRACBITS, 14<<FRACBITS, FRACUNIT, V_PERPLAYER|V_SNAPTOTOP, realnightstime, nightsnum, col);
@@ -2188,7 +2187,7 @@ static void ST_drawMatchHUD(void)
 		{
 			sprintf(penaltystr, "-%d", stplyr->ammoremoval);
 			V_DrawString(offset + 8 + stplyr->ammoremovalweapon * 20, y,
-				V_REDMAP|V_SNAPTOBOTTOM, penaltystr);
+				V_REDMAP|V_SNAPTOBOTTOM|V_PERPLAYER, penaltystr);
 		}
 
 	}
diff --git a/src/w_wad.c b/src/w_wad.c
index 3ff301117bca03d03868e7dead7bb5fa03e44c2b..e49e0ce82f9ffe24c06d757b61b2a69bbc10ee2a 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -105,7 +105,7 @@ static UINT16 lumpnumcacheindex = 0;
 //                                                                    GLOBALS
 //===========================================================================
 UINT16 numwadfiles; // number of active wadfiles
-wadfile_t *wadfiles[MAX_WADFILES]; // 0 to numwadfiles-1 are valid
+wadfile_t **wadfiles; // 0 to numwadfiles-1 are valid
 
 // W_Shutdown
 // Closes all of the WAD files before quitting
@@ -134,6 +134,8 @@ void W_Shutdown(void)
 		Z_Free(wad->lumpinfo);
 		Z_Free(wad);
 	}
+
+	Z_Free(wadfiles);
 }
 
 //===========================================================================
@@ -844,7 +846,6 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 #ifndef NOMD5
 	size_t i;
 #endif
-	size_t packetsize;
 	UINT8 md5sum[16];
 	int important;
 
@@ -862,9 +863,8 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		refreshdirname = NULL;
 
 	//CONS_Debug(DBG_SETUP, "Loading %s\n", filename);
-	//
-	// check if limit of active wadfiles
-	//
+
+	// Check if the game reached the limit of active wadfiles.
 	if (numwadfiles >= MAX_WADFILES)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
@@ -884,24 +884,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		return INT16_MAX;
 	}
 
-	// Check if wad files will overflow fileneededbuffer. Only the filename part
-	// is send in the packet; cf.
-	// see PutFileNeeded in d_netfil.c
-	if ((important = !important))
-	{
-		packetsize = packetsizetally + nameonlylength(filename) + FILENEEDEDSIZE;
-
-		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
-		{
-			CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
-			refreshdirmenu |= REFRESHDIR_MAX;
-			if (handle)
-				fclose(handle);
-			return W_InitFileError(filename, startup);
-		}
-
-		packetsizetally = packetsize;
-	}
+	important = !important;
 
 #ifndef NOMD5
 	//
@@ -913,11 +896,12 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 
 	for (i = 0; i < numwadfiles; i++)
 	{
+		if (wadfiles[i]->type == RET_FOLDER)
+			continue;
+
 		if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), filename);
-			if (important)
-				packetsizetally -= nameonlylength(filename) + FILENEEDEDSIZE;
 			if (handle)
 				fclose(handle);
 			return W_InitFileError(filename, false);
@@ -984,6 +968,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	// add the wadfile
 	//
 	CONS_Printf(M_GetText("Added file %s (%u lumps)\n"), filename, numlumps);
+	wadfiles = Z_Realloc(wadfiles, sizeof(wadfile_t) * (numwadfiles + 1), PU_STATIC, NULL);
 	wadfiles[numwadfiles] = wadfile;
 	numwadfiles++; // must come BEFORE W_LoadDehackedLumps, so any addfile called by COM_BufInsertText called by Lua doesn't overwrite what we just loaded
 
@@ -1046,22 +1031,7 @@ UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
 		return W_InitFileError(path, startup);
 	}
 
-	important = 0; // ???
-
-	/// \todo Implement a W_VerifyFolder.
-	if ((important = !important))
-	{
-		size_t packetsize = packetsizetally + strlen(path) + FILENEEDEDSIZE;
-
-		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
-		{
-			CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
-			refreshdirmenu |= REFRESHDIR_MAX;
-			return W_InitFileError(path, startup);
-		}
-
-		packetsizetally = packetsize;
-	}
+	important = 0; /// \todo Implement a W_VerifyFolder.
 
 	// Remove path delimiters.
 	p = path + (strlen(path) - 1);
@@ -1132,8 +1102,6 @@ UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
 		if (samepaths(wadfiles[i]->path, fullpath) > 0)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), path);
-			if (important)
-				packetsizetally -= strlen(path) + FILENEEDEDSIZE;
 			Z_Free(fn);
 			Z_Free(fullpath);
 			return W_InitFileError(path, false);
@@ -1197,11 +1165,13 @@ UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
   *
   * \param filenames A null-terminated list of files to use.
   */
-void W_InitMultipleFiles(char **filenames)
+void W_InitMultipleFiles(addfilelist_t *list)
 {
-	for (; *filenames; filenames++)
+	size_t i = 0;
+
+	for (; i < list->numfiles; i++)
 	{
-		const char *fn = (*filenames);
+		const char *fn = list->files[i];
 		char pathsep = fn[strlen(fn) - 1];
 		boolean mainfile = (numwadfiles < mainwads);
 
@@ -1219,7 +1189,7 @@ void W_InitMultipleFiles(char **filenames)
   */
 static boolean TestValidLump(UINT16 wad, UINT16 lump)
 {
-	I_Assert(wad < MAX_WADFILES);
+	I_Assert(wad < numwadfiles);
 	if (!wadfiles[wad]) // make sure the wad file exists
 		return false;
 
@@ -1636,7 +1606,7 @@ size_t W_LumpLength(lumpnum_t lumpnum)
 
 //
 // W_IsLumpWad
-// Is the lump a WAD? (presumably in a PK3)
+// Is the lump a WAD? (presumably not in a WAD)
 //
 boolean W_IsLumpWad(lumpnum_t lumpnum)
 {
@@ -1649,12 +1619,12 @@ boolean W_IsLumpWad(lumpnum_t lumpnum)
 		return !strnicmp(lumpfullName + strlen(lumpfullName) - 4, ".wad", 4);
 	}
 
-	return false; // WADs should never be inside non-PK3s as far as SRB2 is concerned
+	return false; // WADs should never be inside WADs as far as SRB2 is concerned
 }
 
 //
 // W_IsLumpFolder
-// Is the lump a folder? (in a PK3 obviously)
+// Is the lump a folder? (not in a WAD obviously)
 //
 boolean W_IsLumpFolder(UINT16 wad, UINT16 lump)
 {
@@ -1665,7 +1635,7 @@ boolean W_IsLumpFolder(UINT16 wad, UINT16 lump)
 		return (name[strlen(name)-1] == '/'); // folders end in '/'
 	}
 
-	return false; // non-PK3s don't have folders
+	return false; // WADs don't have folders
 }
 
 #ifdef HAVE_ZLIB
@@ -2217,7 +2187,7 @@ void W_VerifyFileMD5(UINT16 wadfilenum, const char *matchmd5)
 #else
 		I_Error
 #endif
-			(M_GetText("File is old, is corrupt or has been modified: %s (found md5: %s, wanted: %s)\n"), wadfiles[wadfilenum]->filename, actualmd5text, matchmd5);
+			(M_GetText("File is old, is corrupt or has been modified:\n%s\nFound MD5: %s\nWanted MD5: %s\n"), wadfiles[wadfilenum]->filename, actualmd5text, matchmd5);
 	}
 #endif
 }
diff --git a/src/w_wad.h b/src/w_wad.h
index 949bab9fec1f2c20726bea7ff5ef52c404c3a6b0..a41ba1724a93efad3d583a1ea0d4b065f5dd3798 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -97,9 +97,15 @@ virtlump_t* vres_Find(const virtres_t*, const char*);
 //                         DYNAMIC WAD LOADING
 // =========================================================================
 
+// Maximum of files that can be loaded
+// (there is a max of simultaneous open files anyway)
+#ifdef ENFORCE_WAD_LIMIT
+#define MAX_WADFILES 2048 // This cannot be any higher than UINT16_MAX.
+#else
+#define MAX_WADFILES UINT16_MAX
+#endif
+
 #define MAX_WADPATH 512
-#define MAX_WADFILES 48 // maximum of wad files used at the same time
-// (there is a max of simultaneous open files anyway, and this should be plenty)
 
 #define lumpcache_t void *
 
@@ -134,7 +140,13 @@ typedef struct wadfile_s
 #define LUMPNUM(lumpnum) (UINT16)((lumpnum)&0xFFFF) // lump number for this pwad
 
 extern UINT16 numwadfiles;
-extern wadfile_t *wadfiles[MAX_WADFILES];
+extern wadfile_t **wadfiles;
+
+typedef struct
+{
+	char **files;
+	size_t numfiles;
+} addfilelist_t;
 
 // =========================================================================
 
@@ -148,7 +160,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup);
 UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup);
 
 // W_InitMultipleFiles exits if a file was not found, but not if all is okay.
-void W_InitMultipleFiles(char **filenames);
+void W_InitMultipleFiles(addfilelist_t *list);
 
 #define W_FileHasFolders(wadfile) ((wadfile)->type == RET_PK3 || (wadfile)->type == RET_FOLDER)
 
diff --git a/src/y_inter.c b/src/y_inter.c
index afc75c8b10d184210b416a439f2a008721b7be29..f24436d4082e645fbbd9fe103a7ed33e741a18b7 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -239,12 +239,12 @@ void Y_LoadIntermissionData(void)
 			}
 			data.coop.ptotal = W_CachePatchName("YB_TOTAL", PU_PATCH);
 
-			// get background patches
-			bgpatch = W_CachePatchName("INTERSCR", PU_PATCH);
 
 			// grab an interscreen if appropriate
 			if (mapheaderinfo[gamemap-1]->interscreen[0] != '#')
 				interpic = W_CachePatchName(mapheaderinfo[gamemap-1]->interscreen, PU_PATCH);
+			else // no interscreen? use default background
+				bgpatch = W_CachePatchName("INTERSCR", PU_PATCH);
 			break;
 		}
 		case int_spec:
@@ -255,12 +255,11 @@ void Y_LoadIntermissionData(void)
 			data.spec.pscore = W_CachePatchName("YB_SCORE", PU_PATCH);
 			data.spec.pcontinues = W_CachePatchName("YB_CONTI", PU_PATCH);
 
-			// get background tile
-			bgtile = W_CachePatchName("SPECTILE", PU_PATCH);
-
 			// grab an interscreen if appropriate
 			if (mapheaderinfo[gamemap-1]->interscreen[0] != '#')
 				interpic = W_CachePatchName(mapheaderinfo[gamemap-1]->interscreen, PU_PATCH);
+			else // no interscreen? use default background
+				bgtile = W_CachePatchName("SPECTILE", PU_PATCH);
 			break;
 		}
 		case int_ctf:
diff --git a/src/z_zone.h b/src/z_zone.h
index be55bf9940ead1a1385f92ca50d296b0bd4fbdf1..17f572a905b56597bf955ee28b53814c6108bf04 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -39,6 +39,7 @@ enum
 	// Tags < PU_LEVEL are not purged until freed explicitly.
 	PU_STATIC                = 1, // static entire execution time
 	PU_LUA                   = 2, // static entire execution time -- used by lua so it doesn't get caught in loops forever
+	PU_PERFSTATS             = 3, // static between changes to ps_samplesize cvar
 
 	PU_SOUND                 = 11, // static while playing
 	PU_MUSIC                 = 12, // static while playing