diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 53bbaf0b68443eaeca23eabc7194fb7d144c2dc4..0c8d736de94e2dc7e050c7ea21dea360ac06718d 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -163,6 +163,7 @@ static UINT8 localtextcmd[MAXSPLITSCREENPLAYERS][MAXTEXTCMD];
 static tic_t neededtic;
 SINT8 servernode = 0; // the number of the server node
 char connectedservername[MAXSERVERNAME];
+char connectedservercontact[MAXSERVERCONTACT];
 /// \brief do we accept new players?
 /// \todo WORK!
 boolean acceptnewnode = true;
@@ -1283,6 +1284,9 @@ static boolean SV_SendServerConfig(INT32 node)
 
 	memcpy(netbuffer->u.servercfg.server_context, server_context, 8);
 
+	strncpy(netbuffer->u.servercfg.server_name, cv_servername.string, MAXSERVERNAME);
+	strncpy(netbuffer->u.servercfg.server_contact, cv_server_contact.string, MAXSERVERCONTACT);
+
 	{
 		const size_t len = sizeof (serverconfig_pak);
 
@@ -3799,6 +3803,9 @@ void SV_ResetServer(void)
 	// clear server_context
 	memset(server_context, '-', 8);
 
+	strncpy(connectedservername, "\0", MAXSERVERNAME);
+	strncpy(connectedservercontact, "\0", MAXSERVERCONTACT);
+
 	CV_RevertNetVars();
 
 	// Copy our unlocks to a place where net material can grab at/overwrite them safely.
@@ -3811,9 +3818,10 @@ void SV_ResetServer(void)
 	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
 }
 
-static inline void SV_GenContext(void)
+static void SV_GenContext(void)
 {
 	UINT8 i;
+
 	// generate server_context, as exactly 8 bytes of randomly mixed A-Z and a-z
 	// (hopefully M_Random is initialized!! if not this will be awfully silly!)
 	for (i = 0; i < 8; i++)
@@ -3824,6 +3832,9 @@ static inline void SV_GenContext(void)
 		else // lowercase
 			server_context[i] = 'a'+(a-26);
 	}
+
+	strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
+	strncpy(connectedservercontact, cv_server_contact.string, MAXSERVERCONTACT);
 }
 
 //
@@ -4874,7 +4885,11 @@ static void HandlePacketFromAwayNode(SINT8 node)
 				G_SetGametype(netbuffer->u.servercfg.gametype);
 
 				modifiedgame = netbuffer->u.servercfg.modifiedgame;
+
 				memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
+
+				strncpy(connectedservername, netbuffer->u.servercfg.server_name, MAXSERVERNAME);
+				strncpy(connectedservercontact, netbuffer->u.servercfg.server_contact, MAXSERVERCONTACT);
 			}
 
 #ifdef HAVE_DISCORDRPC
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index e2dd85bff8e22748425b7e50071b4f0a8dcf2f79..fdcf4d0fe521038283d73e864fb0b9edabad8c0a 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -43,6 +43,9 @@ applications may follow different packet versions.
 
 #define HU_MAXMSGLEN 223
 
+#define MAXSERVERNAME 32
+#define MAXSERVERCONTACT 1024
+
 // Networking and tick handling related.
 #define BACKUPTICS 512 // more than enough for most timeouts....
 #define CLIENTBACKUPTICS 32
@@ -226,6 +229,9 @@ struct serverconfig_pak
 	UINT8 maxplayer;
 	boolean allownewplayer;
 	boolean discordinvites;
+
+	char server_name[MAXSERVERNAME];
+	char server_contact[MAXSERVERCONTACT];
 } ATTRPACK;
 
 struct filetx_pak
@@ -276,7 +282,6 @@ struct clientconfig_pak
 #define SV_DEDICATED 0x40		// server is dedicated
 #define SV_LOTSOFADDONS 0x20	// flag used to ask for full file list in d_netfil
 
-#define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
 #define MAX_MIRROR_LENGTH 256
 // This packet is too large
@@ -509,6 +514,7 @@ extern UINT16 software_MAXPACKETLENGTH;
 extern boolean acceptnewnode;
 extern SINT8 servernode;
 extern char connectedservername[MAXSERVERNAME];
+extern char connectedservercontact[MAXSERVERCONTACT];
 extern UINT32 ourIP;
 extern uint8_t lastReceivedKey[MAXNETNODES][MAXSPLITSCREENPLAYERS][PUBKEYLENGTH];
 extern uint8_t lastSentChallenge[MAXNETNODES][CHALLENGELENGTH];
diff --git a/src/d_main.c b/src/d_main.c
index fd05bc3cde549ac4b298041007fbc6a8a215d055..c177370602946612724191c2d041f91a4964fc4a 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1913,8 +1913,6 @@ void D_SRB2Main(void)
 
 		COM_BufExecute(); // ensure the command buffer gets executed before the map starts (+skin)
 
-		strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
-
 		if (M_CheckParm("-gametype") && M_IsNextParm())
 		{
 			// from Command_Map_f
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 6becfd0c3820b036ff675c2ab8f85eda7ab8c046..5694206267b03f94c5fe987ba86666ff75134351 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -3023,8 +3023,6 @@ static void Command_Map_f(void)
 			multiplayer = true;
 			netgame = false;
 
-			strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
-
 			if (cv_maxconnections.value < ssplayers+1)
 				CV_SetValue(&cv_maxconnections, ssplayers+1);
 
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index e97d411836a704d36bd22e0b9e789ba19c37c27c..fd9fa3c973b8cbc155be8db1c38e555e8444c4cb 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -1952,7 +1952,7 @@ void HU_DrawSongCredits(void)
 	}
 	else
 	{
-		y = (r_splitscreen ? (BASEVIDHEIGHT/2)-4 : 32) * FRACUNIT;
+		y = (r_splitscreen ? (BASEVIDHEIGHT/2)-4 : 40) * FRACUNIT;
 	}
 
 	INT32 bgt = (NUMTRANSMAPS/2) + (cursongcredit.trans / 2);
diff --git a/src/k_menudraw.c b/src/k_menudraw.c
index 23c989505d328423ff3b7730431b0b957c4b2f74..800ff3c6c53990f7d29f1df8cb2674e25a65fda8 100644
--- a/src/k_menudraw.c
+++ b/src/k_menudraw.c
@@ -825,6 +825,13 @@ void M_Drawer(void)
 		}
 	}
 
+	if (netgame && Playing())
+	{
+		boolean mainpause_open = menuactive && currentMenu == &PAUSE_MainDef;
+
+		ST_DrawServerSplash(!mainpause_open);
+	}
+
 	// focus lost notification goes on top of everything, even the former everything
 	if (window_notinfocus && cv_showfocuslost.value)
 	{
diff --git a/src/st_stuff.c b/src/st_stuff.c
index c7166c4144347a92c3748bb4ebb736334f0a77d9..748d6da1dda8192243a56a2d8cde685921d98e06 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -1303,6 +1303,124 @@ void ST_AskToJoinEnvelope(void)
 }
 #endif
 
+static INT32 ST_ServerSplash_OpacityFlag(INT32 opacity)
+{
+	if (opacity >= NUMTRANSMAPS)
+	{
+		return 0;
+	}
+
+	opacity = max(opacity, 1);
+	return (NUMTRANSMAPS - opacity) << V_ALPHASHIFT;
+}
+
+void ST_DrawServerSplash(boolean timelimited)
+{
+	static const fixed_t SPLASH_LEN = (FRACUNIT * TICRATE) * 3;
+	static const fixed_t SPLASH_WAIT = (FRACUNIT * TICRATE) / 2;
+
+	static fixed_t splashTime = -SPLASH_WAIT;
+	static char prevContext[8] = {0};
+
+	if (memcmp(prevContext, server_context, 8) != 0)
+	{
+		// Context changed, we want to draw it again
+		splashTime = -SPLASH_WAIT;
+		memcpy(prevContext, server_context, 8);
+	}
+
+	if (lt_ticker < lt_endtime)
+	{
+		// Level title is running rn
+		return;
+	}
+
+	if (timelimited
+		&& splashTime >= SPLASH_LEN)
+	{
+		// We finished drawing it
+		return;
+	}
+
+	splashTime += renderdeltatics;
+	if (splashTime <= 0)
+	{
+		// We're waiting a tiny bit to draw it
+		return;
+	}
+
+	const INT32 splashTic = splashTime >> FRACBITS;
+	INT32 opacity = NUMTRANSMAPS;
+	if (splashTic < NUMTRANSMAPS)
+	{
+		opacity = splashTic;
+	}
+	else if (timelimited
+		&& splashTic > (SPLASH_LEN >> FRACBITS) - NUMTRANSMAPS)
+	{
+		opacity = (SPLASH_LEN >> FRACBITS) - splashTic;
+	}
+
+	INT32 opacityFlag = ST_ServerSplash_OpacityFlag(opacity);
+
+	patch_t *gridPatch = W_CachePatchName("MOTDBG", PU_CACHE);
+
+	if (gridPatch && gridPatch->width)
+	{
+		fixed_t gridX = -(splashTime / 3) % (gridPatch->width * FRACUNIT);
+		fixed_t gridY = (gridPatch->height) * FRACUNIT;
+		INT32 gridOpacity = ST_ServerSplash_OpacityFlag(opacity / 2);
+		fixed_t maxX = (vid.width * FRACUNIT) / vid.dupx;
+
+		while (gridX < maxX)
+		{
+			V_DrawFixedPatch(
+				gridX, gridY,
+				FRACUNIT,
+				(V_SNAPTOLEFT|V_SNAPTOBOTTOM) | V_SUBTRACT | V_VFLIP | gridOpacity,
+				gridPatch,
+				NULL
+			);
+
+			gridX += (gridPatch->width * FRACUNIT);
+		}
+	}
+
+	// We're a bit crunched atm to do this but hopefully in the future
+	// the icon can be made a bmp file on the hard drive that the server
+	// sends on client join instead.
+	patch_t *iconPatch = W_CachePatchName("MOTDICON", PU_CACHE);
+	fixed_t iconX = (BASEVIDWIDTH - 16 - iconPatch->width) * FRACUNIT;
+	fixed_t iconY = (8) * FRACUNIT;
+	V_DrawFixedPatch(
+		iconX, iconY,
+		FRACUNIT,
+		(V_SNAPTORIGHT|V_SNAPTOBOTTOM) | opacityFlag,
+		iconPatch,
+		NULL
+	);
+
+	fixed_t textX = (BASEVIDWIDTH - 16 - 36) * FRACUNIT;
+	fixed_t textY = (24 - 8) * FRACUNIT;
+
+	V_DrawRightAlignedStringAtFixed(
+		textX, textY,
+		(V_SNAPTORIGHT|V_SNAPTOBOTTOM) | opacityFlag,
+		connectedservername
+	);
+
+	textY += 10*FRACUNIT;
+
+	if (connectedservercontact[0] != 0)
+	{
+		V_DrawRightAlignedThinStringAtFixed(
+			textX, textY,
+			(V_SNAPTORIGHT|V_SNAPTOBOTTOM) | opacityFlag,
+			va("Contact @ %c%s", '\x80' + cv_shoutcolor.value, connectedservercontact)
+		);
+	}
+}
+
 void ST_Drawer(void)
 {
 	boolean stagetitle = false; // Decide whether to draw the stage title or not
diff --git a/src/st_stuff.h b/src/st_stuff.h
index 44aa5f25188e916e3628bca127ef025998448e6a..c406bdcaf894e4b8e15b89a58e2069a68b689436 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -75,6 +75,8 @@ void ST_preLevelTitleCardDrawer(void);
 extern tic_t lt_ticker, lt_lasttic;
 extern tic_t lt_exitticker, lt_endtime;
 
+void ST_DrawServerSplash(boolean timelimited);
+
 // return if player a is in the same team as player b
 boolean ST_SameTeam(player_t *a, player_t *b);
 
diff --git a/src/v_video.cpp b/src/v_video.cpp
index 17519f8947961e38ae8c319ed371920625e4a042..351ff4d465b6e026704e81d3b7f0c1060edae846 100644
--- a/src/v_video.cpp
+++ b/src/v_video.cpp
@@ -2789,12 +2789,24 @@ void V_DrawRightAlignedThinString(INT32 x, INT32 y, INT32 option, const char *st
 	V_DrawThinString(x, y, option, string);
 }
 
-void V_DrawCenteredThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
+void V_DrawCenteredStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
 	x -= (V_ThinStringWidth(string, option) / 2) * FRACUNIT;
 	V_DrawThinStringAtFixed(x, y, option, string);
 }
 
+void V_DrawRightAlignedStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
+{
+	x -= V_StringWidth(string, option) * FRACUNIT;
+	V_DrawStringAtFixed(x, y, option, string);
+}
+
+void V_DrawCenteredThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
+{
+	x -= (V_StringWidth(string, option) / 2) * FRACUNIT;
+	V_DrawStringAtFixed(x, y, option, string);
+}
+
 void V_DrawRightAlignedThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
 	x -= V_ThinStringWidth(string, option) * FRACUNIT;
diff --git a/src/v_video.h b/src/v_video.h
index 7a26591dc052d302d00a1c1334d65c4e793e5f26..f7c32505c8d2a356c7faa5ef66ea8e4d8dbf8012 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -334,16 +334,14 @@ void V_DrawRightAlignedThinString(INT32 x, INT32 y, INT32 option, const char *st
 
 #define V_DrawStringAtFixed( x,y,option,string ) \
 	V__DrawOneScaleString (x,y,FRACUNIT,option,NULL,HU_FONT,string)
+void V_DrawCenteredStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
+void V_DrawRightAlignedStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
 
 #define V_DrawThinStringAtFixed( x,y,option,string ) \
 	V__DrawOneScaleString (x,y,FRACUNIT,option,NULL,TINY_FONT,string)
 void V_DrawCenteredThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
 void V_DrawRightAlignedThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
 
-
-void V_DrawCenteredThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
-void V_DrawRightAlignedThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
-
 // Draws a titlecard font string.
 // timer: when the letters start appearing (leave to 0 to disable)
 // threshold: when the letters start disappearing (leave to 0 to disable) (both are INT32 in case you supply negative values...)