diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 6df7718efc354c7e03f51c19aa3ab86a9dde1ef0..438cdcd5454a93fefce6822bf8a344cdb12c88c2 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -236,6 +236,9 @@ static consvar_t cv_dummyconsvar = {"dummyconsvar", "Off", CV_CALL|CV_NOSHOWHELP
 consvar_t cv_restrictskinchange = {"restrictskinchange", "No", CV_NETVAR|CV_CHEAT, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_allowteamchange = {"allowteamchange", "Yes", CV_NETVAR, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
+static CV_PossibleValue_t ingamecap_cons_t[] = {{0, "MIN"}, {MAXPLAYERS-1, "MAX"}, {0, NULL}};
+consvar_t cv_ingamecap = {"ingamecap", "0", CV_NETVAR, ingamecap_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 consvar_t cv_startinglives = {"startinglives", "3", CV_NETVAR|CV_CHEAT|CV_NOSHOWHELP, startingliveslimit_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 static CV_PossibleValue_t respawntime_cons_t[] = {{0, "MIN"}, {30, "MAX"}, {0, NULL}};
@@ -642,6 +645,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_allowexitlevel);
 	CV_RegisterVar(&cv_restrictskinchange);
 	CV_RegisterVar(&cv_allowteamchange);
+	CV_RegisterVar(&cv_ingamecap);
 	CV_RegisterVar(&cv_respawntime);
 	CV_RegisterVar(&cv_killingdead);
 
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 2269996fb95da80c01175189c059de2de13a9565..c590eee6500e77e46af73dbd182db02e29f5cb8d 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -93,7 +93,7 @@ extern consvar_t cv_mute;
 extern consvar_t cv_killingdead;
 extern consvar_t cv_pause;
 
-extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_respawntime;
+extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_ingamecap, cv_respawntime;
 
 /*extern consvar_t cv_teleporters, cv_superring, cv_supersneakers, cv_invincibility;
 extern consvar_t cv_jumpshield, cv_watershield, cv_ringshield, cv_forceshield, cv_bombshield;
diff --git a/src/d_player.h b/src/d_player.h
index 5ce9066b20f8aa2048eb8d9e16e6e2781f5336cf..b430f20a4b074766d7aaa9c8ecaa1123b3d13d1c 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -353,6 +353,7 @@ typedef enum
 	k_itemblinkmode,	// Type of flashing: 0 = white (normal), 1 = red (mashing), 2 = rainbow (enhanced items)
 	k_getsparks,		// Disable drift sparks at low speed, JUST enough to give acceleration the actual headstart above speed
 	k_jawztargetdelay,	// Delay for Jawz target switching, to make it less twitchy
+	k_spectatewait,		// How long have you been waiting as a spectator
 
 	NUMKARTSTUFF
 } kartstufftype_t;
diff --git a/src/dehacked.c b/src/dehacked.c
index b03530a45f194ffed49146ecb29861906b854d66..11aed24d043415d019bb048bd0be0e2d29c11a3a 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -8334,7 +8334,8 @@ static const char *const KARTSTUFF_LIST[] = {
 	"ITEMBLINK",
 	"ITEMBLINKMODE",
 	"GETSPARKS",
-	"JAWZTARGETDELAY"
+	"JAWZTARGETDELAY",
+	"SPECTATEWAIT"
 };
 
 static const char *const HUDITEMS_LIST[] = {
diff --git a/src/k_kart.c b/src/k_kart.c
index e59fbfc931acd0c39fa931174dac11c5a5781d45..c72c65795759ded1806ca39a04380da39811a0f8 100644
--- a/src/k_kart.c
+++ b/src/k_kart.c
@@ -5753,9 +5753,22 @@ void K_CheckBumpers(void)
 void K_CheckSpectateStatus(void)
 {
 	UINT8 respawnlist[MAXPLAYERS];
-	UINT8 i, numingame = 0, numjoiners = 0;
+	UINT8 i, j, numingame = 0, numjoiners = 0;
 
-	if (!cv_allowteamchange.value) return;
+	// Maintain spectate wait timer
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i])
+			continue;
+		if (players[i].spectator && (players[i].pflags & PF_WANTSTOJOIN))
+			players[i].kartstuff[k_spectatewait]++;
+		else
+			players[i].kartstuff[k_spectatewait] = 0;
+	}
+
+	// No one's allowed to join
+	if (!cv_allowteamchange.value)
+		return;
 
 	// Get the number of players in game, and the players to be de-spectated.
 	for (i = 0; i < MAXPLAYERS; i++)
@@ -5766,16 +5779,18 @@ void K_CheckSpectateStatus(void)
 		if (!players[i].spectator)
 		{
 			numingame++;
+			if (cv_ingamecap.value && numingame >= cv_ingamecap.value) // DON'T allow if you've hit the in-game player cap
+				return;
 			if (gamestate != GS_LEVEL) // Allow if you're not in a level
-                continue;
+				continue;
 			if (players[i].exiting) // DON'T allow if anyone's exiting
 				return;
 			if (numingame < 2 || leveltime < starttime || mapreset) // Allow if the match hasn't started yet
-                continue;
+				continue;
 			if (leveltime > (starttime + 20*TICRATE)) // DON'T allow if the match is 20 seconds in
-                return;
-            if (G_RaceGametype() && players[i].laps) // DON'T allow if the race is at 2 laps
-                return;
+				return;
+			if (G_RaceGametype() && players[i].laps) // DON'T allow if the race is at 2 laps
+				return;
 			continue;
 		}
 		else if (!(players[i].pflags & PF_WANTSTOJOIN))
@@ -5788,16 +5803,45 @@ void K_CheckSpectateStatus(void)
 	if (!numjoiners)
 		return;
 
-	// Reset the match if you're in an empty server
-	if (!mapreset && gamestate == GS_LEVEL && leveltime >= starttime && (numingame < 2 && numingame+numjoiners >= 2))
+	// Organize by spectate wait timer
+	if (cv_ingamecap.value)
 	{
-		S_ChangeMusicInternal("chalng", false); // COME ON
-		mapreset = 3*TICRATE; // Even though only the server uses this for game logic, set for everyone for HUD in the future
+		UINT8 oldrespawnlist[MAXPLAYERS];
+		memcpy(oldrespawnlist, respawnlist, numjoiners);
+		for (i = 0; i < numjoiners; i++)
+		{
+			UINT8 pos = 0;
+			INT32 ispecwait = players[oldrespawnlist[i]].kartstuff[k_spectatewait];
+
+			for (j = 0; j < numjoiners; j++)
+			{
+				INT32 jspecwait = players[oldrespawnlist[j]].kartstuff[k_spectatewait];
+				if (j == i)
+					continue;
+				if (jspecwait > ispecwait)
+					pos++;
+				else if (jspecwait == ispecwait && j < i)
+					pos++;
+			}
+
+			respawnlist[pos] = oldrespawnlist[i];
+		}
 	}
 
 	// Finally, we can de-spectate everyone!
 	for (i = 0; i < numjoiners; i++)
+	{
+		if (cv_ingamecap.value && numingame+i >= cv_ingamecap.value) // Hit the in-game player cap while adding people?
+			break;
 		P_SpectatorJoinGame(&players[respawnlist[i]]);
+	}
+
+	// Reset the match if you're in an empty server
+	if (!mapreset && gamestate == GS_LEVEL && leveltime >= starttime && (numingame < 2 && numingame+i >= 2)) // use previous i value
+	{
+		S_ChangeMusicInternal("chalng", false); // COME ON
+		mapreset = 3*TICRATE; // Even though only the server uses this for game logic, set for everyone for HUD
+	}
 }
 
 //}
diff --git a/src/p_user.c b/src/p_user.c
index ce411d2d13fa5246ff6ace225406ccd6f6247ee9..3ebe343c347a222d73e70c0bb8084d19a720e945 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -8695,6 +8695,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 		}
 		player->spectator = false;
 		player->pflags &= ~PF_WANTSTOJOIN;
+		player->kartstuff[k_spectatewait] = 0;
 		player->ctfteam = changeto;
 		player->playerstate = PST_REBORN;
 
@@ -8719,6 +8720,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 		}
 		player->spectator = false;
 		player->pflags &= ~PF_WANTSTOJOIN;
+		player->kartstuff[k_spectatewait] = 0;
 		player->playerstate = PST_REBORN;
 
 		//Reset away view
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 8ebd21321a70f2a5feb41043227a9b295e2da225..36a658aecd3441ed1f5f76462222bc77b3619be3 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -1952,31 +1952,38 @@ static void ST_overlayDrawer(void)
 #endif
 		)
 		{
+			const char *itemtxt = M_GetText("Item - Join Game");
+
+			if (stplyr->powers[pw_flashing])
+				itemtxt = M_GetText("Item - . . .");
+			else if (stplyr->pflags & PF_WANTSTOJOIN)
+				itemtxt = M_GetText("Item - Cancel Join");
+			else if (G_GametypeHasTeams())
+				itemtxt = M_GetText("Item - Join Team");
+
+			if (cv_ingamecap.value)
+			{
+				UINT8 numingame = 0;
+				UINT8 i;
+
+				for (i = 0; i < MAXPLAYERS; i++)
+					if (playeringame[i] && !players[i].spectator)
+						numingame++;
+
+				itemtxt = va("%s (%s: %d)", itemtxt, M_GetText("Slots left"), max(0, cv_ingamecap.value - numingame));
+			}
+
 			// SRB2kart: changed positions & text
 			if (splitscreen)
 			{
 				INT32 splitflags = K_calcSplitFlags(0);
 				V_DrawThinString(2, (BASEVIDHEIGHT/2)-20, V_YELLOWMAP|V_HUDTRANSHALF|splitflags, M_GetText("- SPECTATING -"));
-				if (stplyr->powers[pw_flashing])
-					V_DrawThinString(2, (BASEVIDHEIGHT/2)-10, V_HUDTRANSHALF|splitflags, M_GetText("Item - . . ."));
-				else if (stplyr->pflags & PF_WANTSTOJOIN)
-					V_DrawThinString(2, (BASEVIDHEIGHT/2)-10, V_HUDTRANSHALF|splitflags, M_GetText("Item - Cancel Join"));
-				/*else if (G_GametypeHasTeams())
-					V_DrawThinString(2, (BASEVIDHEIGHT/2)-10, V_HUDTRANSHALF|splitflags, M_GetText("Item - Join Team"));*/
-				else
-					V_DrawThinString(2, (BASEVIDHEIGHT/2)-10, V_HUDTRANSHALF|splitflags, M_GetText("Item - Join Game"));
+				V_DrawThinString(2, (BASEVIDHEIGHT/2)-10, V_HUDTRANSHALF|splitflags, itemtxt);
 			}
 			else
 			{
 				V_DrawString(2, BASEVIDHEIGHT-40, V_HUDTRANSHALF|V_YELLOWMAP, M_GetText("- SPECTATING -"));
-				if (stplyr->powers[pw_flashing])
-					V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF, M_GetText("Item - . . ."));
-				else if (stplyr->pflags & PF_WANTSTOJOIN)
-					V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF, M_GetText("Item - Cancel Join"));
-				/*else if (G_GametypeHasTeams())
-					V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF, M_GetText("Item - Join Team"));*/
-				else
-					V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF, M_GetText("Item - Join Game"));
+				V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF, itemtxt);
 				V_DrawString(2, BASEVIDHEIGHT-20, V_HUDTRANSHALF, M_GetText("Accelerate - Float"));
 				V_DrawString(2, BASEVIDHEIGHT-10, V_HUDTRANSHALF, M_GetText("Brake - Sink"));
 			}