From 62e8bb5774c504f9e08236590635df06058bf15a Mon Sep 17 00:00:00 2001
From: Spoike <acceptthis@users.sourceforge.net>
Date: Thu, 28 Jul 2022 02:17:27 +0000
Subject: [PATCH] Prevent FTE servers from getting mistreated as NQ servers,
 this should restore the 'observe' option.

git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@6294 fc73d0e0-1445-4013-8a0c-d673dee63da5
---
 engine/client/cl_main.c    |  4 +-
 engine/client/cl_master.h  | 11 +++--
 engine/client/m_master.c   | 20 ++++----
 engine/client/net_master.c | 95 ++++++++++++++++++++++----------------
 engine/common/common.c     |  2 +-
 engine/common/net.h        |  2 +-
 engine/common/net_chan.c   |  2 +-
 engine/common/net_ice.c    |  2 +-
 engine/common/net_wins.c   |  2 +-
 engine/server/server.h     |  1 +
 engine/server/sv_main.c    | 72 ++++++++++++++++++++++++++---
 11 files changed, 146 insertions(+), 67 deletions(-)

diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c
index bb4710e26..703db92da 100644
--- a/engine/client/cl_main.c
+++ b/engine/client/cl_main.c
@@ -493,7 +493,7 @@ void CL_ConnectToDarkPlaces(char *challenge, netadr_t *adr)
 
 	connectinfo.time = realtime;	// for retransmit requests
 
-	Q_snprintfz(data, sizeof(data), "%c%c%c%cconnect\\protocol\\darkplaces 3\\protocols\\DP7 DP6 DP5 RMQ FITZ NEHAHRABJP2 NEHAHRABJP NEHAHRABJP3 QUAKE\\challenge\\%s\\name\\%s", 255, 255, 255, 255, challenge, name.string);
+	Q_snprintfz(data, sizeof(data), "%c%c%c%cconnect\\protocol\\darkplaces "STRINGIFY(NQ_NETCHAN_VERSION)"\\protocols\\DP7 DP6 DP5 RMQ FITZ NEHAHRABJP2 NEHAHRABJP NEHAHRABJP3 QUAKE\\challenge\\%s\\name\\%s", 255, 255, 255, 255, challenge, name.string);
 
 	NET_SendPacket (cls.sockets, strlen(data), data, adr);
 
@@ -1160,7 +1160,7 @@ void CL_CheckForResend (void)
 			else if (1)
 			{
 				net_from = connectinfo.adr[connectinfo.nextadr];
-				Q_snprintfz(net_message.data, net_message.maxsize, "xxxxconnect\\protocol\\darkplaces 3\\protocols\\DP7 DP6 DP5 RMQ FITZ NEHAHRABJP2 NEHAHRABJP NEHAHRABJP3 QUAKE\\challenge\\0x%x\\name\\%s", SV_NewChallenge(), name.string);
+				Q_snprintfz(net_message.data, net_message.maxsize, "xxxxconnect\\protocol\\darkplaces "STRINGIFY(NQ_NETCHAN_VERSION)"\\protocols\\DP7 DP6 DP5 RMQ FITZ NEHAHRABJP2 NEHAHRABJP NEHAHRABJP3 QUAKE\\challenge\\0x%x\\name\\%s", SV_NewChallenge(), name.string);
 				Cmd_TokenizeString (net_message.data+4, false, false);
 				SVC_DirectConnect(0);
 			}
diff --git a/engine/client/cl_master.h b/engine/client/cl_master.h
index 6bf00ed94..67d6f2023 100644
--- a/engine/client/cl_master.h
+++ b/engine/client/cl_master.h
@@ -22,17 +22,18 @@ enum masterprotocol_e
 #define SS_UNKNOWN		0
 #define SS_QUAKEWORLD	1
 #define SS_NETQUAKE		2
-#define SS_DARKPLACES	3
-#define SS_QUAKE2		4
-#define SS_QUAKE3		5
+#define SS_QUAKE2		3
+#define SS_QUAKE3		4
+#define SS_QEPROT		5	//needs dtls and a different ccreq version
 //#define SS_UNUSED		6
 //#define SS_UNUSED		7
 
 #define SS_LOCAL		(1<<3u)	//local servers are ones we detected without being listed on a master server (masters will report public ips, so these may appear as dupes if they're also public)
-#define SS_FTESERVER	(1<<4u)	//hehehe...
+#define SS_FTESERVER	(1<<4u)	//just highlighting differences, to give some impression of superiority.
 #define SS_FAVORITE		(1<<5u)	//filter all others.
 #define SS_KEEPINFO		(1<<6u)
-#define SS_PROXY		(1<<7u)
+#define SS_GETINFO		(1<<7u)	//explicitly query via getinfo
+#define SS_PROXY		(1<<8u)	//qizmo/qwfwd/qtv/eztv
 
 #define PING_DEAD		0xffff	//default ping value to denote servers that are not responding.
 #define PING_UNKNOWN	0xfffe	//these servers are considered up, but we can't query them directly so can't determine the final ping from here.
diff --git a/engine/client/m_master.c b/engine/client/m_master.c
index 338ae4e18..0c3395930 100644
--- a/engine/client/m_master.c
+++ b/engine/client/m_master.c
@@ -145,7 +145,7 @@ static void SL_TitlesDraw (int x, int y, menucustom_t *ths, emenu_t *menu)
 	else
 		clr = 'B';
 	x = ths->common.width;
-	if (mx > x || mousecursor_y < y || mousecursor_y >= y+8)
+	if ((mx > x || mousecursor_y < y || mousecursor_y >= y+8) && !serverpreview)
 		filldraw = true;
 	if (sb_showtimelimit.value)	{SL_DrawColumnTitle(&x, y, 3*8, mx, "tl", (sf==SLKEY_TIMELIMIT), clr, &filldraw);}
 	if (sb_showfraglimit.value)	{SL_DrawColumnTitle(&x, y, 3*8, mx, "fl", (sf==SLKEY_FRAGLIMIT), clr, &filldraw);}
@@ -268,8 +268,9 @@ static servertypes_t flagstoservertype(int flags)
 
 	switch(flags & SS_PROTOCOLMASK)
 	{
+	case SS_QEPROT:
+		return ST_NETQUAKE;
 	case SS_NETQUAKE:
-	case SS_DARKPLACES:
 		return ST_NETQUAKE;
 	case SS_QUAKE2:
 		return ST_QUAKE2;
@@ -308,7 +309,7 @@ static void SL_ServerDraw (int x, int y, menucustom_t *ths, emenu_t *menu)
 				serverhighlight[(int)stype][2],
 				1.0));
 		}
-		else if (thisone == info->scrollpos + (int)(mousecursor_y-info->servers_top)/8 && mousecursor_x < x)
+		else if (thisone == info->scrollpos + (int)(mousecursor_y-info->servers_top)/8 && mousecursor_x < x && !serverpreview)
 			R2D_ImageColours(SRGBA((sin(realtime*4.4)*0.25)+0.5, (sin(realtime*4.4)*0.25)+0.5, 0.08, sb_alpha.value));
 		else if (selectedserver.inuse && NET_CompareAdr(&si->adr, &selectedserver.adr) && !strcmp(si->brokerid, selectedserver.brokerid))
 			R2D_ImageColours(SRGBA(((sin(realtime*4.4)*0.25)+0.5) * 0.5, ((sin(realtime*4.4)*0.25)+0.5)*0.5, 0.08*0.5, sb_alpha.value));
@@ -852,9 +853,11 @@ dojoin:
 			}
 
 			//which connect command are we using?
-			if ((server->special & SS_PROTOCOLMASK) == SS_NETQUAKE)
-				Cbuf_AddText("nqconnect ", RESTRICT_LOCAL);
+#ifdef NQPROT
+			if ((server->special & SS_PROTOCOLMASK) == SS_QEPROT)
+				Cbuf_AddText("connectqe ", RESTRICT_LOCAL);
 			else
+#endif
 				Cbuf_AddText("connect ", RESTRICT_LOCAL);
 
 			//output the server's address
@@ -1108,7 +1111,6 @@ static void CalcFilters(emenu_t *menu)
 	else
 	{
 		if (info->filter[SLFILTER_HIDENETQUAKE]) Master_SetMaskInteger(false, SLKEY_BASEGAME, SS_NETQUAKE, SLIST_TEST_NOTEQUAL);
-		if (info->filter[SLFILTER_HIDENETQUAKE]) Master_SetMaskInteger(false, SLKEY_BASEGAME, SS_DARKPLACES, SLIST_TEST_NOTEQUAL);
 		if (info->filter[SLFILTER_HIDEQUAKEWORLD]) Master_SetMaskInteger(false, SLKEY_BASEGAME, SS_QUAKEWORLD, SLIST_TEST_NOTEQUAL);
 	}
 	if (info->filter[SLFILTER_HIDEPROXIES]) Master_SetMaskInteger(false, SLKEY_FLAGS, SS_PROXY, SLIST_TEST_NOTCONTAIN);
@@ -1351,9 +1353,11 @@ static void M_QuickConnect_PreDraw(emenu_t *menu)
 		{
 			Con_Printf("Quick connect found %s (gamedir %s, players %i/%i/%i, ping %ims)\n", best->name, best->gamedir, best->numhumans, best->players, best->maxplayers, best->ping);
 
-			if ((best->special & SS_PROTOCOLMASK) == SS_NETQUAKE)
-				Cbuf_AddText(va("nqconnect %s\n", Master_ServerToString(adr, sizeof(adr), best)), RESTRICT_LOCAL);
+#ifdef NQPROT
+			if ((best->special & SS_PROTOCOLMASK) == SS_QEPROT)
+				Cbuf_AddText(va("connectqe %s\n", Master_ServerToString(adr, sizeof(adr), best)), RESTRICT_LOCAL);
 			else
+#endif
 				Cbuf_AddText(va("join %s\n", Master_ServerToString(adr, sizeof(adr), best)), RESTRICT_LOCAL);
 
 			M_ToggleMenu_f();
diff --git a/engine/client/net_master.c b/engine/client/net_master.c
index 5ac19c126..25b1e3d8e 100644
--- a/engine/client/net_master.c
+++ b/engine/client/net_master.c
@@ -1039,8 +1039,6 @@ char	*Master_ServerToString (char *s, int len, serverinfo_t *a)
 static int Master_BaseGame(serverinfo_t *a)
 {
 	int prot = a->special&SS_PROTOCOLMASK;
-	if (prot == SS_DARKPLACES && (a->special&SS_FTESERVER))
-		prot = SS_QUAKEWORLD;
 	return prot;
 }
 
@@ -2256,32 +2254,32 @@ void Master_CheckPollSockets(void)
 				continue;
 			}
 #endif
-#ifdef Q3CLIENT
+//#ifdef Q3CLIENT
 			if (!strcmp(s, "statusResponse"))
 			{
 				CL_ReadServerInfo(MSG_ReadString(), MP_QUAKE3, false);
 				continue;
 			}
-#endif
+//#endif
 
 #ifdef HAVE_IPV6
 			if (!strncmp(s, "getserversResponse6", 19) && (s[19] == '\\' || s[19] == '/'))	//parse a bit more...
 			{
 				net_message.currentbit = (c+19-1)<<3;
-				CL_MasterListParse(NA_IPV6, SS_DARKPLACES, true);
+				CL_MasterListParse(NA_IPV6, SS_GETINFO, true);
 				continue;
 			}
 #endif
 			if (!strncmp(s, "getserversExtResponse", 21) && (s[21] == '\\' || s[21] == '/'))	//parse a bit more...
 			{
 				net_message.currentbit = (c+21-1)<<3;
-				CL_MasterListParse(NA_IP, SS_DARKPLACES, true);
+				CL_MasterListParse(NA_IP, SS_GETINFO, true);
 				continue;
 			}
 			if (!strncmp(s, "getserversResponse", 18) && (s[18] == '\\' || s[18] == '/'))	//parse a bit more...
 			{
 				net_message.currentbit = (c+18-1)<<3;
-				CL_MasterListParse(NA_IP, SS_DARKPLACES, true);
+				CL_MasterListParse(NA_IP, SS_GETINFO, true);
 				continue;
 			}
 			if (!strcmp(s, "infoResponse"))	//parse a bit more...
@@ -2495,7 +2493,7 @@ void SListOptionChanged(serverinfo_t *newserver)
 #if defined(NQPROT)
 		selectedserver.lastplayer = 0;
 		*selectedserver.lastrule = 0;
-		if ((newserver->special&SS_PROTOCOLMASK) == SS_NETQUAKE)
+		if ((newserver->special&(SS_PROTOCOLMASK|SS_GETINFO)) == SS_NETQUAKE)
 		{	//start spamming the server to get all of its details. silly protocols.
 			SZ_Clear(&net_message);
 			net_message.packing = SZ_RAWBYTES;
@@ -2646,7 +2644,7 @@ static void MasterInfo_ProcessHTTP(struct dl_download *dl)
 			if (protocoltype == MP_QUAKEWORLD)
 				info->special |= SS_QUAKEWORLD;
 			else if (protocoltype == MP_DPMASTER)
-				info->special |= SS_DARKPLACES;
+				info->special |= SS_GETINFO;
 #if defined(Q2CLIENT) || defined(Q2SERVER)
 			else if (protocoltype == MP_QUAKE2)
 				info->special |= SS_QUAKE2;
@@ -2925,16 +2923,17 @@ void Master_QueryServer(serverinfo_t *server)
 		return;	//don't even try. we have no direct route.
 	server->refreshtime = Sys_DoubleTime();
 
-	switch(server->special & SS_PROTOCOLMASK)
+	if (server->special & SS_GETINFO)
 	{
-	case SS_QUAKE3:
-		Q_snprintfz(data, sizeof(data), "%c%c%c%cgetstatus", 255, 255, 255, 255);
-		break;
-	case SS_DARKPLACES:
 		if (server->moreinfo)
 			Q_snprintfz(data, sizeof(data), "%c%c%c%cgetstatus", 255, 255, 255, 255);
 		else
 			Q_snprintfz(data, sizeof(data), "%c%c%c%cgetinfo", 255, 255, 255, 255);
+	}
+	else switch(server->special & SS_PROTOCOLMASK)
+	{
+	case SS_QUAKE3:
+		Q_snprintfz(data, sizeof(data), "%c%c%c%cgetstatus", 255, 255, 255, 255);
 		break;
 #ifdef NQPROT
 	case SS_NETQUAKE:
@@ -3028,14 +3027,13 @@ qboolean CL_QueryServers(void)
 		while(server)
 		{
 			qboolean enabled;
-			switch(server->special & SS_PROTOCOLMASK)
+			switch(Master_BaseGame(server))
 			{
 			case SS_UNKNOWN: enabled = true; break;
 			case SS_QUAKE3: enabled = sb_enablequake3; break;
 			case SS_QUAKE2: enabled = sb_enablequake2; break;
 			case SS_NETQUAKE: enabled = sb_enablenetquake; break;
 			case SS_QUAKEWORLD: enabled = sb_enablequakeworld; break;
-			case SS_DARKPLACES: enabled = sb_enabledarkplaces; break;
 			default: enabled = false; break;
 			}
 			if (enabled)
@@ -3057,14 +3055,13 @@ qboolean CL_QueryServers(void)
 			while (server)
 			{
 				qboolean enabled;
-				switch(server->special & SS_PROTOCOLMASK)
+				switch(Master_BaseGame(server))
 				{
 				case SS_UNKNOWN: enabled = true; break;
 				case SS_QUAKE3: enabled = sb_enablequake3; break;
 				case SS_QUAKE2: enabled = sb_enablequake2; break;
 				case SS_NETQUAKE: enabled = sb_enablenetquake; break;
 				case SS_QUAKEWORLD: enabled = sb_enablequakeworld; break;
-				case SS_DARKPLACES: enabled = sb_enabledarkplaces; break;
 				default: enabled = false; break;
 				}
 				if (enabled)
@@ -3241,38 +3238,54 @@ static int CL_ReadServerInfo(char *msg, enum masterprotocol_e prototype, qboolea
 	if (!*name)
 		name = Info_ValueForKey(msg, "sv_hostname");
 	Q_strncpyz(info->name, name, sizeof(info->name));
-	info->special = info->special & (SS_FAVORITE | SS_KEEPINFO | SS_LOCAL);	//favorite+local is never cleared
+	info->special = info->special & (SS_FAVORITE | SS_KEEPINFO | SS_LOCAL | SS_GETINFO);	//favorite+local is never cleared
 	if (!strcmp(DISTRIBUTION, Info_ValueForKey(msg, "*distrib")))	//outdated
 		info->special |= SS_FTESERVER;
 	else if (!strncmp(DISTRIBUTION, Info_ValueForKey(msg, "*version"), strlen(DISTRIBUTION)))
 		info->special |= SS_FTESERVER;
 
-	info->protocol = atoi(Info_ValueForKey(msg, "protocol"));
-	info->special &= ~SS_PROTOCOLMASK;
+	info->protocol = strtoul(Info_ValueForKey(msg, "protocol"), &token, 0);
 	if (info->protocol)
 	{
 		switch(info->protocol)
 		{
-		case PROTOCOL_VERSION_QW:	info->special = SS_QUAKEWORLD;	break;
+		case PROTOCOL_VERSION_QW:	info->special |= SS_QUAKEWORLD;	break;
 #ifdef NQPROT
-		case PROTOCOL_VERSION_NQ:	info->special = SS_NETQUAKE;	break;
-		case PROTOCOL_VERSION_H2:	info->special = SS_NETQUAKE;	break;	//erk
-		case PROTOCOL_VERSION_NEHD:	info->special = SS_NETQUAKE;	break;
-		case PROTOCOL_VERSION_FITZ:	info->special = SS_NETQUAKE;	break;
-		case PROTOCOL_VERSION_RMQ:	info->special = SS_NETQUAKE;	break;
-		case PROTOCOL_VERSION_DP5:	info->special = SS_DARKPLACES;	break;	//dp actually says 3... but hey, that's dp being WEIRD.
-		case PROTOCOL_VERSION_DP6:	info->special = SS_DARKPLACES;	break;
-		case PROTOCOL_VERSION_DP7:	info->special = SS_DARKPLACES;	break;
+		case PROTOCOL_VERSION_NQ:	info->special |= SS_NETQUAKE;	break;
+		case PROTOCOL_VERSION_H2:	info->special |= SS_NETQUAKE;	break;	//erk
+		case PROTOCOL_VERSION_NEHD:	info->special |= SS_NETQUAKE;	break;
+		case PROTOCOL_VERSION_FITZ:	info->special |= SS_NETQUAKE;	break;
+		case PROTOCOL_VERSION_RMQ:	info->special |= SS_NETQUAKE;	break;
+		case PROTOCOL_VERSION_DP5:	info->special |= SS_NETQUAKE;	break;	//dp actually says 3... but hey, that's dp being WEIRD.
+		case PROTOCOL_VERSION_DP6:	info->special |= SS_NETQUAKE;	break;
+		case PROTOCOL_VERSION_DP7:	info->special |= SS_NETQUAKE;	break;
+		case NQ_NETCHAN_VERSION_QEX:info->special |= SS_QEPROT;		break;
+		case NQ_NETCHAN_VERSION:
 #endif
 		default:
-			if (PROTOCOL_VERSION_Q2 >= info->protocol && info->protocol >= PROTOCOL_VERSION_Q2_MIN)
-				info->special |= SS_QUAKE2;	//q2 has a range!
-			else if (info->protocol > 60)
-				info->special |= SS_QUAKE3;
-			else if (!strcmp(Info_ValueForKey(msg, "gamename"), "DarkPlaces-Quake"))
-				info->special |= SS_DARKPLACES;
-			else
-				info->special |= SS_DARKPLACES|SS_FTESERVER;	//so its listed under qw-servers (but queried using dpmaster getinfo stuff).
+			while (*token)
+			{
+				if (*token == 'w')
+					info->special |= SS_QUAKEWORLD;
+				else if (*token == 'n' || *token == 'd')
+					info->special |= SS_NETQUAKE;
+				else if (*token == 'x')
+					info->special |= SS_QEPROT;
+				else
+					continue;
+				break;
+			}
+			if ((info->special&SS_PROTOCOLMASK) == SS_UNKNOWN)
+			{	//guesses...
+				if (PROTOCOL_VERSION_Q2 >= info->protocol && info->protocol >= PROTOCOL_VERSION_Q2_MIN)
+					info->special |= SS_QUAKE2;	//q2 has a range!
+				else if (info->protocol > 60)
+					info->special |= SS_QUAKE3;
+				else if (!strcmp(Info_ValueForKey(msg, "gamename"), "DarkPlaces-Quake") || *Info_ValueForKey(msg, "nqprotocol"))
+					info->special |= SS_NETQUAKE;
+				else
+					info->special |= SS_QUAKEWORLD;
+			}
 			break;
 		}
 	}
@@ -3342,7 +3355,7 @@ static int CL_ReadServerInfo(char *msg, enum masterprotocol_e prototype, qboolea
 	msg = msg+strlen(msg)+1;
 
 	//clear player info. unless its an NQ server, which have some really annoying protocol to find out the players.
-	if ((info->special & SS_PROTOCOLMASK) == SS_NETQUAKE)
+	if ((info->special & (SS_PROTOCOLMASK|SS_GETINFO)) == SS_NETQUAKE)
 	{
 		if (!info->moreinfo && ((slist_cacheinfo.value == 2 || NET_CompareAdr(&info->adr, &selectedserver.adr)) || (info->special & SS_KEEPINFO)))
 			info->moreinfo = Z_Malloc(sizeof(serverdetailedinfo_t));
@@ -3516,7 +3529,7 @@ static int CL_ReadServerInfo(char *msg, enum masterprotocol_e prototype, qboolea
 				msg++;
 			}
 		}
-		if ((info->special & SS_PROTOCOLMASK) == SS_DARKPLACES && !info->numbots)
+		if (!info->numbots)
 		{
 			info->numbots = atoi(Info_ValueForKey(details.info, "bots"));
 			if (info->numbots > info->players)
@@ -3624,7 +3637,7 @@ void CL_MasterListParse(netadrtype_t adrtype, int type, qboolean slashpad)
 		}
 		if ((old = Master_InfoForServer(&info->adr, NULL)))	//remove if the server already exists.
 		{
-			if ((old->special & (SS_PROTOCOLMASK)) != (type & (SS_PROTOCOLMASK)))
+			if ((old->special & (SS_PROTOCOLMASK|SS_GETINFO)) != (type & (SS_PROTOCOLMASK|SS_GETINFO)))
 				old->special = type | (old->special & (SS_FAVORITE|SS_LOCAL));
 			old->sends = 1;	//reset.
 			old->status |= SRVSTATUS_GLOBAL;
diff --git a/engine/common/common.c b/engine/common/common.c
index 58b4ed858..59eeb13bd 100644
--- a/engine/common/common.c
+++ b/engine/common/common.c
@@ -83,7 +83,7 @@ static cvar_t	pr_engine		= CVARFD("pr_engine",DISTRIBUTION" "STRINGIFY(SVNREVISI
 #endif
 cvar_t	fs_gamename				= CVARAD("com_fullgamename", NULL, "fs_gamename", "The filesystem is trying to run this game");
 cvar_t	com_protocolname		= CVARAD("com_protocolname", NULL, "com_gamename", "The protocol game name used for dpmaster queries. For compatibility with DP, you can set this to 'DarkPlaces-Quake' in order to be listed in DP's master server, and to list DP servers.");
-cvar_t	com_protocolversion		= CVARAD("com_protocolversion", "3", NULL, "The protocol version used for dpmaster queries.");	//3 by default, for compat with DP/NQ, even if our QW protocol uses different versions entirely. really it only matters for master servers.
+cvar_t	com_protocolversion		= CVARAD("com_protocolversion", "3", NULL, "The protocol version used for dpmaster queries.");	//3 as strong default for compat with DP which uses its netchan rather than protocol version here, even if our QW protocol uses different versions entirely. really it only matters for master servers.
 cvar_t	com_parseutf8			= CVARD("com_parseutf8", "1", "Interpret console messages/playernames/etc as UTF-8. Requires special fonts. -1=iso 8859-1. 0=quakeascii(chat uses high chars). 1=utf8, revert to ascii on decode errors. 2=utf8 ignoring errors");	//1 parse. 2 parse, but stop parsing that string if a char was malformed.
 cvar_t	com_highlightcolor		= CVARD("com_highlightcolor", STRINGIFY(COLOR_RED), "ANSI colour to be used for highlighted text, used when com_parseutf8 is active.");
 cvar_t	com_gamedirnativecode	= CVARFD("com_gamedirnativecode", "0", CVAR_NOTFROMSERVER, FULLENGINENAME" blocks all downloads of files with a .dll or .so extension, however other engines (eg: ezquake and fodquake) do not - this omission can be used to trigger delayed eremote exploits in any engine (including "DISTRIBUTION") which is later run from the same gamedir.\nQuake2, Quake3(when debugging), and KTX typically run native gamecode from within gamedirs, so if you wish to run any of these games you will need to ensure this cvar is changed to 1, as well as ensure that you don't run unsafe clients.");
diff --git a/engine/common/net.h b/engine/common/net.h
index f1c20fb55..371471b26 100644
--- a/engine/common/net.h
+++ b/engine/common/net.h
@@ -297,7 +297,7 @@ void Net_Master_Init(void);
 
 void Netchan_Init (void);
 int Netchan_Transmit (netchan_t *chan, int length, qbyte *data, int rate);
-void Netchan_OutOfBand (netsrc_t sock, netadr_t *adr, int length, qbyte *data);
+void Netchan_OutOfBand (netsrc_t sock, netadr_t *adr, int length, const qbyte *data);
 void VARGS Netchan_OutOfBandPrint (netsrc_t sock, netadr_t *adr, char *format, ...) LIKEPRINTF(3);
 void VARGS Netchan_OutOfBandTPrintf (netsrc_t sock, netadr_t *adr, int language, translation_t text, ...);
 qboolean Netchan_Process (netchan_t *chan);
diff --git a/engine/common/net_chan.c b/engine/common/net_chan.c
index 6d9a6911c..ee082f841 100644
--- a/engine/common/net_chan.c
+++ b/engine/common/net_chan.c
@@ -300,7 +300,7 @@ Netchan_OutOfBand
 Sends an out-of-band datagram
 ================
 */
-void Netchan_OutOfBand (netsrc_t sock, netadr_t *adr, int length, qbyte *data)
+void Netchan_OutOfBand (netsrc_t sock, netadr_t *adr, int length, const qbyte *data)
 {
 	sizebuf_t	send;
 	qbyte		send_buf[MAX_QWMSGLEN + PACKET_HEADER];
diff --git a/engine/common/net_ice.c b/engine/common/net_ice.c
index 7b2ed79ef..d14e8f72c 100644
--- a/engine/common/net_ice.c
+++ b/engine/common/net_ice.c
@@ -4349,7 +4349,7 @@ static void FTENET_ICE_Heartbeat(ftenet_ice_connection_t *b)
 		}
 
 		*info = 0;
-		Info_SetValueForKey(info, "protocol", com_protocolversion.string, sizeof(info));
+		Info_SetValueForKey(info, "protocol", SV_GetProtocolVersionString(), sizeof(info));
 		Info_SetValueForKey(info, "maxclients", maxclients.string, sizeof(info));
 		Info_SetValueForKey(info, "clients", va("%i", numclients), sizeof(info));
 		Info_SetValueForKey(info, "hostname", hostname.string, sizeof(info));
diff --git a/engine/common/net_wins.c b/engine/common/net_wins.c
index 4e35d9384..4c1ffaaaa 100644
--- a/engine/common/net_wins.c
+++ b/engine/common/net_wins.c
@@ -7185,7 +7185,7 @@ static void FTENET_WebRTC_Heartbeat(ftenet_websocket_connection_t *b)
 		info[1] =
 		info[2] = 0xff;	//to the broker rather than any actual client
 		info[3] = 0;
-		Info_SetValueForKey(info+3, "protocol", com_protocolversion.string, sizeof(info)-3);
+		Info_SetValueForKey(info+3, "protocol", SV_GetProtocolVersionString(), sizeof(info)-3);
 		Info_SetValueForKey(info+3, "maxclients", maxclients.string, sizeof(info)-3);
 		Info_SetValueForKey(info+3, "clients", va("%i", numclients), sizeof(info)-3);
 		Info_SetValueForKey(info+3, "hostname", hostname.string, sizeof(info)-3);
diff --git a/engine/server/server.h b/engine/server/server.h
index a381b1082..96551ef00 100644
--- a/engine/server/server.h
+++ b/engine/server/server.h
@@ -1157,6 +1157,7 @@ int SV_CalcPing (client_t *cl, qboolean forcecalc);
 void SV_FullClientUpdate (client_t *client, client_t *to);
 char *SV_PlayerPublicAddress(client_t *cl);
 
+const char *SV_GetProtocolVersionString(void);	//decorate the protocol version field of server queries with extra features...
 qboolean SVC_GetChallenge (qboolean respond_dp);
 int SV_NewChallenge (void);
 void SVC_DirectConnect(int expectedreliablesequence);
diff --git a/engine/server/sv_main.c b/engine/server/sv_main.c
index d92672e32..17b42a660 100644
--- a/engine/server/sv_main.c
+++ b/engine/server/sv_main.c
@@ -1227,6 +1227,36 @@ static void SVC_Status (void)
 }
 
 #if 1//def NQPROT
+const char *SV_GetProtocolVersionString(void)
+{
+	char *ret = va("%i", com_protocolversion.ival);	//for compat with DP, this is basically locked at 3. our pexts allow this to be mostly graceful.
+
+	switch(svs.gametype)
+	{
+	case GT_PROGS:
+	case GT_Q1QVM:
+		if (sv_listen_qw.ival)
+			Q_strncatz(ret, "w", 64);
+#ifdef NQPROT
+		if (progstype == PROG_H2)
+			break;	//don't advertise nq protocols when they're blocked.
+		if (sv_listen_nq.ival)
+		{
+			Q_strncatz(ret, "n", 64);
+#ifdef HAVE_DTLS
+			if (*dtls_psk_user.string)
+				Q_strncatz(ret, "x", 64);
+#endif
+		}
+		if (sv_listen_dp.ival)
+			Q_strncatz(ret, "d", 64);
+#endif
+		break;
+	default:
+		break;	//these do their own thing, with their own protocols. don't be weird.
+	}
+	return ret;
+}
 static void SVC_GetInfo (const char *challenge, int fullstatus)
 {
 	//dpmaster support
@@ -1289,7 +1319,7 @@ static void SVC_GetInfo (const char *challenge, int fullstatus)
 		*resp = 0;
 		Info_SetValueForKey(resp, "challenge", challenge, sizeof(response) - (resp-response));	//the challenge can be important for the master protocol to prevent poisoning
 		Info_SetValueForKey(resp, "gamename", protocolname, sizeof(response) - (resp-response));//distinguishes it from other types of games
-		Info_SetValueForKey(resp, "protocol", com_protocolversion.string, sizeof(response) - (resp-response));	//should be an int.
+		Info_SetValueForKey(resp, "protocol", SV_GetProtocolVersionString(), sizeof(response) - (resp-response));
 		Info_SetValueForKey(resp, "modname", FS_GetGamedir(true), sizeof(response) - (resp-response));
 		Info_SetValueForKey(resp, "clients", va("%d", numclients), sizeof(response) - (resp-response));
 		Info_SetValueForKey(resp, "sv_maxclients", maxclients.string, sizeof(response) - (resp-response));
@@ -1549,6 +1579,33 @@ qboolean SVC_GetChallenge (qboolean respond_dp)
 	const qboolean respond_qwoverq3 = false;
 #endif
 
+	//ioq3clchallenge = atoi(Cmd_Argv(1));
+	const char *protocols = Cmd_Argv(2);
+	if (*protocols)
+	{
+		const char *pname;
+		char tprot[64], oprot[64];
+		while ((protocols=COM_ParseOut(protocols, tprot,sizeof(tprot))))
+		{
+			pname = com_protocolname.string;
+			while ((pname=COM_ParseOut(pname, oprot,sizeof(oprot))))
+			{
+				if (!strcmp(tprot, oprot))
+					break;
+			}
+			if (pname)
+				break;
+		}
+
+		if (!protocols)
+		{
+			COM_ParseOut(com_protocolname.string, oprot,sizeof(oprot));
+			pname = va("print\nGame mismatch: This is a %s server\n", oprot);
+			Netchan_OutOfBand(NS_SERVER, &net_from, strlen(pname), pname);
+			return false;
+		}
+	}
+
 	if (sv_listen_qw.value && !sv_listen_dp.value)
 	{
 		respond_std = true;
@@ -3318,7 +3375,7 @@ void SVC_DirectConnect(int expectedreliablesequence)
 		}
 		Q_strncpyz (info.userinfo, net_message.data + 11, sizeof(info.userinfo)-1);
 
-		if (strcmp(Info_ValueForKey(info.userinfo, "protocol"), "darkplaces 3"))
+		if (strcmp(Info_ValueForKey(info.userinfo, "protocol"), "darkplaces "STRINGIFY(NQ_NETCHAN_VERSION)))
 		{
 			SV_RejectMessage (SCP_BAD, "Server is %s.\n", version_string());
 			Con_TPrintf ("* rejected connect from incompatible client\n");
@@ -4165,8 +4222,9 @@ qboolean SV_ConnectionlessPacket (void)
 	}*/
 	else if (!strcmp(c,"getchallenge"))
 	{
-		//qw+q2 always sends "\xff\xff\xff\xffgetchallenge\n"
-		//dp+q3 always sends "\xff\xff\xff\xffgetchallenge"
+		//qw+q2 sends "\xff\xff\xff\xffgetchallenge\n"
+		//dp+q3 sends "\xff\xff\xff\xffgetchallenge"
+		//ioq3 sends "\xff\xff\xff\xffgetchallenge <clientchallenge> <$com_gamename>"
 		//its a subtle difference, but means we can avoid wasteful spam for real qw clients.
 		SVC_GetChallenge ((net_message.cursize==16)?true:false);
 	}
@@ -4407,13 +4465,15 @@ qboolean SVNQ_ConnectionlessPacket(void)
 
 		if (SV_ChallengeRecent())
 			return true;
-		else if (!strncmp(MSG_ReadString(), "getchallenge", 12) && (sv_listen_qw.ival || sv_listen_dp.ival))
+
+		Cmd_TokenizeString (MSG_ReadString(), false, false);
+		if (!strcmp(Cmd_Argv(0), "getchallenge") && (sv_listen_qw.ival || sv_listen_dp.ival))
 		{
 			/*dual-stack client, supporting either DP or QW protocols*/
 			SVC_GetChallenge (false);
 		}
 		else
-		{
+		{	//legacy pure-nq (though often DP).
 			if (progstype == PROG_H2)
 			{
 				SZ_Clear(&sb);