diff --git a/CMakeLists.txt b/CMakeLists.txt
index ec96b7030c4622c842bed0a1dc67a070d7160ac1..f2c43d02ceb26c277c94b9440470f90eb9394244 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.0)
 project(SRB2
-	VERSION 2.1.21
+	VERSION 2.1.22
 	LANGUAGES C)
 
 if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
diff --git a/appveyor.yml b/appveyor.yml
index 061613c4dcb7975848c5b353554514bf59de5539..501aee5b0d4417da5775d179411b40517f42e0a3 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.1.21.{branch}-{build}
+version: 2.1.22.{branch}-{build}
 os: MinGW
 
 environment:
diff --git a/debian/changelog b/debian/changelog
index 855c1c1b35a8ff767f25a017ab5bebbb50c2614f..49f4c8b6b2e36f5ab173259768cdbc49ced74910 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,6 +1,6 @@
-srb2 (2.1.21~9) trusty; urgency=high
+srb2 (2.1.22~9) trusty; urgency=high
 
-  * SRB2 v2.1.21 release
+  * SRB2 v2.1.22 release
 
  -- Marco Zafra <marco.a.zafra@gmail.com>  Mon, 27 Nov 2018 16:45:00 -0500
 
diff --git a/debian/control b/debian/control
index ce3b33fbdae1efb642b80591d8920436f18f0a9f..64b96d6b9aac61bd75dfc696b83e65b46f8c1d6c 100644
--- a/debian/control
+++ b/debian/control
@@ -18,7 +18,7 @@ Homepage: http://www.srb2.org
 
 Package: srb2
 Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, srb2-data (>= 2.1.15), srb2-data (<= 2.1.21)
+Depends: ${shlibs:Depends}, ${misc:Depends}, srb2-data (>= 2.1.15), srb2-data (<= 2.1.22)
 Description: A cross-platform 3D Sonic fangame
  Sonic Robo Blast 2 is a 3D open-source Sonic the Hedgehog
  fangame built using a modified version of the Doom Legacy
@@ -31,7 +31,7 @@ Description: A cross-platform 3D Sonic fangame
 Package: srb2-dbg
 Architecture: any
 # FIXME: should be Depends: ${shlibs:Depends}, ${misc:Depends}, srb2-data (= 2.1.14), srb2 but dh_shlibdeps is being an asshat
-Depends: libc6, ${misc:Depends}, srb2-data (>= 2.1.15), srb2-data (<= 2.1.21), srb2
+Depends: libc6, ${misc:Depends}, srb2-data (>= 2.1.15), srb2-data (<= 2.1.22), srb2
 Description: A cross-platform 3D Sonic fangame
  Sonic Robo Blast 2 is a 3D open-source Sonic the Hedgehog
  fangame built using a modified version of the Doom Legacy
diff --git a/src/command.c b/src/command.c
index 3183ba70bcb1d80abaead0efabec244781d782f7..ba0095e079b4a6be92a0e9983217d9e6432325fa 100644
--- a/src/command.c
+++ b/src/command.c
@@ -50,6 +50,7 @@ static void COM_Wait_f(void);
 static void COM_Help_f(void);
 static void COM_Toggle_f(void);
 
+static void CV_EnforceExecVersion(void);
 static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr);
 static boolean CV_Command(void);
 static consvar_t *CV_FindVar(const char *name);
@@ -64,10 +65,11 @@ CV_PossibleValue_t CV_YesNo[] = {{0, "No"}, {1, "Yes"}, {0, NULL}};
 CV_PossibleValue_t CV_Unsigned[] = {{0, "MIN"}, {999999999, "MAX"}, {0, NULL}};
 CV_PossibleValue_t CV_Natural[] = {{1, "MIN"}, {999999999, "MAX"}, {0, NULL}};
 
-// Filter consvars by MODVERSION
+// Filter consvars by EXECVERSION
 // First implementation is 26 (2.1.21), so earlier configs default at 25 (2.1.20)
 // Also set CV_HIDEN during runtime, after config is loaded
-consvar_t cv_execversion = {"execversion","25",0,CV_Unsigned, NULL, 0, NULL, NULL, 0, 0, NULL};
+static boolean execversion_enabled = false;
+consvar_t cv_execversion = {"execversion","25",CV_CALL,CV_Unsigned, CV_EnforceExecVersion, 0, NULL, NULL, 0, 0, NULL};
 
 // for default joyaxis detection
 static boolean joyaxis_default = false;
@@ -1598,10 +1600,21 @@ void CV_InitFilterVar(void)
 	joyaxis_count = joyaxis2_count = 0;
 }
 
+void CV_ToggleExecVersion(boolean enable)
+{
+	execversion_enabled = enable;
+}
+
+static void CV_EnforceExecVersion(void)
+{
+	if (!execversion_enabled)
+		CV_StealthSetValue(&cv_execversion, EXECVERSION);
+}
+
 static boolean CV_FilterJoyAxisVars(consvar_t *v, const char *valstr)
 {
 	// If ALL axis settings are previous defaults, set them to the new defaults
-	// MODVERSION < 26 (2.1.21)
+	// EXECVERSION < 26 (2.1.21)
 
 	if (joyaxis_default)
 	{
@@ -1749,8 +1762,7 @@ static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr)
 	if (!(v->flags & CV_SAVE))
 		return true;
 
-	// We go by MODVERSION here
-	if (cv_execversion.value < 26) // 26 = 2.1.21
+	if (GETMAJOREXECVERSION(cv_execversion.value) < 26) // 26 = 2.1.21
 	{
 		// MOUSE SETTINGS
 		// alwaysfreelook split between first and third person (chasefreelook)
diff --git a/src/command.h b/src/command.h
index 8dee1174cb2eba85ee9106f652ffd8dae822e482..e6767825c7561c56a0b62995c4ecb66c8399d925 100644
--- a/src/command.h
+++ b/src/command.h
@@ -130,6 +130,7 @@ extern CV_PossibleValue_t CV_Natural[];
 extern consvar_t cv_execversion;
 
 void CV_InitFilterVar(void);
+void CV_ToggleExecVersion(boolean enable);
 
 // register a variable for use at the console
 void CV_RegisterVar(consvar_t *variable);
diff --git a/src/config.h.in b/src/config.h.in
index 7452ec80c206d2111b9d9b52c7d308439565ba01..1e6a7f514939491a6428b08c840679a1d14f4d2f 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -27,14 +27,15 @@
 #else
 
 /* Manually defined asset hashes for non-CMake builds
- * Last updated 2015 / 05 / 03
+ * Last updated 2015 / 05 / 03 - v2.1.15 - main assets
+ * Last updated 2018 / 12 / 23 - v2.1.22 - patch.dta
  */
 #define ASSET_HASH_SRB2_SRB   "c1b9577687f8a795104aef4600720ea7"
 #define ASSET_HASH_ZONES_DTA  "303838c6c534d9540288360fa49cca60"
 #define ASSET_HASH_PLAYER_DTA "cfca0f1c73023cbbd8f844f45480f799"
 #define ASSET_HASH_RINGS_DTA  "85901ad4bf94637e5753d2ac2c03ea26"
 #ifdef USE_PATCH_DTA
-#define ASSET_HASH_PATCH_DTA  "dbbf8bc6121618ee3be2d5b14650429b"
+#define ASSET_HASH_PATCH_DTA  "b04fd9624bfd94dc96dcf4f400f7deb4"
 #endif
 
 #endif
diff --git a/src/console.c b/src/console.c
index 11bf4cb11ab3143f30e7f64fd8bc817f694330cd..5c173e4594cf2d62037779c77af5da27af9716c0 100644
--- a/src/console.c
+++ b/src/console.c
@@ -33,6 +33,7 @@
 #include "i_system.h"
 #include "d_main.h"
 #include "m_menu.h"
+#include "filesrch.h"
 
 #ifdef _WINDOWS
 #include "win32/win_main.h"
@@ -128,10 +129,15 @@ static CV_PossibleValue_t backpic_cons_t[] = {{0, "translucent"}, {1, "picture"}
 // whether to use console background picture, or translucent mode
 static consvar_t cons_backpic = {"con_backpic", "translucent", CV_SAVE, backpic_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
-static CV_PossibleValue_t backcolor_cons_t[] = {{0, "White"}, 	{1, "Gray"},	{2, "Brown"},
-												{3, "Red"},		{4, "Orange"},	{5, "Yellow"},
-												{6, "Green"},	{7, "Blue"},	{8,	"Cyan"},
+static CV_PossibleValue_t backcolor_cons_t[] = {{0, "White"}, 		{1, "Black"},		{2, "Sepia"},
+												{3, "Brown"},		{4, "Pink"},		{5, "Raspberry"},
+												{6, "Red"},			{7, "Creamsicle"},	{8, "Orange"},
+												{9, "Gold"},		{10,"Yellow"},		{11,"Emerald"},
+												{12,"Green"},		{13,"Cyan"},		{14,"Steel"},
+												{15,"Periwinkle"},	{16,"Blue"},		{17,"Purple"},
+												{18,"Lavender"},
 												{0, NULL}};
+
 consvar_t cons_backcolor = {"con_backcolor", "Green", CV_CALL|CV_SAVE, backcolor_cons_t, CONS_backcolor_Change, 0, NULL, NULL, 0, 0, NULL};
 
 static void CON_Print(char *msg);
@@ -238,29 +244,41 @@ void CON_SetupBackColormap(void)
 	UINT16 i, palsum;
 	UINT8 j, palindex;
 	UINT8 *pal = W_CacheLumpName(GetPalette(), PU_CACHE);
+	INT32 shift = 6;
 
 	if (!consolebgmap)
 		consolebgmap = (UINT8 *)Z_Malloc(256, PU_STATIC, NULL);
 
 	switch (cons_backcolor.value)
 	{
-		case 0:		palindex = 15; 	break; // White
-		case 1:		palindex = 31;	break; // Gray
-		case 2:		palindex = 63;	break; // Brown
-		case 3:		palindex = 143;	break; // Red
-		case 4:		palindex = 95;	break; // Orange
-		case 5:		palindex = 111;	break; // Yellow
-		case 6:		palindex = 175;	break; // Green
-		case 7:		palindex = 239;	break; // Blue
-		case 8:		palindex = 219;	break; // Cyan
+		case 0:		palindex = 15; 	break; 	// White
+		case 1:		palindex = 31;	break; 	// Gray
+		case 2:		palindex = 47;	break;	// Sepia
+		case 3:		palindex = 63;	break; 	// Brown
+		case 4:		palindex = 150; shift = 7; 	break; 	// Pink
+		case 5:		palindex = 127; shift = 7;	break; 	// Raspberry
+		case 6:		palindex = 143;	break; 	// Red
+		case 7:		palindex = 86;	shift = 7;	break;	// Creamsicle
+		case 8:		palindex = 95;	break; 	// Orange
+		case 9:		palindex = 119; shift = 7;	break; 	// Gold
+		case 10:	palindex = 111;	break; 	// Yellow
+		case 11:	palindex = 191; shift = 7; 	break; 	// Emerald
+		case 12:	palindex = 175;	break; 	// Green
+		case 13:	palindex = 219;	break; 	// Cyan
+		case 14:	palindex = 207; shift = 7;	break; 	// Steel
+		case 15:	palindex = 230;	shift = 7; 	break; 	// Periwinkle
+		case 16:	palindex = 239;	break; 	// Blue
+		case 17:	palindex = 199; shift = 7; 	break; 	// Purple
+		case 18:	palindex = 255; shift = 7; 	break; 	// Lavender
 		// Default green
 		default:	palindex = 175; break;
+
 }
 
 	// setup background colormap
 	for (i = 0, j = 0; i < 768; i += 3, j++)
 	{
-		palsum = (pal[i] + pal[i+1] + pal[i+2]) >> 6;
+		palsum = (pal[i] + pal[i+1] + pal[i+2]) >> shift;
 		consolebgmap[j] = (UINT8)(palindex - palsum);
 	}
 }
@@ -839,7 +857,7 @@ boolean CON_Responder(event_t *ev)
 
 		// ...why shouldn't it eat the key? if it doesn't, it just means you
 		// can control Sonic from the console, which is silly
-		return true; //return false;
+		return true;//return false;
 	}
 
 	// command completion forward (tab) and backward (shift-tab)
@@ -1033,15 +1051,30 @@ boolean CON_Responder(event_t *ev)
 	else if (key == KEY_KPADSLASH)
 		key = '/';
 
-	if (shiftdown)
+	// capslock
+	if (key == KEY_CAPSLOCK)	// it's a toggle.
+	{
+		if (capslock)
+			capslock = false;
+		else
+			capslock = true;
+		return true;
+	}
+
+	if (key >= 'a' && key <= 'z')
+	{
+		if (capslock ^ shiftdown)
+			key = shiftxform[key];
+	}
+	else if (shiftdown)
 		key = shiftxform[key];
 
 	// enter a char into the command prompt
 	if (key < 32 || key > 127)
-		return true; // even if key can't be printed, eat it anyway
+		return true;
 
 	// add key to cmd line here
-	if (key >= 'A' && key <= 'Z' && !shiftdown) //this is only really necessary for dedicated servers
+	if (key >= 'A' && key <= 'Z' && !(shiftdown ^ capslock)) //this is only really necessary for dedicated servers
 		key = key + 'a' - 'A';
 
 	if (input_sel != input_cur)
@@ -1258,12 +1291,15 @@ void CONS_Alert(alerttype_t level, const char *fmt, ...)
 	switch (level)
 	{
 		case CONS_NOTICE:
+			// no notice for notices, hehe
 			CONS_Printf("\x83" "%s" "\x80 ", M_GetText("NOTICE:"));
 			break;
 		case CONS_WARNING:
+			refreshdirmenu |= REFRESHDIR_WARNING;
 			CONS_Printf("\x82" "%s" "\x80 ", M_GetText("WARNING:"));
 			break;
 		case CONS_ERROR:
+			refreshdirmenu |= REFRESHDIR_ERROR;
 			CONS_Printf("\x85" "%s" "\x80 ", M_GetText("ERROR:"));
 			break;
 	}
@@ -1419,8 +1455,8 @@ static void CON_DrawHudlines(void)
 	if (con_hudlines <= 0)
 		return;
 
-	if (chat_on)
-		y = charheight; // leave place for chat input in the first row of text
+	if (chat_on && OLDCHAT)
+		y = charheight; // leave place for chat input in the first row of text (only do it if consolechat is on.)
 	else
 		y = 0;
 
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index cd8367af874b0318783017668eb55fec628dd03c..2529b05d0e03b908ce3b3249d48dd4e62776c9f4 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -2762,7 +2762,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 		msg = KICK_MSG_CON_FAIL;
 	}
 
-	CONS_Printf("\x82%s ", player_names[pnum]);
+	//CONS_Printf("\x82%s ", player_names[pnum]);
 
 	// If a verified admin banned someone, the server needs to know about it.
 	// If the playernum isn't zero (the server) then the server needs to record the ban.
@@ -2779,17 +2779,17 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	switch (msg)
 	{
 		case KICK_MSG_GO_AWAY:
-			CONS_Printf(M_GetText("has been kicked (Go away)\n"));
+			HU_AddChatText(va("\x82*%s has been kicked (Go away)", player_names[pnum]), false);
 			kickreason = KR_KICK;
 			break;
 #ifdef NEWPING
 		case KICK_MSG_PING_HIGH:
-			CONS_Printf(M_GetText("left the game (Broke ping limit)\n"));
+			HU_AddChatText(va("\x82*%s left the game (Broke ping limit)", player_names[pnum]), false);
 			kickreason = KR_PINGLIMIT;
 			break;
 #endif
 		case KICK_MSG_CON_FAIL:
-			CONS_Printf(M_GetText("left the game (Synch failure)\n"));
+			HU_AddChatText(va("\x82*%s left the game (Synch Failure)", player_names[pnum]), false);
 			kickreason = KR_SYNCH;
 
 			if (M_CheckParm("-consisdump")) // Helps debugging some problems
@@ -2826,26 +2826,26 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			}
 			break;
 		case KICK_MSG_TIMEOUT:
-			CONS_Printf(M_GetText("left the game (Connection timeout)\n"));
+			HU_AddChatText(va("\x82*%s left the game (Connection timeout)", player_names[pnum]), false);
 			kickreason = KR_TIMEOUT;
 			break;
 		case KICK_MSG_PLAYER_QUIT:
 			if (netgame) // not splitscreen/bots
-				CONS_Printf(M_GetText("left the game\n"));
+				HU_AddChatText(va("\x82*%s left the game", player_names[pnum]), false);
 			kickreason = KR_LEAVE;
 			break;
 		case KICK_MSG_BANNED:
-			CONS_Printf(M_GetText("has been banned (Don't come back)\n"));
+			HU_AddChatText(va("\x82*%s has been banned (Don't come back)", player_names[pnum]), false);
 			kickreason = KR_BAN;
 			break;
 		case KICK_MSG_CUSTOM_KICK:
 			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
-			CONS_Printf(M_GetText("has been kicked (%s)\n"), reason);
+			HU_AddChatText(va("\x82*%s has been kicked (%s)", player_names[pnum], reason), false);
 			kickreason = KR_KICK;
 			break;
 		case KICK_MSG_CUSTOM_BAN:
 			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
-			CONS_Printf(M_GetText("has been banned (%s)\n"), reason);
+			HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false);
 			kickreason = KR_BAN;
 			break;
 	}
@@ -3119,9 +3119,6 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 	if (newplayernum+1 > doomcom->numslots)
 		doomcom->numslots = (INT16)(newplayernum+1);
 
-	if (netgame)
-		CONS_Printf(M_GetText("Player %d has joined the game (node %d)\n"), newplayernum+1, node);
-
 	// the server is creating my player
 	if (node == mynode)
 	{
@@ -3143,11 +3140,17 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 		D_SendPlayerConfig();
 		addedtogame = true;
 	}
-	else if (server && netgame && cv_showjoinaddress.value)
+
+	if (netgame)
 	{
-		const char *address;
-		if (I_GetNodeAddress && (address = I_GetNodeAddress(node)) != NULL)
-			CONS_Printf(M_GetText("Player Address is %s\n"), address);
+		if (server && cv_showjoinaddress.value)
+		{
+			const char *address;
+			if (I_GetNodeAddress && (address = I_GetNodeAddress(node)) != NULL)
+				HU_AddChatText(va("\x82*Player %d has joined the game (node %d) (%s)", newplayernum+1, node, address), false);	// merge join notification + IP to avoid clogging console/chat.
+		}
+		else
+			HU_AddChatText(va("\x82*Player %d has joined the game (node %d)", newplayernum+1, node), false);	// if you don't wanna see the join address.
 	}
 
 	if (server && multiplayer && motd[0] != '\0')
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index dd1aaf4eab0bf9754173a4988c62dde8533f53d7..8443b3fc0e72ca6600daca61244d484e355fe9cf 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -309,6 +309,7 @@ typedef struct
 } ATTRPACK clientconfig_pak;
 
 #define MAXSERVERNAME 32
+#define MAXFILENEEDED 915
 // This packet is too large
 typedef struct
 {
@@ -330,7 +331,7 @@ typedef struct
 	unsigned char mapmd5[16];
 	UINT8 actnum;
 	UINT8 iszone;
-	UINT8 fileneeded[915]; // is filled with writexxx (byteptr.h)
+	UINT8 fileneeded[MAXFILENEEDED]; // is filled with writexxx (byteptr.h)
 } ATTRPACK serverinfo_pak;
 
 typedef struct
diff --git a/src/d_main.c b/src/d_main.c
index df422232eb1bda8ec0129b30a62c2c7dc190be5b..dd2cfe0e520308efc722ae06315cf60ba6676154 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -74,6 +74,7 @@ int	snprintf(char *str, size_t n, const char *fmt, ...);
 #include "m_cond.h" // condition initialization
 #include "fastcmp.h"
 #include "keys.h"
+#include "filesrch.h" // refreshdirmenu, mainwadstally
 
 #ifdef CMAKECONFIG
 #include "config.h"
@@ -178,6 +179,7 @@ void D_PostEvent_end(void) {};
 UINT8 shiftdown = 0; // 0x1 left, 0x2 right
 UINT8 ctrldown = 0; // 0x1 left, 0x2 right
 UINT8 altdown = 0; // 0x1 left, 0x2 right
+boolean capslock = 0;	// gee i wonder what this does.
 //
 // D_ModifierKeyResponder
 // Sets global shift/ctrl/alt variables, never actually eats events
@@ -582,6 +584,8 @@ void D_SRB2Loop(void)
 		realtics = entertic - oldentertics;
 		oldentertics = entertic;
 
+		refreshdirmenu = 0; // not sure where to put this, here as good as any?
+
 #ifdef DEBUGFILE
 		if (!realtics)
 			if (debugload)
@@ -846,17 +850,21 @@ static void IdentifyVersion(void)
 
 #if !defined (HAVE_SDL) || defined (HAVE_MIXER)
 	{
+#define MUSICTEST(str) \
+		{\
+			const char *musicpath = va(pandf,srb2waddir,str);\
+			int ms = W_VerifyNMUSlumps(musicpath); \
+			if (ms == 1) \
+				D_AddFile(musicpath); \
+			else if (ms == 0) \
+				I_Error("File "str" has been modified with non-music/sound lumps"); \
+		}
+
 #if defined (DC) && 0
-		const char *musicfile = "music_dc.dta";
+		MUSICTEST("music_dc.dta")
 #else
-		const char *musicfile = "music.dta";
+		MUSICTEST("music.dta")
 #endif
-		const char *musicpath = va(pandf,srb2waddir,musicfile);
-		int ms = W_VerifyNMUSlumps(musicpath); // Don't forget the music!
-		if (ms == 1)
-			D_AddFile(musicpath);
-		else if (ms == 0)
-			I_Error("File %s has been modified with non-music lumps",musicfile);
 	}
 #endif
 }
@@ -1132,25 +1140,36 @@ void D_SRB2Main(void)
 #endif
 	D_CleanFile();
 
+	mainwads = 0;
+
 #ifndef DEVELOP // md5s last updated 12/14/14
 
 	// Check MD5s of autoloaded files
-	W_VerifyFileMD5(0, ASSET_HASH_SRB2_SRB); // srb2.srb/srb2.wad
-	W_VerifyFileMD5(1, ASSET_HASH_ZONES_DTA); // zones.dta
-	W_VerifyFileMD5(2, ASSET_HASH_PLAYER_DTA); // player.dta
-	W_VerifyFileMD5(3, ASSET_HASH_RINGS_DTA); // rings.dta
+	W_VerifyFileMD5(mainwads++, ASSET_HASH_SRB2_SRB); // srb2.srb/srb2.wad
+	W_VerifyFileMD5(mainwads++, ASSET_HASH_ZONES_DTA); // zones.dta
+	W_VerifyFileMD5(mainwads++, ASSET_HASH_PLAYER_DTA); // player.dta
+	W_VerifyFileMD5(mainwads++, ASSET_HASH_RINGS_DTA); // rings.dta
 #ifdef USE_PATCH_DTA
-	W_VerifyFileMD5(4, ASSET_HASH_PATCH_DTA); // patch.dta
+	W_VerifyFileMD5(mainwads++, ASSET_HASH_PATCH_DTA); // patch.dta
 #endif
-
 	// don't check music.dta because people like to modify it, and it doesn't matter if they do
 	// ...except it does if they slip maps in there, and that's what W_VerifyNMUSlumps is for.
-#endif //ifndef DEVELOP
+	//mainwads++; // music.dta does not increment mainwads (see <= 2.1.21)
 
-	mainwads = 4; // there are 4 wads not to unload
+#else
+
+	mainwads++;	// srb2.srb/srb2.wad
+	mainwads++; // zones.dta
+	mainwads++; // player.dta
+	mainwads++; // rings.dta
 #ifdef USE_PATCH_DTA
-	++mainwads; // patch.dta adds one more
+	mainwads++; // patch.dta
 #endif
+	//mainwads++; // music.dta does not increment mainwads (see <= 2.1.21)
+
+#endif //ifndef DEVELOP
+
+	mainwadstally = packetsizetally;
 
 	cht_Init();
 
diff --git a/src/d_net.h b/src/d_net.h
index 55ea308b38100c638425a1e0ebadaef244a18a2a..61c669dbb93cbcd8cafda1fbc7ca132aac5fe6d9 100644
--- a/src/d_net.h
+++ b/src/d_net.h
@@ -22,6 +22,7 @@
 #define MAXNETNODES 32
 #define BROADCASTADDR MAXNETNODES
 #define MAXSPLITSCREENPLAYERS 2 // Max number of players on a single computer
+//#define NETSPLITSCREEN // Kart's splitscreen netgame feature
 
 #define STATLENGTH (TICRATE*2)
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index fc885601abb4bbf7ca00dbafccca8746132c457f..11b9413a82905cf1e9066a72d4848b44e14d32d8 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -38,6 +38,7 @@
 #include "d_main.h"
 #include "m_random.h"
 #include "f_finale.h"
+#include "filesrch.h"
 #include "mserv.h"
 #include "md5.h"
 #include "z_zone.h"
@@ -673,6 +674,14 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_usegamma);
 
 	// m_menu.c
+	CV_RegisterVar(&cv_compactscoreboard);
+	CV_RegisterVar(&cv_chatheight);
+	CV_RegisterVar(&cv_chatwidth);
+	CV_RegisterVar(&cv_chattime);
+	CV_RegisterVar(&cv_chatspamprotection);
+	CV_RegisterVar(&cv_chatbacktint);
+	CV_RegisterVar(&cv_consolechat);
+	CV_RegisterVar(&cv_chatnotifications);
 	CV_RegisterVar(&cv_crosshair);
 	CV_RegisterVar(&cv_crosshair2);
 	CV_RegisterVar(&cv_alwaysfreelook);
@@ -698,6 +707,14 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_firenaxis);
 	CV_RegisterVar(&cv_firenaxis2);
 
+	// filesrch.c
+	CV_RegisterVar(&cv_addons_option);
+	CV_RegisterVar(&cv_addons_folder);
+	CV_RegisterVar(&cv_addons_md5);
+	CV_RegisterVar(&cv_addons_showall);
+	CV_RegisterVar(&cv_addons_search_type);
+	CV_RegisterVar(&cv_addons_search_case);
+
 	// WARNING: the order is important when initialising mouse2
 	// we need the mouse2port
 	CV_RegisterVar(&cv_mouse2port);
@@ -988,8 +1005,8 @@ static void SetPlayerName(INT32 playernum, char *newname)
 		if (strcasecmp(newname, player_names[playernum]) != 0)
 		{
 			if (netgame)
-				CONS_Printf(M_GetText("%s renamed to %s\n"),
-					player_names[playernum], newname);
+				HU_AddChatText(va("\x82*%s renamed to %s", player_names[playernum], newname), false);
+
 			strcpy(player_names[playernum], newname);
 		}
 	}
@@ -3147,25 +3164,12 @@ static void Command_Addfile(void)
 			break;
 	++p;
 	// check total packet size and no of files currently loaded
+	// See W_LoadWadFile in w_wad.c
+	if ((numwadfiles >= MAX_WADFILES)
+	|| ((packetsizetally + nameonlylength(fn) + 22) > MAXFILENEEDED*sizeof(UINT8)))
 	{
-		size_t packetsize = 0;
-		serverinfo_pak *dummycheck = NULL;
-
-		// Shut the compiler up.
-		(void)dummycheck;
-
-		// See W_LoadWadFile in w_wad.c
-		for (i = 0; i < numwadfiles; i++)
-			packetsize += nameonlylength(wadfiles[i]->filename) + 22;
-
-		packetsize += nameonlylength(fn) + 22;
-
-		if ((numwadfiles >= MAX_WADFILES)
-		|| (packetsize > sizeof(dummycheck->fileneeded)))
-		{
-			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
-			return;
-		}
+		CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
+		return;
 	}
 
 	WRITESTRINGN(buf_p,p,240);
@@ -3251,7 +3255,6 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 	boolean kick = false;
 	boolean toomany = false;
 	INT32 i,j;
-	size_t packetsize = 0;
 	serverinfo_pak *dummycheck = NULL;
 
 	// Shut the compiler up.
@@ -3282,13 +3285,8 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 	}
 
 	// See W_LoadWadFile in w_wad.c
-	for (i = 0; i < numwadfiles; i++)
-		packetsize += nameonlylength(wadfiles[i]->filename) + 22;
-
-	packetsize += nameonlylength(filename) + 22;
-
 	if ((numwadfiles >= MAX_WADFILES)
-	|| (packetsize > sizeof(dummycheck->fileneeded)))
+	|| ((packetsizetally + nameonlylength(filename) + 22) > MAXFILENEEDED*sizeof(UINT8)))
 		toomany = true;
 	else
 		ncs = findfile(filename,md5sum,true);
@@ -3518,6 +3516,9 @@ static void Command_Playintro_f(void)
 	if (netgame)
 		return;
 
+	if (dirmenu)
+		closefilemenu(true);
+
 	F_StartIntro();
 }
 
@@ -4075,6 +4076,9 @@ void Command_ExitGame_f(void)
 	cv_debug = 0;
 	emeralds = 0;
 
+	if (dirmenu)
+		closefilemenu(true);
+
 	if (!modeattacking)
 		D_StartTitle();
 }
diff --git a/src/d_netfil.c b/src/d_netfil.c
index f73712418ee3ffc70057904ce0d4187489246c9c..deb04fbe14fa5bd64213129a8ce8ed4979317f14 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -104,6 +104,7 @@ INT32 lastfilenum = -1;
 /** 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 handed via W_LoadWadFile in w_wad.c
   *
   */
 UINT8 *PutFileNeeded(void)
@@ -112,29 +113,22 @@ UINT8 *PutFileNeeded(void)
 	UINT8 *p = netbuffer->u.serverinfo.fileneeded;
 	char wadfilename[MAX_WADPATH] = "";
 	UINT8 filestatus;
-	size_t bytesused = 0;
 
 	for (i = 0; i < numwadfiles; i++)
 	{
-		// If it has only music/sound lumps, mark it as unimportant
-		if (W_VerifyNMUSlumps(wadfiles[i]->filename))
-			filestatus = 0;
-		else
-			filestatus = 1; // Important
+		// If it has only music/sound lumps, don't put it in the list
+		if (!wadfiles[i]->important)
+			continue;
+
+		filestatus = 1; // Importance - not really used any more, holds 1 by default for backwards compat with MS
 
 		// Store in the upper four bits
 		if (!cv_downloading.value)
 			filestatus += (2 << 4); // Won't send
-		else if ((wadfiles[i]->filesize > (UINT32)cv_maxsend.value * 1024))
-			filestatus += (0 << 4); // Won't send
-		else
+		else if ((wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024))
 			filestatus += (1 << 4); // Will send if requested
-
-		bytesused += (nameonlylength(wadfilename) + 22);
-
-		// Don't write too far...
-		if (bytesused > sizeof(netbuffer->u.serverinfo.fileneeded))
-			I_Error("Too many wad files added to host a game. (%s, stopped on %s)\n", sizeu1(bytesused), wadfilename);
+		// else
+			// filestatus += (0 << 4); -- Won't send, too big
 
 		WRITEUINT8(p, filestatus);
 
@@ -167,7 +161,6 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
 	{
 		fileneeded[i].status = FS_NOTFOUND; // We haven't even started looking for the file yet
 		filestatus = READUINT8(p); // The first byte is the file status
-		fileneeded[i].important = (UINT8)(filestatus & 3);
 		fileneeded[i].willsend = (UINT8)(filestatus >> 4);
 		fileneeded[i].totalsize = READUINT32(p); // The four next bytes are the file size
 		fileneeded[i].file = NULL; // The file isn't open yet
@@ -197,7 +190,7 @@ boolean CL_CheckDownloadable(void)
 	UINT8 i,dlstatus = 0;
 
 	for (i = 0; i < fileneedednum; i++)
-		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN && fileneeded[i].important)
+		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			if (fileneeded[i].willsend == 1)
 				continue;
@@ -218,7 +211,7 @@ boolean CL_CheckDownloadable(void)
 	// not downloadable, put reason in console
 	CONS_Alert(CONS_NOTICE, M_GetText("You need additional files to connect to this server:\n"));
 	for (i = 0; i < fileneedednum; i++)
-		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN && fileneeded[i].important)
+		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			CONS_Printf(" * \"%s\" (%dK)", fileneeded[i].filename, fileneeded[i].totalsize >> 10);
 
@@ -271,7 +264,7 @@ boolean CL_SendRequestFile(void)
 
 	for (i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN
-			&& fileneeded[i].important && (fileneeded[i].willsend == 0 || fileneeded[i].willsend == 2))
+			&& (fileneeded[i].willsend == 0 || fileneeded[i].willsend == 2))
 		{
 			I_Error("Attempted to download files that were not sendable");
 		}
@@ -280,8 +273,7 @@ boolean CL_SendRequestFile(void)
 	netbuffer->packettype = PT_REQUESTFILE;
 	p = (char *)netbuffer->u.textcmd;
 	for (i = 0; i < fileneedednum; i++)
-		if ((fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
-			&& fileneeded[i].important)
+		if ((fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD))
 		{
 			totalfreespaceneeded += fileneeded[i].totalsize;
 			nameonly(fileneeded[i].filename);
@@ -339,10 +331,6 @@ INT32 CL_CheckFiles(void)
 	INT32 ret = 1;
 	size_t packetsize = 0;
 	size_t filestoget = 0;
-	serverinfo_pak *dummycheck = NULL;
-
-	// Shut the compiler up.
-	(void)dummycheck;
 
 //	if (M_CheckParm("-nofiles"))
 //		return 1;
@@ -360,13 +348,7 @@ INT32 CL_CheckFiles(void)
 		CONS_Debug(DBG_NETPLAY, "game is modified; only doing basic checks\n");
 		for (i = 1, j = 1; i < fileneedednum || j < numwadfiles;)
 		{
-			if (i < fileneedednum && !fileneeded[i].important)
-			{
-				// Eh whatever, don't care
-				++i;
-				continue;
-			}
-			if (j < numwadfiles && W_VerifyNMUSlumps(wadfiles[j]->filename))
+			if (j < numwadfiles && !wadfiles[j]->important)
 			{
 				// Unimportant on our side. still don't care.
 				++j;
@@ -392,8 +374,7 @@ INT32 CL_CheckFiles(void)
 	}
 
 	// See W_LoadWadFile in w_wad.c
-	for (i = 0; i < numwadfiles; i++)
-		packetsize += nameonlylength(wadfiles[i]->filename) + 22;
+	packetsize = packetsizetally;
 
 	for (i = 1; i < fileneedednum; i++)
 	{
@@ -411,13 +392,13 @@ INT32 CL_CheckFiles(void)
 				break;
 			}
 		}
-		if (fileneeded[i].status != FS_NOTFOUND || !fileneeded[i].important)
+		if (fileneeded[i].status != FS_NOTFOUND)
 			continue;
 
 		packetsize += nameonlylength(fileneeded[i].filename) + 22;
 
 		if ((numwadfiles+filestoget >= MAX_WADFILES)
-		|| (packetsize > sizeof(dummycheck->fileneeded)))
+		|| (packetsize > MAXFILENEEDED*sizeof(UINT8)))
 			return 3;
 
 		filestoget++;
@@ -449,27 +430,8 @@ void CL_LoadServerFiles(void)
 			fileneeded[i].status = FS_OPEN;
 		}
 		else if (fileneeded[i].status == FS_MD5SUMBAD)
-		{
-			// If the file is marked important, don't even bother proceeding.
-			if (fileneeded[i].important)
-				I_Error("Wrong version of important file %s", fileneeded[i].filename);
-
-			// If it isn't, no need to worry the user with a console message,
-			// although it can't hurt to put something in the debug file.
-
-			// ...but wait a second. What if the local version is "important"?
-			if (!W_VerifyNMUSlumps(fileneeded[i].filename))
-				I_Error("File %s should only contain music and sound effects!",
-					fileneeded[i].filename);
-
-			// Okay, NOW we know it's safe. Whew.
-			P_AddWadFile(fileneeded[i].filename);
-			if (fileneeded[i].important)
-				G_SetGameModified(true);
-			fileneeded[i].status = FS_OPEN;
-			DEBFILE(va("File %s found but with different md5sum\n", fileneeded[i].filename));
-		}
-		else if (fileneeded[i].important)
+			I_Error("Wrong version of file %s", fileneeded[i].filename);
+		else
 		{
 			const char *s;
 			switch(fileneeded[i].status)
@@ -939,10 +901,11 @@ void nameonly(char *s)
 		{
 			ns = &(s[j+1]);
 			len = strlen(ns);
-			if (false)
+#if 0
 				M_Memcpy(s, ns, len+1);
-			else
+#else
 				memmove(s, ns, len+1);
+#endif
 			return;
 		}
 }
diff --git a/src/d_netfil.h b/src/d_netfil.h
index a7b692931a3c7d68195150f1e816485ca9665557..3d7c2ed599dafb0b5eb4c772403b42a771d8b236 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -35,7 +35,6 @@ typedef enum
 
 typedef struct
 {
-	UINT8 important;
 	UINT8 willsend; // Is the server willing to send it?
 	char filename[MAX_WADPATH];
 	UINT8 md5sum[16];
diff --git a/src/dehacked.c b/src/dehacked.c
index 60379df07db95a9a9e0074f0d6128a9be9b13ab6..a726ecbc1fa32b6573b373acc261b75af81ca5ce 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -8338,7 +8338,6 @@ static inline int lib_getenum(lua_State *L)
 		lua_pushinteger(L, token);
 		return 1;
 	}
-
 	return 0;
 }
 
diff --git a/src/doomdef.h b/src/doomdef.h
index 796221c91802d4051e1c04a61f50374abc1a0c04..d5ba877c971c9ecf3d8d2304731f09f6e99f97f4 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -150,9 +150,9 @@ extern FILE *logstream;
 // we use comprevision and compbranch instead.
 #else
 #define VERSION    201 // Game version
-#define SUBVERSION 21  // more precise version number
-#define VERSIONSTRING "v2.1.21"
-#define VERSIONSTRINGW L"v2.1.21"
+#define SUBVERSION 22  // more precise version number
+#define VERSIONSTRING "v2.1.22"
+#define VERSIONSTRINGW L"v2.1.22"
 // Hey! If you change this, add 1 to the MODVERSION below!
 // Otherwise we can't force updates!
 #endif
@@ -161,6 +161,9 @@ extern FILE *logstream;
 // Comment or uncomment this as necessary.
 #define USE_PATCH_DTA
 
+// Use .kart extension addons
+//#define USE_KART
+
 // Modification options
 // If you want to take advantage of the Master Server's ability to force clients to update
 // to the latest version, fill these out.  Otherwise, just comment out UPDATE_ALERT and leave
@@ -214,7 +217,21 @@ extern FILE *logstream;
 // it's only for detection of the version the player is using so the MS can alert them of an update.
 // Only set it higher, not lower, obviously.
 // Note that we use this to help keep internal testing in check; this is why v2.1.0 is not version "1".
-#define MODVERSION 26
+#define MODVERSION 27
+
+// To version config.cfg, MAJOREXECVERSION is set equal to MODVERSION automatically.
+// Increment MINOREXECVERSION whenever a config change is needed that does not correspond
+// to an increment in MODVERSION. This might never happen in practice.
+// If MODVERSION increases, set MINOREXECVERSION to 0.
+#define MAJOREXECVERSION MODVERSION
+#define MINOREXECVERSION 0
+// (It would have been nice to use VERSION and SUBVERSION but those are zero'd out for DEVELOP builds)
+
+// Macros
+#define GETMAJOREXECVERSION(v) (v & 0xFFFF)
+#define GETMINOREXECVERSION(v) (v >> 16)
+#define GETEXECVERSION(major,minor) (major + (minor << 16))
+#define EXECVERSION GETEXECVERSION(MAJOREXECVERSION, MINOREXECVERSION)
 
 // =========================================================================
 
@@ -396,6 +413,7 @@ extern INT32 cv_debug;
 
 // Modifier key variables, accessible anywhere
 extern UINT8 shiftdown, ctrldown, altdown;
+extern boolean capslock;
 
 // if we ever make our alloc stuff...
 #define ZZ_Alloc(x) Z_Malloc(x, PU_STATIC, NULL)
@@ -410,6 +428,15 @@ INT32 I_GetKey(void);
 #define max(x, y) (((x) > (y)) ? (x) : (y))
 #endif
 
+// Floating point comparison epsilons from float.h
+#ifndef FLT_EPSILON
+#define FLT_EPSILON 1.1920928955078125e-7f
+#endif
+
+#ifndef DBL_EPSILON
+#define DBL_EPSILON 2.2204460492503131e-16
+#endif
+
 // An assert-type mechanism.
 #ifdef PARANOIA
 #define I_Assert(e) ((e) ? (void)0 : I_Error("assert failed: %s, file %s, line %d", #e, __FILE__, __LINE__))
diff --git a/src/f_finale.c b/src/f_finale.c
index fa6d6dbadedb4c582753333f0a5b470ea5ad8ea9..64e3712118d23264e3b277591b5ea38fdfb727df 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -997,7 +997,9 @@ static const char *credits[] = {
 	"Gregor \"Oogaland\" Dick",
 	"Louis-Antoine \"LJSonic\" de Moulins", // for fixing 2.1's netcode (de Rochefort doesn't quite fit on the screen sorry lol)
 	"Julio \"Chaos Zero 64\" Guir",
+	"\"Jimita\"",
 	"\"Kalaron\"", // Coded some of Sryder13's collection of OpenGL fixes, especially fog
+	"\"Lat'\"", // SRB2-CHAT, the chat window from Kart
 	"Matthew \"Shuffle\" Marsalko",
 	"Steven \"StroggOnMeth\" McGranahan",
 	"\"Morph\"", // For SRB2Morphed stuff
@@ -1006,9 +1008,9 @@ static const char *credits[] = {
 	"Tasos \"tatokis\" Sahanidis", // Corrected C FixedMul, making 64-bit builds netplay compatible
 	"\"Steel Titanium\"",
 	"Ben \"Cue\" Woodford",
-	// Git contributors with 5+ approved merges, at least a few of substantive quality, may be named
-	// Everyone else is acknowledged here
-	"STJr Git Contributors",
+	// Git contributors with 5+ approved merges of substantive quality,
+	// or contributors with at least one groundbreaking merge, may be named.
+	// Everyone else is acknowledged under "Special Thanks > SRB2 Community Contributors".
 	"",
 	"\1Sprite Artists",
 	"Odi \"Iceman404\" Atunzu",
@@ -1089,7 +1091,10 @@ static const char *credits[] = {
 	"Pascal \"CodeImp\" vd Heiden", // Doom Builder developer
 	"Randi Heit (<!>)", // For their MSPaint <!> sprite that we nicked
 	"Simon \"sirjuddington\" Judd", // SLADE developer
-
+	// Acknowledged here are the following:
+	// Minor merge request authors, see guideline above
+	// Golden - Expanded thin font
+	"SRB2 Community Contributors",
 	"",
 	"\1Produced By",
 	"Sonic Team Junior",
diff --git a/src/filesrch.c b/src/filesrch.c
index 2463e71789c29902055eb5f935204f5441d8ff50..0276e1c90c92b00488814768f5503db9fee038f6 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -31,6 +31,8 @@
 #include "filesrch.h"
 #include "d_netfil.h"
 #include "m_misc.h"
+#include "z_zone.h"
+#include "m_menu.h" // Addons_option_Onchange
 
 #if (defined (_WIN32) && !defined (_WIN32_WCE)) && defined (_MSC_VER) && !defined (_XBOX)
 
@@ -255,6 +257,28 @@ readdir (DIR * dirp)
   return (struct dirent *) 0;
 }
 
+/*
+ * rewinddir
+ *
+ * Makes the next readdir start from the beginning.
+ */
+int
+rewinddir (DIR * dirp)
+{
+  errno = 0;
+
+  /* Check for valid DIR struct. */
+  if (!dirp)
+    {
+      errno = EFAULT;
+      return -1;
+    }
+
+  dirp->dd_stat = 0;
+
+  return 0;
+}
+
 /*
  * closedir
  *
@@ -285,6 +309,41 @@ closedir (DIR * dirp)
   return rc;
 }
 #endif
+
+static CV_PossibleValue_t addons_cons_t[] = {{0, "Default"},
+#if 1
+												{1, "HOME"}, {2, "SRB2"},
+#endif
+													{3, "CUSTOM"}, {0, NULL}};
+
+consvar_t cv_addons_option = {"addons_option", "Default", CV_SAVE|CV_CALL, addons_cons_t, Addons_option_Onchange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_addons_folder = {"addons_folder", "", CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+static CV_PossibleValue_t addons_md5_cons_t[] = {{0, "Name"}, {1, "Contents"}, {0, NULL}};
+consvar_t cv_addons_md5 = {"addons_md5", "Name", CV_SAVE, addons_md5_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+consvar_t cv_addons_showall = {"addons_showall", "No", CV_SAVE, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+consvar_t cv_addons_search_case = {"addons_search_case", "No", CV_SAVE, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+static CV_PossibleValue_t addons_search_type_cons_t[] = {{0, "Start"}, {1, "Anywhere"}, {0, NULL}};
+consvar_t cv_addons_search_type = {"addons_search_type", "Anywhere", CV_SAVE, addons_search_type_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+char menupath[1024];
+size_t menupathindex[menudepth];
+size_t menudepthleft = menudepth;
+
+char menusearch[MAXSTRINGLENGTH+1];
+
+char **dirmenu, **coredirmenu; // core only local for this file
+size_t sizedirmenu, sizecoredirmenu; // ditto
+size_t dir_on[menudepth];
+UINT8 refreshdirmenu = 0;
+char *refreshdirname = NULL;
+
+size_t packetsizetally = 0;
+size_t mainwadstally = 0;
+
 #if defined (_XBOX) && defined (_MSC_VER)
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
 	boolean completepath, int maxsearchdepth)
@@ -296,6 +355,25 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 	completepath = false;
 	return FS_NOTFOUND;
 }
+
+void closefilemenu(boolean validsize)
+{
+	(void)validsize;
+	return;
+}
+
+void searchfilemenu(char *tempname)
+{
+	(void)tempname;
+	return;
+}
+
+boolean preparefilemenu(boolean samedepth)
+{
+	(void)samedepth;
+	return false;
+}
+
 #elif defined (_WIN32_WCE)
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
 	boolean completepath, int maxsearchdepth)
@@ -346,7 +424,27 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 #endif
 	return FS_NOTFOUND;
 }
+
+void closefilemenu(boolean validsize)
+{
+	(void)validsize;
+	return;
+}
+
+void searchfilemenu(char *tempname)
+{
+	(void)tempname;
+	return;
+}
+
+boolean preparefilemenu(boolean samedepth)
+{
+	(void)samedepth;
+	return false;
+}
+
 #else
+
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum, boolean completepath, int maxsearchdepth)
 {
 	filestatus_t retval = FS_NOTFOUND;
@@ -387,25 +485,29 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 	{
 		searchpath[searchpathindex[depthleft]]=0;
 		dent = readdir(dirhandle[depthleft]);
-		if (dent)
-			strcpy(&searchpath[searchpathindex[depthleft]],dent->d_name);
 
 		if (!dent)
+		{
 			closedir(dirhandle[depthleft++]);
-		else if (dent->d_name[0]=='.' &&
+			continue;
+		}
+
+		if (dent->d_name[0]=='.' &&
 				(dent->d_name[1]=='\0' ||
 					(dent->d_name[1]=='.' &&
 						dent->d_name[2]=='\0')))
 		{
 			// we don't want to scan uptree
+			continue;
 		}
-		else if (stat(searchpath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
-		{
-			// was the file (re)moved? can't stat it
-		}
+
+		// okay, now we actually want searchpath to incorporate d_name
+		strcpy(&searchpath[searchpathindex[depthleft]],dent->d_name);
+
+		if (stat(searchpath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
+			; // was the file (re)moved? can't stat it
 		else if (S_ISDIR(fsstat.st_mode) && depthleft)
 		{
-			strcpy(&searchpath[searchpathindex[depthleft]],dent->d_name);
 			searchpathindex[--depthleft] = strlen(searchpath) + 1;
 			dirhandle[depthleft] = opendir(searchpath);
 			if (!dirhandle[depthleft])
@@ -444,6 +546,365 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 	free(searchname);
 	free(searchpathindex);
 	free(dirhandle);
+
 	return retval;
 }
+
+char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) plus 3 (null terminator, stop, and length including previous two)
+	"\5.txt", "\5.cfg", // exec
+	"\5.wad",
+#ifdef USE_KART
+	"\6.kart",
+#endif
+	"\5.pk3", "\5.soc", "\5.lua"}; // addfile
+
+char filenamebuf[MAX_WADFILES][MAX_WADPATH];
+
+
+static boolean filemenucmp(char *haystack, char *needle)
+{
+	static char localhaystack[128];
+	strlcpy(localhaystack, haystack, 128);
+	if (!cv_addons_search_case.value)
+		strupr(localhaystack);
+	if (cv_addons_search_type.value)
+		return (strstr(localhaystack, needle) != 0);
+	return (!strncmp(localhaystack, needle, menusearch[0]));
+}
+
+void closefilemenu(boolean validsize)
+{
+	// search
+	if (dirmenu)
+	{
+		if (dirmenu != coredirmenu)
+		{
+			if (dirmenu[0] && ((UINT8)(dirmenu[0][DIR_TYPE]) == EXT_NORESULTS))
+			{
+				Z_Free(dirmenu[0]);
+				dirmenu[0] = NULL;
+			}
+			Z_Free(dirmenu);
+		}
+		dirmenu = NULL;
+		sizedirmenu = 0;
+	}
+
+	if (coredirmenu)
+	{
+		// core
+		if (validsize)
+		{
+			for (; sizecoredirmenu > 0; sizecoredirmenu--)
+			{
+				Z_Free(coredirmenu[sizecoredirmenu-1]);
+				coredirmenu[sizecoredirmenu-1] = NULL;
+			}
+		}
+		else
+			sizecoredirmenu = 0;
+
+		Z_Free(coredirmenu);
+		coredirmenu = NULL;
+	}
+
+	if (refreshdirname)
+		Z_Free(refreshdirname);
+	refreshdirname = NULL;
+}
+
+void searchfilemenu(char *tempname)
+{
+	size_t i, first;
+	char localmenusearch[MAXSTRINGLENGTH] = "";
+
+	if (dirmenu)
+	{
+		if (dirmenu != coredirmenu)
+		{
+			if (dirmenu[0] && ((UINT8)(dirmenu[0][DIR_TYPE]) == EXT_NORESULTS))
+			{
+				Z_Free(dirmenu[0]);
+				dirmenu[0] = NULL;
+			}
+			//Z_Free(dirmenu); -- Z_Realloc later tho...
+		}
+		else
+			dirmenu = NULL;
+	}
+
+	first = (((UINT8)(coredirmenu[0][DIR_TYPE]) == EXT_UP) ? 1 : 0); // skip UP...
+
+	if (!menusearch[0])
+	{
+		if (dirmenu)
+			Z_Free(dirmenu);
+		dirmenu = coredirmenu;
+		sizedirmenu = sizecoredirmenu;
+
+		if (tempname)
+		{
+			for (i = first; i < sizedirmenu; i++)
+			{
+				if (!strcmp(dirmenu[i]+DIR_STRING, tempname))
+				{
+					dir_on[menudepthleft] = i;
+					break;
+				}
+			}
+
+			if (i == sizedirmenu)
+				dir_on[menudepthleft] = first;
+
+			Z_Free(tempname);
+		}
+
+		return;
+	}
+
+	strcpy(localmenusearch, menusearch+1);
+	if (!cv_addons_search_case.value)
+		strupr(localmenusearch);
+
+	sizedirmenu = 0;
+	for (i = first; i < sizecoredirmenu; i++)
+	{
+		if (filemenucmp(coredirmenu[i]+DIR_STRING, localmenusearch))
+			sizedirmenu++;
+	}
+
+	if (!sizedirmenu) // no results...
+	{
+		if ((!(dirmenu = Z_Realloc(dirmenu, sizeof(char *), PU_STATIC, NULL)))
+			|| !(dirmenu[0] = Z_StrDup(va("%c\13No results...", EXT_NORESULTS))))
+				I_Error("searchfilemenu(): could not create \"No results...\".");
+		sizedirmenu = 1;
+		dir_on[menudepthleft] = 0;
+		if (tempname)
+			Z_Free(tempname);
+		return;
+	}
+
+	if (!(dirmenu = Z_Realloc(dirmenu, sizedirmenu*sizeof(char *), PU_STATIC, NULL)))
+		I_Error("searchfilemenu(): could not reallocate dirmenu.");
+
+	sizedirmenu = 0;
+	for (i = first; i < sizecoredirmenu; i++)
+	{
+		if (filemenucmp(coredirmenu[i]+DIR_STRING, localmenusearch))
+		{
+			if (tempname && !strcmp(coredirmenu[i]+DIR_STRING, tempname))
+			{
+				dir_on[menudepthleft] = sizedirmenu;
+				Z_Free(tempname);
+				tempname = NULL;
+			}
+			dirmenu[sizedirmenu++] = coredirmenu[i]; // pointer reuse
+		}
+	}
+
+	if (tempname)
+	{
+		dir_on[menudepthleft] = 0; //first; -- can't be first, causes problems
+		Z_Free(tempname);
+	}
+}
+
+boolean preparefilemenu(boolean samedepth)
+{
+	DIR *dirhandle;
+	struct dirent *dent;
+	struct stat fsstat;
+	size_t pos = 0, folderpos = 0, numfolders = 0;
+	char *tempname = NULL;
+
+	if (samedepth)
+	{
+		if (dirmenu && dirmenu[dir_on[menudepthleft]])
+			tempname = Z_StrDup(dirmenu[dir_on[menudepthleft]]+DIR_STRING); // don't need to I_Error if can't make - not important, just QoL
+	}
+	else
+		menusearch[0] = menusearch[1] = 0; // clear search
+
+	if (!(dirhandle = opendir(menupath))) // get directory
+	{
+		closefilemenu(true);
+		return false;
+	}
+
+	for (; sizecoredirmenu > 0; sizecoredirmenu--) // clear out existing items
+	{
+		Z_Free(coredirmenu[sizecoredirmenu-1]);
+		coredirmenu[sizecoredirmenu-1] = NULL;
+	}
+
+	while (true)
+	{
+		menupath[menupathindex[menudepthleft]] = 0;
+		dent = readdir(dirhandle);
+
+		if (!dent)
+			break;
+		else if (dent->d_name[0]=='.' &&
+				(dent->d_name[1]=='\0' ||
+					(dent->d_name[1]=='.' &&
+						dent->d_name[2]=='\0')))
+			continue; // we don't want to scan uptree
+
+		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
+
+		if (stat(menupath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
+			; // was the file (re)moved? can't stat it
+		else // is a file or directory
+		{
+			if (!S_ISDIR(fsstat.st_mode)) // file
+			{
+				if (!cv_addons_showall.value)
+				{
+					size_t len = strlen(dent->d_name)+1;
+					UINT8 ext;
+					for (ext = 0; ext < NUM_EXT_TABLE; ext++)
+						if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0]))) break; // extension comparison
+					if (ext == NUM_EXT_TABLE) continue; // not an addfile-able (or exec-able) file
+				}
+			}
+			else // directory
+				numfolders++;
+
+			sizecoredirmenu++;
+		}
+	}
+
+	if (!sizecoredirmenu)
+	{
+		closedir(dirhandle);
+		closefilemenu(false);
+		if (tempname)
+			Z_Free(tempname);
+		return false;
+	}
+
+	if (menudepthleft != menudepth-1) // Make room for UP...
+	{
+		sizecoredirmenu++;
+		numfolders++;
+		folderpos++;
+	}
+
+	if (dirmenu && dirmenu == coredirmenu)
+		dirmenu = NULL;
+
+	if (!(coredirmenu = Z_Realloc(coredirmenu, sizecoredirmenu*sizeof(char *), PU_STATIC, NULL)))
+	{
+		closedir(dirhandle); // just in case
+		I_Error("preparefilemenu(): could not reallocate coredirmenu.");
+	}
+
+	rewinddir(dirhandle);
+
+	while ((pos+folderpos) < sizecoredirmenu)
+	{
+		menupath[menupathindex[menudepthleft]] = 0;
+		dent = readdir(dirhandle);
+
+		if (!dent)
+			break;
+		else if (dent->d_name[0]=='.' &&
+				(dent->d_name[1]=='\0' ||
+					(dent->d_name[1]=='.' &&
+						dent->d_name[2]=='\0')))
+			continue; // we don't want to scan uptree
+
+		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
+
+		if (stat(menupath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
+			; // was the file (re)moved? can't stat it
+		else // is a file or directory
+		{
+			char *temp;
+			size_t len = strlen(dent->d_name)+1;
+			UINT8 ext = EXT_FOLDER;
+			UINT8 folder;
+
+			if (!S_ISDIR(fsstat.st_mode)) // file
+			{
+				if (!((numfolders+pos) < sizecoredirmenu)) continue; // crash prevention
+				for (; ext < NUM_EXT_TABLE; ext++)
+					if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0]))) break; // extension comparison
+				if (ext == NUM_EXT_TABLE && !cv_addons_showall.value) continue; // not an addfile-able (or exec-able) file
+				ext += EXT_START; // moving to be appropriate position
+
+				if (ext >= EXT_LOADSTART)
+				{
+					size_t i;
+					for (i = 0; i < numwadfiles; i++)
+					{
+						if (!filenamebuf[i][0])
+						{
+							strncpy(filenamebuf[i], wadfiles[i]->filename, MAX_WADPATH);
+							filenamebuf[i][MAX_WADPATH - 1] = '\0';
+							nameonly(filenamebuf[i]);
+						}
+
+						if (strcmp(dent->d_name, filenamebuf[i]))
+							continue;
+						if (cv_addons_md5.value && !checkfilemd5(menupath, wadfiles[i]->md5sum))
+							continue;
+
+						ext |= EXT_LOADED;
+					}
+				}
+				else if (ext == EXT_TXT)
+				{
+					if (!strcmp(dent->d_name, "log.txt") || !strcmp(dent->d_name, "errorlog.txt"))
+						ext |= EXT_LOADED;
+				}
+
+				if (!strcmp(dent->d_name, configfile))
+					ext |= EXT_LOADED;
+
+				folder = 0;
+			}
+			else // directory
+				len += (folder = 1);
+
+			if (len > 255)
+				len = 255;
+
+			if (!(temp = Z_Malloc((len+DIR_STRING+folder) * sizeof (char), PU_STATIC, NULL)))
+				I_Error("preparefilemenu(): could not create file entry.");
+			temp[DIR_TYPE] = ext;
+			temp[DIR_LEN] = (UINT8)(len);
+			strlcpy(temp+DIR_STRING, dent->d_name, len);
+			if (folder)
+			{
+				strcpy(temp+len, PATHSEP);
+				coredirmenu[folderpos++] = temp;
+			}
+			else
+				coredirmenu[numfolders + pos++] = temp;
+		}
+	}
+
+	closedir(dirhandle);
+
+	if ((menudepthleft != menudepth-1) // now for UP... entry
+		&& !(coredirmenu[0] = Z_StrDup(va("%c\5UP...", EXT_UP))))
+			I_Error("preparefilemenu(): could not create \"UP...\".");
+
+	menupath[menupathindex[menudepthleft]] = 0;
+	sizecoredirmenu = (numfolders+pos); // just in case things shrink between opening and rewind
+
+	if (!sizecoredirmenu)
+	{
+		dir_on[menudepthleft] = 0;
+		closefilemenu(false);
+		return false;
+	}
+
+	searchfilemenu(tempname);
+
+	return true;
+}
+
 #endif
diff --git a/src/filesrch.h b/src/filesrch.h
index 33b148d4b8c0ce668aaf08dd1c3cdcfeec87eb42..4186271b038d23aac0ad9a1f77a0a3b368914ada 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -6,6 +6,9 @@
 
 #include "doomdef.h"
 #include "d_netfil.h"
+#include "m_menu.h" // MAXSTRINGLENGTH
+
+extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_showall, cv_addons_search_case, cv_addons_search_type;
 
 /**	\brief	The filesearch function
 
@@ -25,4 +28,71 @@
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
 	boolean completepath, int maxsearchdepth);
 
+#define menudepth 20
+
+extern char menupath[1024];
+extern size_t menupathindex[menudepth];
+extern size_t menudepthleft;
+
+extern char menusearch[MAXSTRINGLENGTH+1];
+
+extern char **dirmenu;
+extern size_t sizedirmenu;
+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,
+	EXT_UP,
+	EXT_NORESULTS,
+	EXT_START,
+	EXT_TXT = EXT_START,
+	EXT_CFG,
+	EXT_LOADSTART,
+	EXT_WAD = EXT_LOADSTART,
+#ifdef USE_KART
+	EXT_KART,
+#endif
+	EXT_PK3,
+	EXT_SOC,
+	EXT_LUA, // allowed even if not HAVE_BLUA so that we can yell on load attempt
+	NUM_EXT,
+	NUM_EXT_TABLE = NUM_EXT-EXT_START,
+	EXT_LOADED = 0x80
+	/*
+	obviously there can only be 0x7F supported extensions in
+	addons menu because we're cramming this into a char out of
+	laziness/easy memory allocation (what's the difference?)
+	and have stolen a bit to show whether it's loaded or not
+	in practice the size of the data type is probably overkill
+	toast 02/05/17
+	*/
+} ext_enum;
+
+typedef enum
+{
+	DIR_TYPE = 0,
+	DIR_LEN,
+	DIR_STRING
+} dirname_enum;
+
+typedef enum
+{
+	REFRESHDIR_NORMAL = 1,
+	REFRESHDIR_ADDFILE = 2,
+	REFRESHDIR_WARNING = 4,
+	REFRESHDIR_ERROR = 8,
+	REFRESHDIR_NOTLOADED = 16,
+	REFRESHDIR_MAX = 32
+} refreshdir_enum;
+
+void closefilemenu(boolean validsize);
+void searchfilemenu(char *tempname);
+boolean preparefilemenu(boolean samedepth);
+
 #endif // __FILESRCH_H__
diff --git a/src/g_game.c b/src/g_game.c
index 60a5252cd4721f25d6a70cc71818280de2733ac8..213c3b83eb72575769e91b173dfbc5905219842a 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -347,6 +347,37 @@ static CV_PossibleValue_t joyaxis_cons_t[] = {{0, "None"},
 #endif
 #endif
 
+// don't mind me putting these here, I was lazy to figure out where else I could put those without blowing up the compiler.
+
+// it automatically becomes compact with 20+ players, but if you like it, I guess you can turn that on!
+consvar_t cv_compactscoreboard= {"compactscoreboard", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// chat timer thingy
+static CV_PossibleValue_t chattime_cons_t[] = {{5, "MIN"}, {999, "MAX"}, {0, NULL}};
+consvar_t cv_chattime = {"chattime", "8", CV_SAVE, chattime_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// chatwidth
+static CV_PossibleValue_t chatwidth_cons_t[] = {{64, "MIN"}, {150, "MAX"}, {0, NULL}};
+consvar_t cv_chatwidth = {"chatwidth", "128", CV_SAVE, chatwidth_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// chatheight
+static CV_PossibleValue_t chatheight_cons_t[] = {{6, "MIN"}, {22, "MAX"}, {0, NULL}};
+consvar_t cv_chatheight= {"chatheight", "8", CV_SAVE, chatheight_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// chat notifications (do you want to hear beeps? I'd understand if you didn't.)
+consvar_t cv_chatnotifications= {"chatnotifications", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// chat spam protection (why would you want to disable that???)
+consvar_t cv_chatspamprotection= {"chatspamprotection", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// minichat text background
+consvar_t cv_chatbacktint = {"chatbacktint", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// old shit console chat. (mostly exists for stuff like terminal, not because I cared if anyone liked the old chat.)
+static CV_PossibleValue_t consolechat_cons_t[] = {{0, "Window"}, {1, "Console"}, {2, "Window (Hidden)"}, {0, NULL}};
+consvar_t cv_consolechat = {"chatmode", "Window", CV_SAVE, consolechat_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+
 consvar_t cv_crosshair = {"crosshair", "Cross", CV_SAVE, crosshair_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_crosshair2 = {"crosshair2", "Cross", CV_SAVE, crosshair_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_invertmouse = {"invertmouse", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -3662,7 +3693,7 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 		unlocktriggers = 0;
 
 		// clear itemfinder, just in case
-		if (!dedicated) // except in dedicated servers, where it is not registered and can actually I_Error debug builds
+		if (!dedicated)	// except in dedicated servers, where it is not registered and can actually I_Error debug builds
 			CV_StealthSetValue(&cv_itemfinder, 0);
 	}
 
diff --git a/src/g_game.h b/src/g_game.h
index e4d155eb5d67736e1b4dad072aef6ee38a0674a4..5259eacbbf4848ab208e3290b04ffd308fbdaa14 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -54,6 +54,7 @@ extern tic_t timeinmap; // Ticker for time spent in level (used for levelcard di
 extern INT16 rw_maximums[NUM_WEAPONS];
 
 // used in game menu
+extern consvar_t cv_chatwidth, cv_chatnotifications, cv_chatheight, cv_chattime, cv_consolechat, cv_chatbacktint, cv_chatspamprotection, cv_compactscoreboard;
 extern consvar_t cv_crosshair, cv_crosshair2;
 extern consvar_t cv_invertmouse, cv_alwaysfreelook, cv_chasefreelook, cv_mousemove;
 extern consvar_t cv_invertmouse2, cv_alwaysfreelook2, cv_chasefreelook2, cv_mousemove2;
diff --git a/src/g_input.c b/src/g_input.c
index c0ed736474819725d0d56811e0d2ef931a7e1c4e..79623555909687fc6bf49ec637f59886e49bf3d4 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -98,6 +98,8 @@ void G_MapEventsToControls(event_t *ev)
 			break;
 
 		case ev_mouse: // buttons are virtual keys
+			if (menuactive || CON_Ready() || chat_on)
+				break;
 			mousex = (INT32)(ev->data2*((cv_mousesens.value*cv_mousesens.value)/110.0f + 0.1f));
 			mousey = (INT32)(ev->data3*((cv_mousesens.value*cv_mousesens.value)/110.0f + 0.1f));
 			mlooky = (INT32)(ev->data3*((cv_mouseysens.value*cv_mousesens.value)/110.0f + 0.1f));
@@ -105,7 +107,7 @@ void G_MapEventsToControls(event_t *ev)
 
 		case ev_joystick: // buttons are virtual keys
 			i = ev->data1;
-			if (i >= JOYAXISSET)
+			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
 				break;
 			if (ev->data2 != INT32_MAX) joyxmove[i] = ev->data2;
 			if (ev->data3 != INT32_MAX) joyymove[i] = ev->data3;
@@ -113,13 +115,15 @@ void G_MapEventsToControls(event_t *ev)
 
 		case ev_joystick2: // buttons are virtual keys
 			i = ev->data1;
-			if (i >= JOYAXISSET)
+			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
 				break;
 			if (ev->data2 != INT32_MAX) joy2xmove[i] = ev->data2;
 			if (ev->data3 != INT32_MAX) joy2ymove[i] = ev->data3;
 			break;
 
 		case ev_mouse2: // buttons are virtual keys
+			if (menuactive || CON_Ready() || chat_on)
+				break;
 			mouse2x = (INT32)(ev->data2*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
 			mouse2y = (INT32)(ev->data3*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
 			mlook2y = (INT32)(ev->data3*((cv_mouseysens2.value*cv_mousesens2.value)/110.0f + 0.1f));
@@ -1204,6 +1208,7 @@ void G_Controldefault(void)
 	gamecontrol[gc_lookup     ][0] = KEY_UPARROW;
 	gamecontrol[gc_lookdown   ][0] = KEY_DOWNARROW;
 	gamecontrol[gc_centerview ][0] = KEY_END;
+	gamecontrol[gc_centerview ][1] = KEY_JOY1+9; // Right Stick
 	gamecontrol[gc_talkkey    ][0] = 't';
 	gamecontrol[gc_talkkey    ][1] = KEY_HAT1+2; // D-Pad Left
 	gamecontrol[gc_teamkey    ][0] = 'y';
@@ -1224,6 +1229,7 @@ void G_Controldefault(void)
 	gamecontrolbis[gc_tossflag  ][0] = KEY_2JOY1+0; // A
 	gamecontrolbis[gc_use       ][0] = KEY_2JOY1+4; // LB
 	gamecontrolbis[gc_camreset  ][0] = KEY_2JOY1+3; // Y
+	gamecontrolbis[gc_centerview][0] = KEY_2JOY1+9; // Right Stick
 	gamecontrolbis[gc_jump      ][0] = KEY_2JOY1+5; // RB
 	//gamecontrolbis[gc_pause     ][0] = KEY_2JOY1+6; // Back
 	//gamecontrolbis[gc_systemmenu][0] = KEY_2JOY1+7; // Start
@@ -1333,30 +1339,166 @@ void G_SaveKeySetting(FILE *f)
 	}
 }
 
-void G_CheckDoubleUsage(INT32 keynum)
+INT32 G_CheckDoubleUsage(INT32 keynum, boolean modify)
 {
+	INT32 result = gc_null;
 	if (cv_controlperkey.value == 1)
 	{
 		INT32 i;
 		for (i = 0; i < num_gamecontrols; i++)
 		{
 			if (gamecontrol[i][0] == keynum)
-				gamecontrol[i][0] = KEY_NULL;
+			{
+				result = i;
+				if (modify) gamecontrol[i][0] = KEY_NULL;
+			}
 			if (gamecontrol[i][1] == keynum)
-				gamecontrol[i][1] = KEY_NULL;
+			{
+				result = i;
+				if (modify) gamecontrol[i][1] = KEY_NULL;
+			}
 			if (gamecontrolbis[i][0] == keynum)
-				gamecontrolbis[i][0] = KEY_NULL;
+			{
+				result = i;
+				if (modify) gamecontrolbis[i][0] = KEY_NULL;
+			}
 			if (gamecontrolbis[i][1] == keynum)
-				gamecontrolbis[i][1] = KEY_NULL;
+			{
+				result = i;
+				if (modify) gamecontrolbis[i][1] = KEY_NULL;
+			}
+			if (result && !modify)
+				return result;
 		}
 	}
+	return result;
 }
 
-static void setcontrol(INT32 (*gc)[2], INT32 na)
+static INT32 G_FilterKeyByVersion(INT32 numctrl, INT32 keyidx, INT32 player, INT32 *keynum1, INT32 *keynum2, boolean *nestedoverride)
+{
+	// Special case: ignore KEY_PAUSE because it's hardcoded
+	if (keyidx == 0 && *keynum1 == KEY_PAUSE)
+	{
+		if (*keynum2 != KEY_PAUSE)
+		{
+			*keynum1 = *keynum2; // shift down keynum2 and continue
+			*keynum2 = 0;
+		}
+		else
+			return -1; // skip setting control
+	}
+	else if (keyidx == 1 && *keynum2 == KEY_PAUSE)
+		return -1; // skip setting control
+
+#if !defined (DC) && !defined (_PSP) && !defined (GP2X) && !defined (_NDS) && !defined(WMINPUT) && !defined(_WII)
+	if (GETMAJOREXECVERSION(cv_execversion.value) < 27 && ( // v2.1.22
+		numctrl == gc_weaponnext || numctrl == gc_weaponprev || numctrl == gc_tossflag ||
+		numctrl == gc_use || numctrl == gc_camreset || numctrl == gc_jump ||
+		numctrl == gc_pause || numctrl == gc_systemmenu || numctrl == gc_camtoggle ||
+		numctrl == gc_screenshot || numctrl == gc_talkkey || numctrl == gc_scores ||
+		numctrl == gc_centerview
+	))
+	{
+		INT32 keynum = 0, existingctrl = 0;
+		INT32 defaultkey;
+		boolean defaultoverride = false;
+
+		// get the default gamecontrol
+		if (player == 0 && numctrl == gc_systemmenu)
+			defaultkey = gamecontrol[numctrl][0];
+		else
+			defaultkey = (player == 1 ? gamecontrolbis[numctrl][0] : gamecontrol[numctrl][1]);
+
+		// Assign joypad button defaults if there is an open slot.
+		// At this point, gamecontrol/bis should have the default controls
+		// (unless LOADCONFIG is being run)
+		//
+		// If the player runs SETCONTROL in-game, this block should not be reached
+		// because EXECVERSION is locked onto the latest version.
+		if (keyidx == 0 && !*keynum1)
+		{
+			if (*keynum2) // push keynum2 down; this is an edge case
+			{
+				*keynum1 = *keynum2;
+				*keynum2 = 0;
+				keynum = *keynum1;
+			}
+			else
+			{
+				keynum = defaultkey;
+				defaultoverride = true;
+			}
+		}
+		else if (keyidx == 1 && (!*keynum2 || (!*keynum1 && *keynum2))) // last one is the same edge case as above
+		{
+			keynum = defaultkey;
+			defaultoverride = true;
+		}
+		else // default to the specified keynum
+			keynum = (keyidx == 1 ? *keynum2 : *keynum1);
+
+		// Did our last call override keynum2?
+		if (*nestedoverride)
+		{
+			defaultoverride = true;
+			*nestedoverride = false;
+		}
+
+		// Fill keynum2 with the default control
+		if (keyidx == 0 && !*keynum2)
+		{
+			*keynum2 = defaultkey;
+			// Tell the next call that this is an override
+			*nestedoverride = true;
+
+			// if keynum2 already matches keynum1, we probably recursed
+			// so unset it
+			if (*keynum1 == *keynum2)
+			{
+				*keynum2 = 0;
+				*nestedoverride = false;
+		}
+		}
+
+		// check if the key is being used somewhere else before passing it
+		// pass it through if it's the same numctrl. This is an edge case -- when using
+		// LOADCONFIG, gamecontrol is not reset with default.
+		//
+		// Also, only check if we're actually overriding, to preserve behavior where
+		// config'd keys overwrite default keys.
+		if (defaultoverride)
+			existingctrl = G_CheckDoubleUsage(keynum, false);
+
+		if (keynum && (!existingctrl || existingctrl == numctrl))
+			return keynum;
+		else if (keyidx == 0 && *keynum2)
+		{
+			// try it again and push down keynum2
+			*keynum1 = *keynum2;
+			*keynum2 = 0;
+			return G_FilterKeyByVersion(numctrl, keyidx, player, keynum1, keynum2, nestedoverride);
+			// recursion *should* be safe because we only assign keynum2 to a joy default
+			// and then clear it if we find that keynum1 already has the joy default.
+		}
+		else
+			return 0;
+	}
+#endif
+
+	// All's good, so pass the keynum as-is
+	if (keyidx == 1)
+		return *keynum2;
+	else //if (keyidx == 0)
+		return *keynum1;
+}
+
+static void setcontrol(INT32 (*gc)[2])
 {
 	INT32 numctrl;
 	const char *namectrl;
-	INT32 keynum;
+	INT32 keynum, keynum1, keynum2;
+	INT32 player = ((void*)gc == (void*)&gamecontrolbis ? 1 : 0);
+	boolean nestedoverride = false;
 
 	namectrl = COM_Argv(1);
 	for (numctrl = 0; numctrl < num_gamecontrols && stricmp(namectrl, gamecontrolname[numctrl]);
@@ -1367,31 +1509,38 @@ static void setcontrol(INT32 (*gc)[2], INT32 na)
 		CONS_Printf(M_GetText("Control '%s' unknown\n"), namectrl);
 		return;
 	}
-	keynum = G_KeyStringtoNum(COM_Argv(2));
+	keynum1 = G_KeyStringtoNum(COM_Argv(2));
+	keynum2 = G_KeyStringtoNum(COM_Argv(3));
+	keynum = G_FilterKeyByVersion(numctrl, 0, player, &keynum1, &keynum2, &nestedoverride);
 
-	if (keynum == KEY_PAUSE) // fail silently; pause is hardcoded
+	if (keynum >= 0)
 	{
-		if (na == 4)
+		(void)G_CheckDoubleUsage(keynum, true);
+
+		// if keynum was rejected, try it again with keynum2
+		if (!keynum && keynum2)
 		{
-			na--;
-			keynum = G_KeyStringtoNum(COM_Argv(3));
-			if (keynum == KEY_PAUSE)
-				return;
+			keynum1 = keynum2; // push down keynum2
+			keynum2 = 0;
+			keynum = G_FilterKeyByVersion(numctrl, 0, player, &keynum1, &keynum2, &nestedoverride);
+			if (keynum >= 0)
+				(void)G_CheckDoubleUsage(keynum, true);
 		}
-		else
-			return;
 	}
 
-	G_CheckDoubleUsage(keynum);
-	gc[numctrl][0] = keynum;
+	if (keynum >= 0)
+		gc[numctrl][0] = keynum;
 
-	if (na == 4)
+	if (keynum2)
 	{
-		keynum = G_KeyStringtoNum(COM_Argv(3));
-		if (keynum != KEY_PAUSE)
-			gc[numctrl][1] = keynum;
-		else
-			gc[numctrl][1] = 0;
+		keynum = G_FilterKeyByVersion(numctrl, 1, player, &keynum1, &keynum2, &nestedoverride);
+		if (keynum >= 0)
+		{
+			if (keynum != gc[numctrl][0])
+				gc[numctrl][1] = keynum;
+			else
+				gc[numctrl][1] = 0;
+		}
 	}
 	else
 		gc[numctrl][1] = 0;
@@ -1409,7 +1558,7 @@ void Command_Setcontrol_f(void)
 		return;
 	}
 
-	setcontrol(gamecontrol, na);
+	setcontrol(gamecontrol);
 }
 
 void Command_Setcontrol2_f(void)
@@ -1424,5 +1573,5 @@ void Command_Setcontrol2_f(void)
 		return;
 	}
 
-	setcontrol(gamecontrolbis, na);
+	setcontrol(gamecontrolbis);
 }
diff --git a/src/g_input.h b/src/g_input.h
index 102809f7ff0997c8d950ee315d5f37066064b939..8f4e9cb097d7c3f46c8f1ba76daf6008fcda2ec0 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -165,6 +165,6 @@ void Command_Setcontrol_f(void);
 void Command_Setcontrol2_f(void);
 void G_Controldefault(void);
 void G_SaveKeySetting(FILE *f);
-void G_CheckDoubleUsage(INT32 keynum);
+INT32 G_CheckDoubleUsage(INT32 keynum, boolean modify);
 
 #endif
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index 40d38d0594b5fb2ee2d5fed00cc6f7e0c85b189b..55bbde45984dd2607c336ddb15a16aad84147830 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -790,6 +790,110 @@ void HWR_drawAMline(const fline_t *fl, INT32 color)
 	HWD.pfnDraw2DLine(&v1, &v2, color_rgba);
 }
 
+// -------------------+
+// HWR_DrawConsoleFill     : draw flat coloured transparent rectangle because that's cool, and hw sucks less than sw for that.
+// -------------------+
+void HWR_DrawConsoleFill(INT32 x, INT32 y, INT32 w, INT32 h, UINT32 color, INT32 options)
+{
+	FOutVector v[4];
+	FSurfaceInfo Surf;
+	float fx, fy, fw, fh;
+
+	if (w < 0 || h < 0)
+		return; // consistency w/ software
+
+//  3--2
+//  | /|
+//  |/ |
+//  0--1
+
+	fx = (float)x;
+	fy = (float)y;
+	fw = (float)w;
+	fh = (float)h;
+
+	if (!(options & V_NOSCALESTART))
+	{
+		float dupx = (float)vid.dupx, dupy = (float)vid.dupy;
+
+		if (x == 0 && y == 0 && w == BASEVIDWIDTH && h == BASEVIDHEIGHT)
+		{
+			RGBA_t rgbaColour = V_GetColor(color);
+			FRGBAFloat clearColour;
+			clearColour.red = (float)rgbaColour.s.red / 255;
+			clearColour.green = (float)rgbaColour.s.green / 255;
+			clearColour.blue = (float)rgbaColour.s.blue / 255;
+			clearColour.alpha = 1;
+			HWD.pfnClearBuffer(true, false, &clearColour);
+			return;
+		}
+
+		fx *= dupx;
+		fy *= dupy;
+		fw *= dupx;
+		fh *= dupy;
+
+		if (fabsf((float)vid.width - ((float)BASEVIDWIDTH * dupx)) > 1.0E-36f)
+		{
+			if (options & V_SNAPTORIGHT)
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
+			else if (!(options & V_SNAPTOLEFT))
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 2;
+		}
+		if (fabsf((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) > 1.0E-36f)
+		{
+			// same thing here
+			if (options & V_SNAPTOBOTTOM)
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
+			else if (!(options & V_SNAPTOTOP))
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 2;
+		}
+	}
+
+	if (fx >= vid.width || fy >= vid.height)
+		return;
+	if (fx < 0)
+	{
+		fw += fx;
+		fx = 0;
+	}
+	if (fy < 0)
+	{
+		fh += fy;
+		fy = 0;
+	}
+
+	if (fw <= 0 || fh <= 0)
+		return;
+	if (fx + fw > vid.width)
+		fw = (float)vid.width - fx;
+	if (fy + fh > vid.height)
+		fh = (float)vid.height - fy;
+
+	fx = -1 + fx / (vid.width / 2);
+	fy = 1 - fy / (vid.height / 2);
+	fw = fw / (vid.width / 2);
+	fh = fh / (vid.height / 2);
+
+	v[0].x = v[3].x = fx;
+	v[2].x = v[1].x = fx + fw;
+	v[0].y = v[1].y = fy;
+	v[2].y = v[3].y = fy - fh;
+
+	//Hurdler: do we still use this argb color? if not, we should remove it
+	v[0].argb = v[1].argb = v[2].argb = v[3].argb = 0xff00ff00; //;
+	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
+
+	v[0].sow = v[3].sow = 0.0f;
+	v[2].sow = v[1].sow = 1.0f;
+	v[0].tow = v[1].tow = 0.0f;
+	v[2].tow = v[3].tow = 1.0f;
+
+	Surf.FlatColor.rgba = UINT2RGBA(color);
+	Surf.FlatColor.s.alpha = 0x80;
+
+	HWD.pfnDrawPolygon(&Surf, v, 4, PF_NoTexture|PF_Modulated|PF_Translucent|PF_NoDepthTest);
+}
 
 // -----------------+
 // HWR_DrawFill     : draw flat coloured rectangle, with no texture
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index c72843715d3d09769833f9d5d66faf5495eebaa7..59042cf3b1c4c34f030397a8226385af9b726b9b 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -51,6 +51,7 @@ void HWR_CreatePlanePolygons(INT32 bspnum);
 void HWR_CreateStaticLightmaps(INT32 bspnum);
 void HWR_PrepLevelCache(size_t pnumtextures);
 void HWR_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 color);
+void HWR_DrawConsoleFill(INT32 x, INT32 y, INT32 w, INT32 h, UINT32 color, INT32 options);	// Lat: separate flags from color since color needs to be an uint to work right.
 void HWR_DrawPic(INT32 x,INT32 y,lumpnum_t lumpnum);
 
 void HWR_AddCommands(void);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index c6f95613fb6c99c6b5c1a0db3598972ad2fbf388..2cecb5e3e8659d2ed3a66466caeef5add38af895 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -73,6 +73,7 @@ patch_t *cred_font[CRED_FONTSIZE];
 static player_t *plr;
 boolean chat_on; // entering a chat message?
 static char w_chat[HU_MAXMSGLEN];
+static size_t c_input = 0; // let's try to make the chat input less shitty.
 static boolean headsupactive = false;
 boolean hu_showscores; // draw rankings
 static char hu_tick;
@@ -319,6 +320,81 @@ void HU_Start(void)
 //======================================================================
 
 #ifndef NONET
+
+// EVERY CHANGE IN THIS SCRIPT IS LOL XD! BY VINCYTM
+
+static UINT32 chat_nummsg_log = 0;
+static UINT32 chat_nummsg_min = 0;
+static UINT32 chat_scroll = 0;
+static tic_t chat_scrolltime = 0;
+
+static UINT32 chat_maxscroll = 0; // how far can we scroll?
+
+//static chatmsg_t chat_mini[CHAT_BUFSIZE]; // Display the last few messages sent.
+//static chatmsg_t chat_log[CHAT_BUFSIZE]; // Keep every message sent to us in memory so we can scroll n shit, it's cool.
+
+static char chat_log[CHAT_BUFSIZE][255]; // hold the last 48 or so messages in that log.
+static char chat_mini[8][255]; // display up to 8 messages that will fade away / get overwritten
+static tic_t chat_timers[8];
+
+static boolean chat_scrollmedown = false; // force instant scroll down on the chat log. Happens when you open it / send a message.
+
+// remove text from minichat table
+
+static INT16 addy = 0; // use this to make the messages scroll smoothly when one fades away
+
+static void HU_removeChatText_Mini(void)
+{
+    // MPC: Don't create new arrays, just iterate through an existing one
+	size_t i;
+    for(i=0;i<chat_nummsg_min-1;i++) {
+        strcpy(chat_mini[i], chat_mini[i+1]);
+        chat_timers[i] = chat_timers[i+1];
+    }
+	chat_nummsg_min--; // lost 1 msg.
+
+	// use addy and make shit slide smoothly af.
+	addy += (vid.width < 640) ? 8 : 6;
+
+}
+
+// same but w the log. TODO: optimize this and maybe merge in a single func? im bad at C.
+static void HU_removeChatText_Log(void)
+{
+	// MPC: Don't create new arrays, just iterate through an existing one
+	size_t i;
+    for(i=0;i<chat_nummsg_log-1;i++) {
+        strcpy(chat_log[i], chat_log[i+1]);
+    }
+    chat_nummsg_log--; // lost 1 msg.
+}
+
+void HU_AddChatText(const char *text, boolean playsound)
+{
+	if (playsound && cv_consolechat.value != 2) // Don't play the sound if we're using hidden chat.
+		S_StartSound(NULL, sfx_radio);
+	// reguardless of our preferences, put all of this in the chat buffer in case we decide to change from oldchat mid-game.
+
+	if (chat_nummsg_log >= CHAT_BUFSIZE) // too many messages!
+		HU_removeChatText_Log();
+
+	strcpy(chat_log[chat_nummsg_log], text);
+	chat_nummsg_log++;
+
+	if (chat_nummsg_min >= 8)
+		HU_removeChatText_Mini();
+
+	strcpy(chat_mini[chat_nummsg_min], text);
+	chat_timers[chat_nummsg_min] = TICRATE*cv_chattime.value;
+	chat_nummsg_min++;
+
+	if (OLDCHAT) // if we're using oldchat, print directly in console
+		CONS_Printf("%s\n", text);
+	else			// if we aren't, still save the message to log.txt
+		CON_LogMessage(va("%s\n", text));
+}
+
+
 /** Runs a say command, sending an ::XD_SAY message.
   * A say command consists of a signed 8-bit integer for the target, an
   * unsigned 8-bit flag variable, and then the message itself.
@@ -337,6 +413,8 @@ void HU_Start(void)
   * \sa Command_Say_f, Command_Sayteam_f, Command_Sayto_f, Got_Saycmd
   * \author Graue <graue@oceanbase.org>
   */
+
+
 static void DoSayCommand(SINT8 target, size_t usedargs, UINT8 flags)
 {
 	XBOXSTATIC char buf[254];
@@ -347,14 +425,14 @@ static void DoSayCommand(SINT8 target, size_t usedargs, UINT8 flags)
 	numwords = COM_Argc() - usedargs;
 	I_Assert(numwords > 0);
 
-	if (cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))
+	if (CHAT_MUTE) // TODO: Per Player mute.
 	{
-		CONS_Alert(CONS_NOTICE, M_GetText("The chat is muted. You can't say anything at the moment.\n"));
+		HU_AddChatText(va("%s>ERROR: The chat is muted. You can't say anything.", "\x85"), false);
 		return;
 	}
 
 	// Only servers/admins can CSAY.
-	if(!server && IsPlayerAdmin(consoleplayer))
+	if(!server && !(IsPlayerAdmin(consoleplayer)))
 		flags &= ~HU_CSAY;
 
 	// We handle HU_SERVER_SAY, not the caller.
@@ -373,6 +451,57 @@ static void DoSayCommand(SINT8 target, size_t usedargs, UINT8 flags)
 		strlcat(msg, COM_Argv(ix + usedargs), msgspace);
 	}
 
+	if (strlen(msg) > 4 && strnicmp(msg, "/pm", 3) == 0) // used /pm
+	{
+		// what we're gonna do now is check if the node exists
+		// with that logic, characters 4 and 5 are our numbers:
+		const char *newmsg;
+		char *nodenum = (char*) malloc(3);
+		INT32 spc = 1; // used if nodenum[1] is a space.
+
+		strncpy(nodenum, msg+3, 3);
+		// check for undesirable characters in our "number"
+		if 	(((nodenum[0] < '0') || (nodenum[0] > '9')) || ((nodenum[1] < '0') || (nodenum[1] > '9')))
+		{
+			// check if nodenum[1] is a space
+			if (nodenum[1] == ' ')
+				spc = 0;
+				// let it slide
+			else
+			{
+				HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
+				free(nodenum);
+				return;
+			}
+		}
+		// I'm very bad at C, I swear I am, additional checks eww!
+			if (spc != 0)
+			{
+				if (msg[5] != ' ')
+				{
+					HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
+					free(nodenum);
+					return;
+				}
+			}
+
+		target = atoi((const char*) nodenum); // turn that into a number
+		free(nodenum);
+		//CONS_Printf("%d\n", target);
+
+		// check for target player, if it doesn't exist then we can't send the message!
+		if (target < MAXPLAYERS && playeringame[target]) // player exists
+			target++; // even though playernums are from 0 to 31, target is 1 to 32, so up that by 1 to have it work!
+		else
+		{
+			HU_AddChatText(va("\x82NOTICE: \x80Player %d does not exist.", target), false); // same
+			return;
+		}
+		buf[0] = target;
+		newmsg = msg+5+spc;
+		strlcpy(msg, newmsg, 252);
+	}
+
 	SendNetXCmd(XD_SAY, buf, strlen(msg) + 1 + msg-buf);
 }
 
@@ -456,6 +585,7 @@ static void Command_CSay_f(void)
 
 	DoSayCommand(0, 1, HU_CSAY);
 }
+static tic_t stop_spamming[MAXPLAYERS];
 
 /** Receives a message, processing an ::XD_SAY command.
   * \sa DoSayCommand
@@ -469,6 +599,7 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 	char *msg;
 	boolean action = false;
 	char *ptr;
+	INT32 spam_eatmsg = 0;
 
 	CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s)\n", playernum+1, player_names[playernum]);
 
@@ -477,7 +608,7 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 	msg = (char *)*p;
 	SKIPSTRING(*p);
 
-	if ((cv_mute.value || flags & (HU_CSAY|HU_SERVER_SAY)) && playernum != serverplayer && !IsPlayerAdmin(playernum))
+	if ((cv_mute.value || flags & (HU_CSAY|HU_SERVER_SAY)) && playernum != serverplayer && !(IsPlayerAdmin(playernum)))
 	{
 		CONS_Alert(CONS_WARNING, cv_mute.value ?
 			M_GetText("Illegal say command received from %s while muted\n") : M_GetText("Illegal csay command received from non-admin %s\n"),
@@ -515,11 +646,28 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 		}
 	}
 
+	// before we do anything, let's verify the guy isn't spamming, get this easier on us.
+
+	//if (stop_spamming[playernum] != 0 && cv_chatspamprotection.value && !(flags & HU_CSAY))
+	if (stop_spamming[playernum] != 0 && consoleplayer != playernum && cv_chatspamprotection.value && !(flags & HU_CSAY))
+	{
+		CONS_Debug(DBG_NETPLAY,"Received SAY cmd too quickly from Player %d (%s), assuming as spam and blocking message.\n", playernum+1, player_names[playernum]);
+		stop_spamming[playernum] = 4;
+		spam_eatmsg = 1;
+	}
+	else
+		stop_spamming[playernum] = 4; // you can hold off for 4 tics, can you?
+
+	// run the lua hook even if we were supposed to eat the msg, netgame consistency goes first.
+
 #ifdef HAVE_BLUA
 	if (LUAh_PlayerMsg(playernum, target, flags, msg))
 		return;
 #endif
 
+	if (spam_eatmsg)
+		return; // don't proceed if we were supposed to eat the message.
+
 	// If it's a CSAY, just CECHO and be done with it.
 	if (flags & HU_CSAY)
 	{
@@ -559,18 +707,56 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 	|| target == 0 // To everyone
 	|| consoleplayer == target-1) // To you
 	{
-		const char *cstart = "", *cend = "", *adminchar = "~", *remotechar = "@", *fmt;
+		const char *prefix = "", *cstart = "", *cend = "", *adminchar = "\x82~\x83", *remotechar = "\x82@\x83", *fmt2, *textcolor = "\x80";
 		char *tempchar = NULL;
 
-		// In CTF and team match, color the player's name.
-		if (G_GametypeHasTeams())
+		// player is a spectator?
+        if (players[playernum].spectator)
+		{
+			cstart = "\x86";    // grey name
+			textcolor = "\x86";
+		}
+		else if (target == -1) // say team
 		{
-			cend = "\x80";
 			if (players[playernum].ctfteam == 1) // red
+			{
 				cstart = "\x85";
-			else if (players[playernum].ctfteam == 2) // blue
+				textcolor = "\x85";
+			}
+			else // blue
+			{
 				cstart = "\x84";
+				textcolor = "\x84";
+			}
 		}
+		else
+        {
+			const UINT8 color = players[playernum].skincolor;
+
+			cstart = "\x83";
+
+			if (color <= SKINCOLOR_SILVER)
+				cstart = "\x80"; // White
+			else if (color <= SKINCOLOR_BLACK)
+				cstart = "\x86"; // Grey
+			else if (color <= SKINCOLOR_BLUE)
+				cstart = "\x84"; // Blue
+			else if (color <= SKINCOLOR_PEACH)
+				cstart = "\x87"; //... Orange???
+			else if (color == SKINCOLOR_PINK)
+				cstart = "\x85"; // Red.
+			else if (color <= SKINCOLOR_PURPLE)
+				cstart = "\x81"; // Purple
+			else if (color <= SKINCOLOR_ROSEWOOD)
+				cstart = "\x87"; // Orange
+			else if (color <= SKINCOLOR_DARKRED)
+				cstart = "\x85"; // Red
+			else if (color <= SKINCOLOR_OLIVE)
+				cstart = "\x83"; // green
+			else if (color <= SKINCOLOR_GOLD)
+				cstart = "\x82"; // Yellow
+        }
+		prefix = cstart;
 
 		// Give admins and remote admins their symbols.
 		if (playernum == serverplayer)
@@ -579,11 +765,11 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 			tempchar = (char *)Z_Calloc(strlen(cstart) + strlen(remotechar) + 1, PU_STATIC, NULL);
 		if (tempchar)
 		{
-			strcat(tempchar, cstart);
 			if (playernum == serverplayer)
 				strcat(tempchar, adminchar);
 			else
 				strcat(tempchar, remotechar);
+			strcat(tempchar, cstart);
 			cstart = tempchar;
 		}
 
@@ -592,21 +778,39 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 		// name, color end, and the message itself.
 		// '\4' makes the message yellow and beeps; '\3' just beeps.
 		if (action)
-			fmt = "\4* %s%s%s \x82%s\n";
-		else if (target == 0) // To everyone
-			fmt = "\3<%s%s%s> %s\n";
+			fmt2 = "* %s%s%s%s \x82%s%s";
 		else if (target-1 == consoleplayer) // To you
-			fmt = "\3*%s%s%s* %s\n";
+		{
+			prefix = "\x82[PM]";
+			cstart = "\x82";
+			textcolor = "\x82";
+			fmt2 = "%s<%s%s>%s\x80 %s%s";
+		}
 		else if (target > 0) // By you, to another player
 		{
 			// Use target's name.
 			dispname = player_names[target-1];
-			fmt = "\3->*%s%s%s* %s\n";
+			prefix = "\x82[TO]";
+			cstart = "\x82";
+			fmt2 = "%s<%s%s>%s\x80 %s%s";
+
 		}
+		else if (target == 0) // To everyone
+			fmt2 = "%s<%s%s%s>\x80 %s%s";
 		else // To your team
-			fmt = "\3>>%s%s%s<< (team) %s\n";
+		{
+			if (players[playernum].ctfteam == 1) // red
+				prefix = "\x85[TEAM]";
+			else if (players[playernum].ctfteam == 2) // blue
+				prefix = "\x84[TEAM]";
+			else
+				prefix = "\x83"; // makes sure this doesn't implode if you sayteam on non-team gamemodes
+
+			fmt2 = "%s<%s%s>\x80%s %s%s";
+		}
+
+		HU_AddChatText(va(fmt2, prefix, cstart, dispname, cend, textcolor, msg), cv_chatnotifications.value); // add to chat
 
-		CONS_Printf(fmt, cstart, dispname, cend, msg);
 		if (tempchar)
 			Z_Free(tempchar);
 	}
@@ -633,19 +837,53 @@ static inline boolean HU_keyInChatString(char *s, char ch)
 		l = strlen(s);
 		if (l < HU_MAXMSGLEN - 1)
 		{
-			s[l++] = ch;
-			s[l]=0;
+			if (c_input >= strlen(s)) // don't do anything complicated
+			{
+				s[l++] = ch;
+				s[l]=0;
+			}
+			else
+			{
+
+				// move everything past c_input for new characters:
+				size_t m = HU_MAXMSGLEN-1;
+				while (m>=c_input)
+				{
+					if (s[m])
+						s[m+1] = (s[m]);
+					if (m == 0) // prevent overflow
+						break;
+					m--;
+				}
+				s[c_input] = ch; // and replace this.
+			}
+			c_input++;
 			return true;
 		}
 		return false;
 	}
 	else if (ch == KEY_BACKSPACE)
 	{
-		l = strlen(s);
-		if (l)
-			s[--l] = 0;
-		else
+		size_t i = c_input;
+
+		if (c_input <= 0)
+			return false;
+
+		if (!s[i-1])
 			return false;
+
+		if (i >= strlen(s)-1)
+		{
+			s[strlen(s)-1] = 0;
+			c_input--;
+			return false;
+		}
+
+		for (; (i < HU_MAXMSGLEN); i++)
+		{
+			s[i-1] = s[i];
+		}
+		c_input--;
 	}
 	else if (ch != KEY_ENTER)
 		return false; // did not eat key
@@ -669,28 +907,30 @@ void HU_Ticker(void)
 		hu_showscores = false;
 }
 
-#define QUEUESIZE 256
-
 static boolean teamtalk = false;
-static char chatchars[QUEUESIZE];
-static INT32 head = 0, tail = 0;
+/*static char chatchars[QUEUESIZE];
+static INT32 head = 0, tail = 0;*/
+// WHY DO YOU OVERCOMPLICATE EVERYTHING?????????
 
-//
-// HU_dequeueChatChar
-//
-char HU_dequeueChatChar(void)
+// Clear spaces so we don't end up with messages only made out of emptiness
+static boolean HU_clearChatSpaces(void)
 {
-	char c;
+	size_t i = 0; // Used to just check our message
+	char c; // current character we're iterating.
+	boolean nothingbutspaces = true;
 
-	if (head != tail)
+	for (; i < strlen(w_chat); i++) // iterate through message and eradicate all spaces that don't belong.
 	{
-		c = chatchars[tail];
-		tail = (tail + 1) & (QUEUESIZE-1);
-	}
-	else
-		c = 0;
+		c = w_chat[i];
+		if (!c)
+			break; // if there's nothing, it's safe to assume our message has ended, so let's not waste any more time here.
 
-	return c;
+		if (c != ' ') // Isn't a space
+		{
+			nothingbutspaces = false;
+		}
+	}
+	return nothingbutspaces;
 }
 
 //
@@ -701,131 +941,815 @@ static void HU_queueChatChar(char c)
 	if (c == KEY_ENTER)
 	{
 		char buf[2+256];
+		char *msg = &buf[2];
+		size_t i = 0;
 		size_t ci = 2;
+		INT32 target = 0;
+
+		if (HU_clearChatSpaces()) // Avoids being able to send empty messages, or something.
+			return; // If this returns true, that means our message was NOTHING but spaces, so don't send it period.
 
 		do {
-			c = HU_dequeueChatChar();
+			c = w_chat[-2+ci++];
 			if (!c || (c >= ' ' && !(c & 0x80))) // copy printable characters and terminating '\0' only.
-				buf[ci++]=c;
+				buf[ci-1]=c;
 		} while (c);
 
+		for (;(i<HU_MAXMSGLEN);i++)
+			w_chat[i] = 0; // reset this.
+
+		c_input = 0;
+
 		// last minute mute check
-		if (cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))
+		if (CHAT_MUTE)
 		{
-			CONS_Alert(CONS_NOTICE, M_GetText("The chat is muted. You can't say anything at the moment.\n"));
+			HU_AddChatText(va("%s>ERROR: The chat is muted. You can't say anything.", "\x85"), false);
 			return;
 		}
 
+		if (strlen(msg) > 4 && strnicmp(msg, "/pm", 3) == 0) // used /pm
+		{
+			INT32 spc = 1; // used if nodenum[1] is a space.
+			char *nodenum = (char*) malloc(3);
+			const char *newmsg;
+
+			// what we're gonna do now is check if the node exists
+			// with that logic, characters 4 and 5 are our numbers:
+
+			// teamtalk can't send PMs, just don't send it, else everyone would be able to see it, and no one wants to see your sex RP sicko.
+			if (teamtalk)
+			{
+				HU_AddChatText(va("%sCannot send sayto in Say-Team.", "\x85"), false);
+				return;
+			}
+
+			strncpy(nodenum, msg+3, 3);
+			// check for undesirable characters in our "number"
+			if 	(((nodenum[0] < '0') || (nodenum[0] > '9')) || ((nodenum[1] < '0') || (nodenum[1] > '9')))
+			{
+				// check if nodenum[1] is a space
+				if (nodenum[1] == ' ')
+					spc = 0;
+					// let it slide
+				else
+				{
+					HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
+					free(nodenum);
+					return;
+				}
+			}
+			// I'm very bad at C, I swear I am, additional checks eww!
+			if (spc != 0)
+			{
+				if (msg[5] != ' ')
+				{
+					HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
+					free(nodenum);
+					return;
+				}
+			}
+
+			target = atoi((const char*) nodenum); // turn that into a number
+			free(nodenum);
+			//CONS_Printf("%d\n", target);
+
+			// check for target player, if it doesn't exist then we can't send the message!
+			if (target < MAXPLAYERS && playeringame[target]) // player exists
+				target++; // even though playernums are from 0 to 31, target is 1 to 32, so up that by 1 to have it work!
+			else
+			{
+				HU_AddChatText(va("\x82NOTICE: \x80Player %d does not exist.", target), false); // same
+				return;
+			}
+
+			// we need to get rid of the /pm<node>
+			newmsg = msg+5+spc;
+			strlcpy(msg, newmsg, 255);
+		}
 		if (ci > 3) // don't send target+flags+empty message.
 		{
 			if (teamtalk)
 				buf[0] = -1; // target
 			else
-				buf[0] = 0; // target
+				buf[0] = target;
+
 			buf[1] = 0; // flags
 			SendNetXCmd(XD_SAY, buf, 2 + strlen(&buf[2]) + 1);
 		}
 		return;
 	}
-
-	if (((head + 1) & (QUEUESIZE-1)) == tail)
-		CONS_Printf(M_GetText("[Message unsent]\n")); // message not sent
-	else
-	{
-		if (c == KEY_BACKSPACE)
-		{
-			if (tail != head)
-				head = (head - 1) & (QUEUESIZE-1);
-		}
-		else
-		{
-			chatchars[head] = c;
-			head = (head + 1) & (QUEUESIZE-1);
-		}
-	}
 }
 
 void HU_clearChatChars(void)
 {
-	while (tail != head)
-		HU_queueChatChar(KEY_BACKSPACE);
+	size_t i = 0;
+	for (;i<HU_MAXMSGLEN;i++)
+		w_chat[i] = 0; // reset this.
 	chat_on = false;
+	c_input = 0;
 }
 
+static boolean justscrolleddown;
+static boolean justscrolledup;
+static INT16 typelines = 1; // number of drawfill lines we need when drawing the chat. it's some weird hack and might be one frame off but I'm lazy to make another loop.
+// It's up here since it has to be reset when we open the chat.
+
+
 //
 // Returns true if key eaten
 //
 boolean HU_Responder(event_t *ev)
 {
-	UINT8 c;
+	INT32 c=0;
 
 	if (ev->type != ev_keydown)
 		return false;
 
 	// only KeyDown events now...
 
+	/*// Shoot, to prevent P1 chatting from ruining the game for everyone else, it's either:
+	// A. completely disallow opening chat entirely in online splitscreen
+	// or B. iterate through all controls to make sure it's bound to player 1 before eating
+	// You can see which one I chose.
+	// (Unless if you're sharing a keyboard, since you probably establish when you start chatting that you have dibs on it...)
+	// (Ahhh, the good ol days when I was a kid who couldn't afford an extra USB controller...)
+
+	if (ev->data1 >= KEY_MOUSE1)
+	{
+		INT32 i;
+		for (i = 0; i < num_gamecontrols; i++)
+		{
+			if (gamecontrol[i][0] == ev->data1 || gamecontrol[i][1] == ev->data1)
+				break;
+		}
+
+		if (i == num_gamecontrols)
+			return false;
+	}*/	//We don't actually care about that unless we get splitscreen netgames. :V
+
+	c = (INT32)ev->data1;
+
+	// capslock (now handled outside of chat on so that it works everytime......)
+	if (c && c == KEY_CAPSLOCK) // it's a toggle.
+	{
+		if (capslock)
+			capslock = false;
+		else
+			capslock = true;
+		return true;
+	}
+
 	if (!chat_on)
 	{
 		// enter chat mode
 		if ((ev->data1 == gamecontrol[gc_talkkey][0] || ev->data1 == gamecontrol[gc_talkkey][1])
-			&& netgame && (!cv_mute.value || server || IsPlayerAdmin(consoleplayer)))
+			&& netgame && !OLD_MUTE) // check for old chat mute, still let the players open the chat incase they want to scroll otherwise.
 		{
-			if (cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))
-				return false;
 			chat_on = true;
 			w_chat[0] = 0;
 			teamtalk = false;
+			chat_scrollmedown = true;
+			typelines = 1;
 			return true;
 		}
 		if ((ev->data1 == gamecontrol[gc_teamkey][0] || ev->data1 == gamecontrol[gc_teamkey][1])
-			&& netgame && (!cv_mute.value || server || (IsPlayerAdmin(consoleplayer))))
+			&& netgame && !OLD_MUTE)
 		{
-			if (cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))
-				return false;
 			chat_on = true;
 			w_chat[0] = 0;
-			teamtalk = true;
+			teamtalk = G_GametypeHasTeams(); // Don't teamtalk if we don't have teams.
+			chat_scrollmedown = true;
+			typelines = 1;
 			return true;
 		}
 	}
-	else // if chat_on
-	{
-		// Ignore modifier keys
-		// Note that we do this here so users can still set
-		// their chat keys to one of these, if they so desire.
-		if (ev->data1 == KEY_LSHIFT || ev->data1 == KEY_RSHIFT
-		 || ev->data1 == KEY_LCTRL || ev->data1 == KEY_RCTRL
-		 || ev->data1 == KEY_LALT || ev->data1 == KEY_RALT)
-			return true;
-
-		c = (UINT8)ev->data1;
+	else // if chat_on
+	{
+
+		// Ignore modifier keys
+		// Note that we do this here so users can still set
+		// their chat keys to one of these, if they so desire.
+		if (ev->data1 == KEY_LSHIFT || ev->data1 == KEY_RSHIFT
+		 || ev->data1 == KEY_LCTRL || ev->data1 == KEY_RCTRL
+		 || ev->data1 == KEY_LALT || ev->data1 == KEY_RALT)
+			return true;
+
+		c = (INT32)ev->data1;
+
+		// I know this looks very messy but this works. If it ain't broke, don't fix it!
+		// shift LETTERS to uppercase if we have capslock or are holding shift
+		if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
+		{
+			if (shiftdown ^ capslock)
+				c = shiftxform[c];
+		}
+		else	// if we're holding shift we should still shift non letter symbols
+		{
+			if (shiftdown)
+				c = shiftxform[c];
+		}
+
+		// pasting. pasting is cool. chat is a bit limited, though :(
+		if (((c == 'v' || c == 'V') && ctrldown) && !CHAT_MUTE)
+		{
+			const char *paste = I_ClipboardPaste();
+			size_t chatlen;
+			size_t pastelen;
+
+			// create a dummy string real quickly
+
+			if (paste == NULL)
+				return true;
+
+			chatlen = strlen(w_chat);
+			pastelen = strlen(paste);
+			if (chatlen+pastelen > HU_MAXMSGLEN)
+				return true; // we can't paste this!!
+
+			if (c_input >= strlen(w_chat)) // add it at the end of the string.
+			{
+				memcpy(&w_chat[chatlen], paste, pastelen); // copy all of that.
+				c_input += pastelen;
+				/*size_t i = 0;
+				for (;i<pastelen;i++)
+				{
+					HU_queueChatChar(paste[i]); // queue it so that it's actually sent. (this chat write thing is REALLY messy.)
+				}*/
+				return true;
+			}
+			else	// otherwise, we need to shift everything and make space, etc etc
+			{
+				size_t i = HU_MAXMSGLEN-1;
+				while (i >= c_input)
+				{
+					if (w_chat[i])
+						w_chat[i+pastelen] = w_chat[i];
+					if (i == 0) // prevent overflow
+						break;
+					i--;
+				}
+				memcpy(&w_chat[c_input], paste, pastelen); // copy all of that.
+				c_input += pastelen;
+				return true;
+			}
+		}
+
+		if (!CHAT_MUTE && HU_keyInChatString(w_chat,c))
+		{
+			HU_queueChatChar(c);
+		}
+		if (c == KEY_ENTER)
+		{
+			chat_on = false;
+			c_input = 0; // reset input cursor
+			chat_scrollmedown = true; // you hit enter, so you might wanna autoscroll to see what you just sent. :)
+		}
+		else if (c == KEY_ESCAPE
+			|| ((c == gamecontrol[gc_talkkey][0] || c == gamecontrol[gc_talkkey][1]
+			|| c == gamecontrol[gc_teamkey][0] || c == gamecontrol[gc_teamkey][1])
+			&& c >= KEY_MOUSE1)) // If it's not a keyboard key, then the chat button is used as a toggle.
+		{
+			chat_on = false;
+			c_input = 0; // reset input cursor
+		}
+		else if ((c == KEY_UPARROW || c == KEY_MOUSEWHEELUP) && chat_scroll > 0 && !OLDCHAT) // CHAT SCROLLING YAYS!
+		{
+			chat_scroll--;
+			justscrolledup = true;
+			chat_scrolltime = 4;
+		}
+		else if ((c == KEY_DOWNARROW || c == KEY_MOUSEWHEELDOWN) && chat_scroll < chat_maxscroll && chat_maxscroll > 0 && !OLDCHAT)
+		{
+			chat_scroll++;
+			justscrolleddown = true;
+			chat_scrolltime = 4;
+		}
+		else if (c == KEY_LEFTARROW && c_input != 0 && !OLDCHAT) // i said go back
+			c_input--;
+		else if (c == KEY_RIGHTARROW && c_input < strlen(w_chat) && !OLDCHAT) // don't need to check for admin or w/e here since the chat won't ever contain anything if it's muted.
+			c_input++;
+		return true;
+	}
+	return false;
+}
+
+
+//======================================================================
+//                         HEADS UP DRAWING
+//======================================================================
+
+// Precompile a wordwrapped string to any given width.
+// This is a muuuch better method than V_WORDWRAP.
+// again stolen and modified a bit from video.c, don't mind me, will need to rearrange this one day.
+// this one is simplified for the chat drawer.
+static char *CHAT_WordWrap(INT32 x, INT32 w, INT32 option, const char *string)
+{
+	INT32 c;
+	size_t chw, i, lastusablespace = 0;
+	size_t slen;
+	char *newstring = Z_StrDup(string);
+	INT32 spacewidth = (vid.width < 640) ? 8 : 4, charwidth = (vid.width < 640) ? 8 : 4;
+
+	slen = strlen(string);
+	x = 0;
+
+	for (i = 0; i < slen; ++i)
+	{
+		c = newstring[i];
+		if ((UINT8)c >= 0x80 && (UINT8)c <= 0x89) //color parsing! -Inuyasha 2.16.09
+			continue;
+
+		if (c == '\n')
+		{
+			x = 0;
+			lastusablespace = 0;
+			continue;
+		}
+
+		if (!(option & V_ALLOWLOWERCASE))
+			c = toupper(c);
+		c -= HU_FONTSTART;
+
+		if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
+		{
+			chw = spacewidth;
+			lastusablespace = i;
+		}
+		else
+			chw = charwidth;
+
+		x += chw;
+
+		if (lastusablespace != 0 && x > w)
+		{
+			//CONS_Printf("Wrap at index %d\n", i);
+			newstring[lastusablespace] = '\n';
+			i = lastusablespace+1;
+			lastusablespace = 0;
+			x = 0;
+		}
+	}
+	return newstring;
+}
+
+
+// 30/7/18: chaty is now the distance at which the lowest point of the chat will be drawn if that makes any sense.
+
+INT16 chatx = 13, chaty = 169; // let's use this as our coordinates, shh
+
+// chat stuff by VincyTM LOL XD!
+
+// HU_DrawMiniChat
+
+static void HU_drawMiniChat(void)
+{
+	INT32 x = chatx+2;
+	INT32 charwidth = 4, charheight = 6;
+	INT32 boxw = cv_chatwidth.value;
+	INT32 dx = 0, dy = 0;
+	size_t i = chat_nummsg_min;
+	boolean prev_linereturn = false; // a hack to prevent double \n while I have no idea why they happen in the first place.
+
+	INT32 msglines = 0;
+	// process all messages once without rendering anything or doing anything fancy so that we know how many lines each message has...
+	INT32 y;
+
+	if (!chat_nummsg_min)
+		return; // needless to say it's useless to do anything if we don't have anything to draw.
+
+	/*if (splitscreen > 1)
+		boxw = max(64, boxw/2);*/
+
+	for (; i>0; i--)
+	{
+		const char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i-1]);
+		size_t j = 0;
+		INT32 linescount = 0;
+
+		while(msg[j]) // iterate through msg
+		{
+			if (msg[j] < HU_FONTSTART) // don't draw
+			{
+				if (msg[j] == '\n') // get back down.
+				{
+					++j;
+					if (!prev_linereturn)
+					{
+						linescount += 1;
+						dx = 0;
+					}
+					prev_linereturn = true;
+					continue;
+				}
+				else if (msg[j] & 0x80) // stolen from video.c, nice.
+				{
+					++j;
+					continue;
+				}
+
+				++j;
+			}
+			else
+			{
+				j++;
+			}
+			prev_linereturn = false;
+			dx += charwidth;
+			if (dx >= boxw)
+			{
+				dx = 0;
+				linescount += 1;
+			}
+		}
+		dy = 0;
+		dx = 0;
+		msglines += linescount+1;
+	}
+
+	y = chaty - charheight*(msglines+1);
+
+	/*if (splitscreen)
+	{
+		y -= BASEVIDHEIGHT/2;
+		if (splitscreen > 1)
+			y += 16;
+	}*/
+	y -= (G_RingSlingerGametype() ? 16 : 0);
+
+	dx = 0;
+	dy = 0;
+	i = 0;
+	prev_linereturn = false;
+
+	for (; i<=(chat_nummsg_min-1); i++) // iterate through our hot messages
+	{
+		INT32 clrflag = 0;
+		INT32 timer = ((cv_chattime.value*TICRATE)-chat_timers[i]) - cv_chattime.value*TICRATE+9; // see below...
+		INT32 transflag = (timer >= 0 && timer <= 9) ? (timer*V_10TRANS) : 0; // you can make bad jokes out of this one.
+		size_t j = 0;
+		const char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i]); // get the current message, and word wrap it.
+		UINT8 *colormap = NULL;
+
+		while(msg[j]) // iterate through msg
+		{
+			if (msg[j] < HU_FONTSTART) // don't draw
+			{
+				if (msg[j] == '\n') // get back down.
+				{
+					++j;
+					if (!prev_linereturn)
+					{
+						dy += charheight;
+						dx = 0;
+					}
+					prev_linereturn = true;
+					continue;
+				}
+				else if (msg[j] & 0x80) // stolen from video.c, nice.
+				{
+					clrflag = ((msg[j] & 0x7f) << V_CHARCOLORSHIFT) & V_CHARCOLORMASK;
+					colormap = V_GetStringColormap(clrflag);
+					++j;
+					continue;
+				}
+
+				++j;
+			}
+			else
+			{
+				if (cv_chatbacktint.value) // on request of wolfy
+					V_DrawFillConsoleMap(x + dx + 2, y+dy, charwidth, charheight, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT);
+
+				V_DrawChatCharacter(x + dx + 2, y+dy, msg[j++] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|transflag, !cv_allcaps.value, colormap);
+			}
+
+			dx += charwidth;
+			prev_linereturn = false;
+			if (dx >= boxw)
+			{
+				dx = 0;
+				dy += charheight;
+			}
+		}
+		dy += charheight;
+		dx = 0;
+	}
+
+	// decrement addy and make that shit smooth:
+	addy /= 2;
+
+}
+
+// HU_DrawChatLog
+
+static void HU_drawChatLog(INT32 offset)
+{
+	INT32 charwidth = 4, charheight = 6;
+	INT32 boxw = cv_chatwidth.value, boxh = cv_chatheight.value;
+	INT32 x = chatx+2, y, dx = 0, dy = 0;
+	UINT32 i = 0;
+	INT32 chat_topy, chat_bottomy;
+	boolean atbottom = false;
+
+	// make sure that our scroll position isn't "illegal";
+	if (chat_scroll > chat_maxscroll)
+		chat_scroll = chat_maxscroll;
+
+#ifdef NETSPLITSCREEN
+	if (splitscreen)
+	{
+		boxh = max(6, boxh/2);
+		if (splitscreen > 1)
+			boxw = max(64, boxw/2);
+	}
+#endif
+
+	y = chaty - offset*charheight - (chat_scroll*charheight) - boxh*charheight - 12;
+
+#ifdef NETSPLITSCREEN
+	if (splitscreen)
+	{
+		y -= BASEVIDHEIGHT/2;
+		if (splitscreen > 1)
+			y += 16;
+	}
+#endif
+	y -= (G_RingSlingerGametype() ? 16 : 0);
+
+
+	chat_topy = y + chat_scroll*charheight;
+	chat_bottomy = chat_topy + boxh*charheight;
+
+	V_DrawFillConsoleMap(chatx, chat_topy, boxw, boxh*charheight +2, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT); // log box
+
+	for (i=0; i<chat_nummsg_log; i++) // iterate through our chatlog
+	{
+		INT32 clrflag = 0;
+		INT32 j = 0;
+		const char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_log[i]); // get the current message, and word wrap it.
+		UINT8 *colormap = NULL;
+		while(msg[j]) // iterate through msg
+		{
+			if (msg[j] < HU_FONTSTART) // don't draw
+			{
+				if (msg[j] == '\n') // get back down.
+				{
+					++j;
+					dy += charheight;
+					dx = 0;
+					continue;
+				}
+				else if (msg[j] & 0x80) // stolen from video.c, nice.
+				{
+					clrflag = ((msg[j] & 0x7f) << V_CHARCOLORSHIFT) & V_CHARCOLORMASK;
+					colormap = V_GetStringColormap(clrflag);
+					++j;
+					continue;
+				}
+
+				++j;
+			}
+			else
+			{
+				if ((y+dy+2 >= chat_topy) && (y+dy < (chat_bottomy)))
+					V_DrawChatCharacter(x + dx + 2, y+dy+2, msg[j++] |V_SNAPTOBOTTOM|V_SNAPTOLEFT, !cv_allcaps.value, colormap);
+				else
+					j++; // don't forget to increment this or we'll get stuck in the limbo.
+			}
+
+			dx += charwidth;
+			if (dx >= boxw-charwidth-2 && i<chat_nummsg_log && msg[j] >= HU_FONTSTART) // end of message shouldn't count, nor should invisible characters!!!!
+			{
+				dx = 0;
+				dy += charheight;
+			}
+		}
+		dy += charheight;
+		dx = 0;
+	}
+
+
+	if (((chat_scroll >= chat_maxscroll) || (chat_scrollmedown)) && !(justscrolleddown || justscrolledup || chat_scrolltime)) // was already at the bottom of the page before new maxscroll calculation and was NOT scrolling.
+	{
+		atbottom = true; // we should scroll
+	}
+	chat_scrollmedown = false;
+
+	// getmaxscroll through a lazy hack. We do all these loops, so let's not do more loops that are gonna lag the game more. :P
+	chat_maxscroll = (dy/charheight); // welcome to C, we don't know what min() and max() are.
+	if (chat_maxscroll <= (UINT32)cv_chatheight.value)
+		chat_maxscroll = 0;
+	else
+		chat_maxscroll -= cv_chatheight.value;
+
+	// if we're not bound by the time, autoscroll for next frame:
+	if (atbottom)
+		chat_scroll = chat_maxscroll;
+
+	// draw arrows to indicate that we can (or not) scroll.
+	if (chat_scroll > 0)
+		V_DrawThinString(chatx-8, ((justscrolledup) ? (chat_topy-1) : (chat_topy)), V_SNAPTOBOTTOM | V_SNAPTOLEFT | V_YELLOWMAP, "\x1A"); // up arrow
+	if (chat_scroll < chat_maxscroll)
+		V_DrawThinString(chatx-8, chat_bottomy-((justscrolleddown) ? 5 : 6), V_SNAPTOBOTTOM | V_SNAPTOLEFT | V_YELLOWMAP, "\x1B"); // down arrow
+
+	justscrolleddown = false;
+	justscrolledup = false;
+}
+
+//
+// HU_DrawChat
+//
+// Draw chat input
+//
+
+static void HU_DrawChat(void)
+{
+	INT32 charwidth = 4, charheight = 6;
+	INT32 boxw = cv_chatwidth.value;
+	INT32 t = 0, c = 0, y = chaty - (typelines*charheight);
+	UINT32 i = 0, saylen = strlen(w_chat); // You learn new things everyday!
+	INT32 cflag = 0;
+	const char *ntalk = "Say: ", *ttalk = "Team: ";
+	const char *talk = ntalk;
+	const char *mute = "Chat has been muted.";
+
+#ifdef NETSPLITSCREEN
+	if (splitscreen)
+	{
+		y -= BASEVIDHEIGHT/2;
+		if (splitscreen > 1)
+		{
+			y += 16;
+			boxw = max(64, boxw/2);
+		}
+	}
+#endif
+	y -= (G_RingSlingerGametype() ? 16 : 0);
+
+	if (teamtalk)
+	{
+		talk = ttalk;
+#if 0
+		if (players[consoleplayer].ctfteam == 1)
+			t = 0x500;  // Red
+		else if (players[consoleplayer].ctfteam == 2)
+			t = 0x400; // Blue
+#endif
+	}
+
+	if (CHAT_MUTE)
+	{
+		talk = mute;
+		typelines = 1;
+		cflag = V_GRAYMAP; // set text in gray if chat is muted.
+	}
+
+	V_DrawFillConsoleMap(chatx, y-1, boxw, (typelines*charheight), 239 | V_SNAPTOBOTTOM | V_SNAPTOLEFT);
+
+	while (talk[i])
+	{
+		if (talk[i] < HU_FONTSTART)
+			++i;
+		else
+		{
+			V_DrawChatCharacter(chatx + c + 2, y, talk[i] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|cflag, !cv_allcaps.value, V_GetStringColormap(talk[i]|cflag));
+			i++;
+		}
+
+		c += charwidth;
+	}
+
+	// if chat is muted, just draw the log and get it over with, no need to draw anything else.
+	if (CHAT_MUTE)
+	{
+		HU_drawChatLog(0);
+		return;
+	}
+
+	i = 0;
+	typelines = 1;
+
+	if ((strlen(w_chat) == 0 || c_input == 0) && hu_tick < 4)
+		V_DrawChatCharacter(chatx + 2 + c, y+1, '_' |V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, !cv_allcaps.value, NULL);
+
+	while (w_chat[i])
+	{
+		boolean skippedline = false;
+		if (c_input == (i+1))
+		{
+			INT32 cursorx = (c+charwidth < boxw-charwidth) ? (chatx + 2 + c+charwidth) : (chatx+1); // we may have to go down.
+			INT32 cursory = (cursorx != chatx+1) ? (y) : (y+charheight);
+			if (hu_tick < 4)
+				V_DrawChatCharacter(cursorx, cursory+1, '_' |V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, !cv_allcaps.value, NULL);
+
+			if (cursorx == chatx+1 && saylen == i) // a weirdo hack
+			{
+				typelines += 1;
+				skippedline = true;
+			}
+		}
+
+		//Hurdler: isn't it better like that?
+		if (w_chat[i] < HU_FONTSTART)
+			++i;
+		else
+			V_DrawChatCharacter(chatx + c + 2, y, w_chat[i++] | V_SNAPTOBOTTOM|V_SNAPTOLEFT | t, !cv_allcaps.value, NULL);
+
+		c += charwidth;
+		if (c > boxw-(charwidth*2) && !skippedline)
+		{
+			c = 0;
+			y += charheight;
+			typelines += 1;
+		}
+	}
+
+	// handle /pm list. It's messy, horrible and I don't care.
+	if (strnicmp(w_chat, "/pm", 3) == 0 && vid.width >= 400 && !teamtalk) // 320x200 unsupported kthxbai
+	{
+		INT32 count = 0;
+		INT32 p_dispy = chaty - charheight -1;
+#ifdef NETSPLITSCREEN
+		if (splitscreen)
+		{
+			p_dispy -= BASEVIDHEIGHT/2;
+			if (splitscreen > 1)
+				p_dispy += 16;
+		}
+#endif
+		p_dispy -= (G_RingSlingerGametype() ? 16 : 0);
+
+		i = 0;
+		for(i=0; (i<MAXPLAYERS); i++)
+		{
+
+			// filter: (code needs optimization pls help I'm bad with C)
+			if (w_chat[3])
+			{
+				char *nodenum;
+				UINT32 n;
+				// right, that's half important: (w_chat[4] may be a space since /pm0 msg is perfectly acceptable!)
+				if ( ( ((w_chat[3] != 0) && ((w_chat[3] < '0') || (w_chat[3] > '9'))) || ((w_chat[4] != 0) && (((w_chat[4] < '0') || (w_chat[4] > '9'))))) && (w_chat[4] != ' '))
+					break;
+
+
+				nodenum = (char*) malloc(3);
+				strncpy(nodenum, w_chat+3, 3);
+				n = atoi((const char*) nodenum); // turn that into a number
+				free(nodenum);
+				// special cases:
+
+				if ((n == 0) && !(w_chat[4] == '0'))
+				{
+					if (!(i<10))
+						continue;
+				}
+				else if ((n == 1) && !(w_chat[3] == '0'))
+				{
+					if (!((i == 1) || ((i >= 10) && (i <= 19))))
+						continue;
+				}
+				else if ((n == 2) && !(w_chat[3] == '0'))
+				{
+					if (!((i == 2) || ((i >= 20) && (i <= 29))))
+						continue;
+				}
+				else if ((n == 3) && !(w_chat[3] == '0'))
+				{
+					if (!((i == 3) || ((i >= 30) && (i <= 31))))
+						continue;
+				}
+				else	// general case.
+				{
+					if (i != n)
+						continue;
+				}
+			}
+
+			if (playeringame[i])
+			{
+				char name[MAXPLAYERNAME+1];
+				strlcpy(name, player_names[i], 7); // shorten name to 7 characters.
+				V_DrawFillConsoleMap(chatx+ boxw + 2, p_dispy- (6*count), 48, 6, 239 | V_SNAPTOBOTTOM | V_SNAPTOLEFT); // fill it like the chat so the text doesn't become hard to read because of the hud.
+				V_DrawSmallString(chatx+ boxw + 4, p_dispy- (6*count), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, va("\x82%d\x80 - %s", i, name));
+				count++;
+			}
+		}
+		if (count == 0) // no results.
+		{
+			V_DrawFillConsoleMap(chatx+boxw+2, p_dispy- (6*count), 48, 6, 239 | V_SNAPTOBOTTOM | V_SNAPTOLEFT); // fill it like the chat so the text doesn't become hard to read because of the hud.
+			V_DrawSmallString(chatx+boxw+4, p_dispy- (6*count), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, "NO RESULT.");
+		}
+	}
 
-		// use console translations
-		if (shiftdown)
-			c = shiftxform[c];
-
-		if (HU_keyInChatString(w_chat,c))
-			HU_queueChatChar(c);
-		if (c == KEY_ENTER)
-			chat_on = false;
-		else if (c == KEY_ESCAPE)
-			chat_on = false;
+	HU_drawChatLog(typelines-1); // typelines is the # of lines we're typing. If there's more than 1 then the log should scroll up to give us more space.
 
-		return true;
-	}
-	return false;
 }
 
-//======================================================================
-//                         HEADS UP DRAWING
-//======================================================================
 
-//
-// HU_DrawChat
-//
-// Draw chat input
-//
-static void HU_DrawChat(void)
+// For anyone who, for some godforsaken reason, likes oldchat.
+
+static void HU_DrawChat_Old(void)
 {
 	INT32 t = 0, c = 0, y = HU_INPUTY;
 	size_t i = 0;
@@ -833,7 +1757,6 @@ static void HU_DrawChat(void)
 	const char *talk = ntalk;
 	INT32 charwidth = 8 * con_scalefactor; //SHORT(hu_font['A'-HU_FONTSTART]->width) * con_scalefactor;
 	INT32 charheight = 8 * con_scalefactor; //SHORT(hu_font['A'-HU_FONTSTART]->height) * con_scalefactor;
-
 	if (teamtalk)
 	{
 		talk = ttalk;
@@ -860,9 +1783,20 @@ static void HU_DrawChat(void)
 		c += charwidth;
 	}
 
+	if ((strlen(w_chat) == 0 || c_input == 0) && hu_tick < 4)
+		V_DrawCharacter(HU_INPUTX+c, y+2*con_scalefactor, '_' |cv_constextsize.value | V_NOSCALESTART|t, !cv_allcaps.value);
+
 	i = 0;
 	while (w_chat[i])
 	{
+
+		if (c_input == (i+1) && hu_tick < 4)
+		{
+			INT32 cursorx = (HU_INPUTX+c+charwidth < vid.width) ? (HU_INPUTX + c + charwidth) : (HU_INPUTX); // we may have to go down.
+			INT32 cursory = (cursorx != HU_INPUTX) ? (y) : (y+charheight);
+			V_DrawCharacter(cursorx, cursory+2*con_scalefactor, '_' |cv_constextsize.value | V_NOSCALESTART|t, !cv_allcaps.value);
+		}
+
 		//Hurdler: isn't it better like that?
 		if (w_chat[i] < HU_FONTSTART)
 		{
@@ -882,9 +1816,6 @@ static void HU_DrawChat(void)
 			y += charheight;
 		}
 	}
-
-	if (hu_tick < 4)
-		V_DrawCharacter(HU_INPUTX + c, y, '_' | cv_constextsize.value |V_NOSCALESTART|t, !cv_allcaps.value);
 }
 
 
@@ -1057,7 +1988,43 @@ void HU_Drawer(void)
 {
 	// draw chat string plus cursor
 	if (chat_on)
-		HU_DrawChat();
+	{
+		// count down the scroll timer.
+		if (chat_scrolltime > 0)
+			chat_scrolltime--;
+		if (!OLDCHAT)
+			HU_DrawChat();
+		else
+			HU_DrawChat_Old();
+	}
+	else
+	{
+		typelines = 1;
+		chat_scrolltime = 0;
+		if (!OLDCHAT && cv_consolechat.value < 2 && netgame) // Don't display minimized chat if you set the mode to Window (Hidden)
+			HU_drawMiniChat(); // draw messages in a cool fashion.
+	}
+
+	if (netgame) // would handle that in hu_drawminichat, but it's actually kinda awkward when you're typing a lot of messages. (only handle that in netgames duh)
+	{
+		size_t i = 0;
+
+		// handle spam while we're at it:
+		for(; (i<MAXPLAYERS); i++)
+		{
+			if (stop_spamming[i] > 0)
+				stop_spamming[i]--;
+		}
+
+		// handle chat timers
+		for (i=0; (i<chat_nummsg_min); i++)
+		{
+			if (chat_timers[i] > 0)
+				chat_timers[i]--;
+			else
+				HU_removeChatText_Mini();
+		}
+	}
 
 	if (cechotimer)
 		HU_DrawCEcho();
@@ -1153,9 +2120,9 @@ void HU_Erase(void)
 	// clear the message lines that go away, so use _oldclearlines_
 	bottomline = oldclearlines;
 	oldclearlines = con_clearlines;
-	if (chat_on)
+	if (chat_on && OLDCHAT)
 		if (bottomline < 8)
-			bottomline = 8;
+			bottomline = 8; // only do it for consolechat. consolechat is gay.
 
 	if (automapactive || viewwindowx == 0) // hud msgs don't need to be cleared
 		return;
@@ -1191,6 +2158,41 @@ void HU_Erase(void)
 //                   IN-LEVEL MULTIPLAYER RANKINGS
 //======================================================================
 
+//
+// HU_drawPing
+//
+void HU_drawPing(INT32 x, INT32 y, INT32 ping, boolean notext)
+{
+	UINT8 numbars = 1; // how many ping bars do we draw?
+	UINT8 barcolor = 128; // color we use for the bars (green, yellow or red)
+	SINT8 i = 0;
+	SINT8 yoffset = 6;
+	INT32 dx = x+1 - (V_SmallStringWidth(va("%dms", ping), V_ALLOWLOWERCASE)/2);
+
+	if (ping < 128)
+	{
+		numbars = 3;
+		barcolor = 184;
+	}
+	else if (ping < 256)
+	{
+		numbars = 2; // Apparently ternaries w/ multiple statements don't look good in C so I decided against it.
+		barcolor = 103;
+	}
+
+	if (!notext || vid.width >= 640) // how sad, we're using a shit resolution.
+		V_DrawSmallString(dx, y+4, V_ALLOWLOWERCASE, va("%dms", ping));
+
+	for (i=0; (i<3); i++) // Draw the ping bar
+	{
+		V_DrawFill(x+2 *(i-1), y+yoffset-4, 2, 8-yoffset, 31);
+		if (i < numbars)
+			V_DrawFill(x+2 *(i-1), y+yoffset-3, 1, 8-yoffset-1, barcolor);
+
+		yoffset -= 2;
+	}
+}
+
 //
 // HU_DrawTabRankings
 //
@@ -1209,6 +2211,14 @@ void HU_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, I
 		if (players[tab[i].num].spectator)
 			continue; //ignore them.
 
+		if (!splitscreen) // don't draw it on splitscreen,
+		{
+			if (!(tab[i].num == serverplayer))
+				HU_drawPing(x+ 253, y+2, playerpingtable[tab[i].num], false);
+			//else
+			//	V_DrawSmallString(x+ 246, y+4, V_YELLOWMAP, "SERVER");
+		}
+
 		V_DrawString(x + 20, y,
 		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
 		             | ((players[tab[i].num].health > 0) ? 0 : V_60TRANS)
@@ -1285,6 +2295,113 @@ void HU_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, I
 	}
 }
 
+//
+// HU_Draw32Emeralds
+//
+static void HU_Draw32Emeralds(INT32 x, INT32 y, INT32 pemeralds)
+{
+	//Draw the emeralds, in the CORRECT order, using tiny emerald sprites.
+	if (pemeralds & EMERALD1)
+		V_DrawSmallScaledPatch(x  , y, 0, tinyemeraldpics[0]);
+
+	if (pemeralds & EMERALD2)
+		V_DrawSmallScaledPatch(x+4, y, 0, tinyemeraldpics[1]);
+
+	if (pemeralds & EMERALD3)
+		V_DrawSmallScaledPatch(x+8, y, 0, tinyemeraldpics[2]);
+
+	if (pemeralds & EMERALD4)
+		V_DrawSmallScaledPatch(x+12  , y, 0, tinyemeraldpics[3]);
+
+	if (pemeralds & EMERALD5)
+		V_DrawSmallScaledPatch(x+16, y, 0, tinyemeraldpics[4]);
+
+	if (pemeralds & EMERALD6)
+		V_DrawSmallScaledPatch(x+20, y, 0, tinyemeraldpics[5]);
+
+	if (pemeralds & EMERALD7)
+		V_DrawSmallScaledPatch(x+24,   y,   0, tinyemeraldpics[6]);
+}
+
+//
+// HU_Draw32TeamTabRankings
+//
+static void HU_Draw32TeamTabRankings(playersort_t *tab, INT32 whiteplayer)
+{
+	INT32 i,x,y;
+	INT32 redplayers = 0, blueplayers = 0;
+	const UINT8 *colormap;
+	char name[MAXPLAYERNAME+1];
+
+	V_DrawFill(160, 26, 1, 154, 0); //Draw a vertical line to separate the two teams.
+	V_DrawFill(1, 26, 318, 1, 0); //And a horizontal line to make a T.
+	V_DrawFill(1, 180, 318, 1, 0); //And a horizontal line near the bottom.
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (players[tab[i].num].spectator)
+			continue; //ignore them.
+
+		if (tab[i].color == skincolor_redteam) //red
+		{
+			redplayers++;
+			x = 14 + (BASEVIDWIDTH/2);
+			y = (redplayers * 9) + 20;
+		}
+		else if (tab[i].color == skincolor_blueteam) //blue
+		{
+			blueplayers++;
+			x = 14;
+			y = (blueplayers * 9) + 20;
+		}
+		else //er?  not on red or blue, so ignore them
+			continue;
+
+		strlcpy(name, tab[i].name, 8);
+		V_DrawString(x + 10, y,
+		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+		             | ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
+		             | V_ALLOWLOWERCASE, name);
+
+		if (gametype == GT_CTF)
+		{
+			if (players[tab[i].num].gotflag & GF_REDFLAG) // Red
+				V_DrawFixedPatch((x-10)*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, rflagico, 0);
+			else if (players[tab[i].num].gotflag & GF_BLUEFLAG) // Blue
+				V_DrawFixedPatch((x-10)*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, bflagico, 0);
+		}
+
+		// Draw emeralds
+		if (!players[tab[i].num].powers[pw_super]
+			|| ((leveltime/7) & 1))
+		{
+			HU_Draw32Emeralds(x+60, y+2, tab[i].emeralds);
+		}
+
+		if (players[tab[i].num].powers[pw_super])
+		{
+			colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
+			V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, 0, superprefix[players[tab[i].num].skin], colormap);
+		}
+		else
+		{
+			colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
+			if (players[tab[i].num].health <= 0)
+				V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, V_HUDTRANSHALF, faceprefix[players[tab[i].num].skin], colormap);
+			else
+				V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, 0, faceprefix[players[tab[i].num].skin], colormap);
+		}
+		V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+		if (!splitscreen)
+		{
+			if (!(tab[i].num == serverplayer))
+				HU_drawPing(x+ 135, y+3, playerpingtable[tab[i].num], true);
+		//else
+			//V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
+		}
+	}
+}
+
 //
 // HU_DrawTeamTabRankings
 //
@@ -1292,13 +2409,50 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 {
 	INT32 i,x,y;
 	INT32 redplayers = 0, blueplayers = 0;
+	boolean smol = false;
 	const UINT8 *colormap;
 	char name[MAXPLAYERNAME+1];
 
+	// before we draw, we must count how many players are in each team. It makes an additional loop, but we need to know if we have to draw a big or a small ranking.
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (players[tab[i].num].spectator)
+			continue; //ignore them.
+
+		if (tab[i].color == skincolor_redteam) //red
+		{
+			if (redplayers++ > 8)
+			{
+				smol = true;
+				break; // don't make more loops than we need to.
+			}
+		}
+		else if (tab[i].color == skincolor_blueteam) //blue
+		{
+			if (blueplayers++ > 8)
+			{
+				smol = true;
+				break;
+			}
+		}
+		else //er?  not on red or blue, so ignore them
+			continue;
+
+	}
+
+	// I'll be blunt with you, this may add more lines, but I'm not adding weird cases for this, so we're executing a separate function.
+	if (smol == true || cv_compactscoreboard.value)
+	{
+		HU_Draw32TeamTabRankings(tab, whiteplayer);
+		return;
+	}
+
 	V_DrawFill(160, 26, 1, 154, 0); //Draw a vertical line to separate the two teams.
 	V_DrawFill(1, 26, 318, 1, 0); //And a horizontal line to make a T.
 	V_DrawFill(1, 180, 318, 1, 0); //And a horizontal line near the bottom.
 
+	i=0, redplayers=0, blueplayers=0;
+
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		if (players[tab[i].num].spectator)
@@ -1321,7 +2475,7 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		else //er?  not on red or blue, so ignore them
 			continue;
 
-		strlcpy(name, tab[i].name, 9);
+		strlcpy(name, tab[i].name, 7);
 		V_DrawString(x + 20, y,
 		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
 		             | ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
@@ -1355,7 +2509,14 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 			else
 				V_DrawSmallMappedPatch (x, y-4, 0, faceprefix[players[tab[i].num].skin], colormap);
 		}
-		V_DrawRightAlignedThinString(x+120, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+		V_DrawRightAlignedThinString(x+100, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+		if (!splitscreen)
+		{
+			if (!(tab[i].num == serverplayer))
+				HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
+		//else
+		//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
+		}
 	}
 }
 
@@ -1377,7 +2538,12 @@ void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scoreline
 		if (players[tab[i].num].spectator)
 			continue; //ignore them.
 
-		strlcpy(name, tab[i].name, 9);
+		strlcpy(name, tab[i].name, 7);
+		if (!(tab[i].num == serverplayer))
+			HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
+		//else
+		//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
+
 		V_DrawString(x + 20, y,
 		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
 		             | ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
@@ -1432,15 +2598,15 @@ void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scoreline
 			if (circuitmap)
 			{
 				if (players[tab[i].num].exiting)
-					V_DrawRightAlignedThinString(x+156, y, 0, va("%i:%02i.%02i", G_TicsToMinutes(players[tab[i].num].realtime,true), G_TicsToSeconds(players[tab[i].num].realtime), G_TicsToCentiseconds(players[tab[i].num].realtime)));
+					V_DrawRightAlignedThinString(x+146, y, 0, va("%i:%02i.%02i", G_TicsToMinutes(players[tab[i].num].realtime,true), G_TicsToSeconds(players[tab[i].num].realtime), G_TicsToCentiseconds(players[tab[i].num].realtime)));
 				else
-					V_DrawRightAlignedThinString(x+156, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+					V_DrawRightAlignedThinString(x+146, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
 			}
 			else
-				V_DrawRightAlignedThinString(x+156, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%i:%02i.%02i", G_TicsToMinutes(tab[i].count,true), G_TicsToSeconds(tab[i].count), G_TicsToCentiseconds(tab[i].count)));
+				V_DrawRightAlignedThinString(x+146, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%i:%02i.%02i", G_TicsToMinutes(tab[i].count,true), G_TicsToSeconds(tab[i].count), G_TicsToCentiseconds(tab[i].count)));
 		}
 		else
-			V_DrawRightAlignedThinString(x+120, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+			V_DrawRightAlignedThinString(x+100, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
 
 		y += 16;
 		if (y > 160)
@@ -1451,6 +2617,107 @@ void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scoreline
 	}
 }
 
+//
+// HU_Draw32TabRankings
+//
+static void HU_Draw32TabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, INT32 whiteplayer)
+{
+	INT32 i;
+	const UINT8 *colormap;
+	char name[MAXPLAYERNAME+1];
+
+	V_DrawFill(160, 26, 1, 154, 0); //Draw a vertical line to separate the two sides.
+	V_DrawFill(1, 26, 318, 1, 0); //And a horizontal line to make a T.
+	V_DrawFill(1, 180, 318, 1, 0); //And a horizontal line near the bottom.
+
+	for (i = 0; i < scorelines; i++)
+	{
+		if (players[tab[i].num].spectator)
+			continue; //ignore them.
+
+		strlcpy(name, tab[i].name, 7);
+		if (!splitscreen) // don't draw it on splitscreen,
+		{
+			if (!(tab[i].num == serverplayer))
+				HU_drawPing(x+ 135, y+3, playerpingtable[tab[i].num], true);
+		//else
+		//	V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
+		}
+
+		V_DrawString(x + 10, y,
+		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+		             | ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
+		             | V_ALLOWLOWERCASE, name);
+
+		if (G_GametypeUsesLives()) //show lives
+			V_DrawRightAlignedThinString(x-1, y, V_ALLOWLOWERCASE, va("%d", players[tab[i].num].lives));
+		else if (G_TagGametype() && players[tab[i].num].pflags & PF_TAGIT)
+			V_DrawFixedPatch((x-10)*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, tagico, 0);
+
+		// Draw emeralds
+		if (!players[tab[i].num].powers[pw_super]
+			|| ((leveltime/7) & 1))
+		{
+			HU_Draw32Emeralds(x+60, y+2, tab[i].emeralds);
+			//HU_DrawEmeralds(x-12,y+2,tab[i].emeralds);
+		}
+
+		//V_DrawSmallScaledPatch (x, y-4, 0, livesback);
+		if (tab[i].color == 0)
+		{
+			colormap = colormaps;
+			if (players[tab[i].num].powers[pw_super])
+				V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, 0, superprefix[players[tab[i].num].skin], 0);
+			else
+			{
+				if (players[tab[i].num].health <= 0)
+					V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, V_HUDTRANSHALF, faceprefix[players[tab[i].num].skin], 0);
+				else
+					V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, faceprefix[players[tab[i].num].skin], 0);
+			}
+		}
+		else
+		{
+			if (players[tab[i].num].powers[pw_super])
+			{
+				colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
+				V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, 0, superprefix[players[tab[i].num].skin], colormap);
+			}
+			else
+			{
+				colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
+				if (players[tab[i].num].health <= 0)
+					V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, V_HUDTRANSHALF, faceprefix[players[tab[i].num].skin], colormap);
+				else
+					V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, faceprefix[players[tab[i].num].skin], colormap);
+			}
+		}
+
+		// All data drawn with thin string for space.
+		if (gametype == GT_RACE)
+		{
+			if (circuitmap)
+			{
+				if (players[tab[i].num].exiting)
+					V_DrawRightAlignedThinString(x+128, y, 0, va("%i:%02i.%02i", G_TicsToMinutes(players[tab[i].num].realtime,true), G_TicsToSeconds(players[tab[i].num].realtime), G_TicsToCentiseconds(players[tab[i].num].realtime)));
+				else
+					V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+			}
+			else
+				V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%i:%02i.%02i", G_TicsToMinutes(tab[i].count,true), G_TicsToSeconds(tab[i].count), G_TicsToCentiseconds(tab[i].count)));
+		}
+		else
+			V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
+
+		y += 9;
+		if (i == 16)
+		{
+			y = 32;
+			x += BASEVIDWIDTH/2;
+		}
+	}
+}
+
 //
 // HU_DrawEmeralds
 //
@@ -1697,15 +2964,18 @@ static void HU_DrawRankings(void)
 		scorelines++;
 	}
 
-	if (scorelines > 20)
-		scorelines = 20; //dont draw past bottom of screen, show the best only
+	//if (scorelines > 20)
+	//	scorelines = 20; //dont draw past bottom of screen, show the best only
+	// shush, we'll do it anyway.
 
 	if (G_GametypeHasTeams())
 		HU_DrawTeamTabRankings(tab, whiteplayer); //separate function for Spazzo's silly request
-	else if (scorelines <= 9)
+	else if (scorelines <= 9 && !cv_compactscoreboard.value)
 		HU_DrawTabRankings(40, 32, tab, scorelines, whiteplayer);
-	else
+	else if (scorelines <= 20 && !cv_compactscoreboard.value)
 		HU_DrawDualTabRankings(32, 32, tab, scorelines, whiteplayer);
+	else
+		HU_Draw32TabRankings(14, 28, tab, scorelines, whiteplayer);
 
 	// draw spectators in a ticker across the bottom
 	if (!splitscreen && G_GametypeHasSpectators())
diff --git a/src/hu_stuff.h b/src/hu_stuff.h
index 23e421076975d0f4a009b504e11010b3d88ac207..b1d3918281852caf2fdd58dce33721971c9d9c2c 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -21,7 +21,7 @@
 //------------------------------------
 //           heads up font
 //------------------------------------
-#define HU_FONTSTART '\x1F' // the first font character
+#define HU_FONTSTART '\x16' // the first font character
 #define HU_FONTEND '~'
 
 #define HU_FONTSIZE (HU_FONTEND - HU_FONTSTART + 1)
@@ -57,6 +57,20 @@ typedef struct
 //           chat stuff
 //------------------------------------
 #define HU_MAXMSGLEN 224
+#define CHAT_BUFSIZE 64		// that's enough messages, right? We'll delete the older ones when that gets out of hand.
+#ifdef NETSPLITSCREEN
+#define OLDCHAT (cv_consolechat.value == 1 || dedicated || vid.width < 640)
+#else
+#define OLDCHAT (cv_consolechat.value == 1 || dedicated || vid.width < 640 || splitscreen)
+#endif
+#define CHAT_MUTE (cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))	// this still allows to open the chat but not to type. That's used for scrolling and whatnot.
+#define OLD_MUTE (OLDCHAT && cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))	// this is used to prevent oldchat from opening when muted.
+
+// some functions
+void HU_AddChatText(const char *text, boolean playsound);
+
+// set true when entering a chat message
+extern boolean chat_on;
 
 extern patch_t *hu_font[HU_FONTSIZE], *tny_font[HU_FONTSIZE];
 extern patch_t *tallnum[10];
@@ -72,9 +86,6 @@ extern patch_t *bmatcico;
 extern patch_t *tagico;
 extern patch_t *tallminus;
 
-// set true when entering a chat message
-extern boolean chat_on;
-
 // set true whenever the tab rankings are being shown for any reason
 extern boolean hu_showscores;
 
@@ -87,12 +98,12 @@ void HU_LoadGraphics(void);
 void HU_Start(void);
 
 boolean HU_Responder(event_t *ev);
-
 void HU_Ticker(void);
 void HU_Drawer(void);
 char HU_dequeueChatChar(void);
 void HU_Erase(void);
 void HU_clearChatChars(void);
+void HU_drawPing(INT32 x, INT32 y, INT32 ping, boolean notext);	// Lat': Ping drawer for scoreboard.
 void HU_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, INT32 whiteplayer);
 void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer);
 void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, INT32 whiteplayer);
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 4666394edb603cc55449c9194513cf372f450726..fd83cf9552536aebb8310d5167c5fe2174f62396 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -23,7 +23,7 @@
 #include "m_random.h"
 #include "s_sound.h"
 #include "g_game.h"
-#include "hu_stuff.h"
+#include "hu_stuff.h"	// HU_AddChatText
 #include "console.h"
 #include "d_netcmd.h" // IsPlayerAdmin
 
@@ -91,6 +91,51 @@ static int lib_print(lua_State *L)
 	return 0;
 }
 
+// Print stuff in the chat, or in the console if we can't.
+static int lib_chatprint(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);	// retrieve string
+	boolean sound = lua_optboolean(L, 2);	// retrieve sound boolean
+	int len = strlen(str);
+
+	if (str == NULL)	// error if we don't have a string!
+		return luaL_error(L, LUA_QL("tostring") " must return a string to " LUA_QL("chatprint"));
+
+	if (len > 255)	// string is too long!!!
+		return luaL_error(L, "String exceeds the 255 characters limit of the chat buffer.");
+
+	HU_AddChatText(str, sound);
+	return 0;
+}
+
+// Same as above, but do it for only one player.
+static int lib_chatprintf(lua_State *L)
+{
+	int n = lua_gettop(L);  /* number of arguments */
+	const char *str = luaL_checkstring(L, 2);	// retrieve string
+	boolean sound = lua_optboolean(L, 3);	// sound?
+	int len = strlen(str);
+	player_t *plr;
+
+	if (n < 2)
+		return luaL_error(L, "chatprintf requires at least two arguments: player and text.");
+
+	plr = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));	// retrieve player
+	if (!plr)
+		return LUA_ErrInvalid(L, "player_t");
+	if (plr != &players[consoleplayer])
+		return 0;
+
+	if (str == NULL)	// error if we don't have a string!
+		return luaL_error(L, LUA_QL("tostring") " must return a string to " LUA_QL("chatprintf"));
+
+	if (len > 255)	// string is too long!!!
+		return luaL_error(L, "String exceeds the 255 characters limit of the chat buffer.");
+
+	HU_AddChatText(str, sound);
+	return 0;
+}
+
 static int lib_evalMath(lua_State *L)
 {
 	const char *word = luaL_checkstring(L, 1);
@@ -1709,7 +1754,7 @@ static int lib_sStartSound(lua_State *L)
 	const void *origin = NULL;
 	sfxenum_t sound_id = luaL_checkinteger(L, 2);
 	player_t *player = NULL;
-	NOHUD
+	//NOHUD // kys @whoever did this.
 	if (sound_id >= NUMSFX)
 		return luaL_error(L, "sfx %d out of range (0 - %d)", sound_id, NUMSFX-1);
 	if (!lua_isnil(L, 1))
@@ -1725,7 +1770,12 @@ static int lib_sStartSound(lua_State *L)
 			return LUA_ErrInvalid(L, "player_t");
 	}
 	if (!player || P_IsLocalPlayer(player))
+	{
+		if (hud_running)
+			origin = NULL;	// HUD rendering startsound shouldn't have an origin, just remove it instead of having a retarded error.
+
 		S_StartSound(origin, sound_id);
+	}
 	return 0;
 }
 
@@ -2052,6 +2102,8 @@ static int lib_gTicsToMilliseconds(lua_State *L)
 
 static luaL_Reg lib[] = {
 	{"print", lib_print},
+	{"chatprint", lib_chatprint},
+	{"chatprintf", lib_chatprintf},
 	{"EvalMath", lib_evalMath},
 	{"IsPlayerAdmin", lib_isPlayerAdmin},
 
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 7e544ae99f7b82ff1d042f348b3e982ff3b986a4..948eca84c76bf6fc138dd8d1dea5afcb918dc99b 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -952,7 +952,7 @@ boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 	return hooked;
 }
 
-// Hook for player chat
+
 boolean LUAh_PlayerMsg(int source, int target, int flags, char *msg)
 {
 	hook_p hookp;
@@ -1005,6 +1005,7 @@ boolean LUAh_PlayerMsg(int source, int target, int flags, char *msg)
 	return hooked;
 }
 
+
 // Hook for hurt messages
 boolean LUAh_HurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source)
 {
diff --git a/src/m_menu.c b/src/m_menu.c
index 650eac644e75c9e8f3ec7bf46f3d47f640b8e33d..7b49d359e05f19ce3b3e81af3f6436f1b3832dd5 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -33,6 +33,9 @@
 #include "s_sound.h"
 #include "i_system.h"
 
+// Addfile
+#include "filesrch.h"
+
 #include "v_video.h"
 #include "i_video.h"
 #include "keys.h"
@@ -75,7 +78,6 @@ int	snprintf(char *str, size_t n, const char *fmt, ...);
 #define SMALLLINEHEIGHT 8
 #define SLIDER_RANGE 10
 #define SLIDER_WIDTH (8*SLIDER_RANGE+6)
-#define MAXSTRINGLENGTH 32
 #define SERVERS_PER_PAGE 11
 
 typedef enum
@@ -205,6 +207,8 @@ menu_t MessageDef;
 
 menu_t SPauseDef;
 
+#define lsheadingheight 16
+
 // Sky Room
 static void M_CustomLevelSelect(INT32 choice);
 static void M_CustomWarp(INT32 choice);
@@ -292,15 +296,22 @@ menu_t OP_SoundOptionsDef;
 
 //Misc
 menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef;
-menu_t OP_GameOptionsDef, OP_ServerOptionsDef;
+menu_t OP_GameOptionsDef, OP_ChatOptionsDef, OP_ServerOptionsDef;
 menu_t OP_NetgameOptionsDef, OP_GametypeOptionsDef;
 menu_t OP_MonitorToggleDef;
 static void M_ScreenshotOptions(INT32 choice);
 static void M_EraseData(INT32 choice);
 
+static void M_Addons(INT32 choice);
+static void M_AddonsOptions(INT32 choice);
+static patch_t *addonsp[NUM_EXT+5];
+
+#define numaddonsshown 4
+
 // Drawing functions
 static void M_DrawGenericMenu(void);
 static void M_DrawCenteredMenu(void);
+static void M_DrawAddons(void);
 static void M_DrawSkyRoom(void);
 static void M_DrawChecklist(void);
 static void M_DrawEmblemHints(void);
@@ -335,6 +346,7 @@ static boolean M_CancelConnect(void);
 #endif
 static boolean M_ExitPandorasBox(void);
 static boolean M_QuitMultiPlayerMenu(void);
+static void M_HandleAddons(INT32 choice);
 static void M_HandleSoundTest(INT32 choice);
 static void M_HandleImageDef(INT32 choice);
 static void M_HandleLoadSave(INT32 choice);
@@ -442,10 +454,11 @@ static consvar_t cv_dummymares = {"dummymares", "Overall", CV_HIDEN|CV_CALL, dum
 // ---------
 static menuitem_t MainMenu[] =
 {
-	{IT_CALL   |IT_STRING, NULL, "Secrets",     M_SecretsMenu,      84},
-	{IT_CALL   |IT_STRING, NULL, "1  player",   M_SinglePlayerMenu, 92},
-	{IT_SUBMENU|IT_STRING, NULL, "multiplayer", &MP_MainDef,       100},
-	{IT_CALL   |IT_STRING, NULL, "options",     M_Options,         108},
+	{IT_CALL   |IT_STRING, NULL, "Secrets",     M_SecretsMenu,      76},
+	{IT_CALL   |IT_STRING, NULL, "1  player",   M_SinglePlayerMenu, 84},
+	{IT_SUBMENU|IT_STRING, NULL, "multiplayer", &MP_MainDef,        92},
+	{IT_CALL   |IT_STRING, NULL, "options",     M_Options,         100},
+	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,          108},
 	{IT_CALL   |IT_STRING, NULL, "quit  game",  M_QuitSRB2,        116},
 };
 
@@ -455,9 +468,15 @@ typedef enum
 	singleplr,
 	multiplr,
 	options,
+	addons,
 	quitdoom
 } main_e;
 
+static menuitem_t MISC_AddonsMenu[] =
+{
+	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleAddons, 0},     // dummy menuitem for the control func
+};
+
 // ---------------------------------
 // Pause Menu Mode Attacking Edition
 // ---------------------------------
@@ -480,6 +499,7 @@ typedef enum
 // ---------------------
 static menuitem_t MPauseMenu[] =
 {
+	{IT_STRING | IT_CALL,     NULL, "Add-ons...",        M_Addons,               8},
 	{IT_STRING  | IT_SUBMENU, NULL, "Scramble Teams...", &MISC_ScrambleTeamDef, 16},
 	{IT_STRING  | IT_CALL,    NULL, "Switch Map..."    , M_MapChange,           24},
 
@@ -499,7 +519,8 @@ static menuitem_t MPauseMenu[] =
 
 typedef enum
 {
-	mpause_scramble = 0,
+	mpause_addons = 0,
+	mpause_scramble,
 	mpause_switchmap,
 
 	mpause_continue,
@@ -985,6 +1006,7 @@ static menuitem_t OP_MainMenu[] =
 
 	{IT_SUBMENU | IT_STRING, NULL, "Game Options...",       &OP_GameOptionsDef,   70},
 	{IT_SUBMENU | IT_STRING, NULL, "Server Options...",     &OP_ServerOptionsDef, 80},
+	{IT_STRING  | IT_CALL,   NULL, "Add-on Options...",     M_AddonsOptions,      90},
 };
 
 static menuitem_t OP_ControlsMenu[] =
@@ -1293,28 +1315,60 @@ static menuitem_t OP_EraseDataMenu[] =
 	{IT_STRING | IT_CALL, NULL, "\x85" "Erase ALL Data", M_EraseData, 40},
 };
 
+static menuitem_t OP_AddonsOptionsMenu[] =
+{
+	{IT_HEADER,                      NULL, "Menu",                        NULL,                    0},
+	{IT_STRING|IT_CVAR,              NULL, "Location",                    &cv_addons_option,      10},
+	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder",               &cv_addons_folder,      20},
+	{IT_STRING|IT_CVAR,              NULL, "Identify add-ons via",        &cv_addons_md5,         48},
+	{IT_STRING|IT_CVAR,              NULL, "Show unsupported file types", &cv_addons_showall,     58},
+
+	{IT_HEADER,                      NULL, "Search",                      NULL,                   76},
+	{IT_STRING|IT_CVAR,              NULL, "Matching",                    &cv_addons_search_type, 86},
+	{IT_STRING|IT_CVAR,              NULL, "Case-sensitive",              &cv_addons_search_case, 96},
+};
+
+enum
+{
+	op_addons_folder = 2,
+};
+
 static menuitem_t OP_GameOptionsMenu[] =
 {
 #ifndef NONET
 	{IT_STRING | IT_CVAR | IT_CV_STRING,
 	                      NULL, "Master server",          &cv_masterserver,     10},
 #endif
-	{IT_STRING | IT_CVAR, NULL, "Show HUD",               &cv_showhud,     40},
+	{IT_STRING | IT_SUBMENU, NULL, "Chat Options...",     &OP_ChatOptionsDef,   40},
+	{IT_STRING | IT_CVAR, NULL, "Show HUD",               &cv_showhud,     50},
 	{IT_STRING | IT_CVAR | IT_CV_SLIDER,
-	                      NULL, "HUD Visibility",         &cv_translucenthud, 50},
-	{IT_STRING | IT_CVAR, NULL, "Timer Display",          &cv_timetic,     60},
+	                      NULL, "HUD Visibility",         &cv_translucenthud, 60},
+	{IT_STRING | IT_CVAR, NULL, "Timer Display",          &cv_timetic,     70},
+	{IT_STRING | IT_CVAR, NULL, "Always Compact Rankings",          &cv_compactscoreboard,     80},
 #ifdef SEENAMES
-	{IT_STRING | IT_CVAR, NULL, "HUD Player Names",       &cv_seenames,    80},
+	{IT_STRING | IT_CVAR, NULL, "HUD Player Names",       &cv_seenames,    90},
 #endif
-	{IT_STRING | IT_CVAR, NULL, "Log Hazard Damage",      &cv_hazardlog,   90},
+	{IT_STRING | IT_CVAR, NULL, "Log Hazard Damage",      &cv_hazardlog,   100},
 
-	{IT_STRING | IT_CVAR, NULL, "Console Back Color",     &cons_backcolor, 100},
-	{IT_STRING | IT_CVAR, NULL, "Console Text Size",      &cv_constextsize,110},
-	{IT_STRING | IT_CVAR, NULL, "Uppercase Console",      &cv_allcaps,     120},
+	{IT_STRING | IT_CVAR, NULL, "Console Back Color",     &cons_backcolor, 110},
+	{IT_STRING | IT_CVAR, NULL, "Console Text Size",      &cv_constextsize,120},
+	{IT_STRING | IT_CVAR, NULL, "Uppercase Console",      &cv_allcaps,     130},
 
 	{IT_STRING | IT_CVAR, NULL, "Title Screen Demos",     &cv_rollingdemos, 140},
 };
 
+static menuitem_t OP_ChatOptionsMenu[] =
+{
+	{IT_STRING | IT_CVAR, NULL, "Chat Mode",            		 	 &cv_consolechat,  10},
+
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Width",    &cv_chatwidth,     30},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Height",   &cv_chatheight,    40},
+	{IT_STRING | IT_CVAR, NULL, "Message Fadeout Time",              &cv_chattime,    50},
+	{IT_STRING | IT_CVAR, NULL, "Chat Notifications",           	 &cv_chatnotifications,  60},
+	{IT_STRING | IT_CVAR, NULL, "Spam Protection",           		 &cv_chatspamprotection,  70},
+	{IT_STRING | IT_CVAR, NULL, "Chat background tint",           	 &cv_chatbacktint,  80},
+};
+
 static menuitem_t OP_ServerOptionsMenu[] =
 {
 	{IT_STRING | IT_SUBMENU, NULL, "General netgame options...",  &OP_NetgameOptionsDef,  10},
@@ -1407,6 +1461,18 @@ static menuitem_t OP_MonitorToggleMenu[] =
 // Main Menu and related
 menu_t MainDef = CENTERMENUSTYLE(NULL, MainMenu, NULL, 72);
 
+menu_t MISC_AddonsDef =
+{
+	NULL,
+	sizeof (MISC_AddonsMenu)/sizeof (menuitem_t),
+	&MainDef,
+	MISC_AddonsMenu,
+	M_DrawAddons,
+	50, 28,
+	0,
+	NULL
+};
+
 menu_t MAPauseDef = PAUSEMENUSTYLE(MAPauseMenu, 40, 72);
 menu_t SPauseDef = PAUSEMENUSTYLE(SPauseMenu, 40, 72);
 menu_t MPauseDef = PAUSEMENUSTYLE(MPauseMenu, 40, 72);
@@ -1417,6 +1483,9 @@ menu_t MISC_ChangeTeamDef = DEFAULTMENUSTYLE(NULL, MISC_ChangeTeamMenu, &MPauseD
 menu_t MISC_ChangeLevelDef = MAPICONMENUSTYLE(NULL, MISC_ChangeLevelMenu, &MPauseDef);
 menu_t MISC_HelpDef = IMAGEDEF(MISC_HelpMenu);
 
+static INT32 highlightflags, recommendedflags, warningflags;
+
+
 // Sky Room
 menu_t SR_PandoraDef =
 {
@@ -1709,6 +1778,7 @@ menu_t OP_ServerOptionsDef = DEFAULTMENUSTYLE("M_SERVER", OP_ServerOptionsMenu,
 
 menu_t OP_NetgameOptionsDef = DEFAULTMENUSTYLE("M_SERVER", OP_NetgameOptionsMenu, &OP_ServerOptionsDef, 30, 30);
 menu_t OP_GametypeOptionsDef = DEFAULTMENUSTYLE("M_SERVER", OP_GametypeOptionsMenu, &OP_ServerOptionsDef, 30, 30);
+menu_t OP_ChatOptionsDef = DEFAULTMENUSTYLE("M_GAME", OP_ChatOptionsMenu, &OP_GameOptionsDef, 30, 30);
 menu_t OP_MonitorToggleDef =
 {
 	"M_SERVER",
@@ -1751,6 +1821,7 @@ menu_t OP_OpenGLColorDef =
 #endif
 menu_t OP_DataOptionsDef = DEFAULTMENUSTYLE("M_DATA", OP_DataOptionsMenu, &OP_MainDef, 60, 30);
 menu_t OP_ScreenshotOptionsDef = DEFAULTMENUSTYLE("M_DATA", OP_ScreenshotOptionsMenu, &OP_DataOptionsDef, 30, 30);
+menu_t OP_AddonsOptionsDef = DEFAULTMENUSTYLE("M_ADDONS", OP_AddonsOptionsMenu, &OP_MainDef, 30, 30);
 menu_t OP_EraseDataDef = DEFAULTMENUSTYLE("M_DATA", OP_EraseDataMenu, &OP_DataOptionsDef, 60, 30);
 
 // ==========================================================================
@@ -1976,6 +2047,12 @@ void Moviemode_mode_Onchange(void)
 		OP_ScreenshotOptionsMenu[i].status = IT_STRING|IT_CVAR;
 }
 
+void Addons_option_Onchange(void)
+{
+	OP_AddonsOptionsMenu[op_addons_folder].status =
+		(cv_addons_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
+}
+
 // ==========================================================================
 // END ORGANIZATION STUFF.
 // ==========================================================================
@@ -2606,6 +2683,7 @@ void M_StartControlPanel(void)
 	else // multiplayer
 	{
 		MPauseMenu[mpause_switchmap].status = IT_DISABLED;
+		MPauseMenu[mpause_addons].status = IT_DISABLED;
 		MPauseMenu[mpause_scramble].status = IT_DISABLED;
 		MPauseMenu[mpause_psetupsplit].status = IT_DISABLED;
 		MPauseMenu[mpause_psetupsplit2].status = IT_DISABLED;
@@ -2617,6 +2695,7 @@ void M_StartControlPanel(void)
 		if ((server || IsPlayerAdmin(consoleplayer)))
 		{
 			MPauseMenu[mpause_switchmap].status = IT_STRING | IT_CALL;
+			MPauseMenu[mpause_addons].status = IT_STRING | IT_CALL;
 			if (G_GametypeHasTeams())
 				MPauseMenu[mpause_scramble].status = IT_STRING | IT_SUBMENU;
 		}
@@ -3856,6 +3935,548 @@ static void M_HandleImageDef(INT32 choice)
 // MISC MAIN MENU OPTIONS
 // ======================
 
+static void M_AddonsOptions(INT32 choice)
+{
+	(void)choice;
+	Addons_option_Onchange();
+
+	M_SetupNextMenu(&OP_AddonsOptionsDef);
+}
+
+#define LOCATIONSTRING1 "Visit \x83SRB2.ORG/MODS\x80 to get & make add-ons!"
+//#define LOCATIONSTRING2 "Visit \x88SRB2.ORG/MODS\x80 to get & make add-ons!"
+
+static void M_Addons(INT32 choice)
+{
+	const char *pathname = ".";
+
+	(void)choice;
+
+	// If M_GetGameypeColor() is ever ported from Kart, then remove this.
+	highlightflags = V_YELLOWMAP;
+	recommendedflags = V_GREENMAP;
+	warningflags = V_REDMAP;
+
+#if 1
+	if (cv_addons_option.value == 0)
+		pathname = usehome ? srb2home : srb2path;
+	else if (cv_addons_option.value == 1)
+		pathname = srb2home;
+	else if (cv_addons_option.value == 2)
+		pathname = srb2path;
+	else
+#endif
+	if (cv_addons_option.value == 3 && *cv_addons_folder.string != '\0')
+		pathname = cv_addons_folder.string;
+
+	strlcpy(menupath, pathname, 1024);
+	menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath) + 1;
+
+	if (menupath[menupathindex[menudepthleft]-2] != PATHSEP[0])
+	{
+		menupath[menupathindex[menudepthleft]-1] = PATHSEP[0];
+		menupath[menupathindex[menudepthleft]] = 0;
+	}
+	else
+		--menupathindex[menudepthleft];
+
+	if (!preparefilemenu(false))
+	{
+		M_StartMessage(va("No files/folders found.\n\n%s\n\n(Press a key)\n",LOCATIONSTRING1),NULL,MM_NOTHING);
+			// (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1))
+		return;
+	}
+	else
+		dir_on[menudepthleft] = 0;
+
+	if (addonsp[0]) // never going to have some provided but not all, saves individually checking
+	{
+		size_t i;
+		for (i = 0; i < NUM_EXT+5; i++)
+			W_UnlockCachedPatch(addonsp[i]);
+	}
+
+	addonsp[EXT_FOLDER] = W_CachePatchName("M_FFLDR", PU_STATIC);
+	addonsp[EXT_UP] = W_CachePatchName("M_FBACK", PU_STATIC);
+	addonsp[EXT_NORESULTS] = W_CachePatchName("M_FNOPE", PU_STATIC);
+	addonsp[EXT_TXT] = W_CachePatchName("M_FTXT", PU_STATIC);
+	addonsp[EXT_CFG] = W_CachePatchName("M_FCFG", PU_STATIC);
+	addonsp[EXT_WAD] = W_CachePatchName("M_FWAD", PU_STATIC);
+#ifdef USE_KART
+	addonsp[EXT_KART] = W_CachePatchName("M_FKART", PU_STATIC);
+#endif
+	addonsp[EXT_PK3] = W_CachePatchName("M_FPK3", PU_STATIC);
+	addonsp[EXT_SOC] = W_CachePatchName("M_FSOC", PU_STATIC);
+	addonsp[EXT_LUA] = W_CachePatchName("M_FLUA", PU_STATIC);
+	addonsp[NUM_EXT] = W_CachePatchName("M_FUNKN", PU_STATIC);
+	addonsp[NUM_EXT+1] = W_CachePatchName("M_FSEL", PU_STATIC);
+	addonsp[NUM_EXT+2] = W_CachePatchName("M_FLOAD", PU_STATIC);
+	addonsp[NUM_EXT+3] = W_CachePatchName("M_FSRCH", PU_STATIC);
+	addonsp[NUM_EXT+4] = W_CachePatchName("M_FSAVE", PU_STATIC);
+
+	MISC_AddonsDef.prevMenu = currentMenu;
+	M_SetupNextMenu(&MISC_AddonsDef);
+}
+
+#define width 4
+#define vpadding 27
+#define h (BASEVIDHEIGHT-(2*vpadding))
+#define NUMCOLOURS 8 // when toast's coding it's british english hacker fucker
+static void M_DrawTemperature(INT32 x, fixed_t t)
+{
+	INT32 y;
+
+	// bounds check
+	if (t > FRACUNIT)
+		t = FRACUNIT;
+	/*else if (t < 0) -- not needed
+		t = 0;*/
+
+	// scale
+	if (t > 1)
+		t = (FixedMul(h<<FRACBITS, t)>>FRACBITS);
+
+	// border
+	V_DrawFill(x - 1, vpadding, 1, h, 120);
+	V_DrawFill(x + width, vpadding, 1, h, 120);
+	V_DrawFill(x - 1, vpadding-1, width+2, 1, 120);
+	V_DrawFill(x - 1, vpadding+h, width+2, 1, 120);
+
+	// bar itself
+	y = h;
+	if (t)
+		for (t = h - t; y > 0; y--)
+		{
+			UINT8 colours[NUMCOLOURS] = {135, 133, 92, 77, 114, 178, 161, 162};
+			UINT8 c;
+			if (y <= t) break;
+			if (y+vpadding >= BASEVIDHEIGHT/2)
+				c = 185;
+			else
+				c = colours[(NUMCOLOURS*(y-1))/(h/2)];
+			V_DrawFill(x, y-1 + vpadding, width, 1, c);
+		}
+
+	// fill the rest of the backing
+	if (y)
+		V_DrawFill(x, vpadding, width, y, 30);
+}
+#undef width
+#undef vpadding
+#undef h
+#undef NUMCOLOURS
+
+static char *M_AddonsHeaderPath(void)
+{
+	UINT32 len;
+	static char header[1024];
+
+	strlcpy(header, va("%s folder%s", cv_addons_option.string, menupath+menupathindex[menudepth-1]-1), 1024);
+	len = strlen(header);
+	if (len > 34)
+	{
+		len = len-34;
+		header[len] = header[len+1] = header[len+2] = '.';
+	}
+	else
+		len = 0;
+
+	return header+len;
+}
+
+#define UNEXIST S_StartSound(NULL, sfx_lose);\
+		M_SetupNextMenu(MISC_AddonsDef.prevMenu);\
+		M_StartMessage(va("\x82%s\x80\nThis folder no longer exists!\nAborting to main menu.\n\n(Press a key)\n", M_AddonsHeaderPath()),NULL,MM_NOTHING)
+
+#define CLEARNAME Z_Free(refreshdirname);\
+					refreshdirname = NULL
+
+static void M_AddonsClearName(INT32 choice)
+{
+	CLEARNAME;
+	M_StopMessage(choice);
+}
+
+// returns whether to do message draw
+static boolean M_AddonsRefresh(void)
+{
+	if ((refreshdirmenu & REFRESHDIR_NORMAL) && !preparefilemenu(true))
+	{
+		UNEXIST;
+		return true;
+	}
+
+	if (refreshdirmenu & REFRESHDIR_ADDFILE)
+	{
+		char *message = NULL;
+
+		if (refreshdirmenu & REFRESHDIR_NOTLOADED)
+		{
+			S_StartSound(NULL, sfx_lose);
+			if (refreshdirmenu & REFRESHDIR_MAX)
+				message = va("%c%s\x80\nMaximum number of add-ons reached.\nA file could not be loaded.\nIf you want to play with this add-on, restart the game to clear existing ones.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
+			else
+				message = va("%c%s\x80\nA file was not loaded.\nCheck the console log for more information.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
+		}
+		else if (refreshdirmenu & (REFRESHDIR_WARNING|REFRESHDIR_ERROR))
+		{
+			S_StartSound(NULL, sfx_skid);
+			message = va("%c%s\x80\nA file was loaded with %s.\nCheck the console log for more information.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname, ((refreshdirmenu & REFRESHDIR_ERROR) ? "errors" : "warnings"));
+		}
+
+		if (message)
+		{
+			M_StartMessage(message,M_AddonsClearName,MM_EVENTHANDLER);
+			return true;
+		}
+
+		S_StartSound(NULL, sfx_strpst);
+		CLEARNAME;
+	}
+
+	return false;
+}
+
+static void M_DrawAddons(void)
+{
+	INT32 x, y;
+	ssize_t i, m;
+	const UINT8 *flashcol = NULL;
+	UINT8 hilicol;
+
+	// hack - need to refresh at end of frame to handle addfile...
+	if (refreshdirmenu & M_AddonsRefresh())
+	{
+		M_DrawMessageMenu();
+		return;
+	}
+
+	if (Playing())
+		V_DrawCenteredString(BASEVIDWIDTH/2, 5, warningflags, "Adding files mid-game may cause problems.");
+	else
+		V_DrawCenteredString(BASEVIDWIDTH/2, 5, 0, LOCATIONSTRING1);
+			// (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)
+
+	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;
+		if (y > FRACUNIT) // happens because of how we're shrinkin' it a little
+			y = FRACUNIT;
+	}
+
+	M_DrawTemperature(BASEVIDWIDTH - 19 - 5, y);
+
+	// DRAW MENU
+	x = currentMenu->x;
+	y = currentMenu->y + 1;
+
+	hilicol = V_GetStringColormap(highlightflags)[120];
+
+	V_DrawString(x-21, (y - 16) + (lsheadingheight - 12), highlightflags|V_ALLOWLOWERCASE, M_AddonsHeaderPath());
+	V_DrawFill(x-21, (y - 16) + (lsheadingheight - 3), MAXSTRINGLENGTH*8+6, 1, hilicol);
+	V_DrawFill(x-21, (y - 16) + (lsheadingheight - 2), MAXSTRINGLENGTH*8+6, 1, 30);
+
+	m = (BASEVIDHEIGHT - currentMenu->y + 2) - (y - 1);
+	V_DrawFill(x - 21, y - 1, MAXSTRINGLENGTH*8+6, m, 239);
+
+	// scrollbar!
+	if (sizedirmenu <= (2*numaddonsshown + 1))
+		i = 0;
+	else
+	{
+		ssize_t q = m;
+		m = ((2*numaddonsshown + 1) * m)/sizedirmenu;
+		if (dir_on[menudepthleft] <= numaddonsshown) // all the way up
+			i = 0;
+		else if (sizedirmenu <= (dir_on[menudepthleft] + numaddonsshown + 1)) // all the way down
+			i = q-m;
+		else
+			i = ((dir_on[menudepthleft] - numaddonsshown) * (q-m))/(sizedirmenu - (2*numaddonsshown + 1));
+	}
+
+	V_DrawFill(x + MAXSTRINGLENGTH*8+5 - 21, (y - 1) + i, 1, m, hilicol);
+
+	// get bottom...
+	m = dir_on[menudepthleft] + numaddonsshown + 1;
+	if (m > (ssize_t)sizedirmenu)
+		m = sizedirmenu;
+
+	// then compute top and adjust bottom if needed!
+	if (m < (2*numaddonsshown + 1))
+	{
+		m = min(sizedirmenu, 2*numaddonsshown + 1);
+		i = 0;
+	}
+	else
+		i = m - (2*numaddonsshown + 1);
+
+	if (i != 0)
+		V_DrawString(19, y+4 - (skullAnimCounter/5), highlightflags, "\x1A");
+
+	if (skullAnimCounter < 4)
+		flashcol = V_GetStringColormap(highlightflags);
+
+	for (; i < m; i++)
+	{
+		UINT32 flags = V_ALLOWLOWERCASE;
+		if (y > BASEVIDHEIGHT) break;
+		if (dirmenu[i])
+#define type (UINT8)(dirmenu[i][DIR_TYPE])
+		{
+			if (type & EXT_LOADED)
+			{
+				flags |= V_TRANSLUCENT;
+				V_DrawSmallScaledPatch(x-(16+4), y, V_TRANSLUCENT, addonsp[(type & ~EXT_LOADED)]);
+				V_DrawSmallScaledPatch(x-(16+4), y, 0, addonsp[NUM_EXT+2]);
+			}
+			else
+				V_DrawSmallScaledPatch(x-(16+4), y, 0, addonsp[(type & ~EXT_LOADED)]);
+
+			if ((size_t)i == dir_on[menudepthleft])
+			{
+				V_DrawFixedPatch((x-(16+4))<<FRACBITS, (y)<<FRACBITS, FRACUNIT/2, 0, addonsp[NUM_EXT+1], flashcol);
+				flags = V_ALLOWLOWERCASE|highlightflags;
+			}
+
+#define charsonside 14
+			if (dirmenu[i][DIR_LEN] > (charsonside*2 + 3))
+				V_DrawString(x, y+4, flags, va("%.*s...%s", charsonside, dirmenu[i]+DIR_STRING, dirmenu[i]+DIR_STRING+dirmenu[i][DIR_LEN]-(charsonside+1)));
+#undef charsonside
+			else
+				V_DrawString(x, y+4, flags, dirmenu[i]+DIR_STRING);
+		}
+#undef type
+		y += 16;
+	}
+
+	if (m != (ssize_t)sizedirmenu)
+		V_DrawString(19, y-12 + (skullAnimCounter/5), highlightflags, "\x1B");
+
+	y = BASEVIDHEIGHT - currentMenu->y + 1;
+
+	M_DrawTextBox(x - (21 + 5), y, MAXSTRINGLENGTH, 1);
+	if (menusearch[0])
+		V_DrawString(x - 18, y + 8, V_ALLOWLOWERCASE, menusearch+1);
+	else
+		V_DrawString(x - 18, y + 8, V_ALLOWLOWERCASE|V_TRANSLUCENT, "Type to search...");
+	if (skullAnimCounter < 4)
+		V_DrawCharacter(x - 18 + V_StringWidth(menusearch+1, 0), y + 8,
+			'_' | 0x80, false);
+
+	x -= (21 + 5 + 16);
+	V_DrawSmallScaledPatch(x, y + 4, (menusearch[0] ? 0 : V_TRANSLUCENT), addonsp[NUM_EXT+3]);
+
+	x = BASEVIDWIDTH - x - 16;
+	V_DrawSmallScaledPatch(x, y + 4, ((!modifiedgame || savemoddata) ? 0 : V_TRANSLUCENT), addonsp[NUM_EXT+4]);
+
+	if (modifiedgame)
+		V_DrawSmallScaledPatch(x, y + 4, 0, addonsp[NUM_EXT+2]);
+}
+
+static void M_AddonExec(INT32 ch)
+{
+	if (ch != 'y' && ch != KEY_ENTER)
+		return;
+
+	S_StartSound(NULL, sfx_zoom);
+	COM_BufAddText(va("exec \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING));
+}
+
+#define len menusearch[0]
+static boolean M_ChangeStringAddons(INT32 choice)
+{
+	if (shiftdown && choice >= 32 && choice <= 127)
+		choice = shiftxform[choice];
+
+	switch (choice)
+	{
+		case KEY_DEL:
+			if (len)
+			{
+				len = menusearch[1] = 0;
+				return true;
+			}
+			break;
+		case KEY_BACKSPACE:
+			if (len)
+			{
+				menusearch[1+--len] = 0;
+				return true;
+			}
+			break;
+		default:
+			if (choice >= 32 && choice <= 127)
+			{
+				if (len < MAXSTRINGLENGTH - 1)
+				{
+					menusearch[1+len++] = (char)choice;
+					menusearch[1+len] = 0;
+					return true;
+				}
+			}
+			break;
+	}
+	return false;
+}
+#undef len
+
+static void M_HandleAddons(INT32 choice)
+{
+	boolean exitmenu = false; // exit to previous menu
+
+	if (M_ChangeStringAddons(choice))
+	{
+		char *tempname = NULL;
+		if (dirmenu && dirmenu[dir_on[menudepthleft]])
+			tempname = Z_StrDup(dirmenu[dir_on[menudepthleft]]+DIR_STRING); // don't need to I_Error if can't make - not important, just QoL
+#if 0 // much slower
+		if (!preparefilemenu(true))
+		{
+			UNEXIST;
+			return;
+		}
+#else // streamlined
+		searchfilemenu(tempname);
+#endif
+	}
+
+	switch (choice)
+	{
+		case KEY_DOWNARROW:
+			if (dir_on[menudepthleft] < sizedirmenu-1)
+				dir_on[menudepthleft]++;
+			S_StartSound(NULL, sfx_menu1);
+			break;
+		case KEY_UPARROW:
+			if (dir_on[menudepthleft])
+				dir_on[menudepthleft]--;
+			S_StartSound(NULL, sfx_menu1);
+			break;
+		case KEY_PGDN:
+			{
+				UINT8 i;
+				for (i = numaddonsshown; i && (dir_on[menudepthleft] < sizedirmenu-1); i--)
+					dir_on[menudepthleft]++;
+			}
+			S_StartSound(NULL, sfx_menu1);
+			break;
+		case KEY_PGUP:
+			{
+				UINT8 i;
+				for (i = numaddonsshown; i && (dir_on[menudepthleft]); i--)
+					dir_on[menudepthleft]--;
+			}
+			S_StartSound(NULL, sfx_menu1);
+			break;
+		case KEY_ENTER:
+			{
+				boolean refresh = true;
+				if (!dirmenu[dir_on[menudepthleft]])
+					S_StartSound(NULL, sfx_lose);
+				else
+				{
+					switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE])
+					{
+						case EXT_FOLDER:
+							strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING);
+							if (menudepthleft)
+							{
+								menupathindex[--menudepthleft] = strlen(menupath);
+								menupath[menupathindex[menudepthleft]] = 0;
+
+								if (!preparefilemenu(false))
+								{
+									S_StartSound(NULL, sfx_skid);
+									M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
+									menupath[menupathindex[++menudepthleft]] = 0;
+
+									if (!preparefilemenu(true))
+									{
+										UNEXIST;
+										return;
+									}
+								}
+								else
+								{
+									S_StartSound(NULL, sfx_menu1);
+									dir_on[menudepthleft] = 1;
+								}
+								refresh = false;
+							}
+							else
+							{
+								S_StartSound(NULL, sfx_lose);
+								M_StartMessage(va("%c%s\x80\nThis folder is too deep to navigate to!\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
+								menupath[menupathindex[menudepthleft]] = 0;
+							}
+							break;
+						case EXT_UP:
+							S_StartSound(NULL, sfx_menu1);
+							menupath[menupathindex[++menudepthleft]] = 0;
+							if (!preparefilemenu(false))
+							{
+								UNEXIST;
+								return;
+							}
+							break;
+						case EXT_TXT:
+							M_StartMessage(va("%c%s\x80\nThis file may not be a console script.\nAttempt to run anyways? \n\n(Press 'Y' to confirm)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), dirmenu[dir_on[menudepthleft]]+DIR_STRING),M_AddonExec,MM_YESNO);
+							break;
+						case EXT_CFG:
+							M_AddonExec(KEY_ENTER);
+							break;
+						case EXT_LUA:
+#ifndef HAVE_BLUA
+							S_StartSound(NULL, sfx_lose);
+							M_StartMessage(va("%c%s\x80\nThis copy of SRB2 was compiled\nwithout support for .lua files.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), dirmenu[dir_on[menudepthleft]]+DIR_STRING),NULL,MM_NOTHING);
+							break;
+#endif
+						// else intentional fallthrough
+						case EXT_SOC:
+						case EXT_WAD:
+#ifdef USE_KART
+						case EXT_KART:
+#endif
+						case EXT_PK3:
+							COM_BufAddText(va("addfile \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING));
+							break;
+						default:
+							S_StartSound(NULL, sfx_lose);
+					}
+				}
+				if (refresh)
+					refreshdirmenu |= REFRESHDIR_NORMAL;
+			}
+			break;
+
+		case KEY_ESCAPE:
+			exitmenu = true;
+			break;
+
+		default:
+			break;
+	}
+	if (exitmenu)
+	{
+		closefilemenu(true);
+
+		// Secret menu!
+		MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+
+		if (currentMenu->prevMenu)
+			M_SetupNextMenu(currentMenu->prevMenu);
+		else
+			M_ClearMenus(true);
+	}
+}
+
 static void M_PandorasBox(INT32 choice)
 {
 	(void)choice;
@@ -6960,7 +7581,7 @@ static void M_ChangecontrolResponse(event_t *ev)
 				found = 0;
 				setupcontrols[control][1] = KEY_NULL;  //replace key 1,clear key2
 			}
-			G_CheckDoubleUsage(ch);
+			(void)G_CheckDoubleUsage(ch, true);
 			setupcontrols[control][found] = ch;
 		}
 		S_StartSound(NULL, sfx_strpst);
diff --git a/src/m_menu.h b/src/m_menu.h
index de76a2710dd0b68480f31b9e1b4167603da33911..eb770c19425c2b6730bdfbd8479463dd8f567112 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -124,6 +124,8 @@ boolean M_CanShowLevelInList(INT32 mapnum, INT32 gt);
 #define IT_HEADER      (IT_SPACE  +IT_HEADERTEXT)
 #define IT_SECRET      (IT_SPACE  +IT_QUESTIONMARKS)
 
+#define MAXSTRINGLENGTH 32
+
 typedef union
 {
 	struct menu_s *submenu;      // IT_SUBMENU
@@ -223,6 +225,9 @@ void M_CheatActivationResponder(INT32 ch);
 void Moviemode_mode_Onchange(void);
 void Screenshot_option_Onchange(void);
 
+// Addons menu updating
+void Addons_option_Onchange(void);
+
 // These defines make it a little easier to make menus
 #define DEFAULTMENUSTYLE(header, source, prev, x, y)\
 {\
diff --git a/src/m_misc.c b/src/m_misc.c
index 79fb0c99b6a5a1f0d780a9b079d96aec847077b4..7dd0d822c7ff9bd5b7f6c05b35f17dc7fba742c7 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -447,7 +447,7 @@ void Command_LoadConfig_f(void)
 	G_Controldefault();
 
 	// temporarily reset execversion to default
-	cv_execversion.flags &= ~CV_HIDEN;
+	CV_ToggleExecVersion(true);
 	COM_BufInsertText(va("%s \"%s\"\n", cv_execversion.name, cv_execversion.defaultvalue));
 	CV_InitFilterVar();
 
@@ -455,8 +455,8 @@ void Command_LoadConfig_f(void)
 	COM_BufInsertText(va("exec \"%s\"\n", configfile));
 
 	// don't filter anymore vars and don't let this convsvar be changed
-	COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, MODVERSION));
-	cv_execversion.flags |= CV_HIDEN;
+	COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, EXECVERSION));
+	CV_ToggleExecVersion(false);
 }
 
 /** Saves the current configuration and loads another.
@@ -498,7 +498,7 @@ void M_FirstLoadConfig(void)
 
 	// temporarily reset execversion to default
 	// we shouldn't need to do this, but JUST in case...
-	cv_execversion.flags &= ~CV_HIDEN;
+	CV_ToggleExecVersion(true);
 	COM_BufInsertText(va("%s \"%s\"\n", cv_execversion.name, cv_execversion.defaultvalue));
 	CV_InitFilterVar();
 
@@ -507,8 +507,8 @@ void M_FirstLoadConfig(void)
 	// no COM_BufExecute() needed; that does it right away
 
 	// don't filter anymore vars and don't let this convsvar be changed
-	COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, MODVERSION));
-	cv_execversion.flags |= CV_HIDEN;
+	COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, EXECVERSION));
+	CV_ToggleExecVersion(false);
 
 	// make sure I_Quit() will write back the correct config
 	// (do not write back the config if it crash before)
@@ -574,8 +574,8 @@ void M_SaveConfig(const char *filename)
 	fprintf(f, "// SRB2 configuration file.\n");
 
 	// print execversion FIRST, because subsequent consvars need to be filtered
-	// always print current MODVERSION
-	fprintf(f, "%s \"%d\"\n", cv_execversion.name, MODVERSION);
+	// always print current EXECVERSION
+	fprintf(f, "%s \"%d\"\n", cv_execversion.name, EXECVERSION);
 
 	// FIXME: save key aliases if ever implemented..
 
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 4fea158022f91df67cf8adf515778022897f666b..850ec2987f5de2513e59e00ded0824c9fb0a2dbd 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -7274,6 +7274,8 @@ void P_MobjThinker(mobj_t *mobj)
 							// Assumedly in splitscreen players will be on opposing teams
 							if (players[consoleplayer].ctfteam == 1 || splitscreen)
 								S_StartSound(NULL, sfx_hoop1);
+							else if (players[consoleplayer].ctfteam == 2)
+								S_StartSound(NULL, sfx_hoop3);
 
 							redflag = flagmo;
 						}
@@ -7285,6 +7287,8 @@ void P_MobjThinker(mobj_t *mobj)
 							// Assumedly in splitscreen players will be on opposing teams
 							if (players[consoleplayer].ctfteam == 2 || splitscreen)
 								S_StartSound(NULL, sfx_hoop1);
+							else if (players[consoleplayer].ctfteam == 1)
+								S_StartSound(NULL, sfx_hoop3);
 
 							blueflag = flagmo;
 						}
diff --git a/src/p_setup.c b/src/p_setup.c
index 4e66bebbef8075e9e81f6f9d47a83cf96d299777..b1521a53e714d2398ebfb90cb6619bd63fc1aa0a 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -54,6 +54,8 @@
 
 #include "v_video.h"
 
+#include "filesrch.h" // refreshdirmenu
+
 // wipes
 #include "f_finale.h"
 
@@ -3197,6 +3199,7 @@ boolean P_AddWadFile(const char *wadfilename)
 
 	if ((numlumps = W_InitFile(wadfilename)) == INT16_MAX)
 	{
+		refreshdirmenu |= REFRESHDIR_NOTLOADED;
 		CONS_Printf(M_GetText("Errors occurred while loading %s; not added.\n"), wadfilename);
 		return false;
 	}
diff --git a/src/r_data.c b/src/r_data.c
index e0b9a8147f858bd2c4ae740202da6ebd60175b6d..a21ba49ae9c3ae83efc88cf7376fb50af96a3a35 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -1220,7 +1220,7 @@ INT32 R_CreateColormap(char *p1, char *p2, char *p3)
 			continue;
 		if (maskcolor == extra_colormaps[i].maskcolor
 			&& fadecolor == extra_colormaps[i].fadecolor
-			&& fabsf((float)(maskamt - extra_colormaps[i].maskamt)) < 1.0E-36f
+			&& fabs(maskamt - extra_colormaps[i].maskamt) < DBL_EPSILON
 			&& fadestart == extra_colormaps[i].fadestart
 			&& fadeend == extra_colormaps[i].fadeend
 			&& fog == extra_colormaps[i].fog)
diff --git a/src/r_segs.c b/src/r_segs.c
index 6bdc8eabc988bfd22d31fdee6f12c4cd23f03907..6d6ba1efd1c0dd7a62cfd130df0e2cc08adfa133 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -1126,9 +1126,6 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			// Get data for the column
 			col = (column_t *)((UINT8 *)R_GetColumn(texnum,maskedtexturecol[dc_x]) - 3);
 
-			// guess what I just fixed? -monster psychic cat
-			dc_colormap = colormaps;
-
 			// SoM: New code does not rely on R_DrawColumnShadowed_8 which
 			// will (hopefully) put less strain on the stack.
 			if (dc_numlights)
diff --git a/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj b/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
index 6eabee56b6d20e57ff793ed589024203e6046f22..6242e0a05756c2e09db05db109501c16060ac2a8 100644
--- a/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
+++ b/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
@@ -1214,7 +1214,7 @@
 		C01FCF4B08A954540054247B /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.1.21;
+				CURRENT_PROJECT_VERSION = 2.1.22;
 				GCC_PREPROCESSOR_DEFINITIONS = (
 					"$(inherited)",
 					NORMALSRB2,
@@ -1226,7 +1226,7 @@
 		C01FCF4C08A954540054247B /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.1.21;
+				CURRENT_PROJECT_VERSION = 2.1.22;
 				GCC_ENABLE_FIX_AND_CONTINUE = NO;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
 				GCC_PREPROCESSOR_DEFINITIONS = (
diff --git a/src/sdl12/macosx/Srb2mac.xcodeproj/project.pbxproj b/src/sdl12/macosx/Srb2mac.xcodeproj/project.pbxproj
index 93f63309cab89371035681e58ac686c6733b8c78..081d046039f822034fde3453cd10d0efedc9972f 100644
--- a/src/sdl12/macosx/Srb2mac.xcodeproj/project.pbxproj
+++ b/src/sdl12/macosx/Srb2mac.xcodeproj/project.pbxproj
@@ -1214,7 +1214,7 @@
 		C01FCF4B08A954540054247B /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.1.21;
+				CURRENT_PROJECT_VERSION = 2.1.22;
 				GCC_PREPROCESSOR_DEFINITIONS = (
 					"$(inherited)",
 					NORMALSRB2,
@@ -1226,7 +1226,7 @@
 		C01FCF4C08A954540054247B /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.1.21;
+				CURRENT_PROJECT_VERSION = 2.1.22;
 				GCC_ENABLE_FIX_AND_CONTINUE = NO;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
 				GCC_PREPROCESSOR_DEFINITIONS = (
diff --git a/src/v_video.c b/src/v_video.c
index 35b848f98d052b7fbe41f64d0e3a9648dc991882..cfe7d036032c773fef71a8ff75c1c5180dce5ae8 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -841,6 +841,145 @@ void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
 		memset(dest, c, w * vid.bpp);
 }
 
+#ifdef HWRENDER
+// This is now a function since it's otherwise repeated 2 times and honestly looks retarded:
+static UINT32 V_GetHWConsBackColor(void)
+{
+	UINT32 hwcolor;
+	switch (cons_backcolor.value)
+	{
+		case 0:		hwcolor = 0xffffff00;	break; 	// White
+		case 1:		hwcolor = 0x80808000;	break; 	// Gray
+		case 2:		hwcolor = 0xdeb88700;	break;	// Sepia
+		case 3:		hwcolor = 0x40201000;	break; 	// Brown
+		case 4:		hwcolor = 0xfa807200;	break; 	// Pink
+		case 5:		hwcolor = 0xff69b400;	break; 	// Raspberry
+		case 6:		hwcolor = 0xff000000;	break; 	// Red
+		case 7:		hwcolor = 0xffd68300;	break;	// Creamsicle
+		case 8:		hwcolor = 0xff800000;	break; 	// Orange
+		case 9:		hwcolor = 0xdaa52000;	break; 	// Gold
+		case 10:	hwcolor = 0x80800000;	break; 	// Yellow
+		case 11:	hwcolor = 0x00ff0000;	break; 	// Emerald
+		case 12:	hwcolor = 0x00800000;	break; 	// Green
+		case 13:	hwcolor = 0x4080ff00;	break; 	// Cyan
+		case 14:	hwcolor = 0x4682b400;	break; 	// Steel
+		case 15:	hwcolor = 0x1e90ff00;	break;	// Periwinkle
+		case 16:	hwcolor = 0x0000ff00;	break; 	// Blue
+		case 17:	hwcolor = 0xff00ff00;	break; 	// Purple
+		case 18:	hwcolor = 0xee82ee00;	break; 	// Lavender
+		// Default green
+		default:	hwcolor = 0x00800000;	break;
+	}
+	return hwcolor;
+}
+#endif
+
+
+// THANK YOU MPC!!!
+
+void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
+{
+	UINT8 *dest;
+	INT32 u, v;
+	UINT8 *fadetable;
+	UINT32 alphalevel = 0;
+
+	if (rendermode == render_none)
+		return;
+
+#ifdef HWRENDER
+	if (rendermode != render_soft && rendermode != render_none)
+	{
+		UINT32 hwcolor = V_GetHWConsBackColor();
+		HWR_DrawConsoleFill(x, y, w, h, hwcolor, c);	// we still use the regular color stuff but only for flags. actual draw color is "hwcolor" for this.
+		return;
+	}
+#endif
+
+	if (!(c & V_NOSCALESTART))
+	{
+		INT32 dupx = vid.dupx, dupy = vid.dupy;
+
+		if (x == 0 && y == 0 && w == BASEVIDWIDTH && h == BASEVIDHEIGHT)
+		{ // Clear the entire screen, from dest to deststop. Yes, this really works.
+			memset(screens[0], (UINT8)(c&255), vid.width * vid.height * vid.bpp);
+			return;
+		}
+
+		x *= dupx;
+		y *= dupy;
+		w *= dupx;
+		h *= dupy;
+
+		// Center it if necessary
+		if (vid.width != BASEVIDWIDTH * dupx)
+		{
+			// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+			// so center this imaginary screen
+			if (c & V_SNAPTORIGHT)
+				x += (vid.width - (BASEVIDWIDTH * dupx));
+			else if (!(c & V_SNAPTOLEFT))
+				x += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+		}
+		if (vid.height != BASEVIDHEIGHT * dupy)
+		{
+			// same thing here
+			if (c & V_SNAPTOBOTTOM)
+				y += (vid.height - (BASEVIDHEIGHT * dupy));
+			else if (!(c & V_SNAPTOTOP))
+				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 2;
+		}
+	}
+
+	if (x >= vid.width || y >= vid.height)
+		return; // off the screen
+	if (x < 0) {
+		w += x;
+		x = 0;
+	}
+	if (y < 0) {
+		h += y;
+		y = 0;
+	}
+
+	if (w <= 0 || h <= 0)
+		return; // zero width/height wouldn't draw anything
+	if (x + w > vid.width)
+		w = vid.width-x;
+	if (y + h > vid.height)
+		h = vid.height-y;
+
+	dest = screens[0] + y*vid.width + x;
+
+	if ((alphalevel = ((c & V_ALPHAMASK) >> V_ALPHASHIFT)))
+	{
+		if (alphalevel == 13)
+			alphalevel = hudminusalpha[cv_translucenthud.value];
+		else if (alphalevel == 14)
+			alphalevel = 10 - cv_translucenthud.value;
+		else if (alphalevel == 15)
+			alphalevel = hudplusalpha[cv_translucenthud.value];
+
+		if (alphalevel >= 10)
+			return; // invis
+	}
+
+	c &= 255;
+
+	// Jimita (12-04-2018)
+	w = min(w, vid.width);
+	h = min(h, vid.height);
+	fadetable = ((UINT8 *)transtables + ((alphalevel-1)<<FF_TRANSSHIFT) + (c*256));
+	for (v = 0; v < h; v++, dest += vid.width)
+		for (u = 0; u < w; u++)
+		{
+			if (!alphalevel)
+				dest[u] = consolebgmap[dest[u]];
+			else
+				dest[u] = fadetable[consolebgmap[dest[u]]];
+		}
+}
+
 //
 // Fills a box of pixels using a flat texture as a pattern, scaled to screen size.
 //
@@ -983,21 +1122,7 @@ void V_DrawFadeConsBack(INT32 plines)
 #ifdef HWRENDER // not win32 only 19990829 by Kin
 	if (rendermode != render_soft && rendermode != render_none)
 	{
-		UINT32 hwcolor;
-		switch (cons_backcolor.value)
-		{
-			case 0:		hwcolor = 0xffffff00;	break; // White
-			case 1:		hwcolor = 0x80808000;	break; // Gray
-			case 2:		hwcolor = 0x40201000;	break; // Brown
-			case 3:		hwcolor = 0xff000000;	break; // Red
-			case 4:		hwcolor = 0xff800000;	break; // Orange
-			case 5:		hwcolor = 0x80800000;	break; // Yellow
-			case 6:		hwcolor = 0x00800000;	break; // Green
-			case 7:		hwcolor = 0x0000ff00;	break; // Blue
-			case 8:		hwcolor = 0x4080ff00;	break; // Cyan
-			// Default green
-			default:	hwcolor = 0x00800000;	break;
-		}
+		UINT32 hwcolor = V_GetHWConsBackColor();
 		HWR_DrawConsoleBack(hwcolor, plines);
 		return;
 	}
@@ -1012,7 +1137,7 @@ void V_DrawFadeConsBack(INT32 plines)
 
 // Gets string colormap, used for 0x80 color codes
 //
-static const UINT8 *V_GetStringColormap(INT32 colorflags)
+UINT8 *V_GetStringColormap(INT32 colorflags)
 {
 	switch ((colorflags & V_CHARCOLORMASK) >> V_CHARCOLORSHIFT)
 	{
@@ -1061,6 +1186,32 @@ void V_DrawCharacter(INT32 x, INT32 y, INT32 c, boolean lowercaseallowed)
 		V_DrawScaledPatch(x, y, flags, hu_font[c]);
 }
 
+// Writes a single character for the chat. (draw WHITE if bit 7 set)
+// Essentially the same as the above but it's small or big depending on what resolution you've chosen to huge..
+//
+void V_DrawChatCharacter(INT32 x, INT32 y, INT32 c, boolean lowercaseallowed, UINT8 *colormap)
+{
+	INT32 w, flags;
+	//const UINT8 *colormap = V_GetStringColormap(c);
+
+	flags = c & ~(V_CHARCOLORMASK | V_PARAMMASK);
+	c &= 0x7f;
+	if (lowercaseallowed)
+		c -= HU_FONTSTART;
+	else
+		c = toupper(c) - HU_FONTSTART;
+	if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
+		return;
+
+	w = (vid.width < 640 ) ? (SHORT(hu_font[c]->width)/2) : (SHORT(hu_font[c]->width));	// use normal sized characters if we're using a terribly low resolution.
+	if (x + w > vid.width)
+		return;
+
+	V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, (vid.width < 640) ? (FRACUNIT) : (FRACUNIT/2), flags, hu_font[c], colormap);
+
+
+}
+
 // Precompile a wordwrapped string to any given width.
 // This is a muuuch better method than V_WORDWRAP.
 char *V_WordWrap(INT32 x, INT32 w, INT32 option, const char *string)
diff --git a/src/v_video.h b/src/v_video.h
index a60a08a5bb5349c3477214e0c527a42005ec0efe..877a619b5ac56ad60bb423918f1647b7dbb7d0af 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -139,6 +139,7 @@ void V_DrawScaledPic (INT32 px1, INT32 py1, INT32 scrn, INT32 lumpnum);
 
 // fill a box with a single color
 void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c);
+void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c);
 // fill a box with a flat as a pattern
 void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum);
 
@@ -149,11 +150,16 @@ void V_DrawFadeConsBack(INT32 plines);
 
 // draw a single character
 void V_DrawCharacter(INT32 x, INT32 y, INT32 c, boolean lowercaseallowed);
+// draw a single character, but for the chat
+void V_DrawChatCharacter(INT32 x, INT32 y, INT32 c, boolean lowercaseallowed, UINT8 *colormap);
+
+UINT8 *V_GetStringColormap(INT32 colorflags);
 
 void V_DrawLevelTitle(INT32 x, INT32 y, INT32 option, const char *string);
 
 // wordwrap a string using the hu_font
 char *V_WordWrap(INT32 x, INT32 w, INT32 option, const char *string);
+UINT8 *V_GetStringColormap(INT32 colorflags);
 
 // draw a string using the hu_font
 void V_DrawString(INT32 x, INT32 y, INT32 option, const char *string);
diff --git a/src/w_wad.c b/src/w_wad.c
index e1cd16ae9a58dc22b35f8c7c68459aa8ff2aae54..88e89974aab83c201d7f2b0fcdd88dca39481645 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -34,6 +34,8 @@
 #include "z_zone.h"
 #include "fastcmp.h"
 
+#include "filesrch.h"
+
 #include "i_video.h" // rendermode
 #include "d_netfil.h"
 #include "dehacked.h"
@@ -647,12 +649,22 @@ UINT16 W_InitFile(const char *filename)
 	restype_t type;
 	UINT16 numlumps = 0;
 	size_t i;
-	size_t packetsize = 0;
-	serverinfo_pak *dummycheck = NULL;
+	size_t packetsize;
 	UINT8 md5sum[16];
+	boolean important;
+
+	if (!(refreshdirmenu & REFRESHDIR_ADDFILE))
+		refreshdirmenu = REFRESHDIR_NORMAL|REFRESHDIR_ADDFILE; // clean out cons_alerts that happened earlier
 
-	// Shut the compiler up.
-	(void)dummycheck;
+	if (refreshdirname)
+		Z_Free(refreshdirname);
+	if (dirmenu)
+	{
+		refreshdirname = Z_StrDup(filename);
+		nameonly(refreshdirname);
+	}
+	else
+		refreshdirname = NULL;
 
 	//CONS_Debug(DBG_SETUP, "Loading %s\n", filename);
 	//
@@ -661,6 +673,7 @@ UINT16 W_InitFile(const char *filename)
 	if (numwadfiles >= MAX_WADFILES)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
+		refreshdirmenu |= REFRESHDIR_MAX;
 		return INT16_MAX;
 	}
 
@@ -670,21 +683,21 @@ UINT16 W_InitFile(const char *filename)
 
 	// Check if wad files will overflow fileneededbuffer. Only the filename part
 	// is send in the packet; cf.
-	for (i = 0; i < numwadfiles; i++)
+	// see PutFileNeeded in d_netfil.c
+	if ((important = !W_VerifyNMUSlumps(filename)))
 	{
-		packetsize += nameonlylength(wadfiles[i]->filename);
-		packetsize += 22; // MD5, etc.
-	}
+		packetsize = packetsizetally + nameonlylength(filename) + 22;
 
-	packetsize += nameonlylength(filename);
-	packetsize += 22;
+		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
+			refreshdirmenu |= REFRESHDIR_MAX;
+			if (handle)
+				fclose(handle);
+			return INT16_MAX;
+		}
 
-	if (packetsize > sizeof(dummycheck->fileneeded))
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
-		if (handle)
-			fclose(handle);
-		return INT16_MAX;
+		packetsizetally = packetsize;
 	}
 
 #ifndef NOMD5
@@ -741,6 +754,7 @@ UINT16 W_InitFile(const char *filename)
 	wadfile->handle = handle;
 	wadfile->numlumps = (UINT16)numlumps;
 	wadfile->lumpinfo = lumpinfo;
+	wadfile->important = important;
 	fseek(handle, 0, SEEK_END);
 	wadfile->filesize = (unsigned)ftell(handle);
 	wadfile->type = type;
diff --git a/src/w_wad.h b/src/w_wad.h
index 8baf061adce3982a185ff1c9b35489802aad6d9d..e2e17740f76fc24ed7d02a60fc5f854824888296 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -110,6 +110,7 @@ typedef struct wadfile_s
 	FILE *handle;
 	UINT32 filesize; // for network
 	UINT8 md5sum[16];
+	boolean important;
 } wadfile_t;
 
 #define WADFILENUM(lumpnum) (UINT16)((lumpnum)>>16) // wad flumpnum>>16) // wad file number in upper word