diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index f316cd032713086ebe4f83e9a81445cc43947034..7a8f31d5a622c2bf2993b61e13c0ab68c138a410 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -3,6 +3,7 @@
 	For Sonic Robo Blast 2 Version 2.2
 	Contributors (alphabetical):
 	* Foxboy
+	* FuriousFox
 	* JJames19119
 	* Kalaron
 	* Kristos
@@ -44,27 +45,32 @@ formatinterface = "SRB2MapSetIO";
 //Sky textures for vanilla maps
 defaultskytextures
 {
-	SKY1 = "MAP01,MAP02,MAP03,MAP50,MAPA1,MAPA2,MAPA5,MAPA6,MAPA9,MAPAA,MAPAB,MAPAC,MAPAD,MAPAE,MAPAG,MAPAJ,MAPAK,MAPF0,MAPF1,MAPFA,MAPM0,MAPM8,MAPMA,MAPMB,MAPMC";
-	SKY4 = "MAP04,MAP06,MAP51,MAPF8,MAPM1";
-	SKY6 = "MAP05";
-	SKY7 = "MAP07,MAP08,MAP09,MAP52,MAPM2,MAPM5";
-	SKY10 = "MAP12,MAP53,MAPM3";
-	SKY11 = "MAP10,MAP11,MAP16,MAP55,MAPF2,MAPF5,MAPF6,MAPF9,MAPM7";
-	SKY13 = "MAP13,MAP54,MAPAS";
-	SKY21 = "MAPAF,MAPF7,MAPM4";
-	SKY22 = "MAP22,MAP23,MAP24,MAP25,MAP56,MAPAN,MAPAO,MAPF4,MAPM6";
-	SKY29 = "MAP58,MAPAV";
+	SKY1 = "MAP01,MAP02,MAP03,MAP33,MAP50,MAP60,MAPF0,MAPM0";
+	SKY2 = "MAPM7,MAPMB";
+	SKY4 = "MAP04,MAP06,MAP61,MAPF6,MAPM1";
+	SKY6 = "MAP05,MAP51,MAPMA";
+	SKY7 = "MAPM2,MAPM5";
+	SKY8 = "MAP07,MAP08,MAP09,MAP52,MAP62,MAPF1";
+	SKY10 = "MAP10,MAP12,MAP53,MAP63,MAPM3";
+	SKY11 = "MAP11,MAPF7";
+	SKY13 = "MAP13,MAP64";
+	SKY14 = "MAP14";
+	SKY15 = "MAP15,MAP54";
+	SKY17 = "MAP70";
+	SKY20 = "MAP32,MAP55,MAP65,MAPF2,MAPF5";
+	SKY21 = "MAPM4";
+	SKY22 = "MAP22,MAP23,MAP25,MAP26,MAP27,MAP56,MAP66,MAPF4,MAPM6";
 	SKY30 = "MAP30";
-	SKY35 = "MAP41";
-	SKY40 = "MAP40";
-	SKY55 = "MAPF3,MAPM9";
-	SKY66 = "MAPAT";
-	SKY99 = "MAP57";
-	SKY103 = "MAPA3,MAPA4,MAPAU";
-	SKY107 = "MAPA7,MAPA8";
-	SKY117 = "MAPAH,MAPAI";
-	SKY127 = "MAPAR";
-	SKY132 = "MAPAW";
+	SKY31 = "MAP31";
+	SKY35 = "MAP42";
+	SKY40 = "MAP41,MAP71,MAPM9";
+	SKY55 = "MAPF3,MAPM8";
+	SKY68 = "MAPF8";
+	SKY99 = "MAP57,MAPZ0";
+	SKY159 = "MAP16";
+	SKY172 = "MAP40";
+	SKY300 = "MAP72";
+	SKY301 = "MAP73";
 }
 
 // Default lump name for new map
@@ -90,9 +96,9 @@ skins
 	Sonic;
 	Tails;
 	Knuckles;
-	Metalsonic;
-	Fang;
 	Amy;
+	Fang;
+	Metalsonic;
 }
 
 // Gametypes
@@ -3422,7 +3428,7 @@ thingtypes
 		121
 		{
 			title = "Minus";
-			sprite = "MNUSA1";
+			sprite = "MNUSA0";
 			width = 24;
 			height = 32;
 		}
@@ -3457,6 +3463,13 @@ thingtypes
 			height = 34;
 			flags8text = "[8] Start on fire";
 		}
+		137
+		{
+			title = "Dragonbomber";
+			sprite = "DRABA1";
+			width = 28;
+			height = 48;
+		}
 		105
 		{
 			title = "Jetty-Syn Bomber";
@@ -5726,6 +5739,24 @@ thingtypes
 			width = 24;
 			height = 32;
 		}
+		1505
+		{
+			title = "Green Flame";
+			sprite = "CFLMA0E0";
+			width = 8;
+			height = 32;
+		}
+		1506
+		{
+			arrow = 1;
+			blocking = 2;
+			title = "Blue Gargoyle";
+			sprite = "BGARD1";
+			width = 16;
+			height = 40;
+			flags4text = "[4] Slides when pushed";
+			flags8text = "[8] Not pushable";
+		}
 	}
 
 	dreamhill
diff --git a/src/b_bot.c b/src/b_bot.c
index 651aeb03d6f1911be09114118ff034b54367d8c2..895c8d18d8ef81222cf8bd7821cdd90c552737d8 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -24,12 +24,47 @@ static boolean lastForward = false;
 static boolean lastBlocked = false;
 static boolean blocked = false;
 
+static boolean jump_last = false;
+static boolean spin_last = false;
+static UINT8 anxiety = 0;
+static boolean panic = false;
+static UINT8 flymode = 0;
+static boolean spinmode = false;
+static boolean thinkfly = false;
+
+static inline void B_ResetAI(void)
+{
+	jump_last = false;
+	spin_last = false;
+	anxiety = 0;
+	panic = false;
+	flymode = 0;
+	spinmode = false;
+	thinkfly = false;
+}
+
 static inline void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 {
 	boolean forward=false, backward=false, left=false, right=false, jump=false, spin=false;
-	angle_t angle;
-	INT16 rangle;
-	fixed_t dist;
+
+	player_t *player = sonic->player, *bot = tails->player;
+	ticcmd_t *pcmd = &player->cmd;
+	boolean water = tails->eflags & MFE_UNDERWATER;
+	SINT8 flip = P_MobjFlip(tails);
+	boolean _2d = (tails->flags2 & MF2_TWOD) || twodlevel;
+	fixed_t scale = tails->scale;
+
+	fixed_t dist = P_AproxDistance(sonic->x - tails->x, sonic->y - tails->y);
+	fixed_t zdist = flip * (sonic->z - tails->z);
+	angle_t ang = R_PointToAngle2(tails->x, tails->y, sonic->x, sonic->y);
+	fixed_t pmom = P_AproxDistance(sonic->momx, sonic->momy);
+	fixed_t bmom = P_AproxDistance(tails->momx, tails->momy);
+	fixed_t followmax = 128 * 8 * scale; // Max follow distance before AI begins to enter "panic" state
+	fixed_t followthres = 92 * scale; // Distance that AI will try to reach
+	fixed_t followmin = 32 * scale;
+	fixed_t comfortheight = 96 * scale;
+	fixed_t touchdist = 24 * scale;
+	boolean stalled = (bmom < scale >> 1) && dist > followthres; // Helps to see if the AI is having trouble catching up
 
 	// We can't follow Sonic if he's not around!
 	if (!sonic || sonic->health <= 0)
@@ -58,46 +93,263 @@ static inline void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cm
 		return;
 	}
 
-	// Gather data about the environment
-	dist = P_AproxDistance(tails->x-sonic->x, tails->y-sonic->y);
-	if (tails->player->pflags & PF_STARTDASH)
-		angle = sonic->angle;
+	// Adapted from CobaltBW's tails_AI.wad
+
+	// Check water
+	if (water)
+	{
+		followmin = 0;
+		followthres = 16*scale;
+		followmax >>= 1;
+		thinkfly = false;
+	}
+
+	// Check anxiety
+	if (spinmode)
+	{
+		anxiety = 0;
+		panic = false;
+	}
+	else if (dist > followmax || zdist > comfortheight || stalled)
+	{
+		anxiety = min(anxiety + 2, 70);
+		if (anxiety >= 70)
+			panic = true;
+	}
 	else
-		angle = R_PointToAngle2(tails->x, tails->y, sonic->x, sonic->y);
+	{
+		anxiety = max(anxiety - 1, 0);
+		panic = false;
+	}
 
-	// Decide which direction to turn
-	angle = (tails->angle - angle);
-	if (angle < ANGLE_180) {
-		right = true; // We need to turn right
-		rangle = AngleFixed(angle)>>FRACBITS;
-	} else {
-		left = true; // We need to turn left
-		rangle = 360-(AngleFixed(angle)>>FRACBITS);
+	// Orientation
+	if ((bot->pflags & (PF_SPINNING|PF_STARTDASH)) || flymode == 2)
+	{
+		cmd->angleturn = (sonic->angle - tails->angle) >> FRACBITS;
+	}
+	else
+	{
+		cmd->angleturn = (ang - tails->angle) >> FRACBITS;
+	}
+
+	// ********
+	// FLY MODE
+	// spinmode check
+	if (spinmode || player->exiting)
+		thinkfly = false;
+	else
+	{
+		// Activate co-op flight
+		if (thinkfly && player->pflags & PF_JUMPED)
+		{
+			if (!jump_last)
+			{
+				jump = true;
+				flymode = 1;
+				thinkfly = false;
+				bot->pflags |= PF_CANCARRY;
+			}
+		}
+
+		// Check positioning
+		// Thinker for co-op flight
+		if (!(water || pmom || bmom)
+			&& (dist < touchdist)
+			&& !(pcmd->forwardmove || pcmd->sidemove || player->dashspeed)
+			&& P_IsObjectOnGround(sonic) && P_IsObjectOnGround(tails)
+			&& !(player->pflags & PF_STASIS)
+			&& bot->charability == CA_FLY)
+				thinkfly = true;
+		else
+			thinkfly = false;
+
+		// Ready for takeoff
+		if (flymode == 1)
+		{
+			thinkfly = false;
+			if (zdist < -64*scale || (flip * tails->momz) > scale) // Make sure we're not too high up
+				spin = true;
+			else if (!jump_last)
+				jump = true;
+			
+			// Abort if the player moves away or spins
+			if (dist > followthres || player->dashspeed)
+				flymode = 0;
+			
+			// Set carried state
+			if (player->powers[pw_carry] == CR_PLAYER && sonic->tracer == tails)
+			{
+				flymode = 2;
+			}
+		}
+		// Read player inputs while carrying
+		else if (flymode == 2)
+		{
+			cmd->forwardmove = pcmd->forwardmove;
+			cmd->sidemove = pcmd->sidemove;
+			if (pcmd->buttons & BT_USE)
+			{
+				spin = true;
+				jump = false;
+			}
+			else if (!jump_last)
+				jump = true;
+			// End flymode
+			if (player->powers[pw_carry] != CR_PLAYER)
+			{
+				flymode = 0;
+			}
+		}
+	}
+
+	if (flymode && P_IsObjectOnGround(tails) && !(pcmd->buttons & BT_JUMP))
+		flymode = 0;
+
+	// ********
+	// SPINNING
+	if (panic || flymode || !(player->pflags & PF_SPINNING) || (player->pflags & PF_JUMPED))
+		spinmode = false;
+	else
+	{
+		if (!_2d)
+		{
+			// Spindash
+			if (player->dashspeed)
+			{
+				if (dist < followthres && dist > touchdist) // Do positioning
+				{
+					cmd->angleturn = (ang - tails->angle) >> FRACBITS;
+					cmd->forwardmove = 50;
+					spinmode = true;
+				}
+				else if (dist < touchdist)
+				{
+					if (!bmom && (!(bot->pflags & PF_SPINNING) || (bot->dashspeed && bot->pflags & PF_SPINNING)))
+					{
+						cmd->angleturn = (sonic->angle - tails->angle) >> FRACBITS;
+						spin = true;
+					}
+					spinmode = true;
+				}
+				else
+					spinmode = false;
+			}
+			// Spin
+			else if (player->dashspeed == bot->dashspeed && player->pflags & PF_SPINNING)
+			{
+				if (bot->pflags & PF_SPINNING || !spin_last)
+				{
+					spin = true;
+					cmd->angleturn = (sonic->angle - tails->angle) >> FRACBITS;
+					cmd->forwardmove = MAXPLMOVE;
+					spinmode = true;
+				}
+				else
+					spinmode = false;
+			}
+		}
+		// 2D mode
+		else
+		{
+			if (((player->dashspeed && !bmom) || (player->dashspeed == bot->dashspeed && (player->pflags & PF_SPINNING)))
+				&& ((bot->pflags & PF_SPINNING) || !spin_last))
+			{
+				spin = true;
+				spinmode = true;
+			}
+		}
 	}
 
-	// Decide to move forward if you're finished turning
-	if (abs(rangle) < 10) { // We're facing the right way?
-		left = right = false; // Stop turning
-		forward = true; // and walk forward instead.
+	// ********
+	// FOLLOW
+	if (!(flymode || spinmode))
+	{
+		// Too far
+		if (panic || dist > followthres)
+		{
+			if (!_2d)
+				cmd->forwardmove = MAXPLMOVE;
+			else if (sonic->x > tails->x)
+				cmd->sidemove = MAXPLMOVE;
+			else
+				cmd->sidemove = -MAXPLMOVE;
+		}
+		// Within threshold
+		else if (!panic && dist > followmin && abs(zdist) < 192*scale)
+		{
+			if (!_2d)
+				cmd->forwardmove = FixedHypot(pcmd->forwardmove, pcmd->sidemove);
+			else
+				cmd->sidemove = pcmd->sidemove;
+		}
+		// Below min
+		else if (dist < followmin)
+		{
+			// Copy inputs
+			cmd->angleturn = (sonic->angle - tails->angle) >> FRACBITS;
+			bot->drawangle = ang;
+			cmd->forwardmove = 8 * pcmd->forwardmove / 10;
+			cmd->sidemove = 8 * pcmd->sidemove / 10;
+		}
 	}
-	if (dist < (sonic->radius+tails->radius)*3) // We're close enough?
-		forward = false; // Stop walking.
-
-	// Decide when to jump
-	if (!(tails->player->pflags & (PF_JUMPED|PF_JUMPDOWN))) { // We're not jumping yet...
-		if (forward && lastForward && blocked && lastBlocked) // We've been stopped by a wall or something
-			jump = true; // Try to jump up
-	} else if ((tails->player->pflags & (PF_JUMPDOWN|PF_JUMPED)) == (PF_JUMPDOWN|PF_JUMPED)) { // When we're already jumping...
-		if (lastForward && blocked) // We're still stuck on something?
+	
+	// ********
+	// JUMP
+	if (!(flymode || spinmode))
+	{
+		// Flying catch-up
+		if (bot->pflags & PF_THOKKED)
+		{
+			cmd->forwardmove = min(MAXPLMOVE, (dist/scale)>>3);
+			if (zdist < -64*scale)
+				spin = true;
+			else if (zdist > 0 && !jump_last)
+				jump = true;
+		}
+
+		// Just landed
+		if (tails->eflags & MFE_JUSTHITFLOOR)
+			jump = false;
+		// Start jump
+		else if (!jump_last && !(bot->pflags & PF_JUMPED) //&& !(player->pflags & PF_SPINNING)
+			&& ((zdist > 32*scale && player->pflags & PF_JUMPED) // Following
+				|| (zdist > 64*scale && panic) // Vertical catch-up
+				|| (stalled && anxiety > 20 && bot->powers[pw_carry] == CR_NONE)
+				//|| (bmom < scale>>3 && dist > followthres && !(bot->powers[pw_carry])) // Stopped & not in carry state
+				|| (bot->pflags & PF_SPINNING && !(bot->pflags & PF_JUMPED)))) // Spinning
+					jump = true;
+		// Hold jump
+		else if (bot->pflags & PF_JUMPED && jump_last && tails->momz*flip > 0 && (zdist > 0 || panic))
 			jump = true;
-		if (sonic->floorz > tails->floorz) // He's still above us? Jump HIGHER, then!
+		// Start flying
+		else if (bot->pflags & PF_JUMPED && panic && !jump_last && bot->charability == CA_FLY)
 			jump = true;
 	}
 
-	// Decide when to spin
-	if (sonic->player->pflags & PF_STARTDASH
-	&& (tails->player->pflags & PF_STARTDASH || (P_AproxDistance(tails->momx, tails->momy) < 2*FRACUNIT && !forward)))
-		spin = true;
+	// ********
+	// HISTORY
+	jump_last = jump;
+	spin_last = spin;
+
+	// ********
+	// Thinkfly overlay
+	if (thinkfly)
+	{
+		if (!tails->target)
+		{
+			P_SetTarget(&tails->target, P_SpawnMobjFromMobj(tails, 0, 0, 0, MT_OVERLAY));
+			if (tails->target)
+			{
+				P_SetTarget(&tails->target->target, tails);
+				P_SetMobjState(tails->target, S_FLIGHTINDICATOR);
+			}
+		}
+	}
+	else if (tails->target && tails->target->type == MT_OVERLAY && tails->target->state == states+S_FLIGHTINDICATOR)
+	{
+		P_RemoveMobj(tails->target);
+		P_SetTarget(&tails->target, NULL);
+	}
 
 	// Turn the virtual keypresses into ticcmd_t.
 	B_KeysToTiccmd(tails, cmd, forward, backward, left, right, false, false, jump, spin);
@@ -141,7 +393,7 @@ void B_BuildTiccmd(player_t *player, ticcmd_t *cmd)
 void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward, boolean left, boolean right, boolean strafeleft, boolean straferight, boolean jump, boolean spin)
 {
 	// don't try to do stuff if your sonic is in a minecart or something
-	if (players[consoleplayer].powers[pw_carry])
+	if (players[consoleplayer].powers[pw_carry] && players[consoleplayer].powers[pw_carry] != CR_PLAYER)
 		return;
 	// Turn the virtual keypresses into ticcmd_t.
 	if (twodlevel || mo->flags2 & MF2_TWOD) {
@@ -179,6 +431,7 @@ void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward
 				cmd->sidemove += MAXPLMOVE<<FRACBITS>>16;
 		}
 	} else {
+		angle_t angle;
 		if (forward)
 			cmd->forwardmove += MAXPLMOVE<<FRACBITS>>16;
 		if (backward)
@@ -191,6 +444,15 @@ void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward
 			cmd->sidemove -= MAXPLMOVE<<FRACBITS>>16;
 		if (straferight)
 			cmd->sidemove += MAXPLMOVE<<FRACBITS>>16;
+		
+		// cap inputs so the bot can't accelerate faster diagonally
+		angle = R_PointToAngle2(0, 0, cmd->sidemove << FRACBITS, cmd->forwardmove << FRACBITS);
+		{
+			INT32 maxforward = abs(P_ReturnThrustY(NULL, angle, MAXPLMOVE));
+			INT32 maxside = abs(P_ReturnThrustX(NULL, angle, MAXPLMOVE));
+			cmd->forwardmove = max(min(cmd->forwardmove, maxforward), -maxforward);
+			cmd->sidemove = max(min(cmd->sidemove, maxside), -maxside);
+		}
 	}
 	if (jump)
 		cmd->buttons |= BT_JUMP;
@@ -217,7 +479,7 @@ boolean B_CheckRespawn(player_t *player)
 	// If he's doing any of these things, he probably doesn't want to see us.
 	if (sonic->player->pflags & (PF_GLIDING|PF_SLIDING|PF_BOUNCING)
 	|| (sonic->player->panim != PA_IDLE && sonic->player->panim != PA_WALK)
-	|| (sonic->player->powers[pw_carry]))
+	|| (sonic->player->powers[pw_carry] && sonic->player->powers[pw_carry] != CR_PLAYER))
 		return false;
 
 	// Low ceiling, do not want!
@@ -252,6 +514,8 @@ void B_RespawnBot(INT32 playernum)
 	if (!sonic || sonic->health <= 0)
 		return;
 
+	B_ResetAI();
+
 	player->bot = 1;
 	P_SpawnPlayer(playernum);
 	tails = player->mo;
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 92196219d04bc350227974eba9b7f5cd78c67f7a..635bd7ee9bc8f4979da091c11da951d3aecd1b62 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -90,6 +90,7 @@ SINT8 nodetoplayer[MAXNETNODES];
 SINT8 nodetoplayer2[MAXNETNODES]; // say the numplayer for this node if any (splitscreen)
 UINT8 playerpernode[MAXNETNODES]; // used specialy for scplitscreen
 boolean nodeingame[MAXNETNODES]; // set false as nodes leave game
+tic_t servermaxping = 800; // server's max ping. Defaults to 800
 static tic_t nettics[MAXNETNODES]; // what tic the client have received
 static tic_t supposedtics[MAXNETNODES]; // nettics prevision for smaller packet
 static UINT8 nodewaiting[MAXNETNODES];
@@ -610,6 +611,11 @@ static inline void resynch_write_player(resynch_pak *rsp, const size_t i)
 
 	rsp->health = LONG(players[i].mo->health);
 	rsp->angle = (angle_t)LONG(players[i].mo->angle);
+#ifdef ROTSPRITE
+	rsp->rollangle = (angle_t)LONG(players[i].mo->rollangle);
+#else
+	rsp->rollangle = 0;
+#endif
 	rsp->x = LONG(players[i].mo->x);
 	rsp->y = LONG(players[i].mo->y);
 	rsp->z = LONG(players[i].mo->z);
@@ -760,6 +766,9 @@ static void resynch_read_player(resynch_pak *rsp)
 	//At this point, the player should have a body, whether they were respawned or not.
 	P_UnsetThingPosition(players[i].mo);
 	players[i].mo->angle = (angle_t)LONG(rsp->angle);
+#ifdef ROTSPRITE
+	players[i].mo->rollangle = (angle_t)LONG(rsp->rollangle);
+#endif
 	players[i].mo->eflags = (UINT16)SHORT(rsp->eflags);
 	players[i].mo->flags = LONG(rsp->flags);
 	players[i].mo->flags2 = LONG(rsp->flags2);
@@ -1292,10 +1301,23 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 
 	M_Memcpy(netbuffer->u.serverinfo.mapmd5, mapmd5, 16);
 
-	if (strcmp(mapheaderinfo[gamemap-1]->lvlttl, ""))
-		strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, 33);
+	if (*mapheaderinfo[gamemap-1]->lvlttl)
+	{
+		char *read = mapheaderinfo[gamemap-1]->lvlttl, *writ = netbuffer->u.serverinfo.maptitle;
+		while (writ < (netbuffer->u.serverinfo.maptitle+32) && *read != '\0')
+		{
+			if (!(*read & 0x80))
+			{
+				*writ = toupper(*read);
+				writ++;
+			}
+			read++;
+		}
+		*writ = '\0';
+		//strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, 33);
+	}
 	else
-		strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 33);
+		strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 32);
 
 	netbuffer->u.serverinfo.maptitle[32] = '\0';
 
@@ -1349,7 +1371,11 @@ static void SV_SendPlayerInfo(INT32 node)
 
 		netbuffer->u.playerinfo[i].score = LONG(players[i].score);
 		netbuffer->u.playerinfo[i].timeinserver = SHORT((UINT16)(players[i].jointime / TICRATE));
-		netbuffer->u.playerinfo[i].skin = (UINT8)players[i].skin;
+		netbuffer->u.playerinfo[i].skin = (UINT8)(players[i].skin
+#ifdef DEVELOP // it's safe to do this only because PLAYERINFO isn't read by the game itself
+		% 3
+#endif
+		);
 
 		// Extra data
 		netbuffer->u.playerinfo[i].data = 0; //players[i].skincolor;
@@ -1607,7 +1633,7 @@ static void CL_LoadReceivedSavegame(void)
 		{
 			CONS_Printf(": %s", mapheaderinfo[gamemap-1]->lvlttl);
 			if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
-				CONS_Printf(M_GetText(" ZONE"));
+				CONS_Printf(M_GetText(" Zone"));
 			if (actnum > 0)
 				CONS_Printf(" %2d", actnum);
 		}
@@ -4208,10 +4234,12 @@ static void HandlePacketFromPlayer(SINT8 node)
 			//Update client ping table from the server.
 			if (client)
 			{
-				INT32 i;
+				UINT8 i;
 				for (i = 0; i < MAXPLAYERS; i++)
 					if (playeringame[i])
 						playerpingtable[i] = (tic_t)netbuffer->u.pingtable[i];
+
+				servermaxping = (tic_t)netbuffer->u.pingtable[MAXPLAYERS];
 			}
 
 			break;
@@ -4591,7 +4619,7 @@ static void Local_Maketic(INT32 realtics)
 {
 	I_OsPolling(); // I_Getevent
 	D_ProcessEvents(); // menu responder, cons responder,
-	                   // game responder calls HU_Responder, AM_Responder, F_Responder,
+	                   // game responder calls HU_Responder, AM_Responder,
 	                   // and G_MapEventsToControls
 	if (!dedicated) rendergametic = gametic;
 	// translate inputs (keyboard/mouse/joystick) into game controls
@@ -4733,6 +4761,14 @@ void TryRunTics(tic_t realtics)
 	}
 }
 
+/*
+Ping Update except better:
+We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out.
+If they're not lagging, decrement the timer by 1. Of course, reset all of this if they leave.
+*/
+
+static INT32 pingtimeout[MAXPLAYERS];
+
 static inline void PingUpdate(void)
 {
 	INT32 i;
@@ -4753,6 +4789,8 @@ static inline void PingUpdate(void)
 					laggers[i] = true;
 				numlaggers++;
 			}
+			else
+				pingtimeout[i] = 0;
 		}
 
 		//kick lagging players... unless everyone but the server's ping sucks.
@@ -4763,12 +4801,27 @@ static inline void PingUpdate(void)
 			{
 				if (playeringame[i] && laggers[i])
 				{
-					char buf[2];
+					pingtimeout[i]++;
+					if (pingtimeout[i] > cv_pingtimeout.value)
+// ok your net has been bad for too long, you deserve to die.
+					{
+						char buf[2];
 
-					buf[0] = (char)i;
-					buf[1] = KICK_MSG_PING_HIGH;
-					SendNetXCmd(XD_KICK, &buf, 2);
+						pingtimeout[i] = 0;
+
+						buf[0] = (char)i;
+						buf[1] = KICK_MSG_PING_HIGH;
+						SendNetXCmd(XD_KICK, &buf, 2);
+					}
 				}
+				/*
+					you aren't lagging,
+					but you aren't free yet.
+					In case you'll keep spiking,
+					we just make the timer go back down. (Very unstable net must still get kicked).
+				*/
+				else
+					pingtimeout[i] = (pingtimeout[i] == 0 ? 0 : pingtimeout[i]-1);
 			}
 		}
 	}
@@ -4783,10 +4836,13 @@ static inline void PingUpdate(void)
 		realpingtable[i] = 0; //Reset each as we go.
 	}
 
+	// send the server's maxping as last element of our ping table. This is useful to let us know when we're about to get kicked.
+	netbuffer->u.pingtable[MAXPLAYERS] = cv_maxping.value;
+
 	//send out our ping packets
 	for (i = 0; i < MAXNETNODES; i++)
 		if (nodeingame[i])
-			HSendPacket(i, true, 0, sizeof(INT32) * MAXPLAYERS);
+			HSendPacket(i, true, 0, sizeof(INT32) * (MAXPLAYERS+1));
 
 	pingmeasurecount = 1; //Reset count
 }
@@ -4816,7 +4872,7 @@ void NetUpdate(void)
 
 	if (server)
 	{
-		if (netgame && !(gametime % 255))
+		if (netgame && !(gametime % 35)) // update once per second.
 			PingUpdate();
 		// update node latency values so we can take an average later.
 		for (i = 0; i < MAXPLAYERS; i++)
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index d7c210895ab5c2d0a376d384d31d586898925e6e..7313c73554b6c557e40fda53072c1e10d1dcc85b 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -255,6 +255,7 @@ typedef struct
 
 	INT32 health;
 	angle_t angle;
+	angle_t rollangle;
 	fixed_t x;
 	fixed_t y;
 	fixed_t z;
@@ -423,9 +424,9 @@ typedef struct
 		serverrefuse_pak serverrefuse;      //       65025 bytes (somehow I feel like those values are garbage...)
 		askinfo_pak askinfo;                //          61 bytes
 		msaskinfo_pak msaskinfo;            //          22 bytes
-		plrinfo playerinfo[MAXPLAYERS];     //        1152 bytes (I'd say 36~38)
-		plrconfig playerconfig[MAXPLAYERS]; // (up to) 896 bytes (welp they ARE)
-		UINT32 pingtable[MAXPLAYERS];       //         128 bytes
+		plrinfo playerinfo[MAXPLAYERS];     //         576 bytes(?)
+		plrconfig playerconfig[MAXPLAYERS]; // (up to) 528 bytes(?)
+		UINT32 pingtable[MAXPLAYERS+1];     //          68 bytes
 	} u; // This is needed to pack diff packet types data together
 } ATTRPACK doomdata_t;
 
@@ -487,6 +488,7 @@ extern tic_t jointimeout;
 extern UINT16 pingmeasurecount;
 extern UINT32 realpingtable[MAXPLAYERS];
 extern UINT32 playerpingtable[MAXPLAYERS];
+extern tic_t servermaxping;
 
 extern consvar_t cv_joinnextround, cv_allownewplayer, cv_maxplayers, cv_resynchattempts, cv_blamecfail, cv_maxsend, cv_noticedownload, cv_downloadspeed;
 
diff --git a/src/d_main.c b/src/d_main.c
index ebc618be849428912e6d3ed8f297ff3b86a02f2c..5853fccf094c0a0d2fe644aa86753154e80b714b 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -274,7 +274,7 @@ static void D_Display(void)
 			 && wipetypepre != UINT8_MAX)
 			{
 				F_WipeStartScreen();
-				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+				F_WipeColorFill(31);
 				F_WipeEndScreen();
 				F_RunWipe(wipetypepre, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN);
 			}
@@ -291,8 +291,11 @@ static void D_Display(void)
 	switch (gamestate)
 	{
 		case GS_TITLESCREEN:
-			F_TitleScreenDrawer();
-			break;
+			if (!titlemapinaction || !curbghide) {
+				F_TitleScreenDrawer();
+				break;
+			}
+			/* FALLTHRU */
 		case GS_LEVEL:
 			if (!gametic)
 				break;
@@ -363,11 +366,56 @@ static void D_Display(void)
 
 		// clean up border stuff
 		// see if the border needs to be initially drawn
-		if (gamestate == GS_LEVEL)
+		if (gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction && curbghide && (!hidetitlemap)))
 		{
 			// draw the view directly
 
-			D_Render();
+			if (!automapactive && !dedicated && cv_renderview.value)
+			{
+				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
+				{
+					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
+					objectsdrawn = 0;
+	#ifdef HWRENDER
+					if (rendermode != render_soft)
+						HWR_RenderPlayerView(0, &players[displayplayer]);
+					else
+	#endif
+					if (rendermode != render_none)
+						R_RenderPlayerView(&players[displayplayer]);
+				}
+
+				// render the second screen
+				if (splitscreen && players[secondarydisplayplayer].mo)
+				{
+	#ifdef HWRENDER
+					if (rendermode != render_soft)
+						HWR_RenderPlayerView(1, &players[secondarydisplayplayer]);
+					else
+	#endif
+					if (rendermode != render_none)
+					{
+						viewwindowy = vid.height / 2;
+						M_Memcpy(ylookup, ylookup2, viewheight*sizeof (ylookup[0]));
+
+						topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
+
+						R_RenderPlayerView(&players[secondarydisplayplayer]);
+
+						viewwindowy = 0;
+						M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
+					}
+				}
+
+				// Image postprocessing effect
+				if (rendermode == render_soft)
+				{
+					if (postimgtype)
+						V_DoPostProcessor(0, postimgtype, postimgparam);
+					if (postimgtype2)
+						V_DoPostProcessor(1, postimgtype2, postimgparam2);
+				}
+			}
 
 			if (lastdraw)
 			{
@@ -380,9 +428,14 @@ static void D_Display(void)
 				lastdraw = false;
 			}
 
-			ST_Drawer();
-			F_TextPromptDrawer();
-			HU_Drawer();
+			if (gamestate == GS_LEVEL)
+			{
+				ST_Drawer();
+				F_TextPromptDrawer();
+				HU_Drawer();
+			}
+			else
+				F_TitleScreenDrawer();
 		}
 	}
 
@@ -435,6 +488,15 @@ static void D_Display(void)
 		if (rendermode != render_none)
 		{
 			F_WipeEndScreen();
+			// Funny.
+			if (WipeStageTitle && st_overlay)
+			{
+				lt_ticker--;
+				lt_lasttic = lt_ticker;
+				ST_preLevelTitleCardDrawer(0, false);
+				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
+				F_WipeStartScreen();
+			}
 			F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN);
 		}
 
@@ -444,7 +506,7 @@ static void D_Display(void)
 			framecount = 0;
 			demostarttime = I_GetTime();
 		}
-		
+
 		wipetypepost = -1;
 	}
 	else
@@ -485,56 +547,6 @@ static void D_Display(void)
 	}
 }
 
-void D_Render(void)
-{
-	if (!automapactive && !dedicated && cv_renderview.value)
-	{
-		if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
-		{
-			topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
-			objectsdrawn = 0;
-#ifdef HWRENDER
-			if (rendermode != render_soft)
-				HWR_RenderPlayerView(0, &players[displayplayer]);
-			else
-#endif
-			if (rendermode != render_none)
-				R_RenderPlayerView(&players[displayplayer]);
-		}
-
-		// render the second screen
-		if (splitscreen && players[secondarydisplayplayer].mo)
-		{
-#ifdef HWRENDER
-			if (rendermode != render_soft)
-				HWR_RenderPlayerView(1, &players[secondarydisplayplayer]);
-			else
-#endif
-			if (rendermode != render_none)
-			{
-				viewwindowy = vid.height / 2;
-				M_Memcpy(ylookup, ylookup2, viewheight*sizeof (ylookup[0]));
-
-				topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
-
-				R_RenderPlayerView(&players[secondarydisplayplayer]);
-
-				viewwindowy = 0;
-				M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
-			}
-		}
-
-		// Image postprocessing effect
-		if (rendermode == render_soft)
-		{
-			if (postimgtype)
-				V_DoPostProcessor(0, postimgtype, postimgparam);
-			if (postimgtype2)
-				V_DoPostProcessor(1, postimgtype2, postimgparam2);
-		}
-	}
-}
-
 // =========================================================================
 // D_SRB2Loop
 // =========================================================================
@@ -655,6 +667,7 @@ void D_SRB2Loop(void)
 
 		// consoleplayer -> displayplayer (hear sounds from viewpoint)
 		S_UpdateSounds(); // move positional sounds
+		S_UpdateClosedCaptions();
 
 		// check for media change, loop music..
 		I_UpdateCD();
@@ -720,6 +733,8 @@ void D_StartTitle(void)
 	for (i = 0; i < MAXPLAYERS; i++)
 		CL_ClearPlayer(i);
 
+	players[consoleplayer].availabilities = players[1].availabilities = R_GetSkinAvailabilities(); // players[1] is supposed to be for 2p
+
 	splitscreen = false;
 	SplitScreen_OnChange();
 	botingame = false;
diff --git a/src/d_main.h b/src/d_main.h
index 65c51802a7c644c871b862ef0ff28182204d0d6b..d67a5bb498ba0f45d37b2be8c6bd15fde96cc78a 100644
--- a/src/d_main.h
+++ b/src/d_main.h
@@ -54,7 +54,4 @@ const char *D_Home(void);
 void D_AdvanceDemo(void);
 void D_StartTitle(void);
 
-/* Here for title maps */
-void D_Render(void);
-
 #endif //__D_MAIN__
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index d750c81f2db596c8ef98b55fb23e649e27183a3b..8fa4b2e0b9cdf5a47c6a8712dc27de3b4f6fe371 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -87,6 +87,7 @@ static void JoinTimeout_OnChange(void);
 
 static void CoopStarposts_OnChange(void);
 static void CoopLives_OnChange(void);
+static void ExitMove_OnChange(void);
 
 static void Ringslinger_OnChange(void);
 static void Gravity_OnChange(void);
@@ -341,21 +342,32 @@ consvar_t cv_nettimeout = {"nettimeout", "350", CV_CALL|CV_SAVE, nettimeout_cons
 static CV_PossibleValue_t jointimeout_cons_t[] = {{5*TICRATE, "MIN"}, {60*TICRATE, "MAX"}, {0, NULL}};
 consvar_t cv_jointimeout = {"jointimeout", "350", CV_CALL|CV_SAVE, jointimeout_cons_t, JoinTimeout_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_maxping = {"maxping", "0", CV_SAVE, CV_Unsigned, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+static CV_PossibleValue_t pingtimeout_cons_t[] = {{8, "MIN"}, {120, "MAX"}, {0, NULL}};
+consvar_t cv_pingtimeout = {"pingtimeout", "10", CV_SAVE, pingtimeout_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+// show your ping on the HUD next to framerate. Defaults to warning only (shows up if your ping is > maxping)
+static CV_PossibleValue_t showping_cons_t[] = {{0, "Off"}, {1, "Always"}, {2, "Warning"}, {0, NULL}};
+consvar_t cv_showping = {"showping", "Warning", CV_SAVE, showping_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 // Intermission time Tails 04-19-2002
 static CV_PossibleValue_t inttime_cons_t[] = {{0, "MIN"}, {3600, "MAX"}, {0, NULL}};
 consvar_t cv_inttime = {"inttime", "10", CV_NETVAR, inttime_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 static CV_PossibleValue_t coopstarposts_cons_t[] = {{0, "Per-player"}, {1, "Shared"}, {2, "Teamwork"}, {0, NULL}};
-consvar_t cv_coopstarposts = {"coopstarposts", "Teamwork", CV_NETVAR|CV_CALL|CV_CHEAT, coopstarposts_cons_t, CoopStarposts_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_coopstarposts = {"coopstarposts", "Per-player", CV_NETVAR|CV_CALL, coopstarposts_cons_t, CoopStarposts_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
 static CV_PossibleValue_t cooplives_cons_t[] = {{0, "Infinite"}, {1, "Per-player"}, {2, "Avoid Game Over"}, {3, "Single pool"}, {0, NULL}};
 consvar_t cv_cooplives = {"cooplives", "Avoid Game Over", CV_NETVAR|CV_CALL|CV_CHEAT, cooplives_cons_t, CoopLives_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
 static CV_PossibleValue_t advancemap_cons_t[] = {{0, "Off"}, {1, "Next"}, {2, "Random"}, {0, NULL}};
 consvar_t cv_advancemap = {"advancemap", "Next", CV_NETVAR, advancemap_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 static CV_PossibleValue_t playersforexit_cons_t[] = {{0, "One"}, {1, "1/4"}, {2, "Half"}, {3, "3/4"}, {4, "All"}, {0, NULL}};
 consvar_t cv_playersforexit = {"playersforexit", "All", CV_NETVAR, playersforexit_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
+consvar_t cv_exitmove = {"exitmove", "Off", CV_NETVAR|CV_CALL, CV_OnOff, ExitMove_OnChange, 0, NULL, NULL, 0, 0, NULL};
+
 consvar_t cv_runscripts = {"runscripts", "Yes", 0, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 consvar_t cv_pause = {"pausepermission", "Server", CV_NETVAR, pause_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -514,6 +526,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_inttime);
 	CV_RegisterVar(&cv_advancemap);
 	CV_RegisterVar(&cv_playersforexit);
+	CV_RegisterVar(&cv_exitmove);
 	CV_RegisterVar(&cv_timelimit);
 	CV_RegisterVar(&cv_playbackspeed);
 	CV_RegisterVar(&cv_forceskin);
@@ -577,6 +590,8 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_skipmapcheck);
 	CV_RegisterVar(&cv_sleep);
 	CV_RegisterVar(&cv_maxping);
+	CV_RegisterVar(&cv_pingtimeout);
+	CV_RegisterVar(&cv_showping);
 
 #ifdef SEENAMES
 	 CV_RegisterVar(&cv_allowseenames);
@@ -1213,16 +1228,16 @@ static void SendNameAndColor(void)
 		}
 		else if ((foundskin = R_SkinAvailable(cv_skin.string)) != -1 && R_SkinUsable(consoleplayer, foundskin))
 		{
-			boolean notsame;
+			//boolean notsame;
 
 			cv_skin.value = foundskin;
 
-			notsame = (cv_skin.value != players[consoleplayer].skin);
+			//notsame = (cv_skin.value != players[consoleplayer].skin);
 
 			SetPlayerSkin(consoleplayer, cv_skin.string);
 			CV_StealthSet(&cv_skin, skins[cv_skin.value].name);
 
-			if (notsame)
+			/*if (notsame)
 			{
 				CV_StealthSetValue(&cv_playercolor, skins[cv_skin.value].prefcolor);
 
@@ -1230,7 +1245,7 @@ static void SendNameAndColor(void)
 
 				if (players[consoleplayer].mo)
 					players[consoleplayer].mo->color = (UINT8)players[consoleplayer].skincolor;
-			}
+			}*/
 		}
 		else
 		{
@@ -1341,15 +1356,16 @@ static void SendNameAndColor2(void)
 		}
 		else if ((foundskin = R_SkinAvailable(cv_skin2.string)) != -1 && R_SkinUsable(secondplaya, foundskin))
 		{
-			boolean notsame;
+			//boolean notsame;
 
 			cv_skin2.value = foundskin;
 
-			notsame = (cv_skin2.value != players[secondplaya].skin);
+			//notsame = (cv_skin2.value != players[secondplaya].skin);
 
 			SetPlayerSkin(secondplaya, cv_skin2.string);
+			CV_StealthSet(&cv_skin2, skins[cv_skin2.value].name);
 
-			if (notsame)
+			/*if (notsame)
 			{
 				CV_StealthSetValue(&cv_playercolor2, skins[players[secondplaya].skin].prefcolor);
 
@@ -1357,7 +1373,7 @@ static void SendNameAndColor2(void)
 
 				if (players[secondplaya].mo)
 					players[secondplaya].mo->color = players[secondplaya].skincolor;
-			}
+			}*/
 		}
 		else
 		{
@@ -1924,6 +1940,17 @@ static void Command_Map_f(void)
 				d = atoi(gametypename);
 				if (d >= 0 && d < NUMGAMETYPES)
 					newgametype = d;
+				else
+				{
+					CONS_Alert(CONS_ERROR,
+							"Gametype number %d is out of range. Use a number between"
+							" 0 and %d inclusive. ...Or just use the name. :v\n",
+							d,
+							NUMGAMETYPES-1);
+					Z_Free(realmapname);
+					Z_Free(mapname);
+					return;
+				}
 			}
 			else
 			{
@@ -2039,7 +2066,9 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	lastgametype = gametype;
 	gametype = READUINT8(*cp);
 
-	if (gametype != lastgametype)
+	if (gametype < 0 || gametype >= NUMGAMETYPES)
+		gametype = lastgametype;
+	else if (gametype != lastgametype)
 		D_GameTypeChanged(lastgametype); // emulate consvar_t behavior for gametype
 
 	skipprecutscene = ((flags & (1<<2)) != 0);
@@ -3778,6 +3807,17 @@ static void CoopLives_OnChange(void)
 	}
 }
 
+static void ExitMove_OnChange(void)
+{
+	if (!(netgame || multiplayer) || gametype != GT_COOP)
+		return;
+
+	if (cv_exitmove.value)
+		CONS_Printf(M_GetText("Players can now move after completing the level.\n"));
+	else
+		CONS_Printf(M_GetText("Players can no longer move after completing the level.\n"));
+}
+
 UINT32 timelimitintics = 0;
 
 /** Deals with a timelimit change by printing the change to the console.
@@ -4250,6 +4290,8 @@ void Command_ExitGame_f(void)
 	for (i = 0; i < MAXPLAYERS; i++)
 		CL_ClearPlayer(i);
 
+	players[consoleplayer].availabilities = players[1].availabilities = R_GetSkinAvailabilities(); // players[1] is supposed to be for 2p
+
 	splitscreen = false;
 	SplitScreen_OnChange();
 	botingame = false;
@@ -4310,9 +4352,9 @@ static void Command_Isgamemodified_f(void)
 	if (savemoddata)
 		CONS_Printf(M_GetText("modifiedgame is true, but you can save emblem and time data in this mod.\n"));
 	else if (modifiedgame)
-		CONS_Printf(M_GetText("modifiedgame is true, secrets will not be unlocked\n"));
+		CONS_Printf(M_GetText("modifiedgame is true, extras will not be unlocked\n"));
 	else
-		CONS_Printf(M_GetText("modifiedgame is false, you can unlock secrets\n"));
+		CONS_Printf(M_GetText("modifiedgame is false, you can unlock extras\n"));
 }
 
 static void Command_Cheats_f(void)
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 4821aff46951fa22388e8e4076721116ef4b0a11..c2e6f980dc53381c525ec235cf3cde8c9721a1cf 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -94,7 +94,7 @@ extern consvar_t cv_recycler;
 
 extern consvar_t cv_itemfinder;
 
-extern consvar_t cv_inttime, cv_coopstarposts, cv_cooplives, cv_advancemap, cv_playersforexit;
+extern consvar_t cv_inttime, cv_coopstarposts, cv_cooplives, cv_advancemap, cv_playersforexit, cv_exitmove;
 extern consvar_t cv_overtime;
 extern consvar_t cv_startinglives;
 
@@ -106,6 +106,9 @@ extern consvar_t cv_ringslinger, cv_soundtest;
 extern consvar_t cv_specialrings, cv_powerstones, cv_matchboxes, cv_competitionboxes;
 
 extern consvar_t cv_maxping;
+extern consvar_t cv_pingtimeout;
+extern consvar_t cv_showping;
+
 
 extern consvar_t cv_skipmapcheck;
 
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 5e7f59310c84bca583253276bee4927a995cf377..bd7089609f8ed2ff9ac23064173019bc28671416 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -716,7 +716,7 @@ void SV_FileSendTicker(void)
 		if (ram)
 			M_Memcpy(p->data, &f->id.ram[transfer[i].position], size);
 		else if (fread(p->data, 1, size, transfer[i].currentfile) != size)
-			I_Error("SV_FileSendTicker: can't read %s byte on %s at %d because %s", sizeu1(size), f->id.filename, transfer[i].position, strerror(ferror(transfer[i].currentfile)));
+			I_Error("SV_FileSendTicker: can't read %s byte on %s at %d because %s", sizeu1(size), f->id.filename, transfer[i].position, M_FileError(transfer[i].currentfile));
 		p->position = LONG(transfer[i].position);
 		// Put flag so receiver knows the total size
 		if (transfer[i].position + size == f->size)
@@ -794,7 +794,7 @@ void Got_Filetxpak(void)
 		// We can receive packet in the wrong order, anyway all os support gaped file
 		fseek(file->file, pos, SEEK_SET);
 		if (fwrite(netbuffer->u.filetxpak.data,size,1,file->file) != 1)
-			I_Error("Can't write to %s: %s\n",filename, strerror(ferror(file->file)));
+			I_Error("Can't write to %s: %s\n",filename, M_FileError(file->file));
 		file->currentsize += size;
 
 		// Finished?
diff --git a/src/d_player.h b/src/d_player.h
index d3f84d0e6d62b071dc9013f4d763eb98ae5966f6..6d0c5f5d140de5079d67fbb18032b1022d63eb5c 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -151,6 +151,7 @@ typedef enum
 	/*** misc ***/
 	PF_FORCESTRAFE = 1<<28, // Turning inputs are translated into strafing inputs
 	PF_CANCARRY    = 1<<29, // Can carry another player?
+	PF_FINISHED    = 1<<30, // The player finished the level. NOT the same as exiting
 
 	// up to 1<<31 is free
 } pflags_t;
diff --git a/src/dehacked.c b/src/dehacked.c
index fff9dbee87976b290b839fee99cc7569414cc72d..40ba0abca5b7c6e4299a7d33550080f1e24e072b 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -1201,6 +1201,14 @@ static void readlevelheader(MYFILE *f, INT32 num)
 			word2 = tmp += 2;
 			i = atoi(word2); // used for numerical settings
 
+
+			if (fastcmp(word, "LEVELNAME"))
+			{
+				deh_strlcpy(mapheaderinfo[num-1]->lvlttl, word2,
+					sizeof(mapheaderinfo[num-1]->lvlttl), va("Level header %d: levelname", num));
+				strlcpy(mapheaderinfo[num-1]->selectheading, word2, sizeof(mapheaderinfo[num-1]->selectheading)); // not deh_ so only complains once
+				continue;
+			}
 			// CHEAP HACK: move this over here for lowercase subtitles
 			if (fastcmp(word, "SUBTITLE"))
 			{
@@ -1344,12 +1352,6 @@ static void readlevelheader(MYFILE *f, INT32 num)
 			}
 
 			// Strings that can be truncated
-			else if (fastcmp(word, "LEVELNAME"))
-			{
-				deh_strlcpy(mapheaderinfo[num-1]->lvlttl, word2,
-					sizeof(mapheaderinfo[num-1]->lvlttl), va("Level header %d: levelname", num));
-				strlcpy(mapheaderinfo[num-1]->selectheading, word2, sizeof(mapheaderinfo[num-1]->selectheading)); // not deh_ so only complains once
-			}
 			else if (fastcmp(word, "SELECTHEADING"))
 			{
 				deh_strlcpy(mapheaderinfo[num-1]->selectheading, word2,
@@ -1573,6 +1575,20 @@ static void readlevelheader(MYFILE *f, INT32 num)
 				else
 					mapheaderinfo[num-1]->levelflags &= ~LF_MIXNIGHTSCOUNTDOWN;
 			}
+			else if (fastcmp(word, "WARNINGTITLE"))
+			{
+				if (i || word2[0] == 'T' || word2[0] == 'Y')
+					mapheaderinfo[num-1]->levelflags |= LF_WARNINGTITLE;
+				else
+					mapheaderinfo[num-1]->levelflags &= ~LF_WARNINGTITLE;
+			}
+			else if (fastcmp(word, "NOTITLECARD"))
+			{
+				if (i || word2[0] == 'T' || word2[0] == 'Y')
+					mapheaderinfo[num-1]->levelflags |= LF_NOTITLECARD;
+				else
+					mapheaderinfo[num-1]->levelflags &= ~LF_NOTITLECARD;
+			}
 
 			// Individual triggers for menu flags
 			else if (fastcmp(word, "HIDDEN"))
@@ -2781,6 +2797,9 @@ static actionpointer_t actionpointers[] =
 	{{A_PterabyteHover},         "A_PTERABYTEHOVER"},
 	{{A_RolloutSpawn},           "A_ROLLOUTSPAWN"},
 	{{A_RolloutRock},            "A_ROLLOUTROCK"},
+	{{A_DragonbomberSpawn},      "A_DRAGONBOMERSPAWN"},
+	{{A_DragonWing},             "A_DRAGONWING"},
+	{{A_DragonSegment},          "A_DRAGONSEGMENT"},
 	{{NULL},                     "NONE"},
 
 	// This NULL entry must be the last in the list
@@ -3165,7 +3184,6 @@ static void readextraemblemdata(MYFILE *f, INT32 num)
 
 			// Now get the part after
 			word2 = tmp += 2;
-			strupr(word2);
 
 			value = atoi(word2); // used for numerical settings
 
@@ -3177,22 +3195,26 @@ static void readextraemblemdata(MYFILE *f, INT32 num)
 					sizeof (extraemblems[num-1].description), va("Extra emblem %d: objective", num));
 			else if (fastcmp(word, "CONDITIONSET"))
 				extraemblems[num-1].conditionset = (UINT8)value;
-			else if (fastcmp(word, "SPRITE"))
+			else
 			{
-				if (word2[0] >= 'A' && word2[0] <= 'Z')
-					value = word2[0];
-				else
-					value += 'A'-1;
+				strupr(word2);
+				if (fastcmp(word, "SPRITE"))
+				{
+					if (word2[0] >= 'A' && word2[0] <= 'Z')
+						value = word2[0];
+					else
+						value += 'A'-1;
 
-				if (value < 'A' || value > 'Z')
-					deh_warning("Emblem %d: sprite must be from A - Z (1 - 26)", num);
+					if (value < 'A' || value > 'Z')
+						deh_warning("Emblem %d: sprite must be from A - Z (1 - 26)", num);
+					else
+						extraemblems[num-1].sprite = (UINT8)value;
+				}
+				else if (fastcmp(word, "COLOR"))
+					extraemblems[num-1].color = get_number(word2);
 				else
-					extraemblems[num-1].sprite = (UINT8)value;
+					deh_warning("Extra emblem %d: unknown word '%s'", num, word);
 			}
-			else if (fastcmp(word, "COLOR"))
-				extraemblems[num-1].color = get_number(word2);
-			else
-				deh_warning("Extra emblem %d: unknown word '%s'", num, word);
 		}
 	} while (!myfeof(f));
 
@@ -3243,7 +3265,6 @@ static void readunlockable(MYFILE *f, INT32 num)
 
 			// Now get the part after
 			word2 = tmp += 2;
-			strupr(word2);
 
 			i = atoi(word2); // used for numerical settings
 
@@ -3253,54 +3274,58 @@ static void readunlockable(MYFILE *f, INT32 num)
 			else if (fastcmp(word, "OBJECTIVE"))
 				deh_strlcpy(unlockables[num].objective, word2,
 					sizeof (unlockables[num].objective), va("Unlockable %d: objective", num));
-			else if (fastcmp(word, "HEIGHT"))
-				unlockables[num].height = (UINT16)i;
-			else if (fastcmp(word, "CONDITIONSET"))
-				unlockables[num].conditionset = (UINT8)i;
-			else if (fastcmp(word, "NOCECHO"))
-				unlockables[num].nocecho = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
-			else if (fastcmp(word, "NOCHECKLIST"))
-				unlockables[num].nochecklist = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
-			else if (fastcmp(word, "TYPE"))
-			{
-				if (fastcmp(word2, "NONE"))
-					unlockables[num].type = SECRET_NONE;
-				else if (fastcmp(word2, "ITEMFINDER"))
-					unlockables[num].type = SECRET_ITEMFINDER;
-				else if (fastcmp(word2, "EMBLEMHINTS"))
-					unlockables[num].type = SECRET_EMBLEMHINTS;
-				else if (fastcmp(word2, "PANDORA"))
-					unlockables[num].type = SECRET_PANDORA;
-				else if (fastcmp(word2, "CREDITS"))
-					unlockables[num].type = SECRET_CREDITS;
-				else if (fastcmp(word2, "RECORDATTACK"))
-					unlockables[num].type = SECRET_RECORDATTACK;
-				else if (fastcmp(word2, "NIGHTSMODE"))
-					unlockables[num].type = SECRET_NIGHTSMODE;
-				else if (fastcmp(word2, "HEADER"))
-					unlockables[num].type = SECRET_HEADER;
-				else if (fastcmp(word2, "LEVELSELECT"))
-					unlockables[num].type = SECRET_LEVELSELECT;
-				else if (fastcmp(word2, "WARP"))
-					unlockables[num].type = SECRET_WARP;
-				else if (fastcmp(word2, "SOUNDTEST"))
-					unlockables[num].type = SECRET_SOUNDTEST;
-				else
-					unlockables[num].type = (INT16)i;
-			}
-			else if (fastcmp(word, "VAR"))
+			else
 			{
-				// Support using the actual map name,
-				// i.e., Level AB, Level FZ, etc.
+				strupr(word2);
+				if (fastcmp(word, "HEIGHT"))
+					unlockables[num].height = (UINT16)i;
+				else if (fastcmp(word, "CONDITIONSET"))
+					unlockables[num].conditionset = (UINT8)i;
+				else if (fastcmp(word, "NOCECHO"))
+					unlockables[num].nocecho = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
+				else if (fastcmp(word, "NOCHECKLIST"))
+					unlockables[num].nochecklist = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
+				else if (fastcmp(word, "TYPE"))
+				{
+					if (fastcmp(word2, "NONE"))
+						unlockables[num].type = SECRET_NONE;
+					else if (fastcmp(word2, "ITEMFINDER"))
+						unlockables[num].type = SECRET_ITEMFINDER;
+					else if (fastcmp(word2, "EMBLEMHINTS"))
+						unlockables[num].type = SECRET_EMBLEMHINTS;
+					else if (fastcmp(word2, "PANDORA"))
+						unlockables[num].type = SECRET_PANDORA;
+					else if (fastcmp(word2, "CREDITS"))
+						unlockables[num].type = SECRET_CREDITS;
+					else if (fastcmp(word2, "RECORDATTACK"))
+						unlockables[num].type = SECRET_RECORDATTACK;
+					else if (fastcmp(word2, "NIGHTSMODE"))
+						unlockables[num].type = SECRET_NIGHTSMODE;
+					else if (fastcmp(word2, "HEADER"))
+						unlockables[num].type = SECRET_HEADER;
+					else if (fastcmp(word2, "LEVELSELECT"))
+						unlockables[num].type = SECRET_LEVELSELECT;
+					else if (fastcmp(word2, "WARP"))
+						unlockables[num].type = SECRET_WARP;
+					else if (fastcmp(word2, "SOUNDTEST"))
+						unlockables[num].type = SECRET_SOUNDTEST;
+					else
+						unlockables[num].type = (INT16)i;
+				}
+				else if (fastcmp(word, "VAR"))
+				{
+					// Support using the actual map name,
+					// i.e., Level AB, Level FZ, etc.
 
-				// Convert to map number
-				if (word2[0] >= 'A' && word2[0] <= 'Z')
-					i = M_MapNumber(word2[0], word2[1]);
+					// Convert to map number
+					if (word2[0] >= 'A' && word2[0] <= 'Z')
+						i = M_MapNumber(word2[0], word2[1]);
 
-				unlockables[num].variable = (INT16)i;
+					unlockables[num].variable = (INT16)i;
+				}
+				else
+					deh_warning("Unlockable %d: unknown word '%s'", num+1, word);
 			}
-			else
-				deh_warning("Unlockable %d: unknown word '%s'", num+1, word);
 		}
 	} while (!myfeof(f)); // finish when the line is empty
 
@@ -4950,22 +4975,10 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_MINUS_BURST4",
 	"S_MINUS_BURST5",
 	"S_MINUS_POPUP",
-	"S_MINUS_UPWARD1",
-	"S_MINUS_UPWARD2",
-	"S_MINUS_UPWARD3",
-	"S_MINUS_UPWARD4",
-	"S_MINUS_UPWARD5",
-	"S_MINUS_UPWARD6",
-	"S_MINUS_UPWARD7",
-	"S_MINUS_UPWARD8",
-	"S_MINUS_DOWNWARD1",
-	"S_MINUS_DOWNWARD2",
-	"S_MINUS_DOWNWARD3",
-	"S_MINUS_DOWNWARD4",
-	"S_MINUS_DOWNWARD5",
-	"S_MINUS_DOWNWARD6",
-	"S_MINUS_DOWNWARD7",
-	"S_MINUS_DOWNWARD8",
+	"S_MINUS_AERIAL1",
+	"S_MINUS_AERIAL2",
+	"S_MINUS_AERIAL3",
+	"S_MINUS_AERIAL4",
 
 	// Minus dirt
 	"S_MINUSDIRT1",
@@ -5041,6 +5054,26 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_PTERABYTE_SWOOPDOWN",
 	"S_PTERABYTE_SWOOPUP",
 
+	// Dragonbomber
+	"S_DRAGONBOMBER",
+	"S_DRAGONWING1",
+	"S_DRAGONWING2",
+	"S_DRAGONWING3",
+	"S_DRAGONWING4",
+	"S_DRAGONTAIL_LOADED",
+	"S_DRAGONTAIL_EMPTY",
+	"S_DRAGONTAIL_EMPTYLOOP",
+	"S_DRAGONTAIL_RELOAD",
+	"S_DRAGONMINE",
+	"S_DRAGONMINE_LAND1",
+	"S_DRAGONMINE_LAND2",
+	"S_DRAGONMINE_SLOWFLASH1",
+	"S_DRAGONMINE_SLOWFLASH2",
+	"S_DRAGONMINE_SLOWLOOP",
+	"S_DRAGONMINE_FASTFLASH1",
+	"S_DRAGONMINE_FASTFLASH2",
+	"S_DRAGONMINE_FASTLOOP",
+
 	// Boss Explosion
 	"S_BOSSEXPLODE",
 
@@ -5343,6 +5376,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_FSGNA",
 	"S_FSGNB",
 	"S_FSGNC",
+	"S_FSGND",
 
 	// Black Eggman (Boss 7)
 	"S_BLACKEGG_STND",
@@ -7139,6 +7173,8 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_FOUR2",
 	"S_FIVE2",
 
+	"S_FLIGHTINDICATOR",
+
 	"S_LOCKON1",
 	"S_LOCKON2",
 	"S_LOCKON3",
@@ -7322,13 +7358,9 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_FIREFLOWER2",
 	"S_FIREFLOWER3",
 	"S_FIREFLOWER4",
-	"S_FIREBALL1",
-	"S_FIREBALL2",
-	"S_FIREBALL3",
-	"S_FIREBALL4",
-	"S_FIREBALLEXP1",
-	"S_FIREBALLEXP2",
-	"S_FIREBALLEXP3",
+	"S_FIREBALL",
+	"S_FIREBALLTRAIL1",
+	"S_FIREBALLTRAIL2",
 	"S_SHELL",
 	"S_PUMA_START1",
 	"S_PUMA_START2",
@@ -7497,6 +7529,9 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_POPHAT_SHOOT1",
 	"S_POPHAT_SHOOT2",
 	"S_POPHAT_SHOOT3",
+	"S_POPHAT_SHOOT4",
+	"S_POPSHOT",
+	"S_POPSHOT_TRAIL",
 
 	"S_HIVEELEMENTAL_LOOK",
 	"S_HIVEELEMENTAL_PREPARE1",
@@ -7628,8 +7663,6 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_DUST3",
 	"S_DUST4",
 
-	"S_WOODDEBRIS",
-
 	"S_ROCKSPAWN",
 
 	"S_ROCKCRUMBLEA",
@@ -7648,7 +7681,9 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_ROCKCRUMBLEN",
 	"S_ROCKCRUMBLEO",
 	"S_ROCKCRUMBLEP",
+	"S_GFZDEBRIS",
 	"S_BRICKDEBRIS",
+	"S_WOODDEBRIS",
 
 #ifdef SEENAMES
 	"S_NAMECHECK",
@@ -7712,6 +7747,10 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_PTERABYTESPAWNER", // Pterabyte spawner
 	"MT_PTERABYTEWAYPOINT", // Pterabyte waypoint
 	"MT_PTERABYTE", // Pterabyte
+	"MT_DRAGONBOMBER", // Dragonbomber
+	"MT_DRAGONWING", // Dragonbomber wing
+	"MT_DRAGONTAIL", // Dragonbomber tail segment
+	"MT_DRAGONMINE", // Dragonbomber mine
 
 	// Generic Boss Items
 	"MT_BOSSEXPLODE",
@@ -8317,6 +8356,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_BLUEGOOMBA",
 	"MT_FIREFLOWER",
 	"MT_FIREBALL",
+	"MT_FIREBALLTRAIL",
 	"MT_SHELL",
 	"MT_PUMA",
 	"MT_PUMATRAIL",
@@ -8361,6 +8401,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_PENGUINATOR",
 	"MT_POPHAT",
 	"MT_POPSHOT",
+	"MT_POPSHOT_TRAIL",
 
 	"MT_HIVEELEMENTAL",
 	"MT_BUMBLEBORE",
@@ -8398,7 +8439,6 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_EXPLODE", // Robot Explosion
 	"MT_UWEXPLODE", // Underwater Explosion
 	"MT_DUST",
-	"MT_WOODDEBRIS",
 	"MT_ROCKSPAWNER",
 	"MT_FALLINGROCK",
 	"MT_ROCKCRUMBLE1",
@@ -8417,7 +8457,9 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_ROCKCRUMBLE14",
 	"MT_ROCKCRUMBLE15",
 	"MT_ROCKCRUMBLE16",
+	"MT_GFZDEBRIS",
 	"MT_BRICKDEBRIS",
+	"MT_WOODDEBRIS",
 
 #ifdef SEENAMES
 	"MT_NAMECHECK",
@@ -8570,6 +8612,7 @@ static const char *const PLAYERFLAG_LIST[] = {
 	/*** misc ***/
 	"FORCESTRAFE", // Translate turn inputs into strafe inputs
 	"CANCARRY", // Can carry?
+	"FINISHED",
 
 	NULL // stop loop here.
 };
@@ -8856,7 +8899,7 @@ static const char *const MENUTYPES_LIST[] = {
 	"OP_SCREENSHOTS",
 	"OP_ERASEDATA",
 
-	// Secrets
+	// Extras
 	"SR_MAIN",
 	"SR_PANDORA",
 	"SR_LEVELSELECT",
@@ -9012,6 +9055,8 @@ struct {
 	{"LF_NOZONE",LF_NOZONE},
 	{"LF_SAVEGAME",LF_SAVEGAME},
 	{"LF_MIXNIGHTSCOUNTDOWN",LF_MIXNIGHTSCOUNTDOWN},
+	{"LF_NOTITLECARD",LF_NOTITLECARD},
+	{"LF_WARNINGTITLE",LF_WARNINGTITLE},
 	// And map flags
 	{"LF2_HIDEINMENU",LF2_HIDEINMENU},
 	{"LF2_HIDEINSTATS",LF2_HIDEINSTATS},
diff --git a/src/djgppdos/i_video.c b/src/djgppdos/i_video.c
index 6a7641174491d47793895c24677c6221c1f9e73b..56570b7bf8daf871fca8db3a03909823c33ad605 100644
--- a/src/djgppdos/i_video.c
+++ b/src/djgppdos/i_video.c
@@ -98,6 +98,9 @@ void I_FinishUpdate (void)
 	if (cv_ticrate.value)
 		SCR_DisplayTicRate();
 
+	if (cv_showping.value && netgame && consoleplayer != serverplayer)
+		SCR_DisplayLocalPing();
+
 	//blast it to the screen
 	// this code sucks
 	//memcpy(dascreen,screens[0],screenwidth*screenheight);
diff --git a/src/doomstat.h b/src/doomstat.h
index 7d06f03e24445c868d385f6dc83002fbcb542ab2..877f5b50b72fda0b81838200562a88bea305c83e 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -349,6 +349,8 @@ typedef struct
 #define LF_NOZONE        16 ///< Don't include "ZONE" on level title
 #define LF_SAVEGAME      32 ///< Save the game upon loading this level
 #define LF_MIXNIGHTSCOUNTDOWN 64 ///< Play sfx_timeup instead of music change for NiGHTS countdown
+#define LF_WARNINGTITLE 128 ///< WARNING! WARNING! WARNING! WARNING!
+#define LF_NOTITLECARD  256 ///< Don't start the title card
 
 #define LF2_HIDEINMENU     1 ///< Hide in the multiplayer menu
 #define LF2_HIDEINSTATS    2 ///< Hide in the statistics screen
@@ -433,7 +435,6 @@ typedef struct
 	tic_t time;   ///< Time in which the level was finished.
 	UINT32 score; ///< Score when the level was finished.
 	UINT16 rings; ///< Rings when the level was finished.
-	boolean gotperfect; ///< Got perfect bonus?
 } recorddata_t;
 
 /** Setup for one NiGHTS map.
diff --git a/src/f_finale.c b/src/f_finale.c
index 466d8b0f4d096e0df798ac65aa4db8f1d108330e..b8c9dd5f2ae91d39cdc62012a29166e0c05f7661 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -856,7 +856,7 @@ void F_IntroDrawer(void)
 			if (rendermode != render_none)
 			{
 				F_WipeStartScreen();
-				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+				F_WipeColorFill(31);
 				F_WipeEndScreen();
 				F_RunWipe(99,true);
 			}
@@ -866,10 +866,11 @@ void F_IntroDrawer(void)
 		else if (intro_scenenum == 10)
 		{
 			// The only fade to white in the entire damn game.
+			// (not true)
 			if (rendermode != render_none)
 			{
 				F_WipeStartScreen();
-				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 0);
+				F_WipeColorFill(0);
 				F_WipeEndScreen();
 				F_RunWipe(99,true);
 			}
@@ -879,7 +880,7 @@ void F_IntroDrawer(void)
 			if (rendermode != render_none)
 			{
 				F_WipeStartScreen();
-				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+				F_WipeColorFill(31);
 				F_WipeEndScreen();
 				F_RunWipe(99,true);
 			}
@@ -926,7 +927,7 @@ void F_IntroDrawer(void)
 			patch_t *radar = W_CachePatchName("RADAR", PU_CACHE);
 
 			F_WipeStartScreen();
-			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+			F_WipeColorFill(31);
 			V_DrawScaledPatch(0, 0, 0, radar);
 			W_UnlockCachedPatch(radar);
 			V_DrawString(8, 128, V_ALLOWLOWERCASE, cutscene_disptext);
@@ -939,7 +940,7 @@ void F_IntroDrawer(void)
 			patch_t *grass = W_CachePatchName("SGRASS5", PU_CACHE);
 
 			F_WipeStartScreen();
-			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+			F_WipeColorFill(31);
 			V_DrawScaledPatch(0, 0, 0, grass);
 			W_UnlockCachedPatch(grass);
 			V_DrawString(8, 128, V_ALLOWLOWERCASE, cutscene_disptext);
@@ -952,7 +953,7 @@ void F_IntroDrawer(void)
 			patch_t *confront = W_CachePatchName("CONFRONT", PU_CACHE);
 
 			F_WipeStartScreen();
-			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+			F_WipeColorFill(31);
 			V_DrawSmallScaledPatch(0, 0, 0, confront);
 			W_UnlockCachedPatch(confront);
 			V_DrawString(8, 128, V_ALLOWLOWERCASE, cutscene_disptext);
@@ -965,7 +966,7 @@ void F_IntroDrawer(void)
 			patch_t *sdo = W_CachePatchName("SONICDO2", PU_CACHE);
 
 			F_WipeStartScreen();
-			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+			F_WipeColorFill(31);
 			V_DrawSmallScaledPatch(0, 0, 0, sdo);
 			W_UnlockCachedPatch(sdo);
 			V_DrawString(224, 8, V_ALLOWLOWERCASE, cutscene_disptext);
@@ -1385,13 +1386,16 @@ boolean F_CreditResponder(event_t *event)
 
 void F_StartGameEvaluation(void)
 {
-	// Credits option in secrets menu
+	// Credits option in extras menu
 	if (cursaveslot == -1)
 	{
+		S_FadeOutStopMusic(2*MUSICRATE);
 		F_StartGameEnd();
 		return;
 	}
 
+	S_FadeOutStopMusic(5*MUSICRATE);
+
 	G_SetGamestate(GS_EVALUATION);
 
 	// Just in case they're open ... somehow
@@ -1537,9 +1541,9 @@ void F_GameEvaluationDrawer(void)
 			}
 		}
 		else if (netgame)
-			V_DrawString(8, 96, V_YELLOWMAP, "Prizes only\nawarded in\nsingle player!");
+			V_DrawString(8, 96, V_YELLOWMAP, "Multiplayer games\ncan't unlock\nextras!");
 		else
-			V_DrawString(8, 96, V_YELLOWMAP, "Prizes not\nawarded in\nmodified games!");
+			V_DrawString(8, 96, V_YELLOWMAP, "Modified games\ncan't unlock\nextras!");
 	}
 #endif
 }
@@ -1586,7 +1590,7 @@ void F_GameEvaluationTicker(void)
 		{
 			HU_SetCEchoFlags(V_YELLOWMAP|V_RETURN8);
 			HU_SetCEchoDuration(6);
-			HU_DoCEcho("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\Prizes only awarded in singleplayer!");
+			HU_DoCEcho("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\Multiplayer games can't unlock extras!");
 			S_StartSound(NULL, sfx_s3k68);
 		}
 		else if (!modifiedgame || savemoddata)
@@ -1608,7 +1612,7 @@ void F_GameEvaluationTicker(void)
 		{
 			HU_SetCEchoFlags(V_YELLOWMAP|V_RETURN8);
 			HU_SetCEchoDuration(6);
-			HU_DoCEcho("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\Prizes not awarded in modified games!");
+			HU_DoCEcho("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\Modified games can't unlock extras!");
 			S_StartSound(NULL, sfx_s3k68);
 		}
 	}
@@ -1621,6 +1625,7 @@ void F_GameEvaluationTicker(void)
 // ==========
 
 #define INFLECTIONPOINT (6*TICRATE)
+#define STOPPINGPOINT (14*TICRATE)
 #define SPARKLLOOPTIME 15 // must be odd
 
 void F_StartEnding(void)
@@ -1638,7 +1643,7 @@ void F_StartEnding(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
-	S_StopMusic(); // todo: placeholder
+	S_StopMusic();
 	S_StopSounds();
 
 	finalecount = -10; // what? this totally isn't a hack. why are you asking?
@@ -1679,7 +1684,7 @@ void F_StartEnding(void)
 		UINT8 skinnum = players[consoleplayer].skin;
 		spritedef_t *sprdef;
 		spriteframe_t *sprframe;
-		if (skins[skinnum].sprites[SPR2_XTRA].numframes >= (XTRA_ENDING+2)+1)
+		if (skins[skinnum].sprites[SPR2_XTRA].numframes > (XTRA_ENDING+2))
 		{
 			sprdef = &skins[skinnum].sprites[SPR2_XTRA];
 			// character head, skin specific
@@ -1712,13 +1717,16 @@ void F_StartEnding(void)
 
 void F_EndingTicker(void)
 {
-	if (++finalecount > INFLECTIONPOINT*2)
+	if (++finalecount > STOPPINGPOINT)
 	{
 		F_StartCredits();
 		wipetypepre = INT16_MAX;
 		return;
 	}
 
+	if (finalecount == -8)
+		S_ChangeMusicInternal((goodending ? "_endg" : "_endb"), false);
+
 	if (goodending && finalecount == INFLECTIONPOINT) // time to swap some assets
 	{
 		endegrk[0] = W_CachePatchName("ENDEGRK2", PU_LEVEL);
@@ -2105,26 +2113,26 @@ void F_EndingDrawer(void)
 
 		if (finalecount < 10)
 			trans = (10-finalecount)/2;
-		else if (finalecount > (2*INFLECTIONPOINT) - 20)
+		else if (finalecount > STOPPINGPOINT - 20)
 		{
-			trans = 10 + (finalecount/2) - INFLECTIONPOINT;
+			trans = 10 + (finalecount - STOPPINGPOINT)/2;
 			donttouch = true;
 		}
 
-		if (trans != 10)
+		if (trans < 10)
 		{
 			//colset(linkmap,  164, 165, 169); -- the ideal purple colour to represent a clicked in-game link, but not worth it just for a soundtest-controlled secret
 			V_DrawCenteredString(BASEVIDWIDTH/2, 8, V_ALLOWLOWERCASE|(trans<<V_ALPHASHIFT), str);
 			V_DrawCharacter(32, BASEVIDHEIGHT-16, '>'|(trans<<V_ALPHASHIFT), false);
-			V_DrawString(40, ((finalecount == (2*INFLECTIONPOINT)-(20+TICRATE)) ? 1 : 0)+BASEVIDHEIGHT-16, ((timesBeaten || finalecount >= (2*INFLECTIONPOINT)-TICRATE) ? V_PURPLEMAP : V_BLUEMAP)|(trans<<V_ALPHASHIFT), " [S] ===>");
+			V_DrawString(40, ((finalecount == STOPPINGPOINT-(20+TICRATE)) ? 1 : 0)+BASEVIDHEIGHT-16, ((timesBeaten || finalecount >= STOPPINGPOINT-TICRATE) ? V_PURPLEMAP : V_BLUEMAP)|(trans<<V_ALPHASHIFT), " [S] ===>");
 		}
 
-		if (finalecount > (2*INFLECTIONPOINT)-(20+(2*TICRATE)))
+		if (finalecount > STOPPINGPOINT-(20+(2*TICRATE)))
 		{
 			INT32 trans2 = abs((5*FINECOSINE((FixedAngle((finalecount*5)<<FRACBITS)>>ANGLETOFINESHIFT & FINEMASK)))>>FRACBITS)+2;
 			if (!donttouch)
 			{
-				trans = 10 + ((2*INFLECTIONPOINT)-(20+(2*TICRATE))) - finalecount;
+				trans = 10 + (STOPPINGPOINT-(20+(2*TICRATE))) - finalecount;
 				if (trans > trans2)
 					trans2 = trans;
 			}
@@ -2148,7 +2156,6 @@ void F_StartGameEnd(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
-	S_StopMusic();
 	S_StopSounds();
 
 	// In case menus are still up?!!
@@ -2582,9 +2589,7 @@ void F_TitleScreenDrawer(void)
 	// Draw that sky!
 	if (curbgcolor >= 0)
 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
-	else if (titlemapinaction && curbghide && ! hidetitlemap)
-		D_Render();
-	else
+	else if (!curbghide || !titlemapinaction || gamestate == GS_WAITINGPLAYERS)
 		F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
 
 	// Don't draw outside of the title screen, or if the patch isn't there.
@@ -3344,6 +3349,10 @@ void F_TitleScreenTicker(boolean run)
 	if (run)
 		finalecount++;
 
+	// don't trigger if doing anything besides idling on title
+	if (gameaction != ga_nothing || gamestate != GS_TITLESCREEN)
+		return;
+
 	// Execute the titlemap camera settings
 	if (titlemapinaction)
 	{
@@ -3390,10 +3399,6 @@ void F_TitleScreenTicker(boolean run)
 		}
 	}
 
-	// don't trigger if doing anything besides idling on title
-	if (gameaction != ga_nothing || gamestate != GS_TITLESCREEN)
-		return;
-
 	// no demos to play? or, are they disabled?
 	if (!cv_rollingdemos.value || !numDemos)
 		return;
@@ -3546,7 +3551,7 @@ void F_ContinueDrawer(void)
 	if (timetonext >= (11*TICRATE)+10)
 		return;
 
-	V_DrawLevelTitle(x - (V_LevelNameWidth("CONTINUE")>>1), 16, 0, "CONTINUE");
+	V_DrawLevelTitle(x - (V_LevelNameWidth("Continue?")>>1), 16, 0, "Continue?");
 
 	// Two stars...
 	patch = W_CachePatchName("CONTSTAR", PU_CACHE);
diff --git a/src/f_finale.h b/src/f_finale.h
index 3fa7106a98e867909b077ccfd591879d1e346131..0002cb3cfa64e0b5ad523edc657ea96ef088276c 100644
--- a/src/f_finale.h
+++ b/src/f_finale.h
@@ -141,11 +141,42 @@ void F_MenuPresTicker(boolean run);
 #define FORCEWIPEOFF -2
 
 extern boolean WipeInAction;
+extern boolean WipeStageTitle;
+
+typedef enum
+{
+	WIPESTYLE_NORMAL,
+	WIPESTYLE_LEVEL
+} wipestyle_t;
+extern wipestyle_t wipestyle;
+
+typedef enum
+{
+	WSF_FADEOUT   = 1,
+	WSF_FADEIN    = 1<<1,
+	WSF_TOWHITE   = 1<<2,
+	WSF_CROSSFADE = 1<<3,
+} wipestyleflags_t;
+extern wipestyleflags_t wipestyleflags;
+
+#define FADECOLORMAPDIV 8
+#define FADECOLORMAPROWS (256/FADECOLORMAPDIV)
+
+#define FADEREDFACTOR   15
+#define FADEGREENFACTOR 15
+#define FADEBLUEFACTOR  10
+
 extern INT32 lastwipetic;
 
+// Don't know where else to place this constant
+// But this file seems appropriate
+#define PRELEVELTIME 24 // frames in tics
+
 void F_WipeStartScreen(void);
 void F_WipeEndScreen(void);
 void F_RunWipe(UINT8 wipetype, boolean drawMenu);
+void F_WipeStageTitle(void);
+#define F_WipeColorFill(c) V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, c)
 tic_t F_GetWipeLength(UINT8 wipetype);
 boolean F_WipeExists(UINT8 wipetype);
 
diff --git a/src/f_wipe.c b/src/f_wipe.c
index 05229f844dfd6f11bc59311f351cff335d50b30f..b88b39ef00986404cdc487f6454509c16b95dd54 100644
--- a/src/f_wipe.c
+++ b/src/f_wipe.c
@@ -18,6 +18,8 @@
 
 #include "r_draw.h" // transtable
 #include "p_pspr.h" // tr_transxxx
+#include "p_local.h"
+#include "st_stuff.h"
 #include "w_wad.h"
 #include "z_zone.h"
 
@@ -25,8 +27,15 @@
 #include "m_menu.h"
 #include "console.h"
 #include "d_main.h"
+#include "g_game.h"
 #include "m_misc.h" // movie mode
 
+#include "doomstat.h"
+
+#ifdef HAVE_BLUA
+#include "lua_hud.h" // level title
+#endif
+
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
 #endif
@@ -82,8 +91,12 @@ UINT8 wipedefs[NUMWIPEDEFS] = {
 //--------------------------------------------------------------------------
 
 boolean WipeInAction = false;
+boolean WipeStageTitle = false;
 INT32 lastwipetic = 0;
 
+wipestyle_t wipestyle = WIPESTYLE_NORMAL;
+wipestyleflags_t wipestyleflags = WSF_CROSSFADE;
+
 #ifndef NOWIPE
 static UINT8 *wipe_scr_start; //screen 3
 static UINT8 *wipe_scr_end; //screen 4
@@ -148,7 +161,10 @@ static fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum) {
 	{
 		// Determine pixel to use from fademask
 		pcolor = &pMasterPalette[*lump++];
-		*mask++ = FixedDiv((pcolor->s.red+1)<<FRACBITS, paldiv)>>FRACBITS;
+		if (wipestyle == WIPESTYLE_LEVEL)
+			*mask++ = pcolor->s.red / FADECOLORMAPDIV;
+		else
+			*mask++ = FixedDiv((pcolor->s.red+1)<<FRACBITS, paldiv)>>FRACBITS;
 	}
 
 	fm.xscale = FixedDiv(vid.width<<FRACBITS, fm.width<<FRACBITS);
@@ -169,6 +185,21 @@ static fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum) {
 	return NULL;
 }
 
+/** Draw the stage title.
+  */
+void F_WipeStageTitle(void)
+{
+	// draw level title
+	if ((WipeStageTitle && st_overlay)
+	&& (wipestyle == WIPESTYLE_LEVEL)
+	&& !(mapheaderinfo[gamemap-1]->levelflags & LF_NOTITLECARD)
+	&& *mapheaderinfo[gamemap-1]->lvlttl != '\0')
+	{
+		ST_runTitleCard();
+		ST_drawWipeTitleCard();
+	}
+}
+
 /**	Wipe ticker
   *
   * \param	fademask	pixels to change
@@ -251,7 +282,7 @@ static void F_DoWipe(fademask_t *fademask)
 					relativepos += vid.width;
 				}
 			}
-			else if (*mask == 10)
+			else if (*mask >= ((wipestyle == WIPESTYLE_LEVEL) ? FADECOLORMAPROWS : 10))
 			{
 				// shortcut - memcpy target to work
 				while (draw_linestogo--)
@@ -262,8 +293,25 @@ static void F_DoWipe(fademask_t *fademask)
 			}
 			else
 			{
-				// pointer to transtable that this mask would use
-				transtbl = transtables + ((9 - *mask)<<FF_TRANSSHIFT);
+				if (wipestyle == WIPESTYLE_LEVEL)
+				{
+					int nmask;
+					UINT8 *fade = fadecolormap;
+
+					if (wipestyleflags & WSF_TOWHITE)
+						fade = fadecolormap + (FADECOLORMAPROWS * 256);
+
+					nmask = *mask;
+					if (wipestyleflags & WSF_FADEIN)
+						nmask = (FADECOLORMAPROWS-1) - nmask;
+
+					transtbl = fade + (nmask * 256);
+				}
+				else
+				{
+					// pointer to transtable that this mask would use
+					transtbl = transtables + ((9 - *mask)<<FF_TRANSSHIFT);
+				}
 
 				// DRAWING LOOP
 				while (draw_linestogo--)
@@ -273,8 +321,16 @@ static void F_DoWipe(fademask_t *fademask)
 					e = e_base + relativepos;
 					draw_rowstogo = draw_rowend - draw_rowstart;
 
-					while (draw_rowstogo--)
-						*w++ = transtbl[ ( *e++ << 8 ) + *s++ ];
+					if (wipestyle == WIPESTYLE_LEVEL)
+					{
+						while (draw_rowstogo--)
+							*w++ = transtbl[*e++];
+					}
+					else
+					{
+						while (draw_rowstogo--)
+							*w++ = transtbl[ ( *e++ << 8 ) + *s++ ];
+					}
 
 					relativepos += vid.width;
 				}
@@ -346,6 +402,15 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu)
 	WipeInAction = true;
 	wipe_scr = screens[0];
 
+	// don't know where else to put this.
+	// this any good?
+	if ((gamestate == GS_LEVEL || gamestate == GS_TITLESCREEN)
+	&& (wipestyleflags & (WSF_FADEIN|WSF_FADEOUT)) // only if one of those wipestyleflags are actually set
+	&& !(wipestyleflags & WSF_CROSSFADE)) // and if not crossfading
+		wipestyle = WIPESTYLE_LEVEL;
+	else
+		wipestyle = WIPESTYLE_NORMAL;
+
 	// lastwipetic should either be 0 or the tic we last wiped
 	// on for fade-to-black
 	for (;;)
@@ -362,10 +427,20 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu)
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl)
-			HWR_DoWipe(wipetype, wipeframe-1); // send in the wipe type and wipeframe because we need to cache the graphic
+		{
+			// send in the wipe type and wipe frame because we need to cache the graphic
+			if (wipestyle == WIPESTYLE_LEVEL)
+				HWR_DoTintedWipe(wipetype, wipeframe-1);
+			else
+				HWR_DoWipe(wipetype, wipeframe-1);
+		}
 		else
 #endif
-		F_DoWipe(fmask);
+			F_DoWipe(fmask);
+
+		if (wipestyle == WIPESTYLE_LEVEL)
+			F_WipeStageTitle();
+
 		I_OsPolling();
 		I_UpdateNoBlit();
 
@@ -377,7 +452,9 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu)
 		if (moviemode)
 			M_SaveFrame();
 	}
+
 	WipeInAction = false;
+	WipeStageTitle = false;
 #endif
 }
 
@@ -408,6 +485,8 @@ tic_t F_GetWipeLength(UINT8 wipetype)
 #endif
 }
 
+/** Does the specified wipe exist?
+  */
 boolean F_WipeExists(UINT8 wipetype)
 {
 #ifdef NOWIPE
diff --git a/src/g_game.c b/src/g_game.c
index 63caaa15cb0879a6f6c1c495784eac116b3f33bb..340babb10e3a685e11ba2a3436c6b2a91ca3421d 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -47,6 +47,10 @@
 #include "m_cond.h" // condition sets
 #include "md5.h" // demo checksums
 
+#ifdef HAVE_BLUA
+#include "lua_hud.h"
+#endif
+
 gameaction_t gameaction;
 gamestate_t gamestate = GS_NULL;
 UINT8 ultimatemode = false;
@@ -168,6 +172,7 @@ mapheader_t* mapheaderinfo[NUMMAPS] = {NULL};
 
 static boolean exitgame = false;
 static boolean retrying = false;
+static boolean retryingmodeattack = false;
 
 UINT8 stagefailed; // Used for GEMS BONUS? Also to see if you beat the stage.
 
@@ -536,11 +541,99 @@ void G_AddTempNightsRecords(UINT32 pscore, tic_t ptime, UINT8 mare)
 		ntemprecords.nummares = mare;
 }
 
+//
+// G_UpdateRecordReplays
+//
+// Update replay files/data, etc. for Record Attack
+// See G_SetNightsRecords for NiGHTS Attack.
+//
+static void G_UpdateRecordReplays(void)
+{
+	const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
+	char *gpath;
+	char lastdemo[256], bestdemo[256];
+	UINT8 earnedEmblems;
+
+	// Record new best time
+	if (!mainrecords[gamemap-1])
+		G_AllocMainRecordData(gamemap-1);
+
+	if (players[consoleplayer].score > mainrecords[gamemap-1]->score)
+		mainrecords[gamemap-1]->score = players[consoleplayer].score;
+
+	if ((mainrecords[gamemap-1]->time == 0) || (players[consoleplayer].realtime < mainrecords[gamemap-1]->time))
+		mainrecords[gamemap-1]->time = players[consoleplayer].realtime;
+
+	if ((UINT16)(players[consoleplayer].rings) > mainrecords[gamemap-1]->rings)
+		mainrecords[gamemap-1]->rings = (UINT16)(players[consoleplayer].rings);
+
+	// Save demo!
+	bestdemo[255] = '\0';
+	lastdemo[255] = '\0';
+	G_SetDemoTime(players[consoleplayer].realtime, players[consoleplayer].score, (UINT16)(players[consoleplayer].rings));
+	G_CheckDemoStatus();
+
+	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
+	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
+
+	if ((gpath = malloc(glen)) == NULL)
+		I_Error("Out of memory for replay filepath\n");
+
+	sprintf(gpath,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(gamemap));
+	snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, skins[cv_chooseskin.value-1].name);
+
+	if (FIL_FileExists(lastdemo))
+	{
+		UINT8 *buf;
+		size_t len = FIL_ReadFile(lastdemo, &buf);
+
+		snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+		if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
+		{ // Better time, save this demo.
+			if (FIL_FileExists(bestdemo))
+				remove(bestdemo);
+			FIL_WriteFile(bestdemo, buf, len);
+			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
+		}
+
+		snprintf(bestdemo, 255, "%s-%s-score-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<1)))
+		{ // Better score, save this demo.
+			if (FIL_FileExists(bestdemo))
+				remove(bestdemo);
+			FIL_WriteFile(bestdemo, buf, len);
+			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW HIGH SCORE!"), M_GetText("Saved replay as"), bestdemo);
+		}
+
+		snprintf(bestdemo, 255, "%s-%s-rings-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<2)))
+		{ // Better rings, save this demo.
+			if (FIL_FileExists(bestdemo))
+				remove(bestdemo);
+			FIL_WriteFile(bestdemo, buf, len);
+			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW MOST RINGS!"), M_GetText("Saved replay as"), bestdemo);
+		}
+
+		//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
+
+		Z_Free(buf);
+	}
+	free(gpath);
+
+	// Check emblems when level data is updated
+	if ((earnedEmblems = M_CheckLevelEmblems()))
+		CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for Record Attack records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+
+	// Update timeattack menu's replay availability.
+	Nextmap_OnChange();
+}
+
 void G_SetNightsRecords(void)
 {
 	INT32 i;
 	UINT32 totalscore = 0;
 	tic_t totaltime = 0;
+	UINT8 earnedEmblems;
 
 	const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
 	char *gpath;
@@ -640,6 +733,9 @@ void G_SetNightsRecords(void)
 	}
 	free(gpath);
 
+	if ((earnedEmblems = M_CheckLevelEmblems()))
+		CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for NiGHTS records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+
 	// If the mare count changed, this will update the score display
 	Nextmap_OnChange();
 }
@@ -923,11 +1019,11 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics)
 	movebkey = PLAYER1INPUTDOWN(gc_backward);
 
 	mouseaiming = (PLAYER1INPUTDOWN(gc_mouseaiming)) ^
-		(cv_chasecam.value ? cv_chasefreelook.value : cv_alwaysfreelook.value);
+		((cv_chasecam.value && !player->spectator) ? cv_chasefreelook.value : cv_alwaysfreelook.value);
 	analogjoystickmove = cv_usejoystick.value && !Joystick.bGamepadStyle;
 	gamepadjoystickmove = cv_usejoystick.value && Joystick.bGamepadStyle;
 
-	thisjoyaiming = (cv_chasecam.value) ? cv_chasefreelook.value : cv_alwaysfreelook.value;
+	thisjoyaiming = (cv_chasecam.value && !player->spectator) ? cv_chasefreelook.value : cv_alwaysfreelook.value;
 
 	// Reset the vertical look if we're no longer joyaiming
 	if (!thisjoyaiming && joyaiming)
@@ -1252,11 +1348,11 @@ void G_BuildTiccmd2(ticcmd_t *cmd, INT32 realtics)
 	movebkey = PLAYER2INPUTDOWN(gc_backward);
 
 	mouseaiming = (PLAYER2INPUTDOWN(gc_mouseaiming)) ^
-		(cv_chasecam2.value ? cv_chasefreelook2.value : cv_alwaysfreelook2.value);
+		((cv_chasecam2.value && !player->spectator) ? cv_chasefreelook2.value : cv_alwaysfreelook2.value);
 	analogjoystickmove = cv_usejoystick2.value && !Joystick2.bGamepadStyle;
 	gamepadjoystickmove = cv_usejoystick2.value && Joystick2.bGamepadStyle;
 
-	thisjoyaiming = (cv_chasecam2.value) ? cv_chasefreelook2.value : cv_alwaysfreelook2.value;
+	thisjoyaiming = (cv_chasecam2.value && !player->spectator) ? cv_chasefreelook2.value : cv_alwaysfreelook2.value;
 
 	// Reset the vertical look if we're no longer joyaiming
 	if (!thisjoyaiming && joyaiming)
@@ -1703,6 +1799,58 @@ void G_DoLoadLevel(boolean resetplayer)
 	CON_ClearHUD();
 }
 
+//
+// Start the title card.
+//
+void G_StartTitleCard(void)
+{
+	wipestyleflags |= WSF_FADEIN;
+	wipestyleflags &= ~WSF_FADEOUT;
+
+	// The title card has been disabled for this map.
+	// Oh well.
+	if (mapheaderinfo[gamemap-1]->levelflags & LF_NOTITLECARD)
+	{
+		WipeStageTitle = false;
+		return;
+	}
+
+	// clear the hud
+	CON_ClearHUD();
+
+	// prepare status bar
+	ST_startTitleCard();
+
+	// start the title card
+	WipeStageTitle = (!titlemapinaction);
+}
+
+//
+// Run the title card before fading in to the level.
+//
+void G_PreLevelTitleCard(tic_t ticker, boolean update)
+{
+	tic_t starttime = I_GetTime();
+	tic_t endtime = starttime + (PRELEVELTIME*NEWTICRATERATIO);
+	tic_t nowtime = starttime;
+	tic_t lasttime = starttime;
+	while (nowtime < endtime)
+	{
+		// draw loop
+		while (!((nowtime = I_GetTime()) - lasttime))
+			I_Sleep();
+		lasttime = nowtime;
+
+		ST_runTitleCard();
+		ST_preLevelTitleCardDrawer(ticker, update);
+
+		if (moviemode)
+			M_SaveFrame();
+		if (takescreenshot) // Only take screenshots after drawing.
+			M_DoScreenShot();
+	}
+}
+
 INT32 pausedelay = 0;
 boolean pausebreakkey = false;
 static INT32 camtoggledelay, camtoggledelay2 = 0;
@@ -1868,7 +2016,7 @@ boolean G_Responder(event_t *ev)
 						pausedelay = 1+(NEWTICRATE/2);
 					else if (++pausedelay > 1+(NEWTICRATE/2)+(NEWTICRATE/3))
 					{
-						G_SetRetryFlag();
+						G_SetModeAttackRetryFlag();
 						return true;
 					}
 					pausedelay++; // counteract subsequent subtraction this frame
@@ -1992,7 +2140,7 @@ void G_Ticker(boolean run)
 			if (titledemo)
 				F_TitleDemoTicker();
 			P_Ticker(run); // tic the game
-			ST_Ticker();
+			ST_Ticker(run);
 			F_TextPromptTicker();
 			AM_Ticker();
 			HU_Ticker();
@@ -2175,6 +2323,9 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	outofcoop = players[player].outofcoop;
 	pflags = (players[player].pflags & (PF_FLIPCAM|PF_ANALOGMODE|PF_DIRECTIONCHAR|PF_AUTOBRAKE|PF_TAGIT|PF_GAMETYPEOVER));
 
+	if (!betweenmaps)
+		pflags |= (players[player].pflags & PF_FINISHED);
+
 	// As long as we're not in multiplayer, carry over cheatcodes from map to map
 	if (!(netgame || multiplayer))
 		pflags |= (players[player].pflags & (PF_GODMODE|PF_NOCLIP|PF_INVIS));
@@ -2709,6 +2860,7 @@ void G_DoReborn(INT32 playernum)
 
 			// Do a wipe
 			wipegamestate = -1;
+			wipestyleflags = WSF_CROSSFADE;
 
 			if (camera.chase)
 				P_ResetCamera(&players[displayplayer], &camera);
@@ -2829,10 +2981,35 @@ void G_AddPlayer(INT32 playernum)
 	if (G_GametypeUsesLives() || ((netgame || multiplayer) && gametype == GT_COOP))
 		p->lives = cv_startinglives.value;
 
-	if (countplayers && !notexiting)
+	if ((countplayers && !notexiting) || G_IsSpecialStage(gamemap))
 		P_DoPlayerExit(p);
 }
 
+boolean G_EnoughPlayersFinished(void)
+{
+	UINT8 numneeded = (G_IsSpecialStage(gamemap) ? 4 : cv_playersforexit.value);
+	INT32 total = 0;
+	INT32 exiting = 0;
+	INT32 i;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i] || players[i].spectator || players[i].bot)
+			continue;
+		if (players[i].lives <= 0)
+			continue;
+
+		total++;
+		if ((players[i].pflags & PF_FINISHED) || players[i].exiting)
+			exiting++;
+	}
+
+	if (exiting)
+		return exiting * 4 / total >= numneeded;
+	else
+		return false;
+}
+
 void G_ExitLevel(void)
 {
 	if (gamestate == GS_LEVEL)
@@ -2925,7 +3102,7 @@ boolean G_GametypeUsesLives(void)
 	 // Coop, Competitive
 	if ((gametype == GT_COOP || gametype == GT_COMPETITION)
 	 && !(modeattacking || metalrecording) // No lives in Time Attack
-	 //&& !G_IsSpecialStage(gamemap)
+	 && !G_IsSpecialStage(gamemap)
 	 && !(maptol & TOL_NIGHTS)) // No lives in NiGHTS
 		return true;
 	return false;
@@ -3038,12 +3215,51 @@ static INT16 RandMap(INT16 tolflags, INT16 pprevmap)
 	return ix;
 }
 
+//
+// G_UpdateVisited
+//
+static void G_UpdateVisited(void)
+{
+	boolean spec = G_IsSpecialStage(gamemap);
+	// Update visitation flags?
+	if ((!modifiedgame || savemoddata) // Not modified
+		&& !multiplayer && !demoplayback && (gametype == GT_COOP) // SP/RA/NiGHTS mode
+		&& !(spec && stagefailed)) // Not failed the special stage
+	{
+		UINT8 earnedEmblems;
+
+		// Update visitation flags
+		mapvisited[gamemap-1] |= MV_BEATEN;
+		// eh, what the hell
+		if (ultimatemode)
+			mapvisited[gamemap-1] |= MV_ULTIMATE;
+		// may seem incorrect but IS possible in what the main game uses as special stages, and nummaprings will be -1 in NiGHTS
+		if (nummaprings > 0 && players[consoleplayer].rings >= nummaprings)
+			mapvisited[gamemap-1] |= MV_PERFECT;
+		if (!spec)
+		{
+			// not available to special stages because they can only really be done in one order in an unmodified game, so impossible for first six and trivial for seventh
+			if (ALL7EMERALDS(emeralds))
+				mapvisited[gamemap-1] |= MV_ALLEMERALDS;
+		}
+
+		if (modeattacking == ATTACKING_RECORD)
+			G_UpdateRecordReplays();
+		else if (modeattacking == ATTACKING_NIGHTS)
+			G_SetNightsRecords();
+
+		if ((earnedEmblems = M_CompletionEmblems()))
+			CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+	}
+}
+
 //
 // G_DoCompleted
 //
 static void G_DoCompleted(void)
 {
 	INT32 i;
+	boolean spec = G_IsSpecialStage(gamemap);
 
 	tokenlist = 0; // Reset the list
 
@@ -3076,14 +3292,14 @@ static void G_DoCompleted(void)
 		nextmap = (INT16)(mapheaderinfo[gamemap-1]->nextlevel-1);
 
 	// Remember last map for when you come out of the special stage.
-	if (!G_IsSpecialStage(gamemap))
+	if (!spec)
 		lastmap = nextmap;
 
 	// If nextmap is actually going to get used, make sure it points to
 	// a map of the proper gametype -- skip levels that don't support
 	// the current gametype. (Helps avoid playing boss levels in Race,
 	// for instance).
-	if (!token && !G_IsSpecialStage(gamemap)
+	if (!token && !spec
 		&& (nextmap >= 0 && nextmap < NUMMAPS))
 	{
 		register INT16 cm = nextmap;
@@ -3147,7 +3363,7 @@ static void G_DoCompleted(void)
 			gottoken = false;
 	}
 
-	if (G_IsSpecialStage(gamemap) && !gottoken)
+	if (spec && !gottoken)
 		nextmap = lastmap; // Exiting from a special stage? Go back to the game. Tails 08-11-2001
 
 	automapactive = false;
@@ -3166,17 +3382,29 @@ static void G_DoCompleted(void)
 	if (nextmap < NUMMAPS && !mapheaderinfo[nextmap])
 		P_AllocMapHeader(nextmap);
 
-	if (skipstats && !modeattacking) // Don't skip stats if we're in record attack
+	if ((skipstats && !modeattacking) || (spec && modeattacking && stagefailed))
+	{
+		G_UpdateVisited();
 		G_AfterIntermission();
+	}
 	else
 	{
 		G_SetGamestate(GS_INTERMISSION);
 		Y_StartIntermission();
+		G_UpdateVisited();
 	}
 }
 
 void G_AfterIntermission(void)
 {
+	Y_CleanupScreenBuffer();
+
+	if (modeattacking)
+	{
+		M_EndModeAttackRun();
+		return;
+	}
+
 	HU_ClearCEcho();
 
 	if (mapheaderinfo[gamemap-1]->cutscenenum && !modeattacking && skipstats <= 1) // Start a custom cutscene.
@@ -3343,7 +3571,6 @@ void G_LoadGameData(void)
 	UINT32 recscore;
 	tic_t  rectime;
 	UINT16 recrings;
-	boolean gotperf;
 
 	UINT8 recmares;
 	INT32 curmare;
@@ -3441,7 +3668,7 @@ void G_LoadGameData(void)
 		recscore = READUINT32(save_p);
 		rectime  = (tic_t)READUINT32(save_p);
 		recrings = READUINT16(save_p);
-		gotperf = (boolean)READUINT8(save_p);
+		save_p++; // compat
 
 		if (recrings > 10000 || recscore > MAXSCORE)
 			goto datacorrupt;
@@ -3453,9 +3680,6 @@ void G_LoadGameData(void)
 			mainrecords[i]->time = rectime;
 			mainrecords[i]->rings = recrings;
 		}
-
-		if (gotperf)
-			mainrecords[i]->gotperfect = gotperf;
 	}
 
 	// Nights records
@@ -3587,15 +3811,14 @@ void G_SaveGameData(void)
 			WRITEUINT32(save_p, mainrecords[i]->score);
 			WRITEUINT32(save_p, mainrecords[i]->time);
 			WRITEUINT16(save_p, mainrecords[i]->rings);
-			WRITEUINT8(save_p, mainrecords[i]->gotperfect);
 		}
 		else
 		{
 			WRITEUINT32(save_p, 0);
 			WRITEUINT32(save_p, 0);
 			WRITEUINT16(save_p, 0);
-			WRITEUINT8(save_p, 0);
 		}
+		WRITEUINT8(save_p, 0); // compat
 	}
 
 	// NiGHTS records
@@ -4031,7 +4254,7 @@ char *G_BuildMapTitle(INT32 mapnum)
 		len += strlen(mapheaderinfo[mapnum-1]->lvlttl);
 		if (!(mapheaderinfo[mapnum-1]->levelflags & LF_NOZONE))
 		{
-			zonetext = M_GetText("ZONE");
+			zonetext = M_GetText("Zone");
 			len += strlen(zonetext) + 1;	// ' ' + zonetext
 		}
 		if (actnum > 0)
@@ -4642,6 +4865,12 @@ void G_WriteGhostTic(mobj_t *ghost)
 			oldghost.flags2 |= MF2_AMBUSH;
 		}
 
+		if (ghost->player->followmobj->scale != ghost->scale)
+		{
+			followtic |= FZT_SCALE;
+			WRITEFIXED(demo_p,ghost->player->followmobj->scale);
+		}
+
 		temp = (INT16)((ghost->player->followmobj->x-ghost->x)>>8);
 		WRITEINT16(demo_p,temp);
 		temp = (INT16)((ghost->player->followmobj->y-ghost->y)>>8);
@@ -4653,11 +4882,6 @@ void G_WriteGhostTic(mobj_t *ghost)
 		WRITEUINT16(demo_p,ghost->player->followmobj->sprite);
 		WRITEUINT8(demo_p,(ghost->player->followmobj->frame & FF_FRAMEMASK));
 		WRITEUINT8(demo_p,ghost->player->followmobj->color);
-		if (ghost->player->followmobj->scale != ghost->scale)
-		{
-			followtic |= FZT_SCALE;
-			WRITEFIXED(demo_p,ghost->player->followmobj->scale);
-		}
 
 		*followtic_p = followtic;
 	}
@@ -5194,7 +5418,10 @@ void G_ReadMetalTic(mobj_t *metal)
 	{ // But wait, there's more!
 		xziptic = READUINT8(metal_p);
 		if (xziptic & EZT_FLIP)
+		{
 			metal->eflags ^= MFE_VERTICALFLIP;
+			metal->flags2 ^= MF2_OBJECTFLIP;
+		}
 		if (xziptic & EZT_SCALE)
 		{
 			metal->destscale = READFIXED(metal_p);
@@ -6702,6 +6929,22 @@ boolean G_GetRetryFlag(void)
 	return retrying;
 }
 
+void G_SetModeAttackRetryFlag(void)
+{
+	retryingmodeattack = true;
+	G_SetRetryFlag();
+}
+
+void G_ClearModeAttackRetryFlag(void)
+{
+	retryingmodeattack = false;
+}
+
+boolean G_GetModeAttackRetryFlag(void)
+{
+	return retryingmodeattack;
+}
+
 // Time utility functions
 INT32 G_TicsToHours(tic_t tics)
 {
diff --git a/src/g_game.h b/src/g_game.h
index 2489ff09707a4a2717318320bd3eac47c213dfb7..0a575c099c6badee900584b9851eadf1dfeed3ef 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -140,7 +140,8 @@ void G_SpawnPlayer(INT32 playernum, boolean starpost);
 void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 pickedchar,
 	boolean SSSG, boolean FLS);
 void G_DoLoadLevel(boolean resetplayer);
-
+void G_StartTitleCard(void);
+void G_PreLevelTitleCard(tic_t ticker, boolean update);
 void G_DeferedPlayDemo(const char *demo);
 
 // Can be called by the startup code or M_Responder, calls P_SetupLevel.
@@ -208,6 +209,7 @@ boolean G_GametypeHasSpectators(void);
 boolean G_RingSlingerGametype(void);
 boolean G_PlatformGametype(void);
 boolean G_TagGametype(void);
+boolean G_EnoughPlayersFinished(void);
 void G_ExitLevel(void);
 void G_NextLevel(void);
 void G_Continue(void);
@@ -223,10 +225,14 @@ void G_AddPlayer(INT32 playernum);
 void G_SetExitGameFlag(void);
 void G_ClearExitGameFlag(void);
 boolean G_GetExitGameFlag(void);
+
 void G_SetRetryFlag(void);
 void G_ClearRetryFlag(void);
 boolean G_GetRetryFlag(void);
 
+void G_SetModeAttackRetryFlag(void);
+void G_ClearModeAttackRetryFlag(void);
+boolean G_GetModeAttackRetryFlag(void);
 
 void G_LoadGameData(void);
 void G_LoadGameSettings(void);
diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c
index 6eaafca6d0deec87da0404a1ecb508880e06494c..2d4704b8b1d55bf8127fd311f70c572e6fce0bcf 100644
--- a/src/hardware/hw_cache.c
+++ b/src/hardware/hw_cache.c
@@ -771,18 +771,25 @@ void HWR_InitTextureCache(void)
 	gr_textures2 = NULL;
 }
 
-
 // Callback function for HWR_FreeTextureCache.
 static void FreeMipmapColormap(INT32 patchnum, void *patch)
 {
-	GLPatch_t* const grpatch = patch;
+	GLPatch_t* const pat = patch;
 	(void)patchnum; //unused
-	while (grpatch->mipmap->nextcolormap)
+	while (pat->mipmap && pat->mipmap->nextcolormap) // The mipmap must be valid, obviously
 	{
-		GLMipmap_t *grmip = grpatch->mipmap->nextcolormap;
-		grpatch->mipmap->nextcolormap = grmip->nextcolormap;
-		if (grmip->grInfo.data) Z_Free(grmip->grInfo.data);
-		free(grmip);
+		// Confusing at first, but pat->mipmap->nextcolormap
+		// at the beginning of the loop is the first colormap
+		// from the linked list of colormaps
+		GLMipmap_t *next = pat->mipmap->nextcolormap;
+		// Set the first colormap
+		// to the one that comes after it
+		pat->mipmap->nextcolormap = next->nextcolormap;
+		// Free image data from memory
+		if (next->grInfo.data)
+			Z_Free(next->grInfo.data);
+		// Free the old colormap from memory
+		free(next);
 	}
 }
 
@@ -799,7 +806,7 @@ void HWR_FreeTextureCache(void)
 
 	// Alam: free the Z_Blocks before freeing it's users
 
-	// free all skin after each level: must be done after pfnClearMipMapCache!
+	// free all patch colormaps after each level: must be done after ClearMipMapCache!
 	for (i = 0; i < numwadfiles; i++)
 		M_AATreeIterate(wadfiles[i]->hwrcache, FreeMipmapColormap);
 
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index e493dafacbb74406517bc3a4425de717ed11db6e..4519e1280018d4f69015cba9e32b4df015d190bb 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -380,9 +380,9 @@ void HWR_DrawStretchyFixedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t
 	{
 		FSurfaceInfo Surf;
 		Surf.FlatColor.s.red = Surf.FlatColor.s.green = Surf.FlatColor.s.blue = 0xff;
-		if (alphalevel == 13) Surf.FlatColor.s.alpha = softwaretranstogl_lo[cv_translucenthud.value];
-		else if (alphalevel == 14) Surf.FlatColor.s.alpha = softwaretranstogl[cv_translucenthud.value];
-		else if (alphalevel == 15) Surf.FlatColor.s.alpha = softwaretranstogl_hi[cv_translucenthud.value];
+		if (alphalevel == 13) Surf.FlatColor.s.alpha = softwaretranstogl_lo[st_translucency];
+		else if (alphalevel == 14) Surf.FlatColor.s.alpha = softwaretranstogl[st_translucency];
+		else if (alphalevel == 15) Surf.FlatColor.s.alpha = softwaretranstogl_hi[st_translucency];
 		else Surf.FlatColor.s.alpha = softwaretranstogl[10-alphalevel];
 		flags |= PF_Modulated;
 		HWD.pfnDrawPolygon(&Surf, v, 4, flags);
@@ -538,9 +538,9 @@ void HWR_DrawCroppedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscal
 	{
 		FSurfaceInfo Surf;
 		Surf.FlatColor.s.red = Surf.FlatColor.s.green = Surf.FlatColor.s.blue = 0xff;
-		if (alphalevel == 13) Surf.FlatColor.s.alpha = softwaretranstogl_lo[cv_translucenthud.value];
-		else if (alphalevel == 14) Surf.FlatColor.s.alpha = softwaretranstogl[cv_translucenthud.value];
-		else if (alphalevel == 15) Surf.FlatColor.s.alpha = softwaretranstogl_hi[cv_translucenthud.value];
+		if (alphalevel == 13) Surf.FlatColor.s.alpha = softwaretranstogl_lo[st_translucency];
+		else if (alphalevel == 14) Surf.FlatColor.s.alpha = softwaretranstogl[st_translucency];
+		else if (alphalevel == 15) Surf.FlatColor.s.alpha = softwaretranstogl_hi[st_translucency];
 		else Surf.FlatColor.s.alpha = softwaretranstogl[10-alphalevel];
 		flags |= PF_Modulated;
 		HWD.pfnDrawPolygon(&Surf, v, 4, flags);
diff --git a/src/hardware/hw_light.c b/src/hardware/hw_light.c
index 491cb739f999a82c8013c68fcbe45e7ad3798568..4df71d145903532f9336ab2b18c6e7ee5cd6d94b 100644
--- a/src/hardware/hw_light.c
+++ b/src/hardware/hw_light.c
@@ -184,6 +184,7 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_CANG
 	&lspr[NOLIGHT],     // SPR_PYRE
 	&lspr[NOLIGHT],     // SPR_PTER
+	&lspr[NOLIGHT],     // SPR_DRAB
 
 	// Generic Boos Items
 	&lspr[JETLIGHT_L],     // SPR_JETF // Boss jet fumes
@@ -505,6 +506,7 @@ light_t *t_lspr[NUMSPRITES] =
 	// Game Indicators
 	&lspr[NOLIGHT],     // SPR_SCOR
 	&lspr[NOLIGHT],     // SPR_DRWN
+	&lspr[NOLIGHT],     // SPR_FLII
 	&lspr[NOLIGHT],     // SPR_LCKN
 	&lspr[NOLIGHT],     // SPR_TTAG
 	&lspr[NOLIGHT],     // SPR_GFLG
@@ -584,7 +586,6 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[SUPERSPARK_L], // SPR_BOM3
 	&lspr[NOLIGHT],      // SPR_BOM4
 	&lspr[REDBALL_L],    // SPR_BMNB
-	&lspr[NOLIGHT],      // SPR_WDDB
 
 	// Crumbly rocks
 	&lspr[NOLIGHT],     // SPR_ROIA
@@ -604,8 +605,10 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_ROIO
 	&lspr[NOLIGHT],     // SPR_ROIP
 
-	// Bricks
+	// Level debris
+	&lspr[NOLIGHT], // SPR_GFZD
 	&lspr[NOLIGHT], // SPR_BRIC
+	&lspr[NOLIGHT], // SPR_WDDB
 
 	// Gravity Well Objects
 	&lspr[NOLIGHT],     // SPR_GWLG
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index f9e26733e158acebf84369020fd36a214ae45468..51c976973d6a0db7aaeeaaa7e055af3592497fce 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -40,6 +40,7 @@
 #include "../st_stuff.h"
 #include "../i_system.h"
 #include "../m_cheat.h"
+#include "../f_finale.h"
 #ifdef ESLOPE
 #include "../p_slopes.h"
 #endif
@@ -111,7 +112,6 @@ static consvar_t cv_grclipwalls = {"gr_clipwalls", "Off", 0, CV_OnOff, NULL, 0,
 static consvar_t cv_gralpha = {"gr_alpha", "160", 0, CV_Unsigned, NULL, 0, NULL, NULL, 0, 0, NULL};
 static consvar_t cv_grbeta = {"gr_beta", "0", 0, CV_Unsigned, NULL, 0, NULL, NULL, 0, 0, NULL};
 
-static float HWRWipeCounter = 1.0f;
 consvar_t cv_grrounddown = {"gr_rounddown", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_grfov = {"gr_fov", "90", CV_FLOAT|CV_CALL, grfov_cons_t, CV_grFov_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_grfogdensity = {"gr_fogdensity", "150", CV_CALL|CV_NOINIT, CV_Unsigned,
@@ -1973,7 +1973,7 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 	{
 		// Single sided line... Deal only with the middletexture (if one exists)
 		gr_midtexture = R_GetTextureNum(gr_sidedef->midtexture);
-		if (gr_midtexture && gr_linedef->special != 41) // Ignore horizon line for OGL
+		if (gr_midtexture && gr_linedef->special != HORIZONSPECIAL) // Ignore horizon line for OGL
 		{
 			{
 				fixed_t     texturevpeg;
@@ -4068,6 +4068,7 @@ static gr_vissprite_t *HWR_NewVisSprite(void)
 	return HWR_GetVisSprite(gr_visspritecount++);
 }
 
+#ifdef GLBADSHADOWS
 // Finds a floor through which light does not pass.
 static fixed_t HWR_OpaqueFloorAtPos(fixed_t x, fixed_t y, fixed_t z, fixed_t height)
 {
@@ -4098,6 +4099,7 @@ static fixed_t HWR_OpaqueFloorAtPos(fixed_t x, fixed_t y, fixed_t z, fixed_t hei
 
 	return floorz;
 }
+#endif //#ifdef GLBADSHADOWS
 
 //
 // HWR_DoCulling
@@ -4139,6 +4141,7 @@ static boolean HWR_DoCulling(line_t *cullheight, line_t *viewcullheight, float v
 	return false;
 }
 
+#ifdef GLBADSHADOWS
 static void HWR_DrawSpriteShadow(gr_vissprite_t *spr, GLPatch_t *gpatch, float this_scale)
 {
 	FOutVector swallVerts[4];
@@ -4311,6 +4314,7 @@ static void HWR_DrawSpriteShadow(gr_vissprite_t *spr, GLPatch_t *gpatch, float t
 		HWD.pfnDrawPolygon(&sSurf, swallVerts, 4, PF_Translucent|PF_Modulated|PF_Clip);
 	}
 }
+#endif //#ifdef GLBADSHADOWS
 
 // This is expecting a pointer to an array containing 4 wallVerts for a sprite
 static void HWR_RotateSpritePolyToAim(gr_vissprite_t *spr, FOutVector *wallVerts)
@@ -4386,6 +4390,7 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 	//Hurdler: 25/04/2000: now support colormap in hardware mode
 	HWR_GetMappedPatch(gpatch, spr->colormap);
 
+#ifdef GLBADSHADOWS
 	// Draw shadow BEFORE sprite
 	if (cv_shadow.value // Shadows enabled
 		&& (spr->mobj->flags & (MF_SCENERY|MF_SPAWNCEILING|MF_NOGRAVITY)) != (MF_SCENERY|MF_SPAWNCEILING|MF_NOGRAVITY) // Ceiling scenery have no shadow.
@@ -4401,6 +4406,7 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 		////////////////////
 		HWR_DrawSpriteShadow(spr, gpatch, this_scale);
 	}
+#endif //#ifdef GLBADSHADOWS
 
 	baseWallVerts[0].x = baseWallVerts[3].x = spr->x1;
 	baseWallVerts[2].x = baseWallVerts[1].x = spr->x2;
@@ -4788,6 +4794,7 @@ static void HWR_DrawSprite(gr_vissprite_t *spr)
 	//Hurdler: 25/04/2000: now support colormap in hardware mode
 	HWR_GetMappedPatch(gpatch, spr->colormap);
 
+#ifdef GLBADSHADOWS
 	// Draw shadow BEFORE sprite
 	if (cv_shadow.value // Shadows enabled
 		&& (spr->mobj->flags & (MF_SCENERY|MF_SPAWNCEILING|MF_NOGRAVITY)) != (MF_SCENERY|MF_SPAWNCEILING|MF_NOGRAVITY) // Ceiling scenery have no shadow.
@@ -4803,6 +4810,7 @@ static void HWR_DrawSprite(gr_vissprite_t *spr)
 		////////////////////
 		HWR_DrawSpriteShadow(spr, gpatch, this_scale);
 	}
+#endif //#ifdef GLBADSHADOWS
 
 	// if it has a dispoffset, push it a little towards the camera
 	if (spr->dispoffset) {
@@ -7017,7 +7025,6 @@ void HWR_StartScreenWipe(void)
 
 void HWR_EndScreenWipe(void)
 {
-	HWRWipeCounter = 0.0f;
 	//CONS_Debug(DBG_RENDER, "In HWR_EndScreenWipe()\n");
 	HWD.pfnEndScreenWipe();
 }
@@ -7027,38 +7034,55 @@ void HWR_DrawIntermissionBG(void)
 	HWD.pfnDrawIntermissionBG();
 }
 
-void HWR_DoWipe(UINT8 wipenum, UINT8 scrnnum)
+//
+// hwr mode wipes
+//
+static lumpnum_t wipelumpnum;
+
+// puts wipe lumpname in wipename[9]
+static boolean HWR_WipeCheck(UINT8 wipenum, UINT8 scrnnum)
 {
 	static char lumpname[9] = "FADEmmss";
-	lumpnum_t lumpnum;
 	size_t lsize;
 
-	if (wipenum > 99 || scrnnum > 99) // not a valid wipe number
-		return; // shouldn't end up here really, the loop should've stopped running beforehand
+	// not a valid wipe number
+	if (wipenum > 99 || scrnnum > 99)
+		return false; // shouldn't end up here really, the loop should've stopped running beforehand
 
-	// puts the numbers into the lumpname
-	sprintf(&lumpname[4], "%.2hu%.2hu", (UINT16)wipenum, (UINT16)scrnnum);
-	lumpnum = W_CheckNumForName(lumpname);
+	// puts the numbers into the wipename
+	lumpname[4] = '0'+(wipenum/10);
+	lumpname[5] = '0'+(wipenum%10);
+	lumpname[6] = '0'+(scrnnum/10);
+	lumpname[7] = '0'+(scrnnum%10);
+	wipelumpnum = W_CheckNumForName(lumpname);
 
-	if (lumpnum == LUMPERROR) // again, shouldn't be here really
-		return;
-
-	lsize = W_LumpLength(lumpnum);
+	// again, shouldn't be here really
+	if (wipelumpnum == LUMPERROR)
+		return false;
 
+	lsize = W_LumpLength(wipelumpnum);
 	if (!(lsize == 256000 || lsize == 64000 || lsize == 16000 || lsize == 4000))
 	{
 		CONS_Alert(CONS_WARNING, "Fade mask lump %s of incorrect size, ignored\n", lumpname);
-		return; // again, shouldn't get here if it is a bad size
+		return false; // again, shouldn't get here if it is a bad size
 	}
 
-	HWR_GetFadeMask(lumpnum);
+	return true;
+}
+
+void HWR_DoWipe(UINT8 wipenum, UINT8 scrnnum)
+{
+	if (!HWR_WipeCheck(wipenum, scrnnum))
+		return;
 
+	HWR_GetFadeMask(wipelumpnum);
 	HWD.pfnDoScreenWipe();
+}
 
-	HWRWipeCounter += 0.05f; // increase opacity of end screen
-
-	if (HWRWipeCounter > 1.0f)
-		HWRWipeCounter = 1.0f;
+void HWR_DoTintedWipe(UINT8 wipenum, UINT8 scrnnum)
+{
+	// It does the same thing
+	HWR_DoWipe(wipenum, scrnnum);
 }
 
 void HWR_MakeScreenFinalTexture(void)
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index fa230289fa9363bff35db7763f28cea63847fa90..e19c557d03ecfd3821a24b17d69da945a2b57b44 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -66,6 +66,7 @@ void HWR_StartScreenWipe(void);
 void HWR_EndScreenWipe(void);
 void HWR_DrawIntermissionBG(void);
 void HWR_DoWipe(UINT8 wipenum, UINT8 scrnnum);
+void HWR_DoTintedWipe(UINT8 wipenum, UINT8 scrnnum);
 void HWR_MakeScreenFinalTexture(void);
 void HWR_DrawScreenFinalTexture(int width, int height);
 
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index d208580a9444b07e75f9708af265b9603a47a1d8..8c0ca7155faf94264e53e59163e6213f5dddc00a 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -2587,7 +2587,6 @@ EXPORT void HWRAPI(DoScreenWipe)(void)
 	tex_downloaded = endScreenWipe;
 }
 
-
 // Create a texture from the screen.
 EXPORT void HWRAPI(MakeScreenTexture) (void)
 {
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 90f4ceedfde7a4a65856ff1d7467c354d93d65ce..afca4c77308aec4567caf8561c2407802da84999 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -2322,13 +2322,14 @@ void HU_Erase(void)
 //
 // HU_drawPing
 //
-void HU_drawPing(INT32 x, INT32 y, INT32 ping, boolean notext)
+void HU_drawPing(INT32 x, INT32 y, UINT32 ping, boolean notext, INT32 flags)
 {
 	UINT8 numbars = 1; // how many ping bars do we draw?
 	UINT8 barcolor = 35; // 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);
+	INT32 dx = x+1 - (V_SmallStringWidth(va("%dms", ping),
+				V_ALLOWLOWERCASE|flags)/2);
 
 	if (ping < 128)
 	{
@@ -2342,13 +2343,13 @@ void HU_drawPing(INT32 x, INT32 y, INT32 ping, boolean notext)
 	}
 
 	if (!notext || vid.width >= 640) // how sad, we're using a shit resolution.
-		V_DrawSmallString(dx, y+4, V_ALLOWLOWERCASE, va("%dms", ping));
+		V_DrawSmallString(dx, y+4, V_ALLOWLOWERCASE|flags, 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);
+		V_DrawFill(x+2 *(i-1), y+yoffset-4, 2, 8-yoffset, 31|flags);
 		if (i < numbars)
-			V_DrawFill(x+2 *(i-1), y+yoffset-3, 1, 8-yoffset-1, barcolor);
+			V_DrawFill(x+2 *(i-1), y+yoffset-3, 1, 8-yoffset-1, barcolor|flags);
 
 		yoffset -= 2;
 	}
@@ -2379,7 +2380,7 @@ void HU_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, I
 		if (!splitscreen) // don't draw it on splitscreen,
 		{
 			if (!(tab[i].num == serverplayer))
-				HU_drawPing(x+ 253, y+2, playerpingtable[tab[i].num], false);
+				HU_drawPing(x+ 253, y, playerpingtable[tab[i].num], false, 0);
 			//else
 			//	V_DrawSmallString(x+ 246, y+4, V_YELLOWMAP, "SERVER");
 		}
@@ -2443,7 +2444,7 @@ void HU_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, I
 				V_DrawSmallScaledPatch(x-32, y-4, 0, tagico);
 		}
 
-		if (players[tab[i].num].exiting)
+		if (players[tab[i].num].exiting || (players[tab[i].num].pflags & PF_FINISHED))
 			V_DrawSmallScaledPatch(x - SHORT(exiticon->width)/2 - 1, y-3, 0, exiticon);
 
 		if (gametype == GT_RACE)
@@ -2578,7 +2579,7 @@ static void HU_Draw32TeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		if (!splitscreen)
 		{
 			if (!(tab[i].num == serverplayer))
-				HU_drawPing(x+ 135, y+3, playerpingtable[tab[i].num], true);
+				HU_drawPing(x+ 135, y+1, playerpingtable[tab[i].num], true, 0);
 		//else
 			//V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
 		}
@@ -2702,7 +2703,7 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		if (!splitscreen)
 		{
 			if (!(tab[i].num == serverplayer))
-				HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
+				HU_drawPing(x+ 113, y, playerpingtable[tab[i].num], false, 0);
 		//else
 		//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
 		}
@@ -2733,7 +2734,7 @@ void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scoreline
 
 		strlcpy(name, tab[i].name, 7);
 		if (!(tab[i].num == serverplayer))
-			HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
+			HU_drawPing(x+ 113, y, playerpingtable[tab[i].num], false, 0);
 		//else
 		//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
 
@@ -2747,7 +2748,7 @@ void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scoreline
 		else if (G_TagGametype() && players[tab[i].num].pflags & PF_TAGIT)
 			V_DrawSmallScaledPatch(x-28, y-4, 0, tagico);
 
-		if (players[tab[i].num].exiting)
+		if (players[tab[i].num].exiting || (players[tab[i].num].pflags & PF_FINISHED))
 			V_DrawSmallScaledPatch(x - SHORT(exiticon->width)/2 - 1, y-3, 0, exiticon);
 
 		// Draw emeralds
@@ -2841,7 +2842,7 @@ static void HU_Draw32TabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scor
 		if (!splitscreen) // don't draw it on splitscreen,
 		{
 			if (!(tab[i].num == serverplayer))
-				HU_drawPing(x+ 135, y+3, playerpingtable[tab[i].num], true);
+				HU_drawPing(x+ 135, y+1, playerpingtable[tab[i].num], true, 0);
 		//else
 		//	V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
 		}
diff --git a/src/hu_stuff.h b/src/hu_stuff.h
index 55b61d4b7a995d6ffb4f9fd85958ed0d25d2ba66..47419d29ae187a2dd98f6a162cec7095c6815986 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -28,7 +28,7 @@
 
 // Level title font
 #define LT_FONTSTART '!' // the first font characters
-#define LT_FONTEND 'Z' // the last font characters
+#define LT_FONTEND 'z' // the last font characters
 #define LT_FONTSIZE (LT_FONTEND - LT_FONTSTART + 1)
 
 #define CRED_FONTSTART '!' // the first font character
@@ -113,7 +113,7 @@ 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_drawPing(INT32 x, INT32 y, UINT32 ping, boolean notext, INT32 flags);	// 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/info.c b/src/info.c
index 0583ac549b2f6f91c47226e94e8a19ea8b438957..12dd61a840bd8c83496d392a3d45a5837e5c620f 100644
--- a/src/info.c
+++ b/src/info.c
@@ -72,6 +72,7 @@ char sprnames[NUMSPRITES + 1][5] =
 	"CANG", // Canarivore gas
 	"PYRE", // Pyre Fly
 	"PTER", // Pterabyte
+	"DRAB", // Dragonbomber
 
 	// Generic Boss Items
 	"JETF", // Boss jet fumes
@@ -402,6 +403,7 @@ char sprnames[NUMSPRITES + 1][5] =
 	// Game Indicators
 	"SCOR", // Score logo
 	"DRWN", // Drowning Timer
+	"FLII", // Flight indicator
 	"LCKN", // Target
 	"TTAG", // Tag Sign
 	"GFLG", // Got Flag sign
@@ -481,7 +483,6 @@ char sprnames[NUMSPRITES + 1][5] =
 	"BOM3", // Boss Explosion 2
 	"BOM4", // Underwater Explosion
 	"BMNB", // Mine Explosion
-	"WDDB", // Wood Debris
 
 	// Crumbly rocks
 	"ROIA",
@@ -501,8 +502,10 @@ char sprnames[NUMSPRITES + 1][5] =
 	"ROIO",
 	"ROIP",
 
-	// Bricks
-	"BRIC",
+	// Level debris
+	"GFZD", // GFZ debris
+	"BRIC", // Bricks
+	"WDDB", // Wood Debris
 
 	// Gravity Well Objects
 	"GWLG",
@@ -632,7 +635,7 @@ playersprite_t spr2defaults[NUMPLAYERSPRITES] = {
 	0, // SPR2_TRNS,
 
 	FF_SPR2SUPER|SPR2_STND, // SPR2_NSTD,
-	FF_SPR2SUPER|SPR2_FLT , // SPR2_NFLT,
+	FF_SPR2SUPER|SPR2_FALL, // SPR2_NFLT,
 	0, // SPR2_NFLY, (will never be referenced unless skin 0 lacks this)
 	SPR2_NFLY, // SPR2_NDRL,
 	FF_SPR2SUPER|SPR2_STUN, // SPR2_NSTN,
@@ -1071,23 +1074,11 @@ state_t states[NUMSTATES] =
 	{SPR_MNUD, 2|FF_ANIMATE, 5,  {NULL},           1, 2, S_MINUS_BURST4},   // S_MINUS_BURST3
 	{SPR_MNUD, 3|FF_ANIMATE, 5,  {NULL},           1, 2, S_MINUS_BURST5},   // S_MINUS_BURST4
 	{SPR_MNUD, 4|FF_ANIMATE, 5,  {NULL},           1, 2, S_MINUSDIRT2},     // S_MINUS_BURST5
-	{SPR_MNUS,  0, 1, {A_MinusPopup},   0, 0, S_MINUS_UPWARD1},   // S_MINUS_POPUP
-	{SPR_MNUS,  0, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD2},   // S_MINUS_UPWARD1
-	{SPR_MNUS,  1, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD3},   // S_MINUS_UPWARD2
-	{SPR_MNUS,  2, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD4},   // S_MINUS_UPWARD3
-	{SPR_MNUS,  3, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD5},   // S_MINUS_UPWARD4
-	{SPR_MNUS,  4, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD6},   // S_MINUS_UPWARD5
-	{SPR_MNUS,  5, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD7},   // S_MINUS_UPWARD6
-	{SPR_MNUS,  6, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD8},   // S_MINUS_UPWARD7
-	{SPR_MNUS,  7, 1, {A_MinusCheck},   0, 0, S_MINUS_UPWARD1},   // S_MINUS_UPWARD8
-	{SPR_MNUS,  8, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD2}, // S_MINUS_DOWNWARD1
-	{SPR_MNUS,  9, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD3}, // S_MINUS_DOWNWARD2
-	{SPR_MNUS, 10, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD4}, // S_MINUS_DOWNWARD3
-	{SPR_MNUS, 11, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD5}, // S_MINUS_DOWNWARD4
-	{SPR_MNUS, 12, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD6}, // S_MINUS_DOWNWARD5
-	{SPR_MNUS, 13, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD7}, // S_MINUS_DOWNWARD6
-	{SPR_MNUS, 14, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD8}, // S_MINUS_DOWNWARD7
-	{SPR_MNUS, 15, 1, {A_MinusCheck},   0, 0, S_MINUS_DOWNWARD1}, // S_MINUS_DOWNWARD8
+	{SPR_MNUS, 3, 1, {A_MinusPopup}, 0, 0, S_MINUS_AERIAL1}, // S_MINUS_POPUP
+	{SPR_MNUS, 0, 1, {A_MinusCheck}, 0, 1, S_MINUS_AERIAL2},   // S_MINUS_AERIAL1
+	{SPR_MNUS, 1, 1, {A_MinusCheck}, 0, 1, S_MINUS_AERIAL3},   // S_MINUS_AERIAL2
+	{SPR_MNUS, 2, 1, {A_MinusCheck}, 0, 1, S_MINUS_AERIAL4},   // S_MINUS_AERIAL3
+	{SPR_MNUS, 3, 1, {A_MinusCheck}, 0, 1, S_MINUS_AERIAL1},   // S_MINUS_AERIAL4
 
 	{SPR_MNUD, FF_ANIMATE, 6, {NULL}, 1, 5, S_MINUSDIRT2}, // S_MINUSDIRT1
 	{SPR_MNUD, 5,          8, {NULL}, 3, 5, S_MINUSDIRT3}, // S_MINUSDIRT2
@@ -1163,6 +1154,26 @@ state_t states[NUMSTATES] =
 	{SPR_PTER, 4,  1, {NULL},              0, 0, S_PTERABYTE_SWOOPDOWN}, // S_PTERABYTE_SWOOPDOWN
 	{SPR_PTER, 0,  1, {NULL},              0, 0, S_PTERABYTE_SWOOPUP},   // S_PTERABYTE_SWOOPUP
 
+	// Dragonbomber
+	{SPR_DRAB,                0, -1, {A_DragonbomberSpawn}, 6, 0, S_NULL},                          // S_DRAGONBOMBER
+	{SPR_DRAB, FF_PAPERSPRITE|7,  1,        {A_DragonWing}, 0, 0, S_DRAGONWING2},                   // S_DRAGONWING1
+	{SPR_DRAB, FF_PAPERSPRITE|8,  1,        {A_DragonWing}, 0, 0, S_DRAGONWING3},                   // S_DRAGONWING2
+	{SPR_DRAB, FF_PAPERSPRITE|9,  1,        {A_DragonWing}, 0, 0, S_DRAGONWING4},                   // S_DRAGONWING3
+	{SPR_DRAB, FF_PAPERSPRITE|10, 1,        {A_DragonWing}, 0, 0, S_DRAGONWING1},                   // S_DRAGONWING4
+	{SPR_DRAB,                1,  1,     {A_DragonSegment}, 0, 0, S_DRAGONTAIL_LOADED},             // S_DRAGONTAIL_LOADED
+	{SPR_DRAB,                2,  1,     {A_DragonSegment}, 0, 0, S_DRAGONTAIL_EMPTYLOOP},          // S_DRAGONTAIL_EMPTY
+	{SPR_DRAB,                2,  0,            {A_Repeat}, 3*TICRATE, S_DRAGONTAIL_EMPTY, S_DRAGONTAIL_RELOAD}, // S_DRAGONTAIL_EMPTYLOOP
+	{SPR_DRAB,                1,  0,   {A_PlayActiveSound}, 0, 0, S_DRAGONTAIL_LOADED},             // S_DRAGONTAIL_RELOAD
+	{SPR_DRAB,                3,  1,        {A_MinusCheck}, S_DRAGONMINE_LAND1, 0, S_DRAGONMINE},   // S_DRAGONMINE
+	{SPR_DRAB,                4,  0,   {A_PlayActiveSound}, 0, 0, S_DRAGONMINE_LAND2},              // S_DRAGONMINE_LAND1
+	{SPR_DRAB,                4,  2,            {A_Thrust}, 0, 1, S_DRAGONMINE_SLOWFLASH1},         // S_DRAGONMINE_LAND2
+	{SPR_DRAB,                5, 11,                {NULL}, 0, 0, S_DRAGONMINE_SLOWFLASH2},         // S_DRAGONMINE_SLOWFLASH1
+	{SPR_DRAB,  FF_FULLBRIGHT|6,  1,   {A_PlayAttackSound}, 0, 0, S_DRAGONMINE_SLOWLOOP},           // S_DRAGONMINE_SLOWFLASH2
+	{SPR_DRAB,                5,  0,            {A_Repeat}, 4, S_DRAGONMINE_SLOWFLASH1, S_DRAGONMINE_FASTFLASH1}, // S_DRAGONMINE_SLOWLOOP
+	{SPR_DRAB,                5,  3,                {NULL}, 0, 0, S_DRAGONMINE_FASTFLASH2},         // S_DRAGONMINE_FASTFLASH1
+	{SPR_DRAB,  FF_FULLBRIGHT|6,  1,   {A_PlayAttackSound}, 0, 0, S_DRAGONMINE_FASTLOOP},           // S_DRAGONMINE_FASTFLASH2
+	{SPR_DRAB,                5,  0,            {A_Repeat}, 5, S_DRAGONMINE_FASTFLASH1, S_DEATHSTATE}, // S_DRAGONMINE_FASTLOOP
+
 	// Boss Explosion
 	{SPR_BOM2, FF_FULLBRIGHT|FF_ANIMATE, (5*7), {NULL}, 6, 5, S_NULL}, // S_BOSSEXPLODE
 
@@ -1487,6 +1498,7 @@ state_t states[NUMSTATES] =
 	{SPR_FSGN, 0|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_FSGNA
 	{SPR_FSGN, 1|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_FSGNB
 	{SPR_FSGN, 2|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_FSGNC
+	{SPR_FSGN, 3|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_FSGND
 
 	// Black Eggman (Boss 7)
 	{SPR_BRAK, 0, 1, {A_SetReactionTime}, 0, 0, S_BLACKEGG_STND2}, // S_BLACKEGG_STND
@@ -3319,6 +3331,9 @@ state_t states[NUMSTATES] =
 	{SPR_DRWN, 10, 40, {NULL}, 0, 0, S_NULL}, // S_FOUR2
 	{SPR_DRWN, 11, 40, {NULL}, 0, 0, S_NULL}, // S_FIVE2
 
+	// Flight indicator
+	{SPR_FLII, FF_FULLBRIGHT|FF_ANIMATE|0, -1, {NULL}, 4, 4, S_NULL}, // S_FLIGHTINDICATOR
+
 	{SPR_LCKN,   FF_FULLBRIGHT, 2, {NULL}, 0, 0, S_NULL}, // S_LOCKON1
 	{SPR_LCKN, 1|FF_FULLBRIGHT, 2, {NULL}, 0, 0, S_NULL}, // S_LOCKON2
 	{SPR_LCKN, 2|FF_FULLBRIGHT, 2, {NULL}, 0, 0, S_NULL}, // S_LOCKON3
@@ -3527,13 +3542,9 @@ state_t states[NUMSTATES] =
 	{SPR_FFWR, 3, 3, {NULL}, 0, 0, S_FIREFLOWER1}, // S_FIREFLOWER4
 
 	// Thrown Mario Fireball
-	{SPR_FBLL, FF_FULLBRIGHT,   3, {NULL}, 0, 0, S_FIREBALL2},    // S_FIREBALL1
-	{SPR_FBLL, FF_FULLBRIGHT|1, 3, {NULL}, 0, 0, S_FIREBALL3},    // S_FIREBALL2
-	{SPR_FBLL, FF_FULLBRIGHT|2, 3, {NULL}, 0, 0, S_FIREBALL4},    // S_FIREBALL3
-	{SPR_FBLL, FF_FULLBRIGHT|3, 3, {NULL}, 0, 0, S_FIREBALL1},    // S_FIREBALL4
-	{SPR_FBLL, FF_FULLBRIGHT|4, 3, {NULL}, 0, 0, S_FIREBALLEXP2}, // S_FIREBALLEXP1
-	{SPR_FBLL, FF_FULLBRIGHT|5, 3, {NULL}, 0, 0, S_FIREBALLEXP3}, // S_FIREBALLEXP2
-	{SPR_FBLL, FF_FULLBRIGHT|6, 3, {NULL}, 0, 0, S_NULL},         // S_FIREBALLEXP3
+	{SPR_FBLL, FF_FULLBRIGHT,   1, {A_SpawnObjectRelative}, 0, MT_FIREBALLTRAIL, S_FIREBALL},   // S_FIREBALL
+	{SPR_FBLL, 1|FF_FULLBRIGHT|FF_TRANS50, 1, {A_SetScale}, FRACUNIT*3/4, 0, S_FIREBALLTRAIL2}, // S_FIREBALLTRAIL1
+	{SPR_FBLL, 1|FF_FULLBRIGHT|FF_TRANS50, 8, {A_SetScale}, FRACUNIT/6, 1, S_NULL},             // S_FIREBALLTRAIL2
 
 	// Turtle Shell
 	{SPR_SHLL, 0, -1, {NULL}, 0, 0, S_NULL}, // S_SHELL
@@ -3722,9 +3733,12 @@ state_t states[NUMSTATES] =
 	{SPR_PENG, 0,  5, {A_FaceTarget},      0,  0, S_PENGUINATOR_LOOK},   // S_PENGUINATOR_SLIDE5
 
 	{SPR_POPH, 0,  2, {A_Look},  (2048<<16)|1,           0, S_POPHAT_LOOK},   // S_POPHAT_LOOK
-	{SPR_POPH, 1,  2, {A_LobShot}, MT_POPSHOT, (70<<16)|60, S_POPHAT_SHOOT2}, // S_POPHAT_SHOOT1
-	{SPR_POPH, 2,  1, {NULL},               0,           0, S_POPHAT_SHOOT3}, // S_POPHAT_SHOOT2
-	{SPR_POPH, 0, 57, {NULL},               0,           0, S_POPHAT_LOOK},   // S_POPHAT_SHOOT3
+	{SPR_POPH, 1,  0, {A_MultiShotDist}, (MT_SPINDUST<<16)|4, 24, S_POPHAT_SHOOT2}, // S_POPHAT_SHOOT1
+	{SPR_POPH, 1,  2, {A_LobShot}, MT_POPSHOT, (70<<16)|60, S_POPHAT_SHOOT3}, // S_POPHAT_SHOOT2
+	{SPR_POPH, 2,  1, {NULL},               0,           0, S_POPHAT_SHOOT4}, // S_POPHAT_SHOOT3
+	{SPR_POPH, 0, 57, {NULL},               0,           0, S_POPHAT_LOOK},   // S_POPHAT_SHOOT4
+	{SPR_POPH, 3,  3, {A_SpawnObjectRelative}, 0, MT_POPSHOT_TRAIL, S_POPSHOT},   // S_POPSHOT
+	{SPR_NULL, 0,  2, {NULL},                  0, 0,                S_SPINDUST1}, // S_POPSHOT_TRAIL
 
 	{SPR_HIVE, 0,  5, {A_Look}, 1, 1, S_HIVEELEMENTAL_LOOK}, // S_HIVEELEMENTAL_LOOK
 	{SPR_HIVE, 0, 14, {A_PlaySound}, sfx_s3k76, 1, S_HIVEELEMENTAL_PREPARE2}, // S_HIVEELEMENTAL_PREPARE1
@@ -3858,8 +3872,6 @@ state_t states[NUMSTATES] =
 	{SPR_DUST, 2|FF_TRANS60, 3, {NULL}, 0, 0, S_DUST4}, // S_DUST3
 	{SPR_DUST, 3|FF_TRANS70, 2, {NULL}, 0, 0, S_NULL},  // S_DUST4
 
-	{SPR_WDDB, FF_ANIMATE, -1, {A_DebrisRandom}, 7, 2, S_NULL},  // S_WOODDEBRIS
-
 	{SPR_NULL, 0, 1, {A_RockSpawn}, 0, 0, S_ROCKSPAWN}, // S_ROCKSPAWN
 
 	{SPR_ROIA, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 4, 2, S_NULL}, // S_ROCKCRUMBLEA
@@ -3879,7 +3891,9 @@ state_t states[NUMSTATES] =
 	{SPR_ROIO, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 7, 2, S_NULL}, // S_ROCKCRUMBLEO
 	{SPR_ROIP, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 7, 2, S_NULL}, // S_ROCKCRUMBLEP
 
+	{SPR_GFZD, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 31, 1, S_NULL}, // S_GFZDEBRIS
 	{SPR_BRIC, FF_ANIMATE, -1, {A_DebrisRandom}, 7, 2, S_NULL}, // S_BRICKDEBRIS
+	{SPR_WDDB, FF_ANIMATE, -1, {A_DebrisRandom}, 7, 2, S_NULL}, // S_WOODDEBRIS
 
 #ifdef SEENAMES
 	{SPR_NULL, 0, 1, {NULL}, 0, 0, S_NULL}, // S_NAMECHECK
@@ -5238,6 +5252,114 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_DRAGONBOMBER
+		137,            // doomednum
+		S_DRAGONBOMBER, // spawnstate
+		1,              // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		0,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		6,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_XPLD_FLICKY,  // deathstate
+		S_NULL,         // xdeathstate
+		sfx_pop,        // deathsound
+		10*FRACUNIT,    // speed
+		28*FRACUNIT,    // radius
+		48*FRACUNIT,    // height
+		0,              // display offset
+		100,            // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_SPECIAL|MF_SHOOTABLE|MF_ENEMY|MF_NOGRAVITY|MF_BOUNCE|MF_RUNSPAWNFUNC, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_DRAGONWING
+		-1,             // doomednum
+		S_DRAGONWING1,  // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		0,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_XPLD_FLICKY,  // deathstate
+		S_NULL,         // xdeathstate
+		sfx_pop,        // deathsound
+		0,              // speed
+		12*FRACUNIT,    // radius
+		12*FRACUNIT,    // height
+		0,              // display offset
+		100,            // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_NOGRAVITY|MF_SCENERY|MF_NOBLOCKMAP|MF_NOCLIP, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_DRAGONTAIL
+		-1,             // doomednum
+		S_DRAGONTAIL_LOADED, // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		0,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		MT_DRAGONMINE,  // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_XPLD1,        // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		20*FRACUNIT,    // radius
+		40*FRACUNIT,    // height
+		0,              // display offset
+		100,            // mass
+		0,              // damage
+		sfx_tink,       // activesound
+		MF_NOGRAVITY|MF_SLIDEME|MF_PAIN, // flags
+		S_DRAGONTAIL_EMPTY // raisestate
+	},
+
+	{           // MT_DRAGONMINE
+		-1,             // doomednum
+		S_DRAGONMINE,   // spawnstate
+		1,              // spawnhealth
+		S_NULL,         // seestate
+		sfx_s3k76,      // seesound
+		0,              // reactiontime
+		sfx_s3k89,      // attacksound
+		S_NULL,         // painstate
+		6,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_TNTBARREL_EXPL1, // deathstate
+		S_NULL,         // xdeathstate
+		sfx_s3k6e,      // deathsound
+		0,              // speed
+		16*FRACUNIT,    // radius
+		32*FRACUNIT,    // height
+		0,              // display offset
+		100,            // mass
+		0,              // damage
+		sfx_s3k5d,      // activesound
+		MF_SPECIAL|MF_SHOOTABLE, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_BOSSEXPLODE
 		-1,             // doomednum
 		S_BOSSEXPLODE,  // spawnstate
@@ -6115,11 +6237,11 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,            // meleestate
 		S_NULL,            // missilestate
 		S_FSGNC,           // deathstate
-		S_NULL,            // xdeathstate
+		S_FSGND,           // xdeathstate
 		sfx_None,          // deathsound
 		0,                 // speed
-		124*FRACUNIT,      // radius
-		640*FRACUNIT,      // height
+		74*FRACUNIT,       // radius
+		320*FRACUNIT,      // height
 		0,                 // display offset
 		0,                 // mass
 		0,                 // damage
@@ -13247,7 +13369,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		100,            // mass
 		0,              // damage
 		sfx_None,       // activesound
-		MF_SPECIAL|MF_PAIN|MF_NOGRAVITY, // flags
+		MF_SPECIAL|MF_PAIN|MF_NOGRAVITY|MF_FIRE, // flags
 		S_NULL          // raisestate
 	},
 
@@ -13315,7 +13437,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_None,       // attacksound
 		S_NULL,         // painstate
 		12*TICRATE,     // painchance (sets how long an unridden rock should last before disappearing - set to 0 to disable)
-		sfx_None,       // painsound
+		sfx_s3k49,      // painsound
 		S_NULL,         // meleestate
 		S_NULL,         // missilestate
 		S_NULL,         // deathstate
@@ -18910,29 +19032,56 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 
 	{           // MT_FIREBALL
 		-1,             // doomednum
-		S_FIREBALL1,    // spawnstate
+		S_FIREBALL,     // spawnstate
 		1000,           // spawnhealth
-		S_FIREBALLEXP1, // seestate
+		S_NULL,         // seestate
 		sfx_None,       // seesound
 		8,              // reactiontime
 		sfx_None,       // attacksound
 		S_NULL,         // painstate
 		0,              // painchance
 		sfx_None,       // painsound
-		S_FIREBALLEXP1, // meleestate
-		S_FIREBALLEXP1, // missilestate
-		S_FIREBALLEXP1, // deathstate
-		S_FIREBALLEXP1, // xdeathstate
-		sfx_mario1,     // deathsound
-		10*FRACUNIT,    // speed
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		40*FRACUNIT,    // speed
 		4*FRACUNIT,     // radius
 		8*FRACUNIT,     // height
 		0,              // display offset
 		DMG_FIRE,       // mass
 		1,              // damage
 		sfx_None,       // activesound
-		MF_NOBLOCKMAP|MF_FIRE|MF_MISSILE, // flags
-		S_NULL          // raisestate
+		MF_FIRE|MF_BOUNCE|MF_MISSILE, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_FIREBALLTRAIL
+		-1,                  // doomednum
+		S_FIREBALLTRAIL1,    // spawnstate
+		1000,                // spawnhealth
+		S_NULL,              // seestate
+		sfx_None,            // seesound
+		8,                   // reactiontime
+		sfx_None,            // attacksound
+		S_NULL,              // painstate
+		0,                   // painchance
+		sfx_None,            // painsound
+		S_NULL,              // meleestate
+		S_NULL,              // missilestate
+		S_NULL,              // deathstate
+		S_NULL,              // xdeathstate
+		sfx_None,            // deathsound
+		0,                   // speed
+		16*FRACUNIT,         // radius
+		16*FRACUNIT,         // height
+		0,                   // display offset
+		0,                   // mass
+		1,                   // damage
+		sfx_None,            // activesound
+		MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIP|MF_RUNSPAWNFUNC, // flags
+		S_NULL               // raisestate
 	},
 
 	{           // MT_SHELL
@@ -19989,10 +20138,10 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 
 	{           // MT_POPSHOT
 		-1,             // doomednum
-		S_ROCKCRUMBLEI, // spawnstate
+		S_POPSHOT,      // spawnstate
 		1,              // spawnhealth
 		S_NULL,         // seestate
-		sfx_cannon,     // seesound
+		sfx_kc4c,       // seesound
 		0,              // reactiontime
 		sfx_None,       // attacksound
 		S_NULL,         // painstate
@@ -20000,9 +20149,9 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_None,       // painsound
 		S_NULL,         // meleestate
 		S_NULL,         // missilestate
-		S_XPLD1,        // deathstate
+		S_SONIC3KBOSSEXPLOSION1, // deathstate
 		S_NULL,         // xdeathstate
-		sfx_pop,        // deathsound
+		sfx_cybdth,     // deathsound
 		0,              // speed
 		16*FRACUNIT,    // radius
 		32*FRACUNIT,    // height
@@ -20014,6 +20163,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_POPSHOT_TRAIL
+		-1,             // doomednum
+		S_POPSHOT_TRAIL,// spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		4*FRACUNIT,     // speed
+		4*FRACUNIT,     // radius
+		4*FRACUNIT,     // height
+		0,              // display offset
+		4,              // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_HIVEELEMENTAL
 		127,            // doomednum
 		S_HIVEELEMENTAL_LOOK, // spawnstate
@@ -20773,33 +20949,6 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
-	{           // MT_WOODDEBRIS
-		-1,             // doomednum
-		S_WOODDEBRIS,   // spawnstate
-		1,              // spawnhealth
-		S_NULL,         // seestate
-		sfx_None,       // seesound
-		0,              // reactiontime
-		sfx_None,       // attacksound
-		S_NULL,         // painstate
-		0,              // painchance
-		sfx_None,       // painsound
-		S_NULL,         // meleestate
-		S_NULL,         // missilestate
-		S_NULL,         // deathstate
-		S_NULL,         // xdeathstate
-		sfx_None,       // deathsound
-		0,              // speed
-		16*FRACUNIT,    // radius
-		16*FRACUNIT,    // height
-		0,              // display offset
-		100,            // mass
-		0,              // damage
-		sfx_wbreak,     // activesound
-		MF_NOBLOCKMAP|MF_NOCLIPTHING|MF_RUNSPAWNFUNC|MF_NOCLIPHEIGHT|MF_SCENERY, // flags
-		S_NULL          // raisestate
-	},
-
 	{           // MT_ROCKSPAWNER
 		1202,           // doomednum
 		S_ROCKSPAWN,    // spawnstate
@@ -21286,16 +21435,43 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_GFZDEBRIS
+		-1,             // doomednum
+		S_GFZDEBRIS,    // spawnstate
+		1,              // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		0,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		32*FRACUNIT,    // radius
+		64*FRACUNIT,    // height
+		0,              // display offset
+		100,            // mass
+		0,              // damage
+		sfx_crumbl,     // activesound
+		MF_NOBLOCKMAP|MF_NOCLIPTHING|MF_RUNSPAWNFUNC|MF_NOCLIPHEIGHT|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_BRICKDEBRIS
 		-1,             // doomednum
-		S_BRICKDEBRIS, // spawnstate
-		1,           // spawnhealth
+		S_BRICKDEBRIS,  // spawnstate
+		1,              // spawnhealth
 		S_NULL,         // seestate
-		sfx_None,     // seesound
+		sfx_None,       // seesound
 		0,              // reactiontime
 		sfx_None,       // attacksound
 		S_NULL,         // painstate
-		0,            // painchance
+		0,              // painchance
 		sfx_None,       // painsound
 		S_NULL,         // meleestate
 		S_NULL,         // missilestate
@@ -21303,16 +21479,43 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_None,       // deathsound
 		0,              // speed
-		16*FRACUNIT,     // radius
+		16*FRACUNIT,    // radius
 		16*FRACUNIT,    // height
 		0,              // display offset
-		100,           // mass
+		100,            // mass
 		0,              // damage
-		sfx_None,     // activesound
+		sfx_None,       // activesound
 		MF_NOBLOCKMAP|MF_NOCLIPTHING|MF_RUNSPAWNFUNC|MF_NOCLIPHEIGHT|MF_SCENERY,  // flags
 		S_NULL          // raisestate
 	},
 
+	{           // MT_WOODDEBRIS
+		-1,             // doomednum
+		S_WOODDEBRIS,   // spawnstate
+		1,              // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		0,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		16*FRACUNIT,    // radius
+		16*FRACUNIT,    // height
+		0,              // display offset
+		100,            // mass
+		0,              // damage
+		sfx_wbreak,     // activesound
+		MF_NOBLOCKMAP|MF_NOCLIPTHING|MF_RUNSPAWNFUNC|MF_NOCLIPHEIGHT|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
 #ifdef SEENAMES
 	{           // MT_NAMECHECK
 		-1,             // doomednum
diff --git a/src/info.h b/src/info.h
index bbb6a21fcd37d44b219705c31b1dcfef0f168ad9..f3f50fab79a1d3ebde11380ee9c7939dbe7b6f45 100644
--- a/src/info.h
+++ b/src/info.h
@@ -282,6 +282,9 @@ void A_SpawnPterabytes();
 void A_PterabyteHover();
 void A_RolloutSpawn();
 void A_RolloutRock();
+void A_DragonbomberSpawn();
+void A_DragonWing();
+void A_DragonSegment();
 
 // ratio of states to sprites to mobj types is roughly 6 : 1 : 1
 #define NUMMOBJFREESLOTS 512
@@ -334,6 +337,7 @@ typedef enum sprite
 	SPR_CANG, // Canarivore gas
 	SPR_PYRE, // Pyre Fly
 	SPR_PTER, // Pterabyte
+	SPR_DRAB, // Dragonbomber
 
 	// Generic Boss Items
 	SPR_JETF, // Boss jet fumes
@@ -664,6 +668,7 @@ typedef enum sprite
 	// Game Indicators
 	SPR_SCOR, // Score logo
 	SPR_DRWN, // Drowning Timer
+	SPR_FLII, // AI flight indicator
 	SPR_LCKN, // Target
 	SPR_TTAG, // Tag Sign
 	SPR_GFLG, // Got Flag sign
@@ -743,7 +748,6 @@ typedef enum sprite
 	SPR_BOM3, // Boss Explosion 2
 	SPR_BOM4, // Underwater Explosion
 	SPR_BMNB, // Mine Explosion
-	SPR_WDDB, // Wood Debris
 
 	// Crumbly rocks
 	SPR_ROIA,
@@ -763,8 +767,10 @@ typedef enum sprite
 	SPR_ROIO,
 	SPR_ROIP,
 
-	// Bricks
-	SPR_BRIC,
+	// Level debris
+	SPR_GFZD, // GFZ debris
+	SPR_BRIC, // Bricks
+	SPR_WDDB, // Wood Debris
 
 	// Gravity Well Objects
 	SPR_GWLG,
@@ -1264,22 +1270,10 @@ typedef enum state
 	S_MINUS_BURST4,
 	S_MINUS_BURST5,
 	S_MINUS_POPUP,
-	S_MINUS_UPWARD1,
-	S_MINUS_UPWARD2,
-	S_MINUS_UPWARD3,
-	S_MINUS_UPWARD4,
-	S_MINUS_UPWARD5,
-	S_MINUS_UPWARD6,
-	S_MINUS_UPWARD7,
-	S_MINUS_UPWARD8,
-	S_MINUS_DOWNWARD1,
-	S_MINUS_DOWNWARD2,
-	S_MINUS_DOWNWARD3,
-	S_MINUS_DOWNWARD4,
-	S_MINUS_DOWNWARD5,
-	S_MINUS_DOWNWARD6,
-	S_MINUS_DOWNWARD7,
-	S_MINUS_DOWNWARD8,
+	S_MINUS_AERIAL1,
+	S_MINUS_AERIAL2,
+	S_MINUS_AERIAL3,
+	S_MINUS_AERIAL4,
 
 	// Minus dirt
 	S_MINUSDIRT1,
@@ -1355,6 +1349,26 @@ typedef enum state
 	S_PTERABYTE_SWOOPDOWN,
 	S_PTERABYTE_SWOOPUP,
 
+	// Dragonbomber
+	S_DRAGONBOMBER,
+	S_DRAGONWING1,
+	S_DRAGONWING2,
+	S_DRAGONWING3,
+	S_DRAGONWING4,
+	S_DRAGONTAIL_LOADED,
+	S_DRAGONTAIL_EMPTY,
+	S_DRAGONTAIL_EMPTYLOOP,
+	S_DRAGONTAIL_RELOAD,
+	S_DRAGONMINE,
+	S_DRAGONMINE_LAND1,
+	S_DRAGONMINE_LAND2,
+	S_DRAGONMINE_SLOWFLASH1,
+	S_DRAGONMINE_SLOWFLASH2,
+	S_DRAGONMINE_SLOWLOOP,
+	S_DRAGONMINE_FASTFLASH1,
+	S_DRAGONMINE_FASTFLASH2,
+	S_DRAGONMINE_FASTLOOP,
+
 	// Boss Explosion
 	S_BOSSEXPLODE,
 
@@ -1657,6 +1671,7 @@ typedef enum state
 	S_FSGNA,
 	S_FSGNB,
 	S_FSGNC,
+	S_FSGND,
 
 	// Black Eggman (Boss 7)
 	S_BLACKEGG_STND,
@@ -3454,6 +3469,8 @@ typedef enum state
 	S_FOUR2,
 	S_FIVE2,
 
+	S_FLIGHTINDICATOR,
+
 	S_LOCKON1,
 	S_LOCKON2,
 	S_LOCKON3,
@@ -3637,13 +3654,9 @@ typedef enum state
 	S_FIREFLOWER2,
 	S_FIREFLOWER3,
 	S_FIREFLOWER4,
-	S_FIREBALL1,
-	S_FIREBALL2,
-	S_FIREBALL3,
-	S_FIREBALL4,
-	S_FIREBALLEXP1,
-	S_FIREBALLEXP2,
-	S_FIREBALLEXP3,
+	S_FIREBALL,
+	S_FIREBALLTRAIL1,
+	S_FIREBALLTRAIL2,
 	S_SHELL,
 	S_PUMA_START1,
 	S_PUMA_START2,
@@ -3812,6 +3825,9 @@ typedef enum state
 	S_POPHAT_SHOOT1,
 	S_POPHAT_SHOOT2,
 	S_POPHAT_SHOOT3,
+	S_POPHAT_SHOOT4,
+	S_POPSHOT,
+	S_POPSHOT_TRAIL,
 
 	S_HIVEELEMENTAL_LOOK,
 	S_HIVEELEMENTAL_PREPARE1,
@@ -3943,8 +3959,6 @@ typedef enum state
 	S_DUST3,
 	S_DUST4,
 
-	S_WOODDEBRIS,
-
 	S_ROCKSPAWN,
 
 	S_ROCKCRUMBLEA,
@@ -3964,8 +3978,10 @@ typedef enum state
 	S_ROCKCRUMBLEO,
 	S_ROCKCRUMBLEP,
 
-	// Bricks
+	// Level debris
+	S_GFZDEBRIS,
 	S_BRICKDEBRIS,
+	S_WOODDEBRIS,
 
 #ifdef SEENAMES
 	S_NAMECHECK,
@@ -4049,6 +4065,10 @@ typedef enum mobj_type
 	MT_PTERABYTESPAWNER, // Pterabyte spawner
 	MT_PTERABYTEWAYPOINT, // Pterabyte waypoint
 	MT_PTERABYTE, // Pterabyte
+	MT_DRAGONBOMBER, // Dragonbomber
+	MT_DRAGONWING, // Dragonbomber wing
+	MT_DRAGONTAIL, // Dragonbomber tail segment
+	MT_DRAGONMINE, // Dragonbomber mine
 
 	// Generic Boss Items
 	MT_BOSSEXPLODE,
@@ -4654,6 +4674,7 @@ typedef enum mobj_type
 	MT_BLUEGOOMBA,
 	MT_FIREFLOWER,
 	MT_FIREBALL,
+	MT_FIREBALLTRAIL,
 	MT_SHELL,
 	MT_PUMA,
 	MT_PUMATRAIL,
@@ -4698,6 +4719,7 @@ typedef enum mobj_type
 	MT_PENGUINATOR,
 	MT_POPHAT,
 	MT_POPSHOT,
+	MT_POPSHOT_TRAIL,
 
 	MT_HIVEELEMENTAL,
 	MT_BUMBLEBORE,
@@ -4735,7 +4757,6 @@ typedef enum mobj_type
 	MT_EXPLODE, // Robot Explosion
 	MT_UWEXPLODE, // Underwater Explosion
 	MT_DUST,
-	MT_WOODDEBRIS,
 	MT_ROCKSPAWNER,
 	MT_FALLINGROCK,
 	MT_ROCKCRUMBLE1,
@@ -4755,8 +4776,10 @@ typedef enum mobj_type
 	MT_ROCKCRUMBLE15,
 	MT_ROCKCRUMBLE16,
 
-	// Bricks
+	// Level debris
+	MT_GFZDEBRIS,
 	MT_BRICKDEBRIS,
+	MT_WOODDEBRIS,
 
 #ifdef SEENAMES
 	MT_NAMECHECK,
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 2e14b3f269ca8bdc32436881b98f69a13ed6872e..03f1aff188ccd1db9a086fc65ebb853cb59781c1 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1165,6 +1165,17 @@ static int lib_pElementalFire(lua_State *L)
 	return 0;
 }
 
+static int lib_pDoPlayerFinish(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	P_DoPlayerFinish(player);
+	return 0;
+}
+
 static int lib_pDoPlayerExit(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -2674,6 +2685,13 @@ static int lib_gSetCustomExitVars(lua_State *L)
 	return 0;
 }
 
+static int lib_gEnoughPlayersFinished(lua_State *L)
+{
+	INLEVEL
+	lua_pushboolean(L, G_EnoughPlayersFinished());
+	return 1;
+}
+
 static int lib_gExitLevel(lua_State *L)
 {
 	int n = lua_gettop(L); // Num arguments
@@ -2869,6 +2887,7 @@ static luaL_Reg lib[] = {
 	{"P_DoBubbleBounce",lib_pDoBubbleBounce},
 	{"P_BlackOw",lib_pBlackOw},
 	{"P_ElementalFire",lib_pElementalFire},
+	{"P_DoPlayerFinish",lib_pDoPlayerFinish},
 	{"P_DoPlayerExit",lib_pDoPlayerExit},
 	{"P_InstaThrust",lib_pInstaThrust},
 	{"P_ReturnThrustX",lib_pReturnThrustX},
@@ -2981,6 +3000,7 @@ static luaL_Reg lib[] = {
 	{"G_BuildMapName",lib_gBuildMapName},
 	{"G_DoReborn",lib_gDoReborn},
 	{"G_SetCustomExitVars",lib_gSetCustomExitVars},
+	{"G_EnoughPlayersFinished",lib_gEnoughPlayersFinished},
 	{"G_ExitLevel",lib_gExitLevel},
 	{"G_IsSpecialStage",lib_gIsSpecialStage},
 	{"G_GametypeUsesLives",lib_gGametypeUsesLives},
diff --git a/src/lua_hud.h b/src/lua_hud.h
index 7f928f7c4a1c82de246a417173166bfd4e81cb58..d1adef7dc09abb760151aa3c0322cf68078e8931 100644
--- a/src/lua_hud.h
+++ b/src/lua_hud.h
@@ -43,3 +43,4 @@ boolean LUA_HudEnabled(enum hud option);
 void LUAh_GameHUD(player_t *stplyr);
 void LUAh_ScoresHUD(void);
 void LUAh_TitleHUD(void);
+void LUAh_TitleCardHUD(player_t *stplyr);
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 62be6128300eac40493c55cfebbefe78cf40d731..9b12dd3c034f824f2b551d3f349b58e9716874f0 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -92,12 +92,14 @@ static const char *const patch_opt[] = {
 enum hudhook {
 	hudhook_game = 0,
 	hudhook_scores,
-	hudhook_title
+	hudhook_title,
+	hudhook_titlecard
 };
 static const char *const hudhook_opt[] = {
 	"game",
 	"scores",
 	"title",
+	"titlecard",
 	NULL};
 
 // alignment types for v.drawString
@@ -910,9 +912,17 @@ static int libd_RandomChance(lua_State *L)
 	return 1;
 }
 
-// 30/10/18 Lat': Get cv_translucenthud's value for HUD rendering as a normal V_xxTRANS int
+// 30/10/18 Lat': Get st_translucency's value for HUD rendering as a normal V_xxTRANS int
 // Could as well be thrown in global vars for ease of access but I guess it makes sense for it to be a HUD fn
 static int libd_getlocaltransflag(lua_State *L)
+{
+	HUDONLY
+	lua_pushinteger(L, (10-st_translucency)*V_10TRANS);
+	return 1;
+}
+
+// Get cv_translucenthud's value for HUD rendering as a normal V_xxTRANS int
+static int libd_getusertransflag(lua_State *L)
 {
 	HUDONLY
 	lua_pushinteger(L, (10-cv_translucenthud.value)*V_10TRANS);	// A bit weird that it's called "translucenthud" yet 10 is fully opaque :V
@@ -954,6 +964,7 @@ static luaL_Reg lib_draw[] = {
 	{"dupy", libd_dupy},
 	{"renderer", libd_renderer},
 	{"localTransFlag", libd_getlocaltransflag},
+	{"userTransFlag", libd_getusertransflag},
 	{NULL, NULL}
 };
 
@@ -1043,6 +1054,9 @@ int LUA_HudLib(lua_State *L)
 
 		lua_newtable(L);
 		lua_rawseti(L, -2, 4); // HUD[3] = title rendering functions array
+
+		lua_newtable(L);
+		lua_rawseti(L, -2, 5); // HUD[4] = title card rendering functions array
 	lua_setfield(L, LUA_REGISTRYINDEX, "HUD");
 
 	luaL_newmetatable(L, META_HUDINFO);
@@ -1180,4 +1194,38 @@ void LUAh_TitleHUD(void)
 	hud_running = false;
 }
 
+void LUAh_TitleCardHUD(player_t *stplayr)
+{
+	if (!gL || !(hudAvailable & (1<<hudhook_titlecard)))
+		return;
+
+	hud_running = true;
+	lua_pop(gL, -1);
+
+	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
+	I_Assert(lua_istable(gL, -1));
+	lua_rawgeti(gL, -1, 5); // HUD[5] = rendering funcs
+	I_Assert(lua_istable(gL, -1));
+
+	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
+	I_Assert(lua_istable(gL, -1));
+	lua_remove(gL, -3); // pop HUD
+
+	LUA_PushUserdata(gL, stplayr, META_PLAYER);
+	lua_pushinteger(gL, lt_ticker);
+	lua_pushinteger(gL, (lt_endtime + TICRATE));
+	lua_pushnil(gL);
+
+	while (lua_next(gL, -6) != 0) {
+		lua_pushvalue(gL, -6); // graphics library (HUD[1])
+		lua_pushvalue(gL, -6); // stplayr
+		lua_pushvalue(gL, -6); // lt_ticker
+		lua_pushvalue(gL, -6); // lt_endtime
+		LUA_Call(gL, 4);
+	}
+
+	lua_pop(gL, -1);
+	hud_running = false;
+}
+
 #endif
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index b1222ce672fbc64c4487b6c46cb31498a9de7944..6bda1988a53479b388557e20cf6ab0e207793ce1 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -744,7 +744,7 @@ static int power_get(lua_State *L)
 	UINT16 *powers = *((UINT16 **)luaL_checkudata(L, 1, META_POWERS));
 	powertype_t p = luaL_checkinteger(L, 2);
 	if (p >= NUMPOWERS)
-		return luaL_error(L, LUA_QL("powertype_t") " cannot be %u", p);
+		return luaL_error(L, LUA_QL("powertype_t") " cannot be %d", (INT16)p);
 	lua_pushinteger(L, powers[p]);
 	return 1;
 }
@@ -756,7 +756,7 @@ static int power_set(lua_State *L)
 	powertype_t p = luaL_checkinteger(L, 2);
 	UINT16 i = (UINT16)luaL_checkinteger(L, 3);
 	if (p >= NUMPOWERS)
-		return luaL_error(L, LUA_QL("powertype_t") " cannot be %u", p);
+		return luaL_error(L, LUA_QL("powertype_t") " cannot be %d", (INT16)p);
 	if (hud_running)
 		return luaL_error(L, "Do not alter player_t in HUD rendering code!");
 	powers[p] = i;
diff --git a/src/m_argv.c b/src/m_argv.c
index 117ec78339cf9768cab2110da40d00d619178c37..bb43ad57cef30eee074bfc7db6111300eb6afc20 100644
--- a/src/m_argv.c
+++ b/src/m_argv.c
@@ -16,6 +16,7 @@
 #include "doomdef.h"
 #include "command.h"
 #include "m_argv.h"
+#include "m_misc.h"
 
 /**	\brief number of arg
 */
@@ -161,7 +162,7 @@ void M_FindResponseFile(void)
 			if (!file)
 				I_Error("No more free memory for the response file");
 			if (fread(file, size, 1, handle) != 1)
-				I_Error("Couldn't read response file because %s", strerror(ferror(handle)));
+				I_Error("Couldn't read response file because %s", M_FileError(handle));
 			fclose(handle);
 
 			// keep all the command line arguments following @responsefile
diff --git a/src/m_cond.c b/src/m_cond.c
index b7520aba78e1be64ef810e46bfbb58c3d593b155..8c4e3c0b7f06def34e245c748cedc3ce11f576e6 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -284,6 +284,8 @@ void M_SilentUpdateUnlockablesAndEmblems(void)
 			continue;
 		unlockables[i].unlocked = M_Achieved(unlockables[i].conditionset - 1);
 	}
+
+	players[consoleplayer].availabilities = players[1].availabilities = R_GetSkinAvailabilities(); // players[1] is supposed to be for 2p
 }
 
 // Emblem unlocking shit
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 370633c1f6b0de4752f575bc3ab5929fd0a329cd..08bc958841cba10fa9d7b2db2f11d98d8cb0f5bc 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -201,14 +201,7 @@ FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FixedDiv(fixed_t a, fixed_t b)
 */
 FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FixedRem(fixed_t x, fixed_t y)
 {
-	const boolean n = x < 0;
-	x = abs(x);
-	while (x >= y)
-		x -= y;
-	if (n)
-		return -x;
-	else
-		return x;
+	return x % y;
 }
 
 /**	\brief	The FixedSqrt function
diff --git a/src/m_menu.c b/src/m_menu.c
index 47ca7ccd4c4dd9dc93e82acc8a20637616687f9d..7c635a137952a1e3f1d0007c319ab2aa77a3a626 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -312,9 +312,7 @@ menu_t OP_VideoOptionsDef, OP_VideoModeDef, OP_ColorOptionsDef;
 menu_t OP_OpenGLOptionsDef, OP_OpenGLFogDef, OP_OpenGLColorDef;
 #endif
 menu_t OP_SoundOptionsDef;
-#ifdef HAVE_MIXERX
 menu_t OP_SoundAdvancedDef;
-#endif
 
 //Misc
 menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef;
@@ -474,25 +472,25 @@ static consvar_t cv_dummymares = {"dummymares", "Overall", CV_HIDEN|CV_CALL, dum
 // ---------
 static menuitem_t MainMenu[] =
 {
-	{IT_STRING|IT_CALL,    NULL, "Secrets",     M_SecretsMenu,           76},
-	{IT_STRING|IT_CALL,    NULL, "1  player",   M_SinglePlayerMenu,      84},
+	{IT_STRING|IT_CALL,    NULL, "1  Player",   M_SinglePlayerMenu,      76},
 #ifndef NONET
-	{IT_STRING|IT_SUBMENU, NULL, "multiplayer", &MP_MainDef,             92},
+	{IT_STRING|IT_SUBMENU, NULL, "Multiplayer", &MP_MainDef,             84},
 #else
-	{IT_STRING|IT_CALL,    NULL, "multiplayer", M_StartSplitServerMenu,  92},
+	{IT_STRING|IT_CALL,    NULL, "Multiplayer", M_StartSplitServerMenu,  84},
 #endif
-	{IT_STRING|IT_CALL,    NULL, "options",     M_Options,              100},
-	{IT_CALL   |IT_STRING, NULL, "addons",      M_Addons,               108},
-	{IT_STRING|IT_CALL,    NULL, "quit  game",  M_QuitSRB2,             116},
+	{IT_STRING|IT_CALL,    NULL, "Extras",      M_SecretsMenu,           92},
+	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,               100},
+	{IT_STRING|IT_CALL,    NULL, "Options",     M_Options,              108},
+	{IT_STRING|IT_CALL,    NULL, "Quit  Game",  M_QuitSRB2,             116},
 };
 
 typedef enum
 {
-	secrets = 0,
-	singleplr,
+	singleplr = 0,
 	multiplr,
-	options,
+	secrets,
 	addons,
+	options,
 	quitdoom
 } main_e;
 
@@ -661,7 +659,7 @@ static menuitem_t SR_PandorasBox[] =
 // Sky Room Custom Unlocks
 static menuitem_t SR_MainMenu[] =
 {
-	{IT_STRING|IT_SUBMENU,NULL, "Secrets Checklist", &SR_UnlockChecklistDef, 0},
+	{IT_STRING|IT_SUBMENU,NULL, "Extras Checklist", &SR_UnlockChecklistDef, 0},
 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom1
 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom2
 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom3
@@ -726,19 +724,19 @@ static menuitem_t SR_EmblemHintMenu[] =
 // Single Player Main
 static menuitem_t SP_MainMenu[] =
 {
-	{IT_CALL | IT_STRING,                       NULL, "Tutorial",      M_StartTutorial,            84},
-	{IT_CALL | IT_STRING,                       NULL, "Start Game",    M_LoadGame,                 92},
-	{IT_SECRET,                                 NULL, "Record Attack", M_TimeAttack,              100},
-	{IT_SECRET,                                 NULL, "NiGHTS Mode",   M_NightsAttack,            108},
-	{IT_CALL | IT_STRING | IT_CALL_NOTMODIFIED, NULL, "Statistics",    M_Statistics,              116},
+	{IT_CALL | IT_STRING,                       NULL, "Start Game",    M_LoadGame,                 84},
+	{IT_SECRET,                                 NULL, "Record Attack", M_TimeAttack,               92},
+	{IT_SECRET,                                 NULL, "NiGHTS Mode",   M_NightsAttack,            100},
+	{IT_CALL | IT_STRING,                       NULL, "Tutorial",      M_StartTutorial,           108},
+	{IT_CALL | IT_STRING | IT_CALL_NOTMODIFIED, NULL, "Statistics",    M_Statistics,              116}
 };
 
 enum
 {
-	sptutorial,
 	sploadgame,
 	sprecordattack,
 	spnightsmode,
+	sptutorial,
 	spstatistics
 };
 
@@ -1224,32 +1222,33 @@ static menuitem_t OP_VideoOptionsMenu[] =
 	                      NULL, "HUD Transparency",          &cv_translucenthud,   66},
 	{IT_STRING | IT_CVAR, NULL, "Score/Time/Rings",          &cv_timetic,          71},
 	{IT_STRING | IT_CVAR, NULL, "Show Powerups",             &cv_powerupdisplay,   76},
+	{IT_STRING | IT_CVAR, NULL, "Local ping display",		&cv_showping,			81}, // shows ping next to framerate if we want to.
 #ifdef SEENAMES
-	{IT_STRING | IT_CVAR, NULL, "Show player names",         &cv_seenames,         81},
+	{IT_STRING | IT_CVAR, NULL, "Show player names",         &cv_seenames,         86},
 #endif
 
-	{IT_HEADER, NULL, "Console", NULL, 90},
-	{IT_STRING | IT_CVAR, NULL, "Background color",          &cons_backcolor,      96},
-	{IT_STRING | IT_CVAR, NULL, "Text Size",                 &cv_constextsize,    101},
-
-	{IT_HEADER, NULL, "Chat", NULL, 110},
-	{IT_STRING | IT_CVAR, NULL, "Chat Mode",            		 	 &cv_consolechat,  116},
-	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Width",    &cv_chatwidth,     121},
-	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Height",   &cv_chatheight,    126},
-	{IT_STRING | IT_CVAR, NULL, "Message Fadeout Time",              &cv_chattime,    131},
-	{IT_STRING | IT_CVAR, NULL, "Chat Notifications",           	 &cv_chatnotifications,  136},
-	{IT_STRING | IT_CVAR, NULL, "Spam Protection",           		 &cv_chatspamprotection,  141},
-	{IT_STRING | IT_CVAR, NULL, "Chat background tint",           	 &cv_chatbacktint,  146},
-
-	{IT_HEADER, NULL, "Level", NULL, 155},
-	{IT_STRING | IT_CVAR, NULL, "Draw Distance",             &cv_drawdist,        161},
-	{IT_STRING | IT_CVAR, NULL, "Weather Draw Dist.",        &cv_drawdist_precip, 166},
-	{IT_STRING | IT_CVAR, NULL, "NiGHTS Hoop Draw Dist.",    &cv_drawdist_nights, 171},
-
-	{IT_HEADER, NULL, "Diagnostic", NULL, 180},
-	{IT_STRING | IT_CVAR, NULL, "Show FPS",                  &cv_ticrate,         186},
-	{IT_STRING | IT_CVAR, NULL, "Clear Before Redraw",       &cv_homremoval,      191},
-	{IT_STRING | IT_CVAR, NULL, "Show \"FOCUS LOST\"",       &cv_showfocuslost,   196},
+	{IT_HEADER, NULL, "Console", NULL, 95},
+	{IT_STRING | IT_CVAR, NULL, "Background color",          &cons_backcolor,      101},
+	{IT_STRING | IT_CVAR, NULL, "Text Size",                 &cv_constextsize,    106},
+
+	{IT_HEADER, NULL, "Chat", NULL, 115},
+	{IT_STRING | IT_CVAR, NULL, "Chat Mode",            		 	 &cv_consolechat,  121},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Width",    &cv_chatwidth,     126},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Height",   &cv_chatheight,    131},
+	{IT_STRING | IT_CVAR, NULL, "Message Fadeout Time",              &cv_chattime,    136},
+	{IT_STRING | IT_CVAR, NULL, "Chat Notifications",           	 &cv_chatnotifications,  141},
+	{IT_STRING | IT_CVAR, NULL, "Spam Protection",           		 &cv_chatspamprotection,  146},
+	{IT_STRING | IT_CVAR, NULL, "Chat background tint",           	 &cv_chatbacktint,  151},
+
+	{IT_HEADER, NULL, "Level", NULL, 160},
+	{IT_STRING | IT_CVAR, NULL, "Draw Distance",             &cv_drawdist,        166},
+	{IT_STRING | IT_CVAR, NULL, "Weather Draw Dist.",        &cv_drawdist_precip, 171},
+	{IT_STRING | IT_CVAR, NULL, "NiGHTS Hoop Draw Dist.",    &cv_drawdist_nights, 176},
+
+	{IT_HEADER, NULL, "Diagnostic", NULL, 184},
+	{IT_STRING | IT_CVAR, NULL, "Show FPS",                  &cv_ticrate,         190},
+	{IT_STRING | IT_CVAR, NULL, "Clear Before Redraw",       &cv_homremoval,      195},
+	{IT_STRING | IT_CVAR, NULL, "Show \"FOCUS LOST\"",       &cv_showfocuslost,   200},
 };
 
 static menuitem_t OP_VideoModeMenu[] =
@@ -1347,29 +1346,22 @@ static menuitem_t OP_OpenGLColorMenu[] =
 static menuitem_t OP_SoundOptionsMenu[] =
 {
 	{IT_HEADER, NULL, "Game Audio", NULL, 0},
-	{IT_STRING | IT_CVAR,  NULL,  "Sound Effects", &cv_gamesounds, 6},
-	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Sound Volume", &cv_soundvolume, 11},
-
-	{IT_STRING | IT_CVAR,  NULL,  "Digital Music", &cv_gamedigimusic, 21},
-	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Digital Music Volume", &cv_digmusicvolume,  26},
+	{IT_STRING | IT_CVAR,  NULL,  "Sound Effects", &cv_gamesounds, 12},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Sound Volume", &cv_soundvolume, 22},
 
-	{IT_STRING | IT_CVAR,  NULL,  "MIDI Music", &cv_gamemidimusic, 36},
-	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "MIDI Music Volume", &cv_midimusicvolume, 41},
+	{IT_STRING | IT_CVAR,  NULL,  "Digital Music", &cv_gamedigimusic, 42},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Digital Music Volume", &cv_digmusicvolume,  52},
 
-	{IT_HEADER, NULL, "Accessibility", NULL, 50},
-	{IT_STRING | IT_CVAR, NULL, "Closed Captioning", &cv_closedcaptioning, 56},
-	{IT_STRING | IT_CVAR, NULL, "Reset Music Upon Dying", &cv_resetmusic, 61},
+	{IT_STRING | IT_CVAR,  NULL,  "MIDI Music", &cv_gamemidimusic, 72},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "MIDI Music Volume", &cv_midimusicvolume, 82},
 
-	{IT_STRING | IT_CVAR, NULL, "Play Sound Effects if Unfocused", &cv_playsoundsifunfocused, 71},
-	{IT_STRING | IT_CVAR, NULL, "Play Music if Unfocused", &cv_playmusicifunfocused, 76},
+	{IT_HEADER, NULL, "Miscellaneous", NULL, 102},
+	{IT_STRING | IT_CVAR, NULL, "Closed Captioning", &cv_closedcaptioning, 114},
+	{IT_STRING | IT_CVAR, NULL, "Reset Music Upon Dying", &cv_resetmusic, 124},
 
-#ifdef HAVE_MIXERX
-	{IT_STRING | IT_SUBMENU, NULL, "Advanced Settings...", &OP_SoundAdvancedDef, 94},
-#endif
+	{IT_STRING | IT_SUBMENU, NULL, "Advanced Settings...", &OP_SoundAdvancedDef, 144},
 };
 
-#ifdef HAVE_MIXERX
-
 #ifdef HAVE_OPENMPT
 #define OPENMPT_MENUOFFSET 32
 #else
@@ -1385,24 +1377,25 @@ static menuitem_t OP_SoundOptionsMenu[] =
 static menuitem_t OP_SoundAdvancedMenu[] =
 {
 #ifdef HAVE_OPENMPT
-	{IT_HEADER, NULL, "OpenMPT Settings", NULL, 10},
-	{IT_STRING | IT_CVAR, NULL, "Instrument Filter", &cv_modfilter, 22},
+	{IT_HEADER, NULL, "OpenMPT Settings", NULL, 0},
+	{IT_STRING | IT_CVAR, NULL, "Instrument Filter", &cv_modfilter, 12},
 #endif
 
 #ifdef HAVE_MIXERX
-	{IT_HEADER, NULL, "MIDI Settings", NULL, OPENMPT_MENUOFFSET+10},
-	{IT_STRING | IT_CVAR, NULL, "MIDI Player", &cv_midiplayer, OPENMPT_MENUOFFSET+22},
-	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "FluidSynth Sound Font File", &cv_midisoundfontpath, OPENMPT_MENUOFFSET+34},
-	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "TiMidity++ Config Folder", &cv_miditimiditypath, OPENMPT_MENUOFFSET+61},
+	{IT_HEADER, NULL, "MIDI Settings", NULL, OPENMPT_MENUOFFSET},
+	{IT_STRING | IT_CVAR, NULL, "MIDI Player", &cv_midiplayer, OPENMPT_MENUOFFSET+12},
+	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "FluidSynth Sound Font File", &cv_midisoundfontpath, OPENMPT_MENUOFFSET+24},
+	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "TiMidity++ Config Folder", &cv_miditimiditypath, OPENMPT_MENUOFFSET+51},
 #endif
 
-	{IT_HEADER, NULL, "Miscellaneous", NULL, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+10},
-	{IT_STRING | IT_CVAR, NULL, "Let Levels Force Reset Music", &cv_resetmusicbyheader, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+22},
+	{IT_HEADER, NULL, "Miscellaneous", NULL, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET},
+	{IT_STRING | IT_CVAR, NULL, "Play Sound Effects if Unfocused", &cv_playsoundsifunfocused, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+12},
+	{IT_STRING | IT_CVAR, NULL, "Play Music if Unfocused", &cv_playmusicifunfocused, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+22},
+	{IT_STRING | IT_CVAR, NULL, "Let Levels Force Reset Music", &cv_resetmusicbyheader, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+32},
 };
 
 #undef OPENMPT_MENUOFFSET
 #undef MIXERX_MENUOFFSET
-#endif
 
 static menuitem_t OP_DataOptionsMenu[] =
 {
@@ -1454,7 +1447,7 @@ enum
 static menuitem_t OP_EraseDataMenu[] =
 {
 	{IT_STRING | IT_CALL, NULL, "Erase Record Data", M_EraseData, 10},
-	{IT_STRING | IT_CALL, NULL, "Erase Secrets Data", M_EraseData, 20},
+	{IT_STRING | IT_CALL, NULL, "Erase Extras Data", M_EraseData, 20},
 
 	{IT_STRING | IT_CALL, NULL, "\x85" "Erase ALL Data", M_EraseData, 40},
 };
@@ -1665,7 +1658,7 @@ menu_t SP_MainDef = //CENTERMENUSTYLE(NULL, SP_MainMenu, &MainDef, 72);
 	SP_MainMenu,
 	M_DrawCenteredMenu,
 	BASEVIDWIDTH/2, 72,
-	1, // start at "Start Game" on first entry
+	0,
 	NULL
 };
 
@@ -1991,12 +1984,10 @@ menu_t OP_ColorOptionsDef =
 	0,
 	NULL
 };
-menu_t OP_SoundOptionsDef = DEFAULTSCROLLMENUSTYLE(
+menu_t OP_SoundOptionsDef = DEFAULTMENUSTYLE(
 	MN_OP_MAIN + (MN_OP_SOUND << 6),
 	"M_SOUND", OP_SoundOptionsMenu, &OP_MainDef, 30, 30);
-#ifdef HAVE_MIXERX
 menu_t OP_SoundAdvancedDef = DEFAULTMENUSTYLE(MN_OP_MAIN + (MN_OP_SOUND << 6), "M_SOUND", OP_SoundAdvancedMenu, &OP_SoundOptionsDef, 30, 30);
-#endif
 
 menu_t OP_ServerOptionsDef = DEFAULTSCROLLMENUSTYLE(
 	MN_OP_MAIN + (MN_OP_SERVER << 6),
@@ -3363,13 +3354,15 @@ boolean M_Responder(event_t *ev)
 //
 void M_Drawer(void)
 {
+	boolean wipe = WipeInAction;
+
 	if (currentMenu == &MessageDef)
 		menuactive = true;
 
 	if (menuactive)
 	{
 		// now that's more readable with a faded background (yeah like Quake...)
-		if (!WipeInAction && (curfadevalue || (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK)))
+		if (!wipe && (curfadevalue || (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK)))
 			V_DrawFadeScreen(0xFF00, (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK) ? 16 : curfadevalue);
 
 		if (currentMenu->drawroutine)
@@ -3431,6 +3424,8 @@ void M_StartControlPanel(void)
 	if (!Playing())
 	{
 		// Secret menu!
+		MainMenu[singleplr].alphaKey = (M_AnySecretUnlocked()) ? 76 : 84;
+		MainMenu[multiplr].alphaKey = (M_AnySecretUnlocked()) ? 84 : 92;
 		MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		currentMenu = &MainDef;
@@ -3532,6 +3527,7 @@ void M_StartControlPanel(void)
 
 void M_EndModeAttackRun(void)
 {
+	G_ClearModeAttackRetryFlag();
 	M_ModeAttackEndGame(0);
 }
 
@@ -6692,7 +6688,7 @@ static void M_DrawChecklist(void)
 		|| !unlockables[i].conditionset || unlockables[i].conditionset > MAXCONDITIONSETS)
 			continue;
 
-		V_DrawString(currentMenu->x, y, ((unlockables[i].unlocked) ? V_GREENMAP : V_TRANSLUCENT), ((unlockables[i].unlocked || !unlockables[i].nochecklist) ? unlockables[i].name : M_CreateSecretMenuOption(unlockables[i].name)));
+		V_DrawString(currentMenu->x, y, ((unlockables[i].unlocked) ? V_GREENMAP : V_TRANSLUCENT)|V_ALLOWLOWERCASE, ((unlockables[i].unlocked || !unlockables[i].nochecklist) ? unlockables[i].name : M_CreateSecretMenuOption(unlockables[i].name)));
 
 		for (j = i+1; j < MAXUNLOCKABLES; j++)
 		{
@@ -7175,7 +7171,7 @@ static void M_DrawSoundTest(void)
 				titl = va("%s - ", curplaying->title);
 		}
 		else
-			titl = "NONE - ";
+			titl = "None - ";
 
 		i = V_LevelNameWidth(titl);
 
@@ -7189,7 +7185,7 @@ static void M_DrawSoundTest(void)
 		while (x > y)
 		{
 			x -= i;
-			V_DrawLevelTitle(x, 24, 0, titl);
+			V_DrawLevelTitle(x, 22, 0, titl);
 		}
 
 		if (curplaying)
@@ -8338,16 +8334,12 @@ static void M_SetupChoosePlayer(INT32 choice)
 {
 	INT32 skinnum;
 	UINT8 i;
-	UINT8 firstvalid = 255;
-	UINT8 lastvalid = 0;
+	UINT8 firstvalid = 255, lastvalid = 255;
 	boolean allowed = false;
 	char *and;
 	(void)choice;
 
-	if (!(mapheaderinfo[startmap-1]
-			&& (mapheaderinfo[startmap-1]->forcecharacter[0] != '\0'
-			|| (mapheaderinfo[startmap-1]->typeoflevel & TOL_NIGHTS)) // remove this later when everyone gets their own nights sprites, maybe
-		))
+	if (!mapheaderinfo[startmap-1] || mapheaderinfo[startmap-1]->forcecharacter[0] == '\0')
 	{
 		for (i = 0; i < 32; i++) // Handle charsels, availability, and unlocks.
 		{
@@ -8357,6 +8349,8 @@ static void M_SetupChoosePlayer(INT32 choice)
 				if (and)
 				{
 					char firstskin[SKINNAMESIZE+1];
+					if (mapheaderinfo[startmap-1]->typeoflevel & TOL_NIGHTS) // skip tagteam characters for NiGHTS levels
+						continue;
 					strncpy(firstskin, description[i].skinname, (and - description[i].skinname));
 					firstskin[(and - description[i].skinname)] = '\0';
 					description[i].skinnum[0] = R_SkinAvailable(firstskin);
@@ -8385,7 +8379,7 @@ static void M_SetupChoosePlayer(INT32 choice)
 
 					if (!(description[i].picname[0]))
 					{
-						if (skins[skinnum].sprites[SPR2_XTRA].numframes >= XTRA_CHARSEL+1)
+						if (skins[skinnum].sprites[SPR2_XTRA].numframes > XTRA_CHARSEL)
 						{
 							spritedef_t *sprdef = &skins[skinnum].sprites[SPR2_XTRA];
 							spriteframe_t *sprframe = &sprdef->spriteframes[XTRA_CHARSEL];
@@ -8410,17 +8404,16 @@ static void M_SetupChoosePlayer(INT32 choice)
 		}
 	}
 
-	if (firstvalid != 255)
-	{ // One last bit of order we can't do in the iteration above.
-		description[firstvalid].prev = lastvalid;
-		description[lastvalid].next = firstvalid;
-	}
-	else // We're being forced into a specific character, so might as well just skip it.
+	if (firstvalid == lastvalid) // We're being forced into a specific character, so might as well just skip it.
 	{
-		M_ChoosePlayer(-1);
+		M_ChoosePlayer(firstvalid);
 		return;
 	}
 
+	// One last bit of order we can't do in the iteration above.
+	description[firstvalid].prev = lastvalid;
+	description[lastvalid].next = firstvalid;
+
 	M_ChangeMenuMusic("_chsel", true);
 
 	/* the menus suck -James */
@@ -8747,7 +8740,7 @@ static void M_ChoosePlayer(INT32 choice)
 	UINT8 skinnum;
 
 	// skip this if forcecharacter or no characters available
-	if (choice == -1)
+	if (choice == 255)
 	{
 		skinnum = botskin = 0;
 		botingame = false;
@@ -8859,9 +8852,9 @@ static void M_DrawStatsMaps(int location)
 		M_DrawMapEmblems(mnum+1, 292, y);
 
 		if (mapheaderinfo[mnum]->actnum != 0)
-			V_DrawString(20, y, V_YELLOWMAP, va("%s %d", mapheaderinfo[mnum]->lvlttl, mapheaderinfo[mnum]->actnum));
+			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE, va("%s %d", mapheaderinfo[mnum]->lvlttl, mapheaderinfo[mnum]->actnum));
 		else
-			V_DrawString(20, y, V_YELLOWMAP, mapheaderinfo[mnum]->lvlttl);
+			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE, mapheaderinfo[mnum]->lvlttl);
 
 		y += 8;
 
@@ -8905,7 +8898,7 @@ static void M_DrawStatsMaps(int location)
 			else
 				V_DrawSmallScaledPatch(292, y, 0, W_CachePatchName("NEEDIT", PU_CACHE));
 
-			V_DrawString(20, y, V_YELLOWMAP, va("%s", exemblem->description));
+			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE, va("%s", exemblem->description));
 		}
 
 		y += 8;
@@ -9116,7 +9109,7 @@ void M_DrawTimeAttackMenu(void)
 
 	// Character face!
 	{
-		if (skins[cv_chooseskin.value-1].sprites[SPR2_XTRA].numframes >= XTRA_CHARSEL+1)
+		if (skins[cv_chooseskin.value-1].sprites[SPR2_XTRA].numframes > XTRA_CHARSEL)
 		{
 			spritedef_t *sprdef = &skins[cv_chooseskin.value-1].sprites[SPR2_XTRA];
 			spriteframe_t *sprframe = &sprdef->spriteframes[XTRA_CHARSEL];
@@ -9240,10 +9233,7 @@ void M_DrawTimeAttackMenu(void)
 
 		V_DrawString(104-72, 73+lsheadingheight/2, V_YELLOWMAP, "RINGS:");
 
-		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->gotperfect)
-			V_DrawRightAlignedString(104+64, 73+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
-		else
-			V_DrawRightAlignedString(104+64, 73+lsheadingheight/2, V_ALLOWLOWERCASE|V_YELLOWMAP, beststr);
+		V_DrawRightAlignedString(104+64, 73+lsheadingheight/2, V_ALLOWLOWERCASE|((mapvisited[cv_nextmap.value-1] & MV_PERFECT) ? V_YELLOWMAP : 0), beststr);
 
 		V_DrawRightAlignedString(104+72, 83+lsheadingheight/2, V_ALLOWLOWERCASE, reqrings);
 	}
@@ -9395,6 +9385,7 @@ void M_DrawNightsAttackMenu(void)
 	{
 		emblem_t *em;
 		INT32 yHeight;
+		INT32 xpos;
 		patch_t *PictureOfLevel;
 		lumpnum_t lumpnum;
 		char beststr[40];
@@ -9454,17 +9445,23 @@ void M_DrawNightsAttackMenu(void)
 			{
 				switch (em->type)
 				{
-					case ET_NGRADE: yHeight = 48; break;
-					case ET_NTIME:  yHeight = 68; break;
+					case ET_NGRADE:
+						xpos = 104+38;
+						yHeight = 48;
+						break;
+					case ET_NTIME:
+						xpos = 104+76;
+						yHeight = 68;
+						break;
 					default:
 						goto skipThisOne;
 				}
 
 				if (em->collected)
-					V_DrawSmallMappedPatch(104+38, yHeight+lsheadingheight/2, 0, W_CachePatchName(M_GetEmblemPatch(em, false), PU_CACHE),
+					V_DrawSmallMappedPatch(xpos, yHeight+lsheadingheight/2, 0, W_CachePatchName(M_GetEmblemPatch(em, false), PU_CACHE),
 																 R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(em), GTC_CACHE));
 				else
-					V_DrawSmallScaledPatch(104+38, yHeight+lsheadingheight/2, 0, W_CachePatchName("NEEDIT", PU_CACHE));
+					V_DrawSmallScaledPatch(xpos, yHeight+lsheadingheight/2, 0, W_CachePatchName("NEEDIT", PU_CACHE));
 
 				skipThisOne:
 				em = M_GetLevelEmblems(-1);
@@ -10905,7 +10902,7 @@ static void M_EraseData(INT32 choice)
 	if (choice == 0)
 		eschoice = M_GetText("Record Attack data");
 	else if (choice == 1)
-		eschoice = M_GetText("Secrets data");
+		eschoice = M_GetText("Extras data");
 	else
 		eschoice = M_GetText("ALL game data");
 
diff --git a/src/m_menu.h b/src/m_menu.h
index ec7915cc29ee070689fe800787658cfc097c1ecd..ce7198d7545e2462f894e76e1fe30aeced128269 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -99,7 +99,7 @@ typedef enum
 	MN_OP_SCREENSHOTS,
 	MN_OP_ERASEDATA,
 
-	// Secrets
+	// Extras
 	MN_SR_MAIN,
 	MN_SR_PANDORA,
 	MN_SR_LEVELSELECT,
diff --git a/src/m_misc.c b/src/m_misc.c
index 20d5f1de4d61fa804e0bafbefedd074365442d9c..ca9b3a8e1cc1c58df255103848aaf5035eab59b8 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -23,6 +23,8 @@
 #include <unistd.h>
 #endif
 
+#include <errno.h>
+
 // Extended map support.
 #include <ctype.h>
 
@@ -787,7 +789,7 @@ static void M_PNGText(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png
 	if (gamestate == GS_LEVEL && mapheaderinfo[gamemap-1]->lvlttl[0] != '\0')
 		snprintf(lvlttltext, 48, "%s%s%s",
 			mapheaderinfo[gamemap-1]->lvlttl,
-			(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : " ZONE",
+			(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : " Zone",
 			(mapheaderinfo[gamemap-1]->actnum > 0) ? va(" %d",mapheaderinfo[gamemap-1]->actnum) : "");
 	else
 		snprintf(lvlttltext, 48, "Unknown");
@@ -2441,3 +2443,13 @@ void M_SetupMemcpy(void)
 	M_Memcpy = cpu_cpy;
 #endif
 }
+
+/** Return the appropriate message for a file error or end of file.
+*/
+const char *M_FileError(FILE *fp)
+{
+	if (ferror(fp))
+		return strerror(errno);
+	else
+		return "end-of-file";
+}
diff --git a/src/m_misc.h b/src/m_misc.h
index 7038e3e48d94cbed3c1bbb9ee71be9f35e33a942..c37865ff3e6eafec831e8c16f850694d576ec865 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -94,6 +94,8 @@ void strcatbf(char *s1, const char *s2, const char *s3);
 
 void M_SetupMemcpy(void);
 
+const char *M_FileError(FILE *handle);
+
 // counting bits, for weapon ammo code, usually
 FUNCMATH UINT8 M_CountBits(UINT32 num, UINT8 size);
 
diff --git a/src/p_enemy.c b/src/p_enemy.c
index eedbecaa74b84a983219d27364da64f6e37cd478..785d19fb4752d8a3582f042c0566bdb3ff5e59ae 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -312,6 +312,9 @@ void A_SpawnPterabytes(mobj_t *actor);
 void A_PterabyteHover(mobj_t *actor);
 void A_RolloutSpawn(mobj_t *actor);
 void A_RolloutRock(mobj_t *actor);
+void A_DragonbomberSpawn(mobj_t *actor);
+void A_DragonWing(mobj_t *actor);
+void A_DragonSegment(mobj_t *actor);
 
 //for p_enemy.c
 
@@ -2473,12 +2476,8 @@ void A_VultureBlast(mobj_t *actor)
 void A_VultureFly(mobj_t *actor)
 {
 	fixed_t speedmax = 18*FRACUNIT;
-	angle_t angledif = R_PointToAngle2(actor->x, actor->y, actor->target->x, actor->target->y) - actor->angle;
-	fixed_t dx = actor->target->x - actor->x;
-	fixed_t dy = actor->target->y - actor->y;
-	fixed_t dz = actor->target->z - actor->z;
-	fixed_t dxy = FixedHypot(dx, dy);
-	fixed_t dm;
+	angle_t angledif;
+	fixed_t dx, dy, dz, dxy, dm;
 	mobj_t *dust;
 	fixed_t momm;
 
@@ -2487,6 +2486,18 @@ void A_VultureFly(mobj_t *actor)
 		return;
 #endif
 
+	if (!actor->target || P_MobjWasRemoved(actor->target))
+	{
+		P_SetMobjState(actor, actor->info->spawnstate);
+		return;
+	}
+
+	angledif = R_PointToAngle2(actor->x, actor->y, actor->target->x, actor->target->y) - actor->angle;
+	dx = actor->target->x - actor->x;
+	dy = actor->target->y - actor->y;
+	dz = actor->target->z - actor->z;
+	dxy = FixedHypot(dx, dy);
+
 	if (leveltime % 4 == 0)
 		S_StartSound(actor, actor->info->activesound);
 
@@ -4070,19 +4081,28 @@ bossjustdie:
 					mobj_t *pole = P_SpawnMobj(
 						mo->tracer->x - P_ReturnThrustX(mo->tracer, mo->tracer->angle, speed*time),
 						mo->tracer->y - P_ReturnThrustY(mo->tracer, mo->tracer->angle, speed*time),
-						mo->tracer->floorz + 4*FRACUNIT,
+						mo->tracer->floorz + (256+1)*FRACUNIT,
 						MT_FSGNB);
 					P_SetTarget(&pole->tracer, P_SpawnMobj(
+						pole->x, pole->y,
+						pole->z - 256*FRACUNIT,
+						MT_FSGNB));
+					P_SetTarget(&pole->tracer->tracer, P_SpawnMobj(
 						pole->x + P_ReturnThrustX(pole, mo->tracer->angle, FRACUNIT),
 						pole->y + P_ReturnThrustY(pole, mo->tracer->angle, FRACUNIT),
 						pole->z + 256*FRACUNIT,
 						MT_FSGNA));
-					pole->angle = mo->tracer->angle;
-					pole->tracer->angle = pole->angle - ANGLE_90;
+					pole->tracer->flags |= MF_NOCLIPTHING;
+					P_SetScale(pole, (pole->destscale = 2*FRACUNIT));
+					P_SetScale(pole->tracer, (pole->tracer->destscale = 2*FRACUNIT));
+					pole->angle = pole->tracer->angle = mo->tracer->angle;
+					pole->tracer->tracer->angle = pole->angle - ANGLE_90;
 					pole->momx = P_ReturnThrustX(pole, pole->angle, speed);
 					pole->momy = P_ReturnThrustY(pole, pole->angle, speed);
 					pole->tracer->momx = pole->momx;
 					pole->tracer->momy = pole->momy;
+					pole->tracer->tracer->momx = pole->momx;
+					pole->tracer->tracer->momy = pole->momy;
 				}
 			}
 			else
@@ -5121,7 +5141,7 @@ void A_SignPlayer(mobj_t *actor)
 		return;
 #endif
 
-	if (actor->tracer == NULL || locvar1 < -3 || locvar1 >= numskins)
+	if (actor->tracer == NULL || locvar1 < -3 || locvar1 >= numskins || signcolor >= MAXTRANSLATIONS)
 		return;
 
 	// if no face overlay, spawn one
@@ -5148,26 +5168,9 @@ void A_SignPlayer(mobj_t *actor)
 		if (signcolor)
 			;
 		else if ((actor->target->player->skincolor == skin->prefcolor) && (skin->prefoppositecolor)) // Set it as the skin's preferred oppositecolor?
-		{
 			signcolor = skin->prefoppositecolor;
-			/*
-			If you're here from the comment above Color_Opposite,
-			the following line is the one which is dependent on the
-			array being symmetrical. It gets the opposite of the
-			opposite of your desired colour just so it can get the
-			brightness frame for the End Sign. It's not a great
-			design choice, but it's constant time array access and
-			the idea that the colours should be OPPOSITES is kind
-			of in the name. If you have a better idea, feel free
-			to let me know. ~toast 2016/07/20
-			*/
-			signframe += (15 - Color_Opposite[Color_Opposite[skin->prefoppositecolor - 1][0] - 1][1]);
-		}
 		else if (actor->target->player->skincolor) // Set the sign to be an appropriate background color for this player's skincolor.
-		{
 			signcolor = Color_Opposite[actor->target->player->skincolor - 1][0];
-			signframe += (15 - Color_Opposite[actor->target->player->skincolor - 1][1]);
-		}
 		else
 			signcolor = SKINCOLOR_NONE;
 	}
@@ -5188,10 +5191,10 @@ void A_SignPlayer(mobj_t *actor)
 				skinnum = P_RandomKey(skincount);
 				for (skincount = 0; skincount < numskins; skincount++)
 				{
-					if (skincheck(skincount))
-						skinnum++;
 					if (skincount > skinnum)
 						break;
+					if (skincheck(skincount))
+						skinnum++;
 				}
 			}
 			else // otherwise, advance 1 skin
@@ -5203,42 +5206,46 @@ void A_SignPlayer(mobj_t *actor)
 			skin = &skins[skinnum];
 		}
 		else // specific skin
-		{
 			skin = &skins[locvar1];
-		}
 
 		facecolor = skin->prefcolor;
 		if (signcolor)
 			;
 		else if (skin->prefoppositecolor)
-		{
 			signcolor = skin->prefoppositecolor;
-		}
-		else
-		{
+		else if (facecolor)
 			signcolor = Color_Opposite[facecolor - 1][0];
-		}
-		signframe += (15 - Color_Opposite[Color_Opposite[signcolor - 1][0] - 1][1]);
 	}
 
-	if (skin != NULL && skin->sprites[SPR2_SIGN].numframes) // player face
+	if (skin && skin->sprites[SPR2_SIGN].numframes) // player face
 	{
 		ov->color = facecolor;
 		ov->skin = skin;
 		P_SetMobjState(ov, actor->info->seestate); // S_PLAY_SIGN
-		actor->tracer->color = signcolor;
-		actor->tracer->frame = signframe;
 	}
 	else // Eggman face
 	{
 		ov->color = SKINCOLOR_NONE;
 		P_SetMobjState(ov, actor->info->meleestate); // S_EGGMANSIGN
-		if (signcolor)
-			actor->tracer->color = signcolor;
-		else
-			actor->tracer->color = signcolor = SKINCOLOR_CARBON;
-		actor->tracer->frame = signframe += (15 - Color_Opposite[Color_Opposite[signcolor - 1][0] - 1][1]);
-	}
+		if (!signcolor)
+			signcolor = SKINCOLOR_CARBON;
+	}
+
+	actor->tracer->color = signcolor;
+	/*
+	If you're here from the comment above Color_Opposite,
+	the following line is the one which is dependent on the
+	array being symmetrical. It gets the opposite of the
+	opposite of your desired colour just so it can get the
+	brightness frame for the End Sign. It's not a great
+	design choice, but it's constant time array access and
+	the idea that the colours should be OPPOSITES is kind
+	of in the name. If you have a better idea, feel free
+	to let me know. ~toast 2016/07/20
+	*/
+	if (signcolor && signcolor < MAXSKINCOLORS)
+		signframe += (15 - Color_Opposite[Color_Opposite[signcolor - 1][0] - 1][1]);
+	actor->tracer->frame = signframe;
 }
 
 // Function: A_OverlayThink
@@ -5665,10 +5672,10 @@ void A_MinusPopup(mobj_t *actor)
 	S_StartSound(actor, sfx_s3k82);
 	for (i = 1; i <= num; i++)
 	{
-		mobj_t *rock = P_SpawnMobj(actor->x, actor->y, actor->z + actor->height/4, MT_ROCKCRUMBLE1);
+		mobj_t *rock = P_SpawnMobjFromMobj(actor, 0, 0, actor->height/4, MT_ROCKCRUMBLE1);
 		P_Thrust(rock, ani*i, FRACUNIT);
-		rock->momz = 3*FRACUNIT;
-		P_SetScale(rock, FRACUNIT/3);
+		P_SetObjectMomZ(rock, 3*FRACUNIT, false);
+		P_SetScale(rock, rock->scale/3);
 	}
 	P_RadiusAttack(actor, actor, 2*actor->radius, 0);
 	if (actor->tracer)
@@ -5682,11 +5689,12 @@ void A_MinusPopup(mobj_t *actor)
 // Description: If the minus hits the floor, dig back into the ground.
 //
 // var1 = State to switch to (if 0, use seestate).
-// var2 = unused
+// var2 = If not 0, spawn debris when hitting the floor.
 //
 void A_MinusCheck(mobj_t *actor)
 {
 	INT32 locvar1 = var1;
+	INT32 locvar2 = var2;
 
 #ifdef HAVE_BLUA
 	if (LUA_CallAction("A_MinusCheck", actor))
@@ -5697,6 +5705,18 @@ void A_MinusCheck(mobj_t *actor)
 	{
 		P_SetMobjState(actor, locvar1 ? (statenum_t)locvar1 : actor->info->seestate);
 		actor->flags = actor->info->flags;
+		if (locvar2)
+		{
+			INT32 i, num = 6;
+			angle_t ani = FixedAngle(FRACUNIT*360/num);
+			for (i = 1; i <= num; i++)
+			{
+				mobj_t *rock = P_SpawnMobjFromMobj(actor, 0, 0, actor->height/4, MT_ROCKCRUMBLE1);
+				P_Thrust(rock, ani*i, FRACUNIT);
+				P_SetObjectMomZ(rock, 3*FRACUNIT, false);
+				P_SetScale(rock, rock->scale/3);
+			}
+		}
 	}
 }
 
@@ -14535,6 +14555,9 @@ void A_RolloutRock(mobj_t *actor)
 
 	actor->friction = FRACUNIT; // turns out riding on solids sucks, so let's just make it easier on ourselves
 
+	if (actor->eflags & MFE_JUSTHITFLOOR)
+		S_StartSound(actor, actor->info->painsound);
+
 	if (actor->threshold)
 		actor->threshold--;
 
@@ -14588,6 +14611,9 @@ void A_RolloutRock(mobj_t *actor)
 
 	actor->frame = actor->reactiontime % maxframes; // set frame
 
+	if (!actor->tracer || P_MobjWasRemoved(actor->tracer) || !actor->tracer->health)
+		actor->flags |= MF_PUSHABLE;
+
 	if (!(actor->flags & MF_PUSHABLE)) // if being ridden, don't disappear
 		actor->fuse = 0;
 	else if (!actor->fuse && actor->movecount == 1) // otherwise if rock has moved, set its fuse
@@ -14597,3 +14623,97 @@ void A_RolloutRock(mobj_t *actor)
 		actor->flags2 ^= MF2_DONTDRAW;
 
 }
+
+// Function: A_DragonbomberSpawn
+//
+// Description: Spawns the body parts for Dragonbomber
+//
+// var1 = Tail segments to spawn
+// var2 = unused
+//
+void A_DragonbomberSpawn(mobj_t *actor)
+{
+	UINT8 i;
+	mobj_t *mo = actor;
+
+	#ifdef HAVE_BLUA
+		if (LUA_CallAction("A_DragonbomberSpawn", actor))
+			return;
+	#endif
+
+	for (i = 0; i < var1; i++) // spawn tail segments
+	{
+		mobj_t *segment;
+		fixed_t x, y;
+		x = P_ReturnThrustX(mo, mo->angle, -mo->radius << 1);
+		y = P_ReturnThrustY(mo, mo->angle, -mo->radius << 1);
+		segment = P_SpawnMobjFromMobj(mo, x, y, 0, MT_DRAGONTAIL);
+		P_SetTarget(&segment->target, mo);
+		P_SetTarget(&mo->tracer, segment);
+		segment->angle = mo->angle;
+		mo = segment;
+	}
+	for (i = 0; i < 2; i++) // spawn wings
+	{
+		mo = P_SpawnMobjFromMobj(actor, 0, 0, 0, MT_DRAGONWING);
+		P_SetTarget(&mo->target, actor);
+		mo->movedir = ANGLE_90 + i * ANGLE_180;
+	}
+}
+
+// Function: A_DragonWing
+//
+// Description: Moves actor such that it is placed away from its target at a distance equal to the target's radius in the direction of its target's angle.
+// The actor's movedir can be used to offset the angle.
+//
+// var1 = unused
+// var2 = unused
+//
+void A_DragonWing(mobj_t *actor)
+{
+	mobj_t *target = actor->target;
+	fixed_t x, y;
+
+	#ifdef HAVE_BLUA
+		if (LUA_CallAction("A_DragonWing", actor))
+			return;
+	#endif
+
+	if (target == NULL || !target->health)
+	{
+		P_RemoveMobj(actor);
+		return;
+	}
+	actor->angle = target->angle + actor->movedir;
+	x = target->x + P_ReturnThrustX(actor, actor->angle, -target->radius);
+	y = target->y + P_ReturnThrustY(actor, actor->angle, -target->radius);
+	P_TeleportMove(actor, x, y, target->z);
+}
+
+// Function: A_DragonSegment
+//
+// Description: Moves actor such that it is placed away from its target at an absolute distance equal to the sum of the two mobjs' radii.
+//
+// var1 = unused
+// var2 = unused
+//
+void A_DragonSegment(mobj_t *actor)
+{
+	mobj_t *target = actor->target;
+	fixed_t dist = P_AproxDistance(P_AproxDistance(actor->x - target->x, actor->y - target->y), actor->z - target->z);
+	fixed_t radius = actor->radius + target->radius;
+	angle_t hangle = R_PointToAngle2(target->x, target->y, actor->x, actor->y);
+	angle_t zangle = R_PointToAngle2(0, target->z, dist, actor->z);
+	fixed_t hdist = P_ReturnThrustX(target, zangle, radius);
+	fixed_t xdist = P_ReturnThrustX(target, hangle, hdist);
+	fixed_t ydist = P_ReturnThrustY(target, hangle, hdist);
+	fixed_t zdist = P_ReturnThrustY(target, zangle, radius);
+
+	#ifdef HAVE_BLUA
+		if (LUA_CallAction("A_DragonSegment", actor))
+			return;
+	#endif
+
+	actor->angle = hangle;
+	P_TeleportMove(actor, target->x + xdist, target->y + ydist, target->z + zdist);
+}
diff --git a/src/p_inter.c b/src/p_inter.c
index b0a401b1016a4141126b31bca26de0c01af68924..469cec33e9313d216f9ad16f863c7421dd37897f 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -148,13 +148,17 @@ void P_ResetStarposts(void)
 //
 boolean P_CanPickupItem(player_t *player, boolean weapon)
 {
-	if (player->bot && weapon)
+	if (!player->mo || player->mo->health <= 0)
 		return false;
 
-	if (player->powers[pw_flashing] > (flashingtics/4)*3 && player->powers[pw_flashing] < UINT16_MAX)
-		return false;
+	if (player->bot)
+	{
+		if (weapon)
+			return false;
+		return P_CanPickupItem(&players[consoleplayer], true); // weapon is true to prevent infinite recursion if p1 is bot - doesn't occur in vanilla, but may be relevant for mods
+	}
 
-	if (player->mo && player->mo->health <= 0)
+	if (player->powers[pw_flashing] > (flashingtics/4)*3 && player->powers[pw_flashing] < UINT16_MAX)
 		return false;
 
 	return true;
@@ -2521,7 +2525,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 
 		if ((target->player->lives <= 1) && (netgame || multiplayer) && (gametype == GT_COOP) && (cv_cooplives.value == 0))
 			;
-		else if (!target->player->bot && !target->player->spectator && !G_IsSpecialStage(gamemap) && (target->player->lives != INFLIVES)
+		else if (!target->player->bot && !target->player->spectator && (target->player->lives != INFLIVES)
 		 && G_GametypeUsesLives())
 		{
 			target->player->lives -= 1; // Lose a life Tails 03-11-2000
@@ -2678,6 +2682,17 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			target->flags = (target->flags|MF_NOCLIPHEIGHT) & ~MF_NOGRAVITY;
 			break;
 
+		case MT_DRAGONBOMBER:
+			{
+				mobj_t *segment = target;
+				while (segment->tracer != NULL)
+				{
+					P_KillMobj(segment->tracer, NULL, NULL, 0);
+					segment = segment->tracer;
+				}
+				break;
+			}
+
 		case MT_EGGMOBILE3:
 			{
 				mobj_t *mo2;
@@ -2741,7 +2756,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 
 		case MT_EGGTRAP:
 			// Time for birdies! Yaaaaaaaay!
-			target->fuse = TICRATE*2;
+			target->fuse = TICRATE;
 			break;
 
 		case MT_MINECART:
@@ -2803,13 +2818,10 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			if (flip)
 				momz *= -1;
 #define makechunk(angtweak, xmov, ymov) \
-			chunk = P_SpawnMobj(target->x, target->y, target->z, MT_SPIKE);\
-			chunk->eflags |= flip;\
+			chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_SPIKE);\
 			P_SetMobjState(chunk, target->info->xdeathstate);\
 			chunk->health = 0;\
 			chunk->angle = angtweak;\
-			chunk->destscale = scale;\
-			P_SetScale(chunk, scale);\
 			P_UnsetThingPosition(chunk);\
 			chunk->flags = MF_NOCLIP;\
 			chunk->x += xmov;\
@@ -2828,14 +2840,10 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 		if (flip)
 			momz *= -1;
 
-		chunk = P_SpawnMobj(target->x, target->y, target->z, MT_SPIKE);
-		chunk->eflags |= flip;
-
+		chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_SPIKE);
 		P_SetMobjState(chunk, target->info->deathstate);
 		chunk->health = 0;
 		chunk->angle = ang + ANGLE_180;
-		chunk->destscale = scale;
-		P_SetScale(chunk, scale);
 		P_UnsetThingPosition(chunk);
 		chunk->flags = MF_NOCLIP;
 		chunk->x -= xoffs;
@@ -2878,13 +2886,10 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			sprflip = P_RandomChance(FRACUNIT/2);
 
 #define makechunk(angtweak, xmov, ymov) \
-			chunk = P_SpawnMobj(target->x, target->y, target->z, MT_WALLSPIKE);\
-			chunk->eflags |= flip;\
+			chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);\
 			P_SetMobjState(chunk, target->info->xdeathstate);\
 			chunk->health = 0;\
 			chunk->angle = target->angle;\
-			chunk->destscale = scale;\
-			P_SetScale(chunk, scale);\
 			P_UnsetThingPosition(chunk);\
 			chunk->flags = MF_NOCLIP;\
 			chunk->x += xmov - forwardxoffs;\
@@ -2906,14 +2911,11 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 
 		sprflip = P_RandomChance(FRACUNIT/2);
 
-		chunk = P_SpawnMobj(target->x, target->y, target->z, MT_WALLSPIKE);
-		chunk->eflags |= flip;
+		chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);
 
 		P_SetMobjState(chunk, target->info->deathstate);
 		chunk->health = 0;
 		chunk->angle = target->angle;
-		chunk->destscale = scale;
-		P_SetScale(chunk, scale);
 		P_UnsetThingPosition(chunk);
 		chunk->flags = MF_NOCLIP;
 		chunk->x += forwardxoffs - xoffs;
@@ -3002,6 +3004,10 @@ static inline void P_NiGHTSDamage(mobj_t *target, mobj_t *source)
 		P_SetPlayerMobjState(target, S_PLAY_NIGHTS_STUN);
 		S_StartSound(target, sfx_nghurt);
 
+#ifdef ROTSPRITE
+		player->mo->rollangle = 0;
+#endif
+
 		if (oldnightstime > 10*TICRATE
 			&& player->nightstime < 10*TICRATE)
 		{
@@ -3517,7 +3523,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 
 		// Make sure that boxes cannot be popped by enemies, red rings, etc.
 		if (target->flags & MF_MONITOR && ((!source || !source->player || source->player->bot)
-		|| (inflictor && inflictor->type >= MT_REDRING && inflictor->type <= MT_GRENADERING)))
+		|| (inflictor && (inflictor->type == MT_REDRING || (inflictor->type >= MT_THROWNBOUNCE && inflictor->type <= MT_THROWNGRENADE)))))
 			return false;
 	}
 
diff --git a/src/p_local.h b/src/p_local.h
index 17a1c32d2de8c787f991641b1881d31b955b9404..646fa70f2eac77ac91e3c5b29921bc6993395e3a 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -172,6 +172,7 @@ void P_ElementalFire(player_t *player, boolean cropcircle);
 void P_DoPityCheck(player_t *player);
 void P_PlayerThink(player_t *player);
 void P_PlayerAfterThink(player_t *player);
+void P_DoPlayerFinish(player_t *player);
 void P_DoPlayerExit(player_t *player);
 void P_NightserizePlayer(player_t *player, INT32 ptime);
 
@@ -322,6 +323,7 @@ SINT8 P_MobjFlip(mobj_t *mobj);
 fixed_t P_GetMobjGravity(mobj_t *mo);
 FUNCMATH boolean P_WeaponOrPanel(mobjtype_t type);
 
+void P_CalcChasePostImg(player_t *player, camera_t *thiscam);
 boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled);
 
 void P_Attract(mobj_t *source, mobj_t *enemy, boolean nightsgrab);
diff --git a/src/p_map.c b/src/p_map.c
index bb56a50b16932ae818f2a8e62f13a9e940cd3688..8220f3818d5b6701dc9e23587d1f4a54389090be 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -592,7 +592,7 @@ static void P_DoTailsCarry(player_t *sonic, player_t *tails)
 	if (!(tails->pflags & PF_CANCARRY))
 		return;
 
-	if (tails->bot == 1)
+	if (sonic->pflags & PF_FINISHED)
 		return;
 
 	if ((sonic->mo->eflags & MFE_VERTICALFLIP) != (tails->mo->eflags & MFE_VERTICALFLIP))
@@ -661,31 +661,32 @@ static void P_SlapStick(mobj_t *fang, mobj_t *pole)
 	momx2 = fang->momx/dist;
 	momy2 = fang->momy/dist;
 
-	pole->tracer->momx = momx1 + (dist-1)*momx2;
-	pole->tracer->momy = momy1 + (dist-1)*momy2;
+	pole->tracer->tracer->momx = momx1 + (dist-1)*momx2;
+	pole->tracer->tracer->momy = momy1 + (dist-1)*momy2;
 	fang->momx = (dist-1)*momx1 + momx2;
 	fang->momy = (dist-1)*momy1 + momy2;
 #undef dist
 
-	P_SetMobjState(pole, pole->info->deathstate);
-
-	P_SetObjectMomZ(pole->tracer, 6*FRACUNIT, false);
-	pole->tracer->flags &= ~(MF_NOGRAVITY|MF_NOCLIP);
-	pole->tracer->movedir = ANGLE_67h;
-	if ((R_PointToAngle(fang->x - pole->tracer->x, fang->y - pole->tracer->y) - pole->angle) > ANGLE_180)
-		pole->tracer->movedir = InvAngle(pole->tracer->movedir);
+	P_SetObjectMomZ(pole->tracer->tracer, 6*FRACUNIT, false);
+	pole->tracer->tracer->flags &= ~(MF_NOGRAVITY|MF_NOCLIP);
+	pole->tracer->tracer->movedir = ANGLE_67h;
+	if ((R_PointToAngle(fang->x - pole->tracer->tracer->x, fang->y - pole->tracer->tracer->y) - pole->angle) > ANGLE_180)
+		pole->tracer->tracer->movedir = InvAngle(pole->tracer->movedir);
 
 	P_SetObjectMomZ(fang, 14*FRACUNIT, false);
 	fang->flags |= MF_NOGRAVITY|MF_NOCLIP;
 	P_SetMobjState(fang, fang->info->xdeathstate);
 
-	pole->tracer->tics = pole->tics = fang->tics;
+	pole->tracer->tracer->tics = pole->tracer->tics = pole->tics = fang->tics;
 
 	var1 = var2 = 0;
-	A_Scream(pole->tracer);
+	A_Scream(pole->tracer->tracer);
 	S_StartSound(fang, sfx_altdi1);
 
+	P_SetTarget(&pole->tracer->tracer, NULL);
+	P_SetMobjState(pole->tracer, pole->info->xdeathstate);
 	P_SetTarget(&pole->tracer, NULL);
+	P_SetMobjState(pole, pole->info->deathstate);
 }
 
 static void P_PlayerBarrelCollide(mobj_t *toucher, mobj_t *barrel)
@@ -783,12 +784,12 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (thing->type == MT_SPIKE
 		|| thing->type == MT_WALLSPIKE)
 		{
-			mobjtype_t type = thing->type;
+			mobj_t *iter;
 			if (thing->flags & MF_SOLID)
 				S_StartSound(tmthing, thing->info->deathsound);
-			for (thing = thing->subsector->sector->thinglist; thing; thing = thing->snext)
-				if (thing->type == type && thing->health > 0 && thing->flags & MF_SOLID && P_AproxDistance(P_AproxDistance(thing->x - tmthing->x, thing->y - tmthing->y), thing->z - tmthing->z) < 56*thing->scale)//FixedMul(56*FRACUNIT, thing->scale))
-					P_KillMobj(thing, tmthing, tmthing, 0);
+			for (iter = thing->subsector->sector->thinglist; iter; iter = iter->snext)
+				if (iter->type == thing->type && iter->health > 0 && iter->flags & MF_SOLID && (iter == thing || P_AproxDistance(P_AproxDistance(thing->x - iter->x, thing->y - iter->y), thing->z - iter->z) < 56*thing->scale))//FixedMul(56*FRACUNIT, thing->scale))
+					P_KillMobj(iter, tmthing, tmthing, 0);
 		}
 		else
 		{
@@ -822,12 +823,12 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (thing->type == MT_SPIKE
 		|| thing->type == MT_WALLSPIKE)
 		{
-			mobjtype_t type = thing->type;
+			mobj_t *iter;
 			if (thing->flags & MF_SOLID)
 				S_StartSound(tmthing, thing->info->deathsound);
-			for (thing = thing->subsector->sector->thinglist; thing; thing = thing->snext)
-				if (thing->type == type && thing->health > 0 && thing->flags & MF_SOLID && P_AproxDistance(P_AproxDistance(thing->x - tmthing->x, thing->y - tmthing->y), thing->z - tmthing->z) < 56*thing->scale)//FixedMul(56*FRACUNIT, thing->scale))
-					P_KillMobj(thing, tmthing, tmthing, 0);
+			for (iter = thing->subsector->sector->thinglist; iter; iter = iter->snext)
+				if (iter->type == thing->type && iter->health > 0 && iter->flags & MF_SOLID && (iter == thing || P_AproxDistance(P_AproxDistance(thing->x - iter->x, thing->y - iter->y), thing->z - iter->z) < 56*thing->scale))//FixedMul(56*FRACUNIT, thing->scale))
+					P_KillMobj(iter, tmthing, tmthing, 0);
 		}
 		else
 		{
@@ -1020,7 +1021,6 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if ((thing->flags & MF_PUSHABLE) // not carrying a player
 			&& (tmthing->player->powers[pw_carry] == CR_NONE) // player is not already riding something
 			&& ((tmthing->eflags & MFE_VERTICALFLIP) == (thing->eflags & MFE_VERTICALFLIP))
-			&& (P_AproxDistance(thing->x - tmthing->x, thing->y - tmthing->y) < (thing->radius))
 			&& (P_MobjFlip(tmthing)*tmthing->momz <= 0)
 			&& ((!(tmthing->eflags & MFE_VERTICALFLIP) && abs(thing->z + thing->height - tmthing->z) < (thing->height>>2))
 				|| (tmthing->eflags & MFE_VERTICALFLIP && abs(tmthing->z + tmthing->height - thing->z) < (thing->height>>2))))
@@ -1032,7 +1032,8 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_SetPlayerMobjState(tmthing, S_PLAY_WALK);
 			tmthing->player->powers[pw_carry] = CR_ROLLOUT;
 			P_SetTarget(&tmthing->tracer, thing);
-			P_SetObjectMomZ(thing, tmthing->momz, true);
+			if (!P_IsObjectOnGround(thing))
+				thing->momz += tmthing->momz;
 			return true;
 		}
 	}
@@ -1063,6 +1064,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			thing->momy = tmthing->momy;
 			tmthing->momx = tempmomx;
 			tmthing->momy = tempmomy;
+			S_StartSound(thing, thing->info->painsound);
 		}
 	}
 
@@ -1089,7 +1091,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			return true; // overhead
 		if (thing->z + thing->height < tmthing->z)
 			return true; // underneath
-		if (!thing->tracer)
+		if (!thing->tracer || !thing->tracer->tracer)
 			return true;
 		P_SlapStick(tmthing, thing);
 		// no return value was used in the original prototype script at this point,
@@ -1349,6 +1351,11 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_DamageMobj(thing, tmthing, tmthing->target, 1, damagetype);
 		}
 
+		// Fireball touched an enemy
+		// Don't bounce though, just despawn right there
+		if ((tmthing->type == MT_FIREBALL) && (thing->flags & MF_ENEMY))
+			P_KillMobj(tmthing, NULL, NULL, 0);
+
 		// don't traverse any more
 
 		if (tmthing->type == MT_SHELL)
@@ -1712,8 +1719,8 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		}
 	}
 
-	if ((tmthing->flags & MF_SPRING || tmthing->type == MT_STEAM) && (thing->player))
-		; // springs and gas jets should never be able to step up onto a player
+	if ((tmthing->flags & MF_SPRING || tmthing->type == MT_STEAM || tmthing->type == MT_SPIKE || tmthing->type == MT_WALLSPIKE) && (thing->player))
+		; // springs, gas jets and springs should never be able to step up onto a player
 	// z checking at last
 	// Treat noclip things as non-solid!
 	else if ((thing->flags & (MF_SOLID|MF_NOCLIP)) == MF_SOLID
@@ -3464,7 +3471,7 @@ isblocking:
 		}
 
 		// see about climbing on the wall
-		if (!(checkline->flags & ML_NOCLIMB))
+		if (!(checkline->flags & ML_NOCLIMB) && checkline->special != HORIZONSPECIAL)
 		{
 			boolean canclimb;
 			angle_t climbangle, climbline;
@@ -3751,6 +3758,33 @@ void P_SlideMove(mobj_t *mo)
 			v2.x = tmhitthing->x + cosradius;
 			v2.y = tmhitthing->y + sinradius;
 
+			// Can we box collision our way into smooth movement..?
+			if (sinradius && mo->y + mo->radius <= min(v1.y, v2.y))
+			{
+				mo->momy = 0;
+				P_TryMove(mo, mo->x + mo->momx, min(v1.y, v2.y) - mo->radius, true);
+				return;
+			}
+			else if (sinradius && mo->y - mo->radius >= max(v1.y, v2.y))
+			{
+				mo->momy = 0;
+				P_TryMove(mo, mo->x + mo->momx, max(v1.y, v2.y) + mo->radius, true);
+				return;
+			}
+			else if (cosradius && mo->x + mo->radius <= min(v1.x, v2.x))
+			{
+				mo->momx = 0;
+				P_TryMove(mo, min(v1.x, v2.x) - mo->radius, mo->y + mo->momy, true);
+				return;
+			}
+			else if (cosradius && mo->x - mo->radius >= max(v1.x, v2.x))
+			{
+				mo->momx = 0;
+				P_TryMove(mo, max(v1.x, v2.x) + mo->radius, mo->y + mo->momy, true);
+				return;
+			}
+
+			// nope, gotta fuck around with a fake linedef!
 			junk.v1 = &v1;
 			junk.v2 = &v2;
 			junk.dx = 2*cosradius; // v2.x - v1.x;
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 5d6a8a10f65ffbd412312d7c0c84c0d096ebd9b2..6a250a7832fafa61d051da9d692a1488939afbe2 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -246,6 +246,7 @@ boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 	{
 	case S_PLAY_STND:
 	case S_PLAY_WAIT:
+	case S_PLAY_NIGHTS_STAND:
 		player->panim = PA_IDLE;
 		break;
 	case S_PLAY_EDGE:
@@ -269,6 +270,7 @@ boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 		break;
 	case S_PLAY_ROLL:
 	//case S_PLAY_SPINDASH: -- everyone can ROLL thanks to zoom tubes...
+	case S_PLAY_NIGHTS_ATTACK:
 		player->panim = PA_ROLL;
 		break;
 	case S_PLAY_JUMP:
@@ -278,6 +280,7 @@ boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 		player->panim = PA_SPRING;
 		break;
 	case S_PLAY_FALL:
+	case S_PLAY_NIGHTS_FLOAT:
 		player->panim = PA_FALL;
 		break;
 	case S_PLAY_FLY:
@@ -3405,7 +3408,7 @@ void P_MobjCheckWater(mobj_t *mobj)
 
 		// Drown timer setting
 		if ((p->powers[pw_shield] & SH_PROTECTWATER) // Has water protection
-		 || (p->exiting) // Or exiting
+		 || (p->exiting) || (p->pflags & PF_FINISHED) // Or finished/exiting
 		 || (maptol & TOL_NIGHTS) // Or in NiGHTS mode
 		 || (mariomode)) // Or in Mario mode...
 		{
@@ -3721,17 +3724,10 @@ void P_DestroyRobots(void)
 	}
 }
 
-// P_CameraThinker
-//
-// Process the mobj-ish required functions of the camera
-boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled)
+// the below is chasecam only, if you're curious. check out P_CalcPostImg in p_user.c for first person
+void P_CalcChasePostImg(player_t *player, camera_t *thiscam)
 {
-	boolean itsatwodlevel = false;
 	postimg_t postimg = postimg_none;
-	if (twodlevel
-		|| (thiscam == &camera && players[displayplayer].mo && (players[displayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera2 && players[secondarydisplayplayer].mo && (players[secondarydisplayplayer].mo->flags2 & MF2_TWOD)))
-		itsatwodlevel = true;
 
 	if (player->pflags & PF_FLIPCAM && !(player->powers[pw_carry] == CR_NIGHTSMODE) && player->mo->eflags & MFE_VERTICALFLIP)
 		postimg = postimg_flip;
@@ -3759,13 +3755,27 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 			postimg = postimg_heat;
 	}
 
-	if (postimg != postimg_none)
-	{
-		if (splitscreen && player == &players[secondarydisplayplayer])
-			postimgtype2 = postimg;
-		else
-			postimgtype = postimg;
-	}
+	if (postimg == postimg_none)
+		return;
+
+	if (splitscreen && player == &players[secondarydisplayplayer])
+		postimgtype2 = postimg;
+	else
+		postimgtype = postimg;
+}
+
+// P_CameraThinker
+//
+// Process the mobj-ish required functions of the camera
+boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled)
+{
+	boolean itsatwodlevel = false;
+	if (twodlevel
+		|| (thiscam == &camera && players[displayplayer].mo && (players[displayplayer].mo->flags2 & MF2_TWOD))
+		|| (thiscam == &camera2 && players[secondarydisplayplayer].mo && (players[secondarydisplayplayer].mo->flags2 & MF2_TWOD)))
+		itsatwodlevel = true;
+
+	P_CalcChasePostImg(player, thiscam);
 
 	if (thiscam->momx || thiscam->momy)
 	{
@@ -4535,23 +4545,29 @@ static void P_Boss3Thinker(mobj_t *mobj)
 	}
 	else if (mobj->movecount) // Firing mode
 	{
-		// look for a new target
-		P_BossTargetPlayer(mobj, false);
-
-		if (!mobj->target || !mobj->target->player)
-			return;
-
-		// Always face your target.
-		A_FaceTarget(mobj);
-
 		// Check if the attack animation is running. If not, play it.
 		if (mobj->state < &states[mobj->info->missilestate] || mobj->state > &states[mobj->info->raisestate])
 		{
+			// look for a new target
+			P_BossTargetPlayer(mobj, true);
+
+			if (!mobj->target || !mobj->target->player)
+				return;
+
 			if (mobj->health <= mobj->info->damage) // pinch phase
 				mobj->movecount--; // limited number of shots before diving again
 			if (mobj->movecount)
 				P_SetMobjState(mobj, mobj->info->missilestate+1);
 		}
+		else if (mobj->target && mobj->target->player)
+		{
+			angle_t diff = R_PointToAngle2(mobj->x, mobj->y, mobj->target->x, mobj->target->y) - mobj->angle;
+			if (diff > ANGLE_180)
+				diff = InvAngle(InvAngle(diff)/4);
+			else
+				diff /= 4;
+			mobj->angle += diff;
+		}
 	}
 	else if (mobj->threshold >= 0) // Traveling mode
 	{
@@ -4666,13 +4682,10 @@ static void P_Boss3Thinker(mobj_t *mobj)
 				S_StartSound(mobj, shock->info->seesound);
 
 				// look for a new target
-				P_BossTargetPlayer(mobj, false);
+				P_BossTargetPlayer(mobj, true);
 
 				if (mobj->target && mobj->target->player)
-				{
-					A_FaceTarget(mobj);
 					P_SetMobjState(mobj, mobj->info->missilestate);
-				}
 			}
 			else if (mobj->flags2 & (MF2_STRONGBOX|MF2_CLASSICPUSH)) // just hit the bottom of your tube
 			{
@@ -7076,7 +7089,7 @@ static void P_SpawnMinecartSegments(mobj_t *mobj, boolean mode)
 		seg = P_SpawnMobj(x, y, z, MT_MINECARTSEG);
 		P_SetMobjState(seg, (statenum_t)(S_MINECARTSEG_FRONT + i));
 		if (i >= 2)
-			seg->extravalue1 = (i == 2) ? -18 : 18;
+			seg->extravalue1 = (i == 2) ? -20 : 20;
 		else
 		{
 			seg->extravalue2 = (i == 0) ? 24 : -24;
@@ -8227,7 +8240,7 @@ void P_MobjThinker(mobj_t *mobj)
 			mobj->flags2 ^= MF2_DONTDRAW;
 			break;
 		case MT_EGGTRAP: // Egg Capsule animal release
-			if (mobj->fuse > 0 && mobj->fuse < 2*TICRATE-(TICRATE/7))
+			if (mobj->fuse > 0)// && mobj->fuse < TICRATE-(TICRATE/7))
 			{
 				INT32 i;
 				fixed_t x,y,z;
@@ -8236,9 +8249,9 @@ void P_MobjThinker(mobj_t *mobj)
 				mobj_t *flicky;
 
 				z = mobj->subsector->sector->floorheight + FRACUNIT + (P_RandomKey(64)<<FRACBITS);
-				for (i = 0; i < 2; i++)
+				for (i = 0; i < 3; i++)
 				{
-					const angle_t fa = (P_RandomByte()*FINEANGLES/16) & FINEMASK;
+					const angle_t fa = P_RandomKey(FINEANGLES) & FINEMASK;
 					ns = 64 * FRACUNIT;
 					x = mobj->x + FixedMul(FINESINE(fa),ns);
 					y = mobj->y + FixedMul(FINECOSINE(fa),ns);
@@ -8690,6 +8703,13 @@ void P_MobjThinker(mobj_t *mobj)
 			case MT_KOOPA:
 				P_KoopaThinker(mobj);
 				break;
+			case MT_FIREBALL:
+				if (P_AproxDistance(mobj->momx, mobj->momy) <= 16*FRACUNIT) // Once fireballs lose enough speed, kill them
+				{
+					P_KillMobj(mobj, NULL, NULL, 0);
+					return;
+				}
+				break;
 			case MT_REDRING:
 				if (((mobj->z < mobj->floorz) || (mobj->z + mobj->height > mobj->ceilingz))
 					&& mobj->flags & MF_MISSILE)
@@ -9622,6 +9642,90 @@ void P_MobjThinker(mobj_t *mobj)
 					}
 					break;
 				}
+			case MT_DRAGONBOMBER:
+				{
+#define DRAGONTURNSPEED ANG2
+					mobj->movecount = (mobj->movecount + 9) % 360;
+					P_SetObjectMomZ(mobj, 4*FINESINE(((mobj->movecount*ANG1) >> ANGLETOFINESHIFT) & FINEMASK), false);
+					if (mobj->threshold > 0) // are we dropping mines?
+					{
+						mobj->threshold--;
+						if (mobj->threshold == 0) // if the timer hits 0, look for a mine to drop!
+						{
+							mobj_t *segment = mobj;
+							while (segment->tracer != NULL && !P_MobjWasRemoved(segment->tracer) && segment->tracer->state == &states[segment->tracer->info->spawnstate])
+							{
+								segment = segment->tracer;
+							}
+							if (segment != mobj) // found an unactivated segment?
+							{
+								mobj_t *mine = P_SpawnMobjFromMobj(segment, 0, 0, 0, segment->info->painchance);
+								mine->angle = segment->angle;
+								P_InstaThrust(mine, mobj->angle, P_AproxDistance(mobj->momx, mobj->momy) >> 1);
+								P_SetObjectMomZ(mine, -2*FRACUNIT, true);
+								S_StartSound(mine, mine->info->seesound);
+								P_SetMobjState(segment, segment->info->raisestate);
+								mobj->threshold = mobj->info->painchance;
+							}
+						}
+					}
+					if (mobj->target != NULL) // Are we chasing a player?
+					{
+						fixed_t dist = P_AproxDistance(mobj->x - mobj->target->x, mobj->y - mobj->target->y);
+						if (dist > 2000 * mobj->scale) // Not anymore!
+							P_SetTarget(&mobj->target, NULL);
+						else
+						{
+							fixed_t vspeed = FixedMul(mobj->info->speed >> 3, mobj->scale);
+							fixed_t z = mobj->target->z + (mobj->height >> 1) + (mobj->flags & MFE_VERTICALFLIP ? -128*mobj->scale : 128*mobj->scale + mobj->target->height);
+							angle_t diff = R_PointToAngle2(mobj->x, mobj->y, mobj->target->x, mobj->target->y) - mobj->angle;
+							if (diff > ANGLE_180)
+								mobj->angle -= DRAGONTURNSPEED;
+							else
+								mobj->angle += DRAGONTURNSPEED;
+							if (!mobj->threshold && dist < 512 * mobj->scale) // Close enough to drop bombs
+							{
+								mobj->threshold = mobj->info->painchance;
+							}
+							mobj->momz += max(min(z - mobj->z, vspeed), -vspeed);
+						}
+					}
+					else // Can we find a player to chase?
+					{
+						if (mobj->tracer == NULL || mobj->tracer->state != &states[mobj->tracer->info->spawnstate]
+							|| !P_LookForPlayers(mobj, true, false, 2000*mobj->scale)) // if not, circle around the spawnpoint
+						{
+							if (!mobj->spawnpoint) // unless we don't have one, in which case uhhh just circle around wherever we currently are I guess??
+								mobj->angle += DRAGONTURNSPEED;
+							else
+							{
+								fixed_t vspeed = FixedMul(mobj->info->speed >> 3, mobj->scale);
+								fixed_t x = mobj->spawnpoint->x << FRACBITS;
+								fixed_t y = mobj->spawnpoint->y << FRACBITS;
+								fixed_t z = mobj->spawnpoint->z << FRACBITS;
+								angle_t diff = R_PointToAngle2(mobj->x, mobj->y, x, y) - mobj->angle;
+								if (diff > ANGLE_180)
+									mobj->angle -= DRAGONTURNSPEED;
+								else
+									mobj->angle += DRAGONTURNSPEED;
+								mobj->momz += max(min(z - mobj->z, vspeed), -vspeed);
+							}
+						}
+					}
+					P_InstaThrust(mobj, mobj->angle, FixedMul(mobj->info->speed, mobj->scale));
+#undef DRAGONTURNSPEED
+				}
+				break;
+			case MT_MINUS:
+#ifdef ROTSPRITE
+				{
+					if (P_IsObjectOnGround(mobj))
+						mobj->rollangle = 0;
+					else
+						mobj->rollangle = R_PointToAngle2(0, 0, mobj->momz, (mobj->scale << 1) - min(abs(mobj->momz), mobj->scale << 1));
+				}
+#endif
+				break;
 			case MT_SPINFIRE:
 				if (mobj->flags & MF_NOGRAVITY)
 				{
@@ -11207,7 +11311,7 @@ void P_SpawnPlayer(INT32 playernum)
 	mobj->radius = FixedMul(skins[p->skin].radius, mobj->scale);
 	mobj->height = P_GetPlayerHeight(p);
 
-	if (!leveltime && ((maptol & TOL_NIGHTS) == TOL_NIGHTS) != (G_IsSpecialStage(gamemap))) // non-special NiGHTS stage or special non-NiGHTS stage
+	if (!leveltime && !p->spectator && ((maptol & TOL_NIGHTS) == TOL_NIGHTS) != (G_IsSpecialStage(gamemap))) // non-special NiGHTS stage or special non-NiGHTS stage
 	{
 		if (maptol & TOL_NIGHTS)
 		{
diff --git a/src/p_setup.c b/src/p_setup.c
index 461c81b05e0b76e630afc30837b547f34be9f87f..2216334e727b29bb49eac63d38082568a4ff7027 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -2704,6 +2704,7 @@ boolean P_SetupLevel(boolean skipprecip)
 
 	// Cancel all d_main.c fadeouts (keep fade in though).
 	wipegamestate = FORCEWIPEOFF;
+	wipestyleflags = 0;
 
 	// Special stage fade to white
 	// This is handled BEFORE sounds are stopped.
@@ -2724,11 +2725,22 @@ boolean P_SetupLevel(boolean skipprecip)
 			S_FadeOutStopMusic(MUSICRATE/4); //FixedMul(FixedDiv(F_GetWipeLength(wipedefs[wipe_speclevel_towhite])*NEWTICRATERATIO, NEWTICRATE), MUSICRATE)
 
 		F_WipeStartScreen();
-		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 0);
+		wipestyleflags |= (WSF_FADEOUT|WSF_TOWHITE);
+
+#ifdef HWRENDER
+		// uh..........
+		if (rendermode == render_opengl)
+			F_WipeColorFill(0);
+#endif
 
 		F_WipeEndScreen();
 		F_RunWipe(wipedefs[wipe_speclevel_towhite], false);
 
+		I_OsPolling();
+		I_FinishUpdate(); // page flip or blit buffer
+		if (moviemode)
+			M_SaveFrame();
+
 		nowtime = lastwipetic;
 
 		// Hold on white for extra effect.
@@ -2745,6 +2757,13 @@ boolean P_SetupLevel(boolean skipprecip)
 		ranspecialwipe = 1;
 	}
 
+	if (G_GetModeAttackRetryFlag())
+	{
+		if (modeattacking)
+			wipestyleflags |= (WSF_FADEOUT|WSF_TOWHITE);
+		G_ClearModeAttackRetryFlag();
+	}
+
 	// Make sure all sounds are stopped before Z_FreeTags.
 	S_StopSounds();
 	S_ClearSfx();
@@ -2762,7 +2781,13 @@ boolean P_SetupLevel(boolean skipprecip)
 	if (rendermode != render_none && !ranspecialwipe)
 	{
 		F_WipeStartScreen();
-		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+		wipestyleflags |= WSF_FADEOUT;
+
+#ifdef HWRENDER
+		// uh..........
+		if (rendermode == render_opengl)
+			F_WipeColorFill(31);
+#endif
 
 		F_WipeEndScreen();
 		// for titlemap: run a specific wipe if specified
@@ -2787,12 +2812,12 @@ boolean P_SetupLevel(boolean skipprecip)
 		{
 			// Don't include these in the fade!
 			char tx[64];
-			V_DrawSmallString(1, 191, V_ALLOWLOWERCASE, M_GetText("Speeding off to..."));
+			V_DrawSmallString(1, 191, V_ALLOWLOWERCASE|V_TRANSLUCENT|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Speeding off to..."));
 			snprintf(tx, 63, "%s%s%s",
 				mapheaderinfo[gamemap-1]->lvlttl,
-				(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : " ZONE",
-				(mapheaderinfo[gamemap-1]->actnum > 0) ? va(", Act %d",mapheaderinfo[gamemap-1]->actnum) : "");
-			V_DrawSmallString(1, 195, V_ALLOWLOWERCASE, tx);
+				(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : " Zone",
+				(mapheaderinfo[gamemap-1]->actnum > 0) ? va(" %d",mapheaderinfo[gamemap-1]->actnum) : "");
+			V_DrawSmallString(1, 195, V_ALLOWLOWERCASE|V_TRANSLUCENT|V_SNAPTOLEFT|V_SNAPTOBOTTOM, tx);
 			I_UpdateNoVsync();
 		}
 
@@ -3177,7 +3202,7 @@ boolean P_SetupLevel(boolean skipprecip)
 
 	// Remove the loading shit from the screen
 	if (rendermode != render_none && !titlemapinaction)
-		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
+		F_WipeColorFill(levelfadecol);
 
 	if (precache || dedicated)
 		R_PrecacheLevel();
@@ -3226,44 +3251,22 @@ boolean P_SetupLevel(boolean skipprecip)
 #endif
 	}
 
-	// Stage title!
-	if (rendermode != render_none
-		&& (!titlemapinaction)
-		&& ranspecialwipe != 2
-		&& *mapheaderinfo[gamemap-1]->lvlttl != '\0'
-#ifdef HAVE_BLUA
-		&& LUA_HudEnabled(hud_stagetitle)
-#endif
-	)
-	{
-		tic_t starttime = I_GetTime();
-		tic_t endtime = starttime + (10*NEWTICRATERATIO);
-		tic_t nowtime = starttime;
-		tic_t lasttime = starttime;
-		while (nowtime < endtime)
-		{
-			// draw loop
-			while (!((nowtime = I_GetTime()) - lasttime))
-				I_Sleep();
-			lasttime = nowtime;
+	// No render mode, stop here.
+	if (rendermode == render_none)
+		return true;
 
-			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
-			stplyr = &players[consoleplayer];
-			ST_drawLevelTitle(nowtime - starttime);
-			if (splitscreen)
-			{
-				stplyr = &players[secondarydisplayplayer];
-				ST_drawLevelTitle(nowtime - starttime);
-			}
+	// Title card!
+	G_StartTitleCard();
 
-			I_OsPolling();
-			I_UpdateNoBlit();
-			I_FinishUpdate(); // page flip or blit buffer
+	// Can the title card actually run, though?
+	if (!WipeStageTitle)
+		return true;
+	if (ranspecialwipe == 2)
+		return true;
 
-			if (moviemode) // make sure we save frames for the white hold too
-				M_SaveFrame();
-		}
-	}
+	// If so...
+	if ((!(mapheaderinfo[gamemap-1]->levelflags & LF_NOTITLECARD)) && (*mapheaderinfo[gamemap-1]->lvlttl != '\0'))
+		G_PreLevelTitleCard(lt_ticker, true);
 
 	return true;
 }
diff --git a/src/p_spec.c b/src/p_spec.c
index 355f5e72851bc8802a1f2e9434c5aa23a0c97f36..605638bd33456bf3b91caad0fc55ce3709180a1f 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2721,6 +2721,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 				if (line->tag != 0) // Do special stuff only if a non-zero linedef tag is set
 				{
+					// Play sounds from tagged sectors' origins.
 					if (line->flags & ML_EFFECT5) // Repeat Midtexture
 					{
 						// Additionally play the sound from tagged sectors' soundorgs
@@ -2732,31 +2733,45 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 							S_StartSound(&sec->soundorg, sfxnum);
 						}
 					}
-					else if (mo) // A mobj must have triggered the executor
+
+					// Play the sound without origin for anyone, as long as they're inside tagged areas.
+					else
 					{
-						// Only trigger if mobj is touching the tag
+						UINT8 i = 0;
+						mobj_t* camobj = players[displayplayer].mo;
 						ffloor_t *rover;
 						boolean foundit = false;
 
-						for(rover = mo->subsector->sector->ffloors; rover; rover = rover->next)
+						for (i = 0; i < 2; camobj = players[secondarydisplayplayer].mo, i++)
 						{
-							if (rover->master->frontsector->tag != line->tag)
+							if (!camobj)
 								continue;
 
-							if (mo->z > P_GetSpecialTopZ(mo, sectors + rover->secnum, mo->subsector->sector))
-								continue;
+							if (foundit || (camobj->subsector->sector->tag == line->tag))
+							{
+								foundit = true;
+								break;
+							}
 
-							if (mo->z + mo->height < P_GetSpecialBottomZ(mo, sectors + rover->secnum, mo->subsector->sector))
-								continue;
+							// Only trigger if mobj is touching the tag
+							for(rover = camobj->subsector->sector->ffloors; rover; rover = rover->next)
+							{
+								if (rover->master->frontsector->tag != line->tag)
+									continue;
 
-							foundit = true;
-						}
+								if (camobj->z > P_GetSpecialTopZ(camobj, sectors + rover->secnum, camobj->subsector->sector))
+									continue;
 
-						if (mo->subsector->sector->tag == line->tag)
-							foundit = true;
+								if (camobj->z + camobj->height < P_GetSpecialBottomZ(camobj, sectors + rover->secnum, camobj->subsector->sector))
+									continue;
+
+								foundit = true;
+								break;
+							}
+						}
 
-						if (!foundit)
-							return;
+						if (foundit)
+							S_StartSound(NULL, sfxnum);
 					}
 				}
 				else
@@ -4439,59 +4454,55 @@ void P_ProcessSpecialSector(player_t *player, sector_t *sector, sector_t *rovers
 		case 3: // Linedef executor requires all players present
 			/// \todo check continues for proper splitscreen support?
 			for (i = 0; i < MAXPLAYERS; i++)
-				if (playeringame[i] && !players[i].bot && players[i].mo && (gametype != GT_COOP || players[i].lives > 0))
+			{
+				if (!playeringame[i])
+					continue;
+				if (!players[i].mo)
+					continue;
+				if (players[i].spectator)
+					continue;
+				if (players[i].bot)
+					continue;
+				if (gametype == GT_COOP && players[i].lives <= 0)
+					continue;
+				if (roversector)
 				{
-					if (roversector)
+					if (sector->flags & SF_TRIGGERSPECIAL_TOUCH)
 					{
-						if (players[i].mo->subsector->sector == roversector)
-							;
-						else if (sector->flags & SF_TRIGGERSPECIAL_TOUCH)
+						msecnode_t *node;
+						for (node = players[i].mo->touching_sectorlist; node; node = node->m_sectorlist_next)
 						{
-							boolean insector = false;
-							msecnode_t *node;
-							for (node = players[i].mo->touching_sectorlist; node; node = node->m_sectorlist_next)
-							{
-								if (node->m_sector == roversector)
-								{
-									insector = true;
-									break;
-								}
-							}
-							if (!insector)
-								goto DoneSection2;
+							if (P_ThingIsOnThe3DFloor(players[i].mo, sector, node->m_sector))
+								break;
 						}
-						else
-							goto DoneSection2;
-
-						if (!P_ThingIsOnThe3DFloor(players[i].mo, sector, roversector))
+						if (!node)
 							goto DoneSection2;
 					}
-					else
+					else if (players[i].mo->subsector && !P_ThingIsOnThe3DFloor(players[i].mo, sector, players[i].mo->subsector->sector)) // this function handles basically everything for us lmao
+						goto DoneSection2;
+				}
+				else
+				{
+					if (players[i].mo->subsector->sector == sector)
+						;
+					else if (sector->flags & SF_TRIGGERSPECIAL_TOUCH)
 					{
-						if (players[i].mo->subsector->sector == sector)
-							;
-						else if (sector->flags & SF_TRIGGERSPECIAL_TOUCH)
+						msecnode_t *node;
+						for (node = players[i].mo->touching_sectorlist; node; node = node->m_sectorlist_next)
 						{
-							boolean insector = false;
-							msecnode_t *node;
-							for (node = players[i].mo->touching_sectorlist; node; node = node->m_sectorlist_next)
-							{
-								if (node->m_sector == sector)
-								{
-									insector = true;
-									break;
-								}
-							}
-							if (!insector)
-								goto DoneSection2;
+							if (node->m_sector == sector)
+								break;
 						}
-						else
-							goto DoneSection2;
-
-						if (special == 3 && !P_MobjReadyToTrigger(players[i].mo, sector))
+						if (!node)
 							goto DoneSection2;
 					}
+					else
+						goto DoneSection2;
+
+					if (special == 3 && !P_MobjReadyToTrigger(players[i].mo, sector))
+						goto DoneSection2;
 				}
+			}
 			/* FALLTHRU */
 		case 4: // Linedef executor that doesn't require touching floor
 		case 5: // Linedef executor
@@ -4675,7 +4686,7 @@ DoneSection2:
 			{
 				INT32 lineindex;
 
-				P_DoPlayerExit(player);
+				P_DoPlayerFinish(player);
 
 				P_SetupSignExit(player);
 				// important: use sector->tag on next line instead of player->mo->subsector->tag
diff --git a/src/p_tick.c b/src/p_tick.c
index e02b11f4907fd5d87bb722467485b0409f66a28e..237d6b593c5c46640a0cd309a3eb847c76ecf002 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -481,6 +481,9 @@ static inline void P_DoSpecialStageStuff(void)
 				tic_t oldnightstime = players[i].nightstime;
 				countspheres += players[i].spheres;
 
+				if (!oldnightstime)
+					continue;
+
 				// If in water, deplete timer 6x as fast.
 				if (players[i].mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(players[i].powers[pw_shield] & SH_PROTECTWATER))
 					players[i].nightstime -= 5;
@@ -506,12 +509,11 @@ static inline void P_DoSpecialStageStuff(void)
 			{
 				// Halt all the players
 				for (i = 0; i < MAXPLAYERS; i++)
-					if (playeringame[i])
+					if (playeringame[i] && !players[i].exiting)
 					{
 						players[i].mo->momx = players[i].mo->momy = 0;
 						players[i].exiting = (14*TICRATE)/5 + 1;
 					}
-
 				sstimer = 0;
 				P_GiveEmerald(true);
 				P_RestoreMusic(&players[consoleplayer]);
diff --git a/src/p_user.c b/src/p_user.c
index 3c481e7e2f34fc7e6765b8cb9a692d0faa1e7d3e..37f1a5ec538c0638ebb18f5b6980a0f0bc496b27 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -343,13 +343,15 @@ void P_GiveEmerald(boolean spawnObj)
 				continue;
 
 			emmo = P_SpawnMobjFromMobj(players[i].mo, 0, 0, players[i].mo->height, MT_GOTEMERALD);
+			if (!emmo)
+				continue;
 			P_SetTarget(&emmo->target, players[i].mo);
 			P_SetMobjState(emmo, mobjinfo[MT_GOTEMERALD].meleestate + em);
 			P_SetTarget(&players[i].mo->tracer, emmo);
 
 			if (pnum == 255)
 			{
-				i = pnum;
+				pnum = i;
 				continue;
 			}
 
@@ -636,6 +638,10 @@ static void P_DeNightserizePlayer(player_t *player)
 	player->marebonuslap = 0;
 	player->flyangle = 0;
 	player->anotherflyangle = 0;
+#ifdef ROTSPRITE
+	player->mo->rollangle = 0;
+#endif
+
 	P_SetTarget(&player->mo->target, NULL);
 	P_SetTarget(&player->axis1, P_SetTarget(&player->axis2, NULL));
 
@@ -762,6 +768,9 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	player->secondjump = 0;
 	player->flyangle = 0;
 	player->anotherflyangle = 0;
+#ifdef ROTSPRITE
+	player->mo->rollangle = 0;
+#endif
 
 	player->powers[pw_shield] = SH_NONE;
 	player->powers[pw_super] = 0;
@@ -1216,6 +1225,7 @@ void P_GivePlayerSpheres(player_t *player, INT32 num_spheres)
 //
 void P_GivePlayerLives(player_t *player, INT32 numlives)
 {
+	UINT8 prevlives = player->lives;
 	if (!player)
 		return;
 
@@ -1232,10 +1242,9 @@ void P_GivePlayerLives(player_t *player, INT32 numlives)
 
 		if ((netgame || multiplayer) && gametype == GT_COOP && cv_cooplives.value == 0)
 		{
-			UINT8 prevlives = player->lives;
 			P_GivePlayerRings(player, 100*numlives);
 			if (player->lives - prevlives >= numlives)
-				return;
+				goto docooprespawn;
 
 			numlives = (numlives + prevlives - player->lives);
 		}
@@ -1249,6 +1258,15 @@ void P_GivePlayerLives(player_t *player, INT32 numlives)
 		player->lives = 99;
 	else if (player->lives < 1)
 		player->lives = 1;
+
+docooprespawn:
+	if (cv_coopstarposts.value)
+		return;
+	if (prevlives > 0)
+		return;
+	if (!player->spectator)
+		return;
+	P_SpectatorJoinGame(player);
 }
 
 void P_GiveCoopLives(player_t *player, INT32 numlives, boolean sound)
@@ -2124,6 +2142,34 @@ void P_SpawnSpinMobj(player_t *player, mobjtype_t type)
 	P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
 }
 
+/** Called when \p player finishes the level.
+  *
+  * Only use for cases where the player should be able to move
+  * while waiting for others to finish. Otherwise, use P_DoPlayerExit().
+  *
+  * In single player or if ::cv_exitmove is disabled, this will also cause
+  * P_PlayerThink() to call P_DoPlayerExit(), so you do not need to
+  * make a special cases for those.
+  *
+  * \param player The player who finished the level.
+  * \sa P_DoPlayerExit
+  *
+  */
+void P_DoPlayerFinish(player_t *player)
+{
+	if (player->pflags & PF_FINISHED)
+		return;
+
+	player->pflags |= PF_FINISHED;
+
+	if (netgame)
+		CONS_Printf(M_GetText("%s has completed the level.\n"), player_names[player-players]);
+
+	player->powers[pw_underwater] = 0;
+	player->powers[pw_spacetime] = 0;
+	P_RestoreMusic(player);
+}
+
 //
 // P_DoPlayerExit
 //
@@ -2158,12 +2204,14 @@ void P_DoPlayerExit(player_t *player)
 		player->pflags |= P_GetJumpFlags(player);
 		P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
 	}
+	else if (player->pflags & PF_STARTDASH)
+	{
+		player->pflags &= ~PF_STARTDASH;
+		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+	}
 	player->powers[pw_underwater] = 0;
 	player->powers[pw_spacetime] = 0;
 	P_RestoreMusic(player);
-
-	if (playeringame[player-players] && netgame && !circuitmap)
-		CONS_Printf(M_GetText("%s has completed the level.\n"), player_names[player-players]);
 }
 
 #define SPACESPECIAL 12
@@ -2263,7 +2311,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				else if (!player->skidtime)
 					player->pflags &= ~PF_GLIDING;
 			}
-			else if (player->charability == CA_GLIDEANDCLIMB && player->pflags & PF_THOKKED && !(player->pflags & PF_SHIELDABILITY) && player->mo->state-states == S_PLAY_FALL)
+			else if (player->charability == CA_GLIDEANDCLIMB && player->pflags & PF_THOKKED && !(player->pflags & (PF_JUMPED|PF_SHIELDABILITY)) && player->mo->state-states == S_PLAY_FALL)
 			{
 				if (player->mo->state-states != S_PLAY_GLIDE_LANDING)
 				{
@@ -2326,11 +2374,23 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				;
 			else if (player->panim != PA_IDLE && player->panim != PA_WALK && player->panim != PA_RUN && player->panim != PA_DASH)
 			{
+				fixed_t runspd = FixedMul(player->runspeed, player->mo->scale);
+
+				// See comments in P_MovePlayer for explanation of changes.
+
+				if (player->powers[pw_super])
+					runspd = FixedMul(runspd, 5*FRACUNIT/3);
+
+				runspd = FixedMul(runspd, player->mo->movefactor);
+
+				if (maptol & TOL_2D)
+					runspd = FixedMul(runspd, 2*FRACUNIT/3);
+
 				if (player->cmomx || player->cmomy)
 				{
 					if (player->charflags & SF_DASHMODE && player->dashmode >= DASHMODE_THRESHOLD && player->panim != PA_DASH)
 						P_SetPlayerMobjState(player->mo, S_PLAY_DASH);
-					else if (player->speed >= FixedMul(player->runspeed, player->mo->scale)
+					else if (player->speed >= runspd
 					&& (player->panim != PA_RUN || player->mo->state-states == S_PLAY_FLOAT_RUN))
 						P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
 					else if ((player->rmomx || player->rmomy)
@@ -2343,7 +2403,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				{
 					if (player->charflags & SF_DASHMODE && player->dashmode >= DASHMODE_THRESHOLD && player->panim != PA_DASH)
 						P_SetPlayerMobjState(player->mo, S_PLAY_DASH);
-					else if (player->speed >= FixedMul(player->runspeed, player->mo->scale)
+					else if (player->speed >= runspd
 					&& (player->panim != PA_RUN || player->mo->state-states == S_PLAY_FLOAT_RUN))
 						P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
 					else if ((player->mo->momx || player->mo->momy)
@@ -2827,7 +2887,7 @@ static void P_CheckUnderwaterAndSpaceTimer(player_t *player)
 {
 	tic_t timeleft = (player->powers[pw_spacetime]) ? player->powers[pw_spacetime] : player->powers[pw_underwater];
 
-	if (player->exiting)
+	if (player->exiting || (player->pflags & PF_FINISHED))
 		player->powers[pw_underwater] = player->powers[pw_spacetime] = 0;
 
 	timeleft--; // The original code was all n*TICRATE + 1, so let's remove 1 tic for simplicity
@@ -3478,7 +3538,7 @@ static void P_DoClimbing(player_t *player)
 			{
 				P_SetObjectMomZ(player->mo, 2*FRACUNIT, true);
 				if (cmd->forwardmove)
-					P_SetObjectMomZ(player->mo, 2*player->mo->momz/3, false);
+					player->mo->momz = 2*player->mo->momz/3;
 			}
 			if (thrust)
 				P_Thrust(player->mo, player->mo->angle, FixedMul(4*FRACUNIT, player->mo->scale)); // Lil' boost up.
@@ -3993,12 +4053,14 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 	if (player->pflags & PF_ATTACKDOWN || player->climbing || (G_TagGametype() && !(player->pflags & PF_TAGIT)))
 		return;
 
-	if ((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER)
+	if (((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER) && !(player->weapondelay))
 	{
 		player->pflags |= PF_ATTACKDOWN;
 		mo = P_SpawnPlayerMissile(player->mo, MT_FIREBALL, 0);
-		P_InstaThrust(mo, player->mo->angle, ((mo->info->speed>>FRACBITS)*player->mo->scale) + player->speed);
+		if (mo)
+			P_InstaThrust(mo, player->mo->angle, ((mo->info->speed>>FRACBITS)*player->mo->scale) + player->speed);
 		S_StartSound(player->mo, sfx_mario7);
+		P_SetWeaponDelay(player, TICRATE); // Short delay between fireballs so you can't spam them everywhere
 		return;
 	}
 
@@ -4017,8 +4079,8 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 
 		mo = P_SpawnPlayerMissile(player->mo, MT_THROWNBOUNCE, MF2_BOUNCERING);
 
-	if (mo)
-		mo->fuse = 3*TICRATE; // Bounce Ring time
+		if (mo)
+			mo->fuse = 3*TICRATE; // Bounce Ring time
 	}
 	// Rail ring
 	else if (player->currentweapon == WEP_RAIL && player->powers[pw_railring])
@@ -4391,7 +4453,11 @@ void P_DoJump(player_t *player, boolean soundandstate)
 		}
 		else if (player->powers[pw_carry] == CR_ROLLOUT)
 		{
-			player->mo->momz = 9*FRACUNIT + player->mo->tracer->momz;
+			player->mo->momz = 9*FRACUNIT;
+			if (P_MobjFlip(player->mo->tracer)*player->mo->tracer->momz > 0)
+				player->mo->momz += player->mo->tracer->momz;
+			if (!P_IsObjectOnGround(player->mo->tracer))
+				P_SetObjectMomZ(player->mo->tracer, -9*FRACUNIT, true);
 			player->powers[pw_carry] = CR_NONE;
 			player->mo->tracer->flags |= MF_PUSHABLE;
 			P_SetTarget(&player->mo->tracer->tracer, NULL);
@@ -4562,6 +4628,13 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 				 // Revving
 				else if ((cmd->buttons & BT_USE) && (player->pflags & PF_STARTDASH))
 				{
+					if (player->speed > 5*player->mo->scale)
+					{
+						player->pflags &= ~PF_STARTDASH;
+						P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+						S_StartSound(player->mo, sfx_spin);
+						break;
+					}
 					if (player->dashspeed < player->maxdash)
 					{
 #define chargecalculation (6*(player->dashspeed - player->mindash))/(player->maxdash - player->mindash)
@@ -4577,7 +4650,6 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 						G_GhostAddRev();
 					}
 				}
-
 				// If not moving up or down, and travelling faster than a speed of five while not holding
 				// down the spin button and not spinning.
 				// AKA Just go into a spin on the ground, you idiot. ;)
@@ -4729,10 +4801,10 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 
 	// Rolling normally
 	if (onground && player->pflags & PF_SPINNING && !(player->pflags & PF_STARTDASH)
-		&& player->speed < FixedMul(5*FRACUNIT,player->mo->scale) && canstand)
+		&& player->speed < 5*player->mo->scale && canstand)
 	{
 		if (GETSECSPECIAL(player->mo->subsector->sector->special, 4) == 7 || (player->mo->ceilingz - player->mo->floorz < P_GetPlayerHeight(player)))
-			P_InstaThrust(player->mo, player->mo->angle, FixedMul(10*FRACUNIT, player->mo->scale));
+			P_InstaThrust(player->mo, player->mo->angle, 10*player->mo->scale);
 		else
 		{
 			player->skidtime = 0;
@@ -5295,7 +5367,10 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						player->powers[pw_tailsfly] = tailsflytics + 1; // Set the fly timer
 
 						player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE|PF_SPINNING|PF_STARTDASH);
-						player->pflags |= (PF_THOKKED|PF_CANCARRY);
+						if (player->bot == 1)
+							player->pflags |= PF_THOKKED;
+						else
+							player->pflags |= (PF_THOKKED|PF_CANCARRY);
 					}
 					break;
 				case CA_GLIDEANDCLIMB:
@@ -5496,7 +5571,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 				else
 					potentialmomz = ((player->speed < 10*player->mo->scale)
 					? (player->speed - 10*player->mo->scale)/5
-					: 0);
+					: -1); // Should be 0, but made negative to ensure P_PlayerHitFloor runs upon touching ground
 				if (P_MobjFlip(player->mo)*player->mo->momz < potentialmomz)
 					player->mo->momz = P_MobjFlip(player->mo)*potentialmomz;
 				player->pflags &= ~PF_SPINNING;
@@ -5891,6 +5966,8 @@ static void P_3dMovement(player_t *player)
 	// When sliding, don't allow forward/back
 	if (player->pflags & PF_SLIDING)
 		cmd->forwardmove = 0;
+	else if (onground && player->mo->state == states+S_PLAY_PAIN)
+		P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
 
 	player->aiming = cmd->aiming<<FRACBITS;
 
@@ -7705,6 +7782,9 @@ void P_ElementalFire(player_t *player, boolean cropcircle)
 	else
 		ground = player->mo->floorz;
 
+	if (cropcircle)
+		ground += P_MobjFlip(player->mo);
+
 	if (cropcircle)
 	{
 #define numangles 8
@@ -7882,6 +7962,11 @@ static void P_MovePlayer(player_t *player)
 	cmd = &player->cmd;
 	runspd = FixedMul(player->runspeed, player->mo->scale);
 
+	// This was done in Sonic 3 & Knuckles, but has been missed in Sonic Mania and the Taxman/Stealth mobile remakes. Thanks to NeoHazard for his 2017 blogpost on the matter, because this oversight otherwise almost made it all the way to 2.2's release.
+	//https://s3unlocked.blogspot.com/2017/12/over-threshold.html
+	if (player->powers[pw_super])
+		runspd = FixedMul(runspd, 5*FRACUNIT/3);
+
 	// Let's have some movement speed fun on low-friction surfaces, JUST for players... (high friction surfaces shouldn't have any adjustment, since the acceleration in this game is super high and that ends up cheesing high-friction surfaces.)
 	runspd = FixedMul(runspd, player->mo->movefactor);
 
@@ -8364,7 +8449,7 @@ static void P_MovePlayer(player_t *player)
 
 			// Tails Put-Put noise
 			if (player->charability == CA_FLY
-				&& player->bot != 1
+				&& (player->pflags & PF_CANCARRY)
 				&& !(player->mo->eflags & MFE_UNDERWATER)
 				&& leveltime % 10 == 0
 				&& !player->spectator)
@@ -9420,7 +9505,6 @@ static void P_DeathThink(player_t *player)
 	}
 	else if ((netgame || multiplayer) && player->deadtimer >= 8*TICRATE)
 	{
-
 		INT32 i, deadtimercheck = INT32_MAX;
 
 		// In a net/multiplayer game, and out of lives
@@ -9590,8 +9674,25 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 
 	mo = player->mo;
 
-	if (player->exiting && mo->target && mo->target->type == MT_SIGN)
-		sign = mo->target;
+	if (player->playerstate == PST_REBORN)
+	{
+		P_CalcChasePostImg(player, thiscam);
+		return true;
+	}
+
+	if (player->exiting)
+	{
+		if (mo->target && mo->target->type == MT_SIGN && mo->target->spawnpoint
+		&& !(gametype == GT_COOP && (netgame || multiplayer) && cv_exitmove.value))
+			sign = mo->target;
+		else if ((player->powers[pw_carry] == CR_NIGHTSMODE)
+		&& !(player->mo->state >= &states[S_PLAY_NIGHTS_TRANS1]
+		&& player->mo->state <= &states[S_PLAY_NIGHTS_TRANS6]))
+		{
+			P_CalcChasePostImg(player, thiscam);
+			return true;
+		}
+	}
 
 	cameranoclip = (player->powers[pw_carry] == CR_NIGHTSMODE || player->pflags & PF_NOCLIP) || (mo->flags & (MF_NOCLIP|MF_NOCLIPHEIGHT)); // Noclipping player camera noclips too!!
 
@@ -9671,7 +9772,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		camorbit = (!stricmp(cv_cam_orbit.defaultvalue, "off")) ? false : true;
 		camrotate = atoi(cv_cam_rotate.defaultvalue);
 		camdist = FixedMul((INT32)(atof(cv_cam_dist.defaultvalue) * FRACUNIT), mo->scale);
-		camheight = FixedMul((INT32)(atof(cv_cam_height.defaultvalue) * FRACUNIT), FixedMul(player->camerascale, mo->scale));
+		camheight = FixedMul((INT32)(atof(cv_cam_height.defaultvalue) * FRACUNIT), mo->scale);
 	}
 	else if (thiscam == &camera)
 	{
@@ -9680,7 +9781,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		camorbit = cv_cam_orbit.value;
 		camrotate = cv_cam_rotate.value;
 		camdist = FixedMul(cv_cam_dist.value, mo->scale);
-		camheight = FixedMul(cv_cam_height.value, FixedMul(player->camerascale, mo->scale));
+		camheight = FixedMul(cv_cam_height.value, mo->scale);
 	}
 	else // Camera 2
 	{
@@ -9689,9 +9790,12 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		camorbit = cv_cam2_orbit.value;
 		camrotate = cv_cam2_rotate.value;
 		camdist = FixedMul(cv_cam2_dist.value, mo->scale);
-		camheight = FixedMul(cv_cam2_height.value, FixedMul(player->camerascale, mo->scale));
+		camheight = FixedMul(cv_cam2_height.value, mo->scale);
 	}
 
+	if (!(twodlevel || (mo->flags2 & MF2_TWOD)) && !(player->powers[pw_carry] == CR_NIGHTSMODE))
+		camheight = FixedMul(camheight, player->camerascale);
+
 #ifdef REDSANALOG
 	if (P_AnalogMove(player) && (player->cmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) == (BT_CAMLEFT|BT_CAMRIGHT)) {
 		camstill = true;
@@ -9802,9 +9906,10 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 			dist <<= 1;
 	}
 
+	if (!(twodlevel || (mo->flags2 & MF2_TWOD)) && !(player->powers[pw_carry] == CR_NIGHTSMODE))
+		dist = FixedMul(dist, player->camerascale);
 
-
-	checkdist = (dist = FixedMul(dist, player->camerascale));
+	checkdist = dist;
 
 	if (checkdist < 128*FRACUNIT)
 		checkdist = 128*FRACUNIT;
@@ -9889,10 +9994,20 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 
 	pviewheight = FixedMul(41*player->height/48, mo->scale);
 
-	if (mo->eflags & MFE_VERTICALFLIP)
-		z = mo->z + mo->height - pviewheight - camheight + distz;
+	if (sign)
+	{
+		if (mo->eflags & MFE_VERTICALFLIP)
+			z = sign->ceilingz - pviewheight - camheight;
+		else
+			z = sign->floorz + pviewheight + camheight;
+	}
 	else
-		z = mo->z + pviewheight + camheight + distz;
+	{
+		if (mo->eflags & MFE_VERTICALFLIP)
+			z = mo->z + mo->height - pviewheight - camheight + distz;
+		else
+			z = mo->z + pviewheight + camheight + distz;
+	}
 
 	// move camera down to move under lower ceilings
 	newsubsec = R_IsPointInSubsector(((mo->x>>FRACBITS) + (thiscam->x>>FRACBITS))<<(FRACBITS-1), ((mo->y>>FRACBITS) + (thiscam->y>>FRACBITS))<<(FRACBITS-1));
@@ -10112,17 +10227,6 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	if (!camstill && !resetcalled && !paused)
 		thiscam->angle = R_PointToAngle2(thiscam->x, thiscam->y, viewpointx, viewpointy);
 
-	if (sign)
-	{
-		viewpointx = sign->x + FixedMul(FINECOSINE((angle>>ANGLETOFINESHIFT) & FINEMASK), dist);
-		viewpointy = sign->y + FixedMul(FINESINE((angle>>ANGLETOFINESHIFT) & FINEMASK), dist);
-	}
-	else
-	{
-		viewpointx = mo->x + FixedMul(FINECOSINE((angle>>ANGLETOFINESHIFT) & FINEMASK), dist);
-		viewpointy = mo->y + FixedMul(FINESINE((angle>>ANGLETOFINESHIFT) & FINEMASK), dist);
-	}
-
 /*
 	if (twodlevel || (mo->flags2 & MF2_TWOD))
 		thiscam->angle = angle;
@@ -10166,9 +10270,9 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	dist = FixedHypot(f1, f2);
 
 	if (mo->eflags & MFE_VERTICALFLIP)
-		angle = R_PointToAngle2(0, thiscam->z + thiscam->height, dist, mo->z + mo->height - P_GetPlayerHeight(player));
+		angle = R_PointToAngle2(0, thiscam->z + thiscam->height, dist, (sign ? sign->ceilingz : mo->z + mo->height) - P_GetPlayerHeight(player));
 	else
-		angle = R_PointToAngle2(0, thiscam->z, dist, mo->z + P_GetPlayerHeight(player));
+		angle = R_PointToAngle2(0, thiscam->z, dist, (sign ? sign->floorz : mo->z) + P_GetPlayerHeight(player));
 	if (player->playerstate != PST_DEAD)
 		angle += (focusaiming < ANGLE_180 ? focusaiming/2 : InvAngle(InvAngle(focusaiming)/2)); // overcomplicated version of '((signed)focusaiming)/2;'
 
@@ -10328,6 +10432,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 	return false;
 }
 
+// the below is first person only, if you're curious. check out P_CalcChasePostImg in p_mobj.c for chasecam
 static void P_CalcPostImg(player_t *player)
 {
 	sector_t *sector = player->mo->subsector->sector;
@@ -11400,6 +11505,14 @@ void P_PlayerThink(player_t *player)
 		}
 	}
 
+	if (player->pflags & PF_FINISHED)
+	{
+		if ((gametype == GT_COOP && cv_exitmove.value) && !G_EnoughPlayersFinished())
+			player->exiting = 0;
+		else
+			P_DoPlayerExit(player);
+	}
+
 	// check water content, set stuff in mobj
 	P_MobjCheckWater(player->mo);
 
@@ -12276,7 +12389,7 @@ void P_PlayerAfterThink(player_t *player)
 					player->mo->momz = tails->momz;
 				}
 
-				if (gametype == GT_COOP)
+				if (gametype == GT_COOP && (!tails->player || tails->player->bot != 1))
 				{
 					player->mo->angle = tails->angle;
 
diff --git a/src/r_data.c b/src/r_data.c
index d9d8a8f3054ee5ab1431a8e52ac86e1588264890..574c4e194307a4d6849e1cf19d61a5f33f0f0102 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -24,6 +24,7 @@
 #include "z_zone.h"
 #include "p_setup.h" // levelflats
 #include "v_video.h" // pMasterPalette
+#include "f_finale.h" // wipes
 #include "byteptr.h"
 #include "dehacked.h"
 
@@ -113,6 +114,7 @@ INT32 *texturetranslation;
 sprcache_t *spritecachedinfo;
 
 lighttable_t *colormaps;
+lighttable_t *fadecolormap;
 
 // for debugging/info purposes
 size_t flatmemory, spritememory, texturememory;
@@ -1455,18 +1457,111 @@ static void R_InitSpriteLumps(void)
 	Z_Malloc(max_spritelumps*sizeof(*spritecachedinfo), PU_STATIC, &spritecachedinfo);
 }
 
+//
+// R_CreateFadeColormaps
+//
+
+static void R_CreateFadeColormaps(void)
+{
+	UINT8 px, fade;
+	RGBA_t rgba;
+	INT32 r, g, b;
+	size_t len, i;
+
+	len = (256 * FADECOLORMAPROWS);
+	fadecolormap = Z_MallocAlign(len*2, PU_STATIC, NULL, 8);
+	for (i = 0; i < len*2; i++)
+		fadecolormap[i] = (i%256);
+
+	// Load in the light tables, now 64k aligned for smokie...
+	{
+		lumpnum_t lump = W_CheckNumForName("FADECMAP");
+		lumpnum_t wlump = W_CheckNumForName("FADEWMAP");
+
+		// to black
+		if (lump != LUMPERROR)
+			W_ReadLumpHeader(lump, fadecolormap, len, 0U);
+		// to white
+		if (wlump != LUMPERROR)
+			W_ReadLumpHeader(wlump, fadecolormap+len, len, 0U);
+
+		// missing "to white" colormap lump
+		if (lump != LUMPERROR && wlump == LUMPERROR)
+			goto makewhite;
+		// missing "to black" colormap lump
+		else if (lump == LUMPERROR && wlump != LUMPERROR)
+			goto makeblack;
+		// both lumps found
+		else if (lump != LUMPERROR && wlump != LUMPERROR)
+			return;
+	}
+
+#define GETCOLOR \
+	px = colormaps[i%256]; \
+	fade = (i/256) * (256 / FADECOLORMAPROWS); \
+	rgba = V_GetColor(px);
+
+	// to black
+	makeblack:
+	for (i = 0; i < len; i++)
+	{
+		// find pixel and fade amount
+		GETCOLOR;
+
+		// subtractive color blending
+		r = rgba.s.red - FADEREDFACTOR*fade/10;
+		g = rgba.s.green - FADEGREENFACTOR*fade/10;
+		b = rgba.s.blue - FADEBLUEFACTOR*fade/10;
+
+		// clamp values
+		if (r < 0) r = 0;
+		if (g < 0) g = 0;
+		if (b < 0) b = 0;
+
+		// find nearest color in palette
+		fadecolormap[i] = NearestColor(r,g,b);
+	}
+
+	// to white
+	makewhite:
+	for (i = len; i < len*2; i++)
+	{
+		// find pixel and fade amount
+		GETCOLOR;
+
+		// additive color blending
+		r = rgba.s.red + FADEREDFACTOR*fade/10;
+		g = rgba.s.green + FADEGREENFACTOR*fade/10;
+		b = rgba.s.blue + FADEBLUEFACTOR*fade/10;
+
+		// clamp values
+		if (r > 255) r = 255;
+		if (g > 255) g = 255;
+		if (b > 255) b = 255;
+
+		// find nearest color in palette
+		fadecolormap[i] = NearestColor(r,g,b);
+	}
+#undef GETCOLOR
+}
+
 //
 // R_InitColormaps
 //
 static void R_InitColormaps(void)
 {
+	size_t len;
 	lumpnum_t lump;
 
 	// Load in the light tables
 	lump = W_GetNumForName("COLORMAP");
-	colormaps = Z_MallocAlign(W_LumpLength (lump), PU_STATIC, NULL, 8);
+	len = W_LumpLength(lump);
+	colormaps = Z_MallocAlign(len, PU_STATIC, NULL, 8);
 	W_ReadLump(lump, colormaps);
 
+	// Make colormap for fades
+	R_CreateFadeColormaps();
+
 	// Init Boom colormaps.
 	R_ClearColormaps();
 #ifdef EXTRACOLORMAPLUMPS
@@ -1495,6 +1590,9 @@ void R_ReInitColormaps(UINT16 num)
 	}
 
 	W_ReadLumpHeader(lump, colormaps, W_LumpLength(basecolormaplump), 0U);
+	if (fadecolormap)
+		Z_Free(fadecolormap);
+	R_CreateFadeColormaps();
 
 	// Init Boom colormaps.
 	R_ClearColormaps();
diff --git a/src/r_defs.h b/src/r_defs.h
index 4d41f4f706c491b985af8eee2ba0f5087a5cfd71..6d0b10b706d27897c7445c92766ad7a4f074f9e2 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -410,6 +410,8 @@ typedef enum
 	ST_NEGATIVE
 } slopetype_t;
 
+#define HORIZONSPECIAL 41
+
 typedef struct line_s
 {
 	// Vertices, from v1 to v2.
diff --git a/src/r_main.c b/src/r_main.c
index 3ed509af568565c95c67a443ab76b5625ff4c840..5e758e52a02d967fd21ccb48d8868753ed07eb46 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -123,8 +123,12 @@ consvar_t cv_chasecam2 = {"chasecam2", "On", CV_CALL, CV_OnOff, ChaseCam2_OnChan
 consvar_t cv_flipcam = {"flipcam", "No", CV_SAVE|CV_CALL|CV_NOINIT, CV_YesNo, FlipCam_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_flipcam2 = {"flipcam2", "No", CV_SAVE|CV_CALL|CV_NOINIT, CV_YesNo, FlipCam2_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
+#if defined(FLOORSPLATS) || defined(GLBADSHADOWS)
 consvar_t cv_shadow = {"shadow", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+#endif //#if defined(FLOORSPLATS) || defined(GLBADSHADOWS)
+#ifdef GLBADSHADOWS
 consvar_t cv_shadowoffs = {"offsetshadows", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+#endif //#ifdef GLBADSHADOWS
 consvar_t cv_skybox = {"skybox", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_allowmlook = {"allowmlook", "Yes", CV_NETVAR, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_showhud = {"showhud", "Yes", CV_CALL,  CV_YesNo, R_SetViewSize, 0, NULL, NULL, 0, 0, NULL};
@@ -1172,8 +1176,12 @@ void R_RegisterEngineStuff(void)
 
 	CV_RegisterVar(&cv_chasecam);
 	CV_RegisterVar(&cv_chasecam2);
+#if defined(FLOORSPLATS) || defined(GLBADSHADOWS)
 	CV_RegisterVar(&cv_shadow);
+#endif //#if defined(FLOORSPLATS) || defined(GLBADSHADOWS)
+#ifdef GLBADSHADOWS
 	CV_RegisterVar(&cv_shadowoffs);
+#endif //#ifdef GLBADSHADOWS
 	CV_RegisterVar(&cv_skybox);
 
 	CV_RegisterVar(&cv_cam_dist);
diff --git a/src/r_main.h b/src/r_main.h
index 2c9b5cc3d536aa5a53404b7dbf72d3b4464ffd7c..ae74ee067d145abc8ed5eb8bacb1e723442050eb 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -76,7 +76,12 @@ extern consvar_t cv_showhud, cv_translucenthud;
 extern consvar_t cv_homremoval;
 extern consvar_t cv_chasecam, cv_chasecam2;
 extern consvar_t cv_flipcam, cv_flipcam2;
-extern consvar_t cv_shadow, cv_shadowoffs;
+#if defined(FLOORSPLATS) || defined(GLBADSHADOWS)
+extern consvar_t cv_shadow;
+#endif
+#ifdef GLBADSHADOWS
+extern conscar_t cv_shadowoffs;
+#endif //#ifdef GLBADSHADOWS
 extern consvar_t cv_translucency;
 extern consvar_t cv_drawdist, cv_drawdist_nights, cv_drawdist_precip;
 extern consvar_t cv_skybox;
diff --git a/src/r_plane.c b/src/r_plane.c
index a3075c9d845ba4c3ddd74b3f1d9101cc74920d13..f21f49101266d1bd044d2eceedbfb31f19bcdf61 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -1007,6 +1007,8 @@ void R_DrawSinglePlane(visplane_t *pl)
 			R_CheckFlatLength(W_LumpLength(levelflat->u.flat.lumpnum));
 			// Raw flats always have dimensions that are powers-of-two numbers.
 			ds_powersoftwo = true;
+			if (spanfunc == basespanfunc)
+				spanfunc = mmxspanfunc;
 			break;
 		default:
 			switch (type)
diff --git a/src/r_segs.c b/src/r_segs.c
index 6eb81ce7a4a64a78a3cda0765bd64149fc0eb280..ee62bfc739faf207ade72cda308433522621592a 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -2694,7 +2694,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 	worldbottomslope >>= 4;
 #endif
 
-	if (linedef->special == 41) { // HORIZON LINES
+	if (linedef->special == HORIZONSPECIAL) { // HORIZON LINES
 		topstep = bottomstep = 0;
 		topfrac = bottomfrac = (centeryfrac>>4);
 		topfrac++; // Prevent 1px HOM
@@ -2825,7 +2825,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 #ifdef ESLOPE
 			ffloor[i].f_pos_slope >>= 4;
 #endif
-			if (linedef->special == 41) // Horizon lines extend FOFs in contact with them too.
+			if (linedef->special == HORIZONSPECIAL) // Horizon lines extend FOFs in contact with them too.
 			{
 				ffloor[i].f_step = 0;
 				ffloor[i].f_frac = (centeryfrac>>4);
diff --git a/src/r_state.h b/src/r_state.h
index da9425bdf0405160810e7102a4d53ce1241eb881..9773849265ef77a17f4fd6712078388c2180aa46 100644
--- a/src/r_state.h
+++ b/src/r_state.h
@@ -38,6 +38,7 @@ typedef struct
 extern sprcache_t *spritecachedinfo;
 
 extern lighttable_t *colormaps;
+extern lighttable_t *fadecolormap;
 
 // Boom colormaps.
 extern extracolormap_t *extra_colormaps;
diff --git a/src/r_things.c b/src/r_things.c
index ae3c47db48f50009e402b2bb300666597575d6a9..9763648cd01446d5dbd1e632bca7853e4def287d 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -1137,8 +1137,6 @@ static void R_ProjectSprite(mobj_t *thing)
 	UINT32 rollangle = AngleFixed(arollangle)>>FRACBITS;
 #endif
 
-	fixed_t ang_scale = FRACUNIT;
-
 	// transform the origin point
 	tr_x = thing->x - viewx;
 	tr_y = thing->y - viewy;
@@ -1196,20 +1194,20 @@ static void R_ProjectSprite(mobj_t *thing)
 #ifdef ROTSPRITE
 		sprinfo = NULL;
 #endif
-	}
 
-	if (rot >= sprdef->numframes)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("R_ProjectSprite: invalid sprite frame %s/%s for %s\n"),
-			sizeu1(rot), sizeu2(sprdef->numframes), sprnames[thing->sprite]);
-		thing->sprite = states[S_UNKNOWN].sprite;
-		thing->frame = states[S_UNKNOWN].frame;
-		sprdef = &sprites[thing->sprite];
-		rot = thing->frame&FF_FRAMEMASK;
-		if (!thing->skin)
+		if (rot >= sprdef->numframes)
 		{
-			thing->state->sprite = thing->sprite;
-			thing->state->frame = thing->frame;
+			CONS_Alert(CONS_ERROR, M_GetText("R_ProjectSprite: invalid sprite frame %s/%s for %s\n"),
+				sizeu1(rot), sizeu2(sprdef->numframes), sprnames[thing->sprite]);
+			if (thing->sprite == thing->state->sprite && thing->frame == thing->state->frame)
+			{
+				thing->state->sprite = states[S_UNKNOWN].sprite;
+				thing->state->frame = states[S_UNKNOWN].frame;
+			}
+			thing->sprite = states[S_UNKNOWN].sprite;
+			thing->frame = states[S_UNKNOWN].frame;
+			sprdef = &sprites[thing->sprite];
+			rot = thing->frame&FF_FRAMEMASK;
 		}
 	}
 
@@ -1223,8 +1221,6 @@ static void R_ProjectSprite(mobj_t *thing)
 	if (sprframe->rotate != SRF_SINGLE || papersprite)
 	{
 		ang = R_PointToAngle (thing->x, thing->y) - (thing->player ? thing->player->drawangle : thing->angle);
-		if (papersprite)
-			ang_scale = abs(FINESINE(ang>>ANGLETOFINESHIFT));
 	}
 
 	if (sprframe->rotate == SRF_SINGLE)
@@ -1286,24 +1282,11 @@ static void R_ProjectSprite(mobj_t *thing)
 	else
 		offset = -spr_offset;
 	offset = FixedMul(offset, this_scale);
-	tx += FixedMul(offset, ang_scale);
-	x1 = (centerxfrac + FixedMul (tx,xscale)) >>FRACBITS;
-
-	// off the right side?
-	if (x1 > viewwidth)
-		return;
-
 	offset2 = FixedMul(spr_width, this_scale);
-	tx += FixedMul(offset2, ang_scale);
-	x2 = ((centerxfrac + FixedMul (tx,xscale)) >> FRACBITS) - (papersprite ? 2 : 1);
-
-	// off the left side
-	if (x2 < 0)
-		return;
 
 	if (papersprite)
 	{
-		fixed_t yscale2, cosmul, sinmul, tz2;
+		fixed_t xscale2, yscale2, cosmul, sinmul, tz2;
 		INT32 range;
 
 		if (ang >= ANGLE_180)
@@ -1323,6 +1306,16 @@ static void R_ProjectSprite(mobj_t *thing)
 		yscale = FixedDiv(projectiony, tz);
 		if (yscale < 64) return; // Fix some funky visuals
 
+		gxt = -FixedMul(tr_x, viewsin);
+		gyt = FixedMul(tr_y, viewcos);
+		tx = -(gyt + gxt);
+		xscale = FixedDiv(projection, tz);
+		x1 = (centerxfrac + FixedMul(tx,xscale))>>FRACBITS;
+
+		// off the right side?
+		if (x1 > viewwidth)
+			return;
+
 		tr_x += FixedMul(offset2, cosmul);
 		tr_y += FixedMul(offset2, sinmul);
 		gxt = FixedMul(tr_x, viewcos);
@@ -1331,15 +1324,25 @@ static void R_ProjectSprite(mobj_t *thing)
 		yscale2 = FixedDiv(projectiony, tz2);
 		if (yscale2 < 64) return; // ditto
 
+		gxt = -FixedMul(tr_x, viewsin);
+		gyt = FixedMul(tr_y, viewcos);
+		tx = -(gyt + gxt);
+		xscale2 = FixedDiv(projection, tz2);
+		x2 = (centerxfrac + FixedMul(tx,xscale2))>>FRACBITS; x2--;
+
+		// off the left side
+		if (x2 < 0)
+			return;
+
 		if (max(tz, tz2) < FixedMul(MINZ, this_scale)) // non-papersprite clipping is handled earlier
 			return;
 
-		if (x2 > x1)
-			range = (x2 - x1);
-		else
+		if ((range = x2 - x1) <= 0)
 			range = 1;
 
-		scalestep = (yscale2 - yscale)/range ?: 1;
+		scalestep = (yscale2 - yscale)/range;
+		scalestep = scalestep ? scalestep : 1;
+		xscale = FixedDiv(range<<FRACBITS, abs(offset2))+1;
 
 		// The following two are alternate sorting methods which might be more applicable in some circumstances. TODO - maybe enable via MF2?
 		// sortscale = max(yscale, yscale2);
@@ -1349,9 +1352,20 @@ static void R_ProjectSprite(mobj_t *thing)
 	{
 		scalestep = 0;
 		yscale = sortscale;
-	}
+		tx += offset;
+		x1 = (centerxfrac + FixedMul(tx,xscale))>>FRACBITS;
+
+		// off the right side?
+		if (x1 > viewwidth)
+			return;
+
+		tx += offset2;
+		x2 = ((centerxfrac + FixedMul(tx,xscale))>>FRACBITS); x2--;
 
-	xscale = FixedMul(xscale, ang_scale);
+		// off the left side
+		if (x2 < 0)
+			return;
+	}
 
 	if ((thing->flags2 & MF2_LINKDRAW) && thing->tracer) // toast 16/09/16 (SYMMETRY)
 	{
diff --git a/src/s_sound.c b/src/s_sound.c
index ef673c9afe87ee6f557951f48ec25ce0565c241a..2f88349f1503775bc0c7388d8002f268111a7ad3 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -860,7 +860,6 @@ static INT32 actualmidimusicvolume;
 void S_UpdateSounds(void)
 {
 	INT32 audible, cnum, volume, sep, pitch;
-	UINT8 i;
 	channel_t *c;
 
 	listener_t listener;
@@ -1017,28 +1016,30 @@ void S_UpdateSounds(void)
 
 notinlevel:
 	I_UpdateSound();
+}
 
+void S_UpdateClosedCaptions(void)
+{
+	UINT8 i;
+	boolean gamestopped = (paused || P_AutoPause());
+	for (i = 0; i < NUMCAPTIONS; i++) // update captions
 	{
-		boolean gamestopped = (paused || P_AutoPause());
-		for (i = 0; i < NUMCAPTIONS; i++) // update captions
-		{
-			if (!closedcaptions[i].s)
-				continue;
+		if (!closedcaptions[i].s)
+			continue;
 
-			if (i == 0 && (closedcaptions[0].s-S_sfx == sfx_None) && gamestopped)
-				continue;
+		if (i == 0 && (closedcaptions[0].s-S_sfx == sfx_None) && gamestopped)
+			continue;
 
-			if (!(--closedcaptions[i].t))
-			{
-				closedcaptions[i].c = NULL;
-				closedcaptions[i].s = NULL;
-			}
-			else if (closedcaptions[i].c && !I_SoundIsPlaying(closedcaptions[i].c->handle))
-			{
-				closedcaptions[i].c = NULL;
-				if (closedcaptions[i].t > CAPTIONFADETICS)
-					closedcaptions[i].t = CAPTIONFADETICS;
-			}
+		if (!(--closedcaptions[i].t))
+		{
+			closedcaptions[i].c = NULL;
+			closedcaptions[i].s = NULL;
+		}
+		else if (closedcaptions[i].c && !I_SoundIsPlaying(closedcaptions[i].c->handle))
+		{
+			closedcaptions[i].c = NULL;
+			if (closedcaptions[i].t > CAPTIONFADETICS)
+				closedcaptions[i].t = CAPTIONFADETICS;
 		}
 	}
 }
@@ -1685,7 +1686,7 @@ boolean S_PrepareSoundTest(void)
 		soundtestdefs[pos++] = def;
 		if (def->soundtestcond > 0 && !(mapvisited[def->soundtestcond-1] & MV_BEATEN))
 			continue;
-		if (def->soundtestcond < 0 && !M_Achieved(1-def->soundtestcond))
+		if (def->soundtestcond < 0 && !M_Achieved(-1-def->soundtestcond))
 			continue;
 		def->allowed = true;
 	}
diff --git a/src/s_sound.h b/src/s_sound.h
index d1551df0bdcad2210517dd3ae894fa7848169da2..f9bbf67673a7f49b3a2cacdc5150f6df1f934636 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -303,6 +303,7 @@ boolean S_FadeOutStopMusic(UINT32 ms);
 // Updates music & sounds
 //
 void S_UpdateSounds(void);
+void S_UpdateClosedCaptions(void);
 
 FUNCMATH fixed_t S_CalculateSoundDistance(fixed_t px1, fixed_t py1, fixed_t pz1, fixed_t px2, fixed_t py2, fixed_t pz2);
 
diff --git a/src/screen.c b/src/screen.c
index f1d91de66b2017ea8347e494293b5ec0848f2466..5005118b6c2a1de28d37d2f447488e8e3e362559 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -421,13 +421,24 @@ void SCR_DisplayTicRate(void)
 	else if (totaltics == TICRATE) ticcntcolor = V_GREENMAP;
 
 	V_DrawString(vid.width-(72*vid.dupx), h,
-		V_YELLOWMAP|V_NOSCALESTART|V_HUDTRANS, "FPS:");
+		V_YELLOWMAP|V_NOSCALESTART|V_USERHUDTRANS, "FPS:");
 	V_DrawString(vid.width-(40*vid.dupx), h,
-		ticcntcolor|V_NOSCALESTART|V_HUDTRANS, va("%02d/%02u", totaltics, TICRATE));
+		ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%02d/%02u", totaltics, TICRATE));
 
 	lasttic = ontic;
 }
 
+void SCR_DisplayLocalPing(void)
+{
+	UINT32 ping = playerpingtable[consoleplayer];	// consoleplayer's ping is everyone's ping in a splitnetgame :P
+	if (cv_showping.value == 1 || (cv_showping.value == 2 && servermaxping && ping > servermaxping))	// only show 2 (warning) if our ping is at a bad level
+	{
+		INT32 dispy = cv_ticrate.value ? 180 : 189;
+		HU_drawPing(307, dispy, ping, true, V_SNAPTORIGHT | V_SNAPTOBOTTOM);
+	}
+}
+
+
 void SCR_ClosedCaptions(void)
 {
 	UINT8 i;
diff --git a/src/screen.h b/src/screen.h
index 79f21e8e470bd4832d0c20c5d0b04c586b0635b0..6f03612c46b86b08bec2b6c68b131a9e16371b9b 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -167,5 +167,6 @@ FUNCMATH boolean SCR_IsAspectCorrect(INT32 width, INT32 height);
 // move out to main code for consistency
 void SCR_DisplayTicRate(void);
 void SCR_ClosedCaptions(void);
+void SCR_DisplayLocalPing(void);
 #undef DNWH
 #endif //__SCREEN_H__
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index fb0f4b2ba3eaea01e6881a77f2717672860cb21f..95ddab3cc51615079a1a3f553ab9ac66e0baedc3 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -1176,11 +1176,11 @@ void I_FinishUpdate(void)
 	if (cv_closedcaptioning.value)
 		SCR_ClosedCaptions();
 
-	if (st_overlay)
-	{
-		if (cv_ticrate.value)
-			SCR_DisplayTicRate();
-	}
+	if (cv_ticrate.value)
+		SCR_DisplayTicRate();
+
+	if (cv_showping.value && netgame && consoleplayer != serverplayer)
+		SCR_DisplayLocalPing();
 
 	if (rendermode == render_soft && screens[0])
 	{
diff --git a/src/sounds.c b/src/sounds.c
index b067903b1fca6fdb5ae0bc61e083db79ad7d9919..596ed74f2fa8f49eebb873c5321a0ff7b3dc7212 100644
--- a/src/sounds.c
+++ b/src/sounds.c
@@ -92,7 +92,7 @@ sfxinfo_t S_sfx[NUMSFX] =
   {"pstop",  false, 100,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Crusher stomp"},
   {"steam1", false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Steam jet"}, // Tails 06-19-2001
   {"steam2", false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Steam jet"}, // Tails 06-19-2001
-  {"wbreak", false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Wood breaking"},
+  {"wbreak",  true,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Wood breaking"},
   {"ambmac", false,  60,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Machinery"},
   {"spsmsh", false,  60,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Heavy impact"},
 
@@ -208,7 +208,7 @@ sfxinfo_t S_sfx[NUMSFX] =
   {"shrpsp",  true,  60,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Spincushion"},
   {"shrpgo", false,  60,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Launch"},
   {"mswarp", false,  60, 16, -1, NULL, 0,        -1,  -1, LUMPERROR, "Spinning out"},
-  {"mspogo", false,  60,  8, -1, NULL, 0,        -1,  -1, LUMPERROR, "Breaking through"},
+  {"mspogo",  true,  60,  8, -1, NULL, 0,        -1,  -1, LUMPERROR, "Breaking through"},
   {"boingf", false,  60,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Bouncing"},
   {"corkp",  false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Cork fired"},
   {"corkh",  false,  32,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Cork hit"},
@@ -779,7 +779,7 @@ sfxinfo_t S_sfx[NUMSFX] =
   {"kc49",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
   {"kc4a",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
   {"kc4b",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
-  {"kc4c",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
+  {"kc4c",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Pop-shot"},
   {"kc4d",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, "Power up"},
   {"kc4e",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
   {"kc4f",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 3fbee27e468ca31501186970bc9cf8f11019f292..8b3ceac9d4e6f7106805338b29197d5f90d6eaa3 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -23,6 +23,7 @@
 #include "v_video.h"
 #include "z_zone.h"
 #include "hu_stuff.h"
+#include "console.h"
 #include "s_sound.h"
 #include "i_system.h"
 #include "m_menu.h"
@@ -186,14 +187,18 @@ boolean ST_SameTeam(player_t *a, player_t *b)
 
 static boolean st_stopped = true;
 
-void ST_Ticker(void)
+void ST_Ticker(boolean run)
 {
 	if (st_stopped)
 		return;
+
+	if (run)
+		ST_runTitleCard();
 }
 
 // 0 is default, any others are special palettes.
 INT32 st_palette = 0;
+INT32 st_translucency = 10;
 
 void ST_doPaletteStuff(void)
 {
@@ -348,12 +353,12 @@ void ST_LoadGraphics(void)
 // made separate so that skins code can reload custom face graphics
 void ST_LoadFaceGraphics(INT32 skinnum)
 {
-	if (skins[skinnum].sprites[SPR2_XTRA].numframes)
+	if (skins[skinnum].sprites[SPR2_XTRA].numframes > XTRA_LIFEPIC)
 	{
 		spritedef_t *sprdef = &skins[skinnum].sprites[SPR2_XTRA];
 		spriteframe_t *sprframe = &sprdef->spriteframes[XTRA_LIFEPIC];
 		faceprefix[skinnum] = W_CachePatchNum(sprframe->lumppat[0], PU_HUDGFX);
-		if (skins[skinnum].sprites[(SPR2_XTRA|FF_SPR2SUPER)].numframes)
+		if (skins[skinnum].sprites[(SPR2_XTRA|FF_SPR2SUPER)].numframes > XTRA_LIFEPIC)
 		{
 			sprdef = &skins[skinnum].sprites[SPR2_XTRA|FF_SPR2SUPER];
 			sprframe = &sprdef->spriteframes[0];
@@ -818,7 +823,7 @@ static void ST_drawLivesArea(void)
 			face = superprefix[stplyr->skin];
 		V_DrawSmallMappedPatch(hudinfo[HUD_LIVES].x, hudinfo[HUD_LIVES].y,
 			hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, face, colormap);
-		if (cv_translucenthud.value == 10 && stplyr->powers[pw_super] == 1 && stplyr->mo->tracer)
+		if (st_translucency == 10 && stplyr->powers[pw_super] == 1 && stplyr->mo->tracer)
 		{
 			INT32 v_supertrans = (stplyr->mo->tracer->frame & FF_TRANSMASK) >> FF_TRANSSHIFT;
 			if (v_supertrans < 10)
@@ -1160,16 +1165,142 @@ static void ST_drawInput(void)
 		V_DrawThinString(x, y, hudinfo[HUD_LIVES].f|((leveltime & 4) ? V_YELLOWMAP : V_REDMAP), "BAD DEMO!!");
 }
 
-void ST_drawLevelTitle(tic_t titletime)
+static patch_t *lt_patches[3];
+static INT32 lt_scroll = 0;
+static INT32 lt_mom = 0;
+static INT32 lt_zigzag = 0;
+
+tic_t lt_ticker = 0, lt_lasttic = 0;
+tic_t lt_exitticker = 0, lt_endtime = 0;
+
+//
+// Load the graphics for the title card.
+//
+static void ST_cacheLevelTitle(void)
+{
+	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_WARNINGTITLE))
+	{
+		lt_patches[0] = (patch_t *)W_CachePatchName("LTACTBLU", PU_HUDGFX);
+		lt_patches[1] = (patch_t *)W_CachePatchName("LTZIGZAG", PU_HUDGFX);
+		lt_patches[2] = (patch_t *)W_CachePatchName("LTZZTEXT", PU_HUDGFX);
+	}
+	else // boss map
+	{
+		lt_patches[0] = (patch_t *)W_CachePatchName("LTACTRED", PU_HUDGFX);
+		lt_patches[1] = (patch_t *)W_CachePatchName("LTZIGRED", PU_HUDGFX);
+		lt_patches[2] = (patch_t *)W_CachePatchName("LTZZWARN", PU_HUDGFX);
+	}
+}
+
+//
+// Start the title card.
+//
+void ST_startTitleCard(void)
+{
+	// cache every HUD patch used
+	ST_cacheLevelTitle();
+
+	// initialize HUD variables
+	lt_ticker = lt_exitticker = lt_lasttic = 0;
+	lt_endtime = 2*TICRATE + (10*NEWTICRATERATIO);
+	lt_scroll = BASEVIDWIDTH * FRACUNIT;
+	lt_zigzag = -((lt_patches[1])->width * FRACUNIT);
+	lt_mom = 0;
+}
+
+//
+// What happens before drawing the title card.
+// Which is just setting the HUD translucency.
+//
+void ST_preDrawTitleCard(void)
+{
+	if (lt_ticker >= (lt_endtime + TICRATE))
+		return;
+
+	if (!lt_exitticker)
+		st_translucency = 0;
+	else
+		st_translucency = max(0, min((INT32)lt_exitticker-4, cv_translucenthud.value));
+}
+
+//
+// Run the title card.
+// Called from ST_Ticker.
+//
+void ST_runTitleCard(void)
+{
+	if (lt_ticker >= (lt_endtime + TICRATE))
+		return;
+
+	if (!(paused || P_AutoPause()))
+	{
+		// tick
+		lt_ticker++;
+		if (lt_ticker >= lt_endtime)
+			lt_exitticker++;
+
+		// scroll to screen (level title)
+		if (!lt_exitticker)
+		{
+			if (abs(lt_scroll) > FRACUNIT)
+				lt_scroll -= (lt_scroll>>2);
+			else
+				lt_scroll = 0;
+		}
+		// scroll away from screen (level title)
+		else
+		{
+			lt_mom -= FRACUNIT*6;
+			lt_scroll += lt_mom;
+		}
+
+		// scroll to screen (zigzag)
+		if (!lt_exitticker)
+		{
+			if (abs(lt_zigzag) > FRACUNIT)
+				lt_zigzag -= (lt_zigzag>>2);
+			else
+				lt_zigzag = 0;
+		}
+		// scroll away from screen (zigzag)
+		else
+			lt_zigzag += lt_mom;
+	}
+}
+
+//
+// Draw the title card itself.
+//
+void ST_drawTitleCard(void)
 {
 	char *lvlttl = mapheaderinfo[gamemap-1]->lvlttl;
 	char *subttl = mapheaderinfo[gamemap-1]->subttl;
 	INT32 actnum = mapheaderinfo[gamemap-1]->actnum;
-	INT32 lvlttly, zoney, lvlttlxpos, ttlnumxpos, zonexpos;
+	INT32 lvlttlxpos, ttlnumxpos, zonexpos;
 	INT32 subttlxpos = BASEVIDWIDTH/2;
+	INT32 ttlscroll = FixedInt(lt_scroll);
+	INT32 zzticker;
+	patch_t *actpat, *zigzag, *zztext;
+
+#ifdef HAVE_BLUA
+	if (!LUA_HudEnabled(hud_stagetitle))
+		goto luahook;
+#endif
 
-	if (!(titletime > 2 && titletime-3 < 110))
+	if (lt_ticker >= (lt_endtime + TICRATE))
+#ifdef HAVE_BLUA
+		goto luahook;
+#else
 		return;
+#endif
+
+	if ((lt_ticker-lt_lasttic) > 1)
+		lt_ticker = lt_lasttic+1;
+
+	ST_cacheLevelTitle();
+	actpat = lt_patches[0];
+	zigzag = lt_patches[1];
+	zztext = lt_patches[2];
 
 	lvlttlxpos = ((BASEVIDWIDTH/2) - (V_LevelNameWidth(lvlttl)/2));
 
@@ -1177,72 +1308,71 @@ void ST_drawLevelTitle(tic_t titletime)
 		lvlttlxpos -= V_LevelActNumWidth(actnum);
 
 	ttlnumxpos = lvlttlxpos + V_LevelNameWidth(lvlttl);
-	zonexpos = ttlnumxpos - V_LevelNameWidth(M_GetText("ZONE"));
+	zonexpos = ttlnumxpos - V_LevelNameWidth(M_GetText("Zone"));
 	ttlnumxpos++;
 
 	if (lvlttlxpos < 0)
 		lvlttlxpos = 0;
 
-#if 0 // toaster's experiment. srb2&toast.exe one day, maybe? Requires stuff below to be converted to fixed point.
-#define MIDTTLY 79
-#define MIDZONEY 105
-#define MIDDIFF 4
-
-	if (titletime < 10)
-	{
-		fixed_t z = ((titletime - 3)<<FRACBITS)/7;
-		INT32 ttlh = V_LevelNameHeight(lvlttl);
-		zoney = (200<<FRACBITS) - ((200 - (MIDZONEY + MIDDIFF))*z);
-		lvlttly = ((MIDTTLY + ttlh - MIDDIFF)*z) - (ttlh<<FRACBITS);
-	}
-	else if (titletime < 105)
+	if (!splitscreen || (splitscreen && stplyr == &players[displayplayer]))
 	{
-		fixed_t z = (((titletime - 10)*MIDDIFF)<<(FRACBITS+1))/95;
-		zoney = ((MIDZONEY + MIDDIFF)<<FRACBITS) - z;
-		lvlttly = ((MIDTTLY - MIDDIFF)<<FRACBITS) + z;
+		zzticker = lt_ticker;
+		V_DrawScaledPatch(FixedInt(lt_zigzag), (-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag);
+		V_DrawScaledPatch(FixedInt(lt_zigzag), (zigzag->height-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag);
+		V_DrawScaledPatch(FixedInt(lt_zigzag), (-zigzag->height+zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext);
+		V_DrawScaledPatch(FixedInt(lt_zigzag), (zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext);
 	}
-	else
+
+	if (actnum)
 	{
-		fixed_t z = ((titletime - 105)<<FRACBITS)/7;
-		INT32 zoneh = V_LevelNameHeight(M_GetText("ZONE"));
-		zoney = (MIDZONEY + zoneh - MIDDIFF)*(FRACUNIT - z) - (zoneh<<FRACBITS);
-		lvlttly = ((MIDTTLY + MIDDIFF)<<FRACBITS) + ((200 - (MIDTTLY + MIDDIFF))*z);
+		if (!splitscreen)
+			V_DrawScaledPatch(ttlnumxpos + ttlscroll, 104 - ttlscroll, 0, actpat);
+		V_DrawLevelActNum(ttlnumxpos + ttlscroll, 104, V_PERPLAYER, actnum);
 	}
 
-#undef MIDTTLY
-#undef MIDZONEY
-#undef MIDDIFF
-#else
-	// There's no consistent algorithm that can accurately define the old positions
-	// so I just ended up resorting to a single switch statement to define them
-	switch (titletime-3)
-	{
-		case 0:   zoney = 200; lvlttly =   0; break;
-		case 1:   zoney = 188; lvlttly =  12; break;
-		case 2:   zoney = 176; lvlttly =  24; break;
-		case 3:   zoney = 164; lvlttly =  36; break;
-		case 4:   zoney = 152; lvlttly =  48; break;
-		case 5:   zoney = 140; lvlttly =  60; break;
-		case 6:   zoney = 128; lvlttly =  72; break;
-		case 105: zoney =  80; lvlttly = 104; break;
-		case 106: zoney =  56; lvlttly = 128; break;
-		case 107: zoney =  32; lvlttly = 152; break;
-		case 108: zoney =   8; lvlttly = 176; break;
-		case 109: zoney =   0; lvlttly = 200; break;
-		default:  zoney = 104; lvlttly =  80; break;
-	}
-#endif
+	V_DrawLevelTitle(lvlttlxpos - ttlscroll, 80, V_PERPLAYER, lvlttl);
+	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
+		V_DrawLevelTitle(zonexpos + ttlscroll, 104, V_PERPLAYER, M_GetText("Zone"));
+	V_DrawCenteredString(subttlxpos - ttlscroll, 135, V_PERPLAYER|V_ALLOWLOWERCASE, subttl);
 
-	if (actnum)
-		V_DrawLevelActNum(ttlnumxpos, zoney, V_PERPLAYER, actnum);
+	lt_lasttic = lt_ticker;
 
-	V_DrawLevelTitle(lvlttlxpos, lvlttly, V_PERPLAYER, lvlttl);
+#ifdef HAVE_BLUA
+luahook:
+	LUAh_TitleCardHUD(stplyr);
+#endif
+}
 
-	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
-		V_DrawLevelTitle(zonexpos, zoney, V_PERPLAYER, M_GetText("ZONE"));
+//
+// Drawer for G_PreLevelTitleCard.
+//
+void ST_preLevelTitleCardDrawer(tic_t ticker, boolean update)
+{
+	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
+	if (ticker < PRELEVELTIME-1)
+		ST_drawWipeTitleCard();
+
+	I_OsPolling();
+	I_UpdateNoBlit();
+	if (update)
+		I_FinishUpdate(); // page flip or blit buffer
+}
 
-	if (lvlttly+48 < 200)
-		V_DrawCenteredString(subttlxpos, lvlttly+48, V_PERPLAYER|V_ALLOWLOWERCASE, subttl);
+//
+// Draw the title card while on a wipe.
+// Also used in G_PreLevelTitleCard.
+//
+void ST_drawWipeTitleCard(void)
+{
+	stplyr = &players[consoleplayer];
+	ST_preDrawTitleCard();
+	ST_drawTitleCard();
+	if (splitscreen)
+	{
+		stplyr = &players[secondarydisplayplayer];
+		ST_preDrawTitleCard();
+		ST_drawTitleCard();
+	}
 }
 
 static void ST_drawPowerupHUD(void)
@@ -2106,7 +2236,7 @@ static void ST_drawTextHUD(void)
 			textHUDdraw(M_GetText("\x82""FIRE:""\x80 Enter game"))
 	}
 
-	if (gametype == GT_COOP && (!stplyr->spectator || (!(maptol & TOL_NIGHTS) && G_IsSpecialStage(gamemap))) && stplyr->exiting)
+	if (gametype == GT_COOP && (!stplyr->spectator || (!(maptol & TOL_NIGHTS) && G_IsSpecialStage(gamemap))) && (stplyr->exiting || (stplyr->pflags & PF_FINISHED)))
 	{
 		UINT8 numneeded = (G_IsSpecialStage(gamemap) ? 4 : cv_playersforexit.value);
 		if (numneeded)
@@ -2121,7 +2251,7 @@ static void ST_drawTextHUD(void)
 					continue;
 
 				total++;
-				if (players[i].exiting)
+				if (players[i].exiting || (players[i].pflags & PF_FINISHED))
 					exiting++;
 			}
 
@@ -2394,12 +2524,25 @@ static void ST_doItemFinderIconsAndSound(void)
 		S_StartSound(NULL, sfx_emfind);
 }
 
+//
 // Draw the status bar overlay, customisable: the user chooses which
 // kind of information to overlay
 //
 static void ST_overlayDrawer(void)
 {
-	//hu_showscores = auto hide score/time/rings when tab rankings are shown
+	// Decide whether to draw the stage title or not
+	boolean stagetitle = false;
+
+	// Check for a valid level title
+	// If the HUD is enabled
+	// And, if Lua is running, if the HUD library has the stage title enabled
+	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOTITLECARD) && *mapheaderinfo[gamemap-1]->lvlttl != '\0' && !(hu_showscores && (netgame || multiplayer)))
+	{
+		stagetitle = true;
+		ST_preDrawTitleCard();
+	}
+
+	// hu_showscores = auto hide score/time/rings when tab rankings are shown
 	if (!(hu_showscores && (netgame || multiplayer)))
 	{
 		if ((maptol & TOL_NIGHTS || G_IsSpecialStage(gamemap)) &&
@@ -2550,12 +2693,8 @@ static void ST_overlayDrawer(void)
 #endif
 
 	// draw level title Tails
-	if (*mapheaderinfo[gamemap-1]->lvlttl != '\0' && !(hu_showscores && (netgame || multiplayer))
-#ifdef HAVE_BLUA
-	&& LUA_HudEnabled(hud_stagetitle)
-#endif
-	)
-		ST_drawLevelTitle(timeinmap+70);
+	if (stagetitle && (!WipeInAction) && (!WipeStageTitle))
+		ST_drawTitleCard();
 
 	if (!hu_showscores && (netgame || multiplayer)
 #ifdef HAVE_BLUA
@@ -2631,6 +2770,8 @@ void ST_Drawer(void)
 		}
 	}
 
+	st_translucency = cv_translucenthud.value;
+
 	if (st_overlay)
 	{
 		// No deadview!
diff --git a/src/st_stuff.h b/src/st_stuff.h
index aaf01ca15a2c67ad7bfb84859d4df8efde9505fd..33ffa957a8002f2798e9c3c0fb7459c95d1c197c 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -24,7 +24,7 @@
 //
 
 // Called by main loop.
-void ST_Ticker(void);
+void ST_Ticker(boolean run);
 
 // Called by main loop.
 void ST_Drawer(void);
@@ -47,8 +47,16 @@ void ST_ReloadSkinFaceGraphics(void);
 
 void ST_doPaletteStuff(void);
 
-// level title draw
-void ST_drawLevelTitle(tic_t titletime);
+// title card
+void ST_startTitleCard(void);
+void ST_runTitleCard(void);
+void ST_drawTitleCard(void);
+void ST_preDrawTitleCard(void);
+void ST_preLevelTitleCardDrawer(tic_t ticker, boolean update);
+void ST_drawWipeTitleCard(void);
+
+extern tic_t lt_ticker, lt_lasttic;
+extern tic_t lt_exitticker, lt_endtime;
 
 // return if player a is in the same team as player b
 boolean ST_SameTeam(player_t *a, player_t *b);
@@ -59,6 +67,7 @@ boolean ST_SameTeam(player_t *a, player_t *b);
 
 extern boolean st_overlay; // sb overlay on or off when fullscreen
 extern INT32 st_palette; // 0 is default, any others are special palettes.
+extern INT32 st_translucency;
 
 extern lumpnum_t st_borderpatchnum;
 // patches, also used in intermission
diff --git a/src/strcasestr.c b/src/strcasestr.c
index 2077dc3ffa322e96c38c66536199cafebd8c0447..86c7ec5b0bc02e2519fe3b871d108a89c9514a7a 100644
--- a/src/strcasestr.c
+++ b/src/strcasestr.c
@@ -75,7 +75,7 @@ strcasestr (const char *s, const char *q)
 	if (!( (intptr_t)up|(intptr_t)lp ))
 		return 0;
 
-	if (!lp || up < lp)
+	if (!lp || ( up && up < lp ))
 	{
 		ppa = &up;
 		ppb = &lp;
diff --git a/src/v_video.c b/src/v_video.c
index 44e80c9f5698ebed10d0172eaeec425387898a50..5813a451b645765a22bd3ef612d28c0ab74eefed 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -18,7 +18,9 @@
 #include "p_local.h" // stplyr
 #include "g_game.h" // players
 #include "v_video.h"
+#include "st_stuff.h"
 #include "hu_stuff.h"
+#include "f_finale.h"
 #include "r_draw.h"
 #include "console.h"
 
@@ -574,11 +576,11 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 	if ((alphalevel = ((scrn & V_ALPHAMASK) >> V_ALPHASHIFT)))
 	{
 		if (alphalevel == 13)
-			alphalevel = hudminusalpha[cv_translucenthud.value];
+			alphalevel = hudminusalpha[st_translucency];
 		else if (alphalevel == 14)
-			alphalevel = 10 - cv_translucenthud.value;
+			alphalevel = 10 - st_translucency;
 		else if (alphalevel == 15)
-			alphalevel = hudplusalpha[cv_translucenthud.value];
+			alphalevel = hudplusalpha[st_translucency];
 
 		if (alphalevel >= 10)
 			return; // invis
@@ -874,11 +876,11 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 	if ((alphalevel = ((scrn & V_ALPHAMASK) >> V_ALPHASHIFT)))
 	{
 		if (alphalevel == 13)
-			alphalevel = hudminusalpha[cv_translucenthud.value];
+			alphalevel = hudminusalpha[st_translucency];
 		else if (alphalevel == 14)
-			alphalevel = 10 - cv_translucenthud.value;
+			alphalevel = 10 - st_translucency;
 		else if (alphalevel == 15)
-			alphalevel = hudplusalpha[cv_translucenthud.value];
+			alphalevel = hudplusalpha[st_translucency];
 
 		if (alphalevel >= 10)
 			return; // invis
@@ -1074,7 +1076,7 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 //
 void V_DrawContinueIcon(INT32 x, INT32 y, INT32 flags, INT32 skinnum, UINT8 skincolor)
 {
-	if (skinnum >= 0 && skinnum < numskins && skins[skinnum].sprites[SPR2_XTRA].numframes >= 4)
+	if (skinnum >= 0 && skinnum < numskins && skins[skinnum].sprites[SPR2_XTRA].numframes > XTRA_CONTINUE)
 	{
 		spritedef_t *sprdef = &skins[skinnum].sprites[SPR2_XTRA];
 		spriteframe_t *sprframe = &sprdef->spriteframes[XTRA_CONTINUE];
@@ -1393,11 +1395,11 @@ void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
 	if ((alphalevel = ((c & V_ALPHAMASK) >> V_ALPHASHIFT)))
 	{
 		if (alphalevel == 13)
-			alphalevel = hudminusalpha[cv_translucenthud.value];
+			alphalevel = hudminusalpha[st_translucency];
 		else if (alphalevel == 14)
-			alphalevel = 10 - cv_translucenthud.value;
+			alphalevel = 10 - st_translucency;
 		else if (alphalevel == 15)
-			alphalevel = hudplusalpha[cv_translucenthud.value];
+			alphalevel = hudplusalpha[st_translucency];
 
 		if (alphalevel >= 10)
 			return; // invis
@@ -1860,7 +1862,9 @@ void V_DrawFadeScreen(UINT16 color, UINT8 strength)
 
 	{
 		const UINT8 *fadetable = ((color & 0xFF00) // Color is not palette index?
-		? ((UINT8 *)colormaps + strength*256) // Do COLORMAP fade.
+		? ((UINT8 *)(((color & 0x0F00) == 0x0A00) ? fadecolormap // Do fadecolormap fade.
+		: (((color & 0x0F00) == 0x0B00) ? fadecolormap + (256 * FADECOLORMAPROWS) // Do white fadecolormap fade.
+		: colormaps)) + strength*256) // Do COLORMAP fade.
 		: ((UINT8 *)transtables + ((9-strength)<<FF_TRANSSHIFT) + color*256)); // Else, do TRANSMAP** fade.
 		const UINT8 *deststop = screens[0] + vid.rowbytes * vid.height;
 		UINT8 *buf = screens[0];
@@ -1898,14 +1902,15 @@ void V_DrawPromptBack(INT32 boxheight, INT32 color)
 {
 	UINT8 *deststop, *buf;
 
-	boxheight = ((boxheight * 4) + (boxheight/2)*5);
-
 	if (color >= 256 && color < 512)
 	{
+		boxheight = ((boxheight * 4) + (boxheight/2)*5);
 		V_DrawFill((BASEVIDWIDTH-(vid.width/vid.dupx))/2, BASEVIDHEIGHT-boxheight, (vid.width/vid.dupx),boxheight, (color-256)|V_SNAPTOBOTTOM);
 		return;
 	}
 
+	boxheight *= vid.dupy;
+
 	if (color == INT32_MAX)
 		color = cons_backcolor.value;
 
@@ -1947,7 +1952,7 @@ void V_DrawPromptBack(INT32 boxheight, INT32 color)
 	// heavily simplified -- we don't need to know x or y position,
 	// just the start and stop positions
 	deststop = screens[0] + vid.rowbytes * vid.height;
-	buf = deststop - vid.rowbytes * boxheight * vid.dupy; // 4 lines of space plus gaps between and some leeway
+	buf = deststop - vid.rowbytes * ((boxheight * 4) + (boxheight/2)*5); // 4 lines of space plus gaps between and some leeway
 	for (; buf < deststop; ++buf)
 		*buf = promptbgmap[*buf];
 }
@@ -2899,7 +2904,7 @@ void V_DrawLevelTitle(INT32 x, INT32 y, INT32 option, const char *string)
 			continue;
 		}
 
-		c = toupper(*ch) - LT_FONTSTART;
+		c = *ch - LT_FONTSTART;
 		if (c < 0 || c >= LT_FONTSIZE || !lt_font[c])
 		{
 			cx += 16*dupx;
@@ -2934,7 +2939,7 @@ INT32 V_LevelNameWidth(const char *string)
 	{
 		if (string[i] & 0x80)
 			continue;
-		c = toupper(string[i]) - LT_FONTSTART;
+		c = string[i] - LT_FONTSTART;
 		if (c < 0 || c >= LT_FONTSIZE || !lt_font[c])
 			w += 16;
 		else
@@ -2953,7 +2958,7 @@ INT32 V_LevelNameHeight(const char *string)
 
 	for (i = 0; i < strlen(string); i++)
 	{
-		c = toupper(string[i]) - LT_FONTSTART;
+		c = string[i] - LT_FONTSTART;
 		if (c < 0 || c >= LT_FONTSIZE || !lt_font[c])
 			continue;
 
@@ -2964,7 +2969,7 @@ INT32 V_LevelNameHeight(const char *string)
 	return w;
 }
 
-// For ST_drawLevelTitle
+// For ST_drawTitleCard
 // Returns the width of the act num patch
 INT32 V_LevelActNumWidth(INT32 num)
 {
diff --git a/src/v_video.h b/src/v_video.h
index cd32ac5f89ed5879f01a0e44adfae872e1f2c908..e3dbb75dc0617c79bec46b43aecadc462c6ad494 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -106,6 +106,10 @@ extern RGBA_t *pMasterPalette;
 #define V_HUDTRANSHALF       0x000D0000
 #define V_HUDTRANS           0x000E0000 // draw the hud translucent
 #define V_HUDTRANSDOUBLE     0x000F0000
+// Macros follow
+#define V_USERHUDTRANSHALF   ((10-(cv_translucenthud.value/2))<<V_ALPHASHIFT)
+#define V_USERHUDTRANS       ((10-cv_translucenthud.value)<<V_ALPHASHIFT)
+#define V_USERHUDTRANSDOUBLE ((10-min(cv_translucenthud.value*2, 10))<<V_ALPHASHIFT)
 
 #define V_AUTOFADEOUT        0x00100000 // used by CECHOs, automatic fade out when almost over
 #define V_RETURN8            0x00200000 // 8 pixel return instead of 12
diff --git a/src/w_wad.c b/src/w_wad.c
index c3f38e7376c912e9d1a91cbcd65c3fc6a8aea379..0cd2175ecf394af5144e5bb75fd94c1842ec9505 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -383,7 +383,7 @@ static lumpinfo_t* ResGetLumpsWad (FILE* handle, UINT16* nlmp, const char* filen
 	// read the header
 	if (fread(&header, 1, sizeof header, handle) < sizeof header)
 	{
-		CONS_Alert(CONS_ERROR, M_GetText("Can't read wad header because %s\n"), strerror(ferror(handle)));
+		CONS_Alert(CONS_ERROR, M_GetText("Can't read wad header because %s\n"), M_FileError(handle));
 		return NULL;
 	}
 
@@ -406,7 +406,7 @@ static lumpinfo_t* ResGetLumpsWad (FILE* handle, UINT16* nlmp, const char* filen
 	if (fseek(handle, header.infotableofs, SEEK_SET) == -1
 		|| fread(fileinfo, 1, i, handle) < i)
 	{
-		CONS_Alert(CONS_ERROR, M_GetText("Corrupt wadfile directory (%s)\n"), strerror(ferror(handle)));
+		CONS_Alert(CONS_ERROR, M_GetText("Corrupt wadfile directory (%s)\n"), M_FileError(handle));
 		free(fileinfov);
 		return NULL;
 	}
@@ -427,7 +427,7 @@ static lumpinfo_t* ResGetLumpsWad (FILE* handle, UINT16* nlmp, const char* filen
 				handle) < sizeof realsize)
 			{
 				I_Error("corrupt compressed file: %s; maybe %s", /// \todo Avoid the bailout?
-					filename, strerror(ferror(handle)));
+					filename, M_FileError(handle));
 			}
 			realsize = LONG(realsize);
 			if (realsize != 0)
@@ -565,7 +565,7 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 	fseek(handle, -4, SEEK_CUR);
 	if (fread(&zend, 1, sizeof zend, handle) < sizeof zend)
 	{
-		CONS_Alert(CONS_ERROR, "Corrupt central directory (%s)\n", strerror(ferror(handle)));
+		CONS_Alert(CONS_ERROR, "Corrupt central directory (%s)\n", M_FileError(handle));
 		return NULL;
 	}
 	numlumps = zend.entries;
@@ -582,7 +582,7 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 
 		if (fread(zentry, 1, sizeof(zentry_t), handle) < sizeof(zentry_t))
 		{
-			CONS_Alert(CONS_ERROR, "Failed to read central directory (%s)\n", strerror(ferror(handle)));
+			CONS_Alert(CONS_ERROR, "Failed to read central directory (%s)\n", M_FileError(handle));
 			Z_Free(lumpinfo);
 			free(zentries);
 			return NULL;
@@ -602,7 +602,7 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 		fullname = malloc(zentry->namelen + 1);
 		if (fgets(fullname, zentry->namelen + 1, handle) != fullname)
 		{
-			CONS_Alert(CONS_ERROR, "Unable to read lumpname (%s)\n", strerror(ferror(handle)));
+			CONS_Alert(CONS_ERROR, "Unable to read lumpname (%s)\n", M_FileError(handle));
 			Z_Free(lumpinfo);
 			free(zentries);
 			free(fullname);
diff --git a/src/win32/win_vid.c b/src/win32/win_vid.c
index 11c7a6744b4132baff1a48a4a2b72ec8d43736d8..39a60cd93b936ba3e524d18c4614a8834128a8a4 100644
--- a/src/win32/win_vid.c
+++ b/src/win32/win_vid.c
@@ -371,6 +371,9 @@ void I_FinishUpdate(void)
 	if (cv_ticrate.value)
 		SCR_DisplayTicRate();
 
+	if (cv_showping.value && netgame && consoleplayer != serverplayer)
+		SCR_DisplayLocalPing();
+
 	//
 	if (bDIBMode)
 	{
diff --git a/src/y_inter.c b/src/y_inter.c
index 21e4bc56e480503693286014246ea7dbc0c08679..32548d26369e7c7afe1509ef5b7213045c9dbaaa 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -166,13 +166,11 @@ static INT32 endtic = -1;
 intertype_t intertype = int_none;
 
 static void Y_RescaleScreenBuffer(void);
-static void Y_CleanupScreenBuffer(void);
 static void Y_AwardCoopBonuses(void);
 static void Y_AwardSpecialStageBonus(void);
 static void Y_CalculateCompetitionWinners(void);
 static void Y_CalculateTimeRaceWinners(void);
 static void Y_CalculateMatchWinners(void);
-static void Y_FollowIntermission(void);
 static void Y_UnloadData(void);
 
 // Stuff copy+pasted from st_stuff.c
@@ -293,7 +291,7 @@ static void Y_RescaleScreenBuffer(void)
 //
 // Free all related memory.
 //
-static void Y_CleanupScreenBuffer(void)
+void Y_CleanupScreenBuffer(void)
 {
 	// Who knows?
 	if (y_buffer == NULL)
@@ -399,10 +397,13 @@ void Y_IntermissionDrawer(void)
 
 		// draw the "got through act" lines and act number
 		V_DrawLevelTitle(data.coop.passedx1, 49, 0, data.coop.passed1);
-		V_DrawLevelTitle(data.coop.passedx2, 49+V_LevelNameHeight(data.coop.passed2)+2, 0, data.coop.passed2);
+		{
+			INT32 h = V_LevelNameHeight(data.coop.passed2);
+			V_DrawLevelTitle(data.coop.passedx2, 49+h+2, 0, data.coop.passed2);
 
-		if (data.coop.actnum)
-			V_DrawLevelActNum(244, 57, 0, data.coop.actnum);
+			if (data.coop.actnum)
+				V_DrawLevelActNum(244, 42+h, 0, data.coop.actnum);
+		}
 
 		bonusy = 150;
 		// Total
@@ -485,10 +486,10 @@ void Y_IntermissionDrawer(void)
 
 		if (drawsection == 1)
 		{
-			const char *ringtext = "\x82" "50 RINGS, NO SHIELD";
-			const char *tut1text = "\x82" "PRESS " "\x80" "SPIN";
-			const char *tut2text = "\x82" "MID-" "\x80" "JUMP";
-			ttheight = 16;
+			const char *ringtext = "\x82" "50 rings, no shield";
+			const char *tut1text = "\x82" "press " "\x80" "spin";
+			const char *tut2text = "\x82" "mid-" "\x80" "jump";
+			ttheight = 8;
 			V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
 			ttheight += V_LevelNameHeight(data.spec.passed3) + 2;
 			V_DrawLevelTitle(data.spec.passedx3 + xoffset2, ttheight, 0, data.spec.passed3);
@@ -497,9 +498,9 @@ void Y_IntermissionDrawer(void)
 
 			ttheight = 108;
 			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset4 - (V_LevelNameWidth(ringtext)/2), ttheight, 0, ringtext);
-			ttheight += V_LevelNameHeight(ringtext) + 2;
-			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset5 - (V_LevelNameWidth(tut1text)/2), ttheight, 0, tut1text);
 			ttheight += V_LevelNameHeight(tut1text) + 2;
+			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset5 - (V_LevelNameWidth(tut1text)/2), ttheight, 0, tut1text);
+			ttheight += V_LevelNameHeight(tut2text) + 2;
 			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset6 - (V_LevelNameWidth(tut2text)/2), ttheight, 0, tut2text);
 		}
 		else
@@ -816,7 +817,7 @@ void Y_IntermissionDrawer(void)
 			}
 		}
 	}
-	else if (intertype == int_classicrace)
+	else if (intertype == int_comp)
 	{
 		INT32 x = 4;
 		INT32 y = 48;
@@ -950,7 +951,7 @@ void Y_Ticker(void)
 		if (!--timer)
 		{
 			Y_EndIntermission();
-			Y_FollowIntermission();
+			G_AfterIntermission();
 			return;
 		}
 	}
@@ -958,7 +959,7 @@ void Y_Ticker(void)
 	else if (intertic == endtic)
 	{
 		Y_EndIntermission();
-		Y_FollowIntermission();
+		G_AfterIntermission();
 		return;
 	}
 
@@ -1142,7 +1143,7 @@ void Y_Ticker(void)
 		if (data.match.numplayers != D_NumPlayers())
 			Y_CalculateMatchWinners();
 	}
-	else if (intertype == int_race || intertype == int_classicrace) // race
+	else if (intertype == int_race || intertype == int_comp) // race
 	{
 		if (!intertic) // first time only
 			S_ChangeMusicInternal("_inter", true); // loop it
@@ -1151,96 +1152,6 @@ void Y_Ticker(void)
 	}
 }
 
-//
-// Y_UpdateRecordReplays
-//
-// Update replay files/data, etc. for Record Attack
-// See G_SetNightsRecords for NiGHTS Attack.
-//
-static void Y_UpdateRecordReplays(void)
-{
-	const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
-	char *gpath;
-	char lastdemo[256], bestdemo[256];
-	UINT8 earnedEmblems;
-
-	// Record new best time
-	if (!mainrecords[gamemap-1])
-		G_AllocMainRecordData(gamemap-1);
-
-	if (players[consoleplayer].score > mainrecords[gamemap-1]->score)
-		mainrecords[gamemap-1]->score = players[consoleplayer].score;
-
-	if ((mainrecords[gamemap-1]->time == 0) || (players[consoleplayer].realtime < mainrecords[gamemap-1]->time))
-		mainrecords[gamemap-1]->time = players[consoleplayer].realtime;
-
-	if ((UINT16)(players[consoleplayer].rings) > mainrecords[gamemap-1]->rings)
-		mainrecords[gamemap-1]->rings = (UINT16)(players[consoleplayer].rings);
-
-	if (data.coop.gotperfbonus)
-		mainrecords[gamemap-1]->gotperfect = true;
-
-	// Save demo!
-	bestdemo[255] = '\0';
-	lastdemo[255] = '\0';
-	G_SetDemoTime(players[consoleplayer].realtime, players[consoleplayer].score, (UINT16)(players[consoleplayer].rings));
-	G_CheckDemoStatus();
-
-	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
-	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
-
-	if ((gpath = malloc(glen)) == NULL)
-		I_Error("Out of memory for replay filepath\n");
-
-	sprintf(gpath,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(gamemap));
-	snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, skins[cv_chooseskin.value-1].name);
-
-	if (FIL_FileExists(lastdemo))
-	{
-		UINT8 *buf;
-		size_t len = FIL_ReadFile(lastdemo, &buf);
-
-		snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
-		{ // Better time, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
-		}
-
-		snprintf(bestdemo, 255, "%s-%s-score-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<1)))
-		{ // Better score, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW HIGH SCORE!"), M_GetText("Saved replay as"), bestdemo);
-		}
-
-		snprintf(bestdemo, 255, "%s-%s-rings-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<2)))
-		{ // Better rings, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW MOST RINGS!"), M_GetText("Saved replay as"), bestdemo);
-		}
-
-		//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
-
-		Z_Free(buf);
-	}
-	free(gpath);
-
-	// Check emblems when level data is updated
-	if ((earnedEmblems = M_CheckLevelEmblems()))
-		CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for Record Attack records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
-
-	// Update timeattack menu's replay availability.
-	Nextmap_OnChange();
-}
-
 //
 // Y_StartIntermission
 //
@@ -1249,7 +1160,6 @@ static void Y_UpdateRecordReplays(void)
 void Y_StartIntermission(void)
 {
 	INT32 i;
-	UINT8 completionEmblems = M_CompletionEmblems();
 
 	intertic = -1;
 
@@ -1262,10 +1172,7 @@ void Y_StartIntermission(void)
 	{
 		timer = 0;
 
-		if (G_IsSpecialStage(gamemap))
-			intertype = (maptol & TOL_NIGHTS) ? int_nightsspec : int_spec;
-		else
-			intertype = (maptol & TOL_NIGHTS) ? int_nights : int_coop;
+		intertype = (G_IsSpecialStage(gamemap)) ? int_spec : int_coop;
 	}
 	else
 	{
@@ -1280,14 +1187,7 @@ void Y_StartIntermission(void)
 		}
 
 		if (gametype == GT_COOP)
-		{
-			// Nights intermission is single player only
-			// Don't add it here
-			if (G_IsSpecialStage(gamemap))
-				intertype = int_spec;
-			else
-				intertype = int_coop;
-		}
+			intertype = (G_IsSpecialStage(gamemap)) ? int_spec : int_coop;
 		else if (gametype == GT_TEAMMATCH)
 			intertype = int_teammatch;
 		else if (gametype == GT_MATCH
@@ -1297,7 +1197,7 @@ void Y_StartIntermission(void)
 		else if (gametype == GT_RACE)
 			intertype = int_race;
 		else if (gametype == GT_COMPETITION)
-			intertype = int_classicrace;
+			intertype = int_comp;
 		else if (gametype == GT_CTF)
 			intertype = int_ctf;
 	}
@@ -1312,20 +1212,6 @@ void Y_StartIntermission(void)
 
 	switch (intertype)
 	{
-		case int_nights:
-			// Can't fail
-			G_SetNightsRecords();
-
-			// Check records
-			{
-				UINT8 earnedEmblems = M_CheckLevelEmblems();
-				if (earnedEmblems)
-					CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for NiGHTS records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
-			}
-
-			// fall back into the coop intermission for now
-			intertype = int_coop;
-			/* FALLTHRU */
 		case int_coop: // coop or single player, normal level
 		{
 			// award time and ring bonuses
@@ -1334,24 +1220,6 @@ void Y_StartIntermission(void)
 			// setup time data
 			data.coop.tics = players[consoleplayer].realtime;
 
-			if ((!modifiedgame || savemoddata) && !multiplayer && !demoplayback)
-			{
-				// Update visitation flags
-				mapvisited[gamemap-1] |= MV_BEATEN;
-				if (ALL7EMERALDS(emeralds))
-					mapvisited[gamemap-1] |= MV_ALLEMERALDS;
-				if (ultimatemode)
-					mapvisited[gamemap-1] |= MV_ULTIMATE;
-				if (data.coop.gotperfbonus)
-					mapvisited[gamemap-1] |= MV_PERFECT;
-
-				if (modeattacking == ATTACKING_RECORD)
-					Y_UpdateRecordReplays();
-
-				if (completionEmblems)
-					CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)completionEmblems, completionEmblems > 1 ? "s" : "");
-			}
-
 			for (i = 0; i < 4; ++i)
 				data.coop.bonuspatches[i] = W_CachePatchName(data.coop.bonuses[i].patch, PU_STATIC);
 			data.coop.ptotal = W_CachePatchName("YB_TOTAL", PU_STATIC);
@@ -1384,21 +1252,21 @@ void Y_StartIntermission(void)
 			// too long so just show "YOU GOT THROUGH THE ACT"
 			if (strlen(skins[players[consoleplayer].skin].realname) > 13)
 			{
-				strcpy(data.coop.passed1, "YOU GOT");
-				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "THROUGH ACT" : "THROUGH THE ACT");
+				strcpy(data.coop.passed1, "you got");
+				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "through act" : "through the act");
 			}
 			// long enough that "X GOT" won't fit so use "X PASSED THE ACT"
 			else if (strlen(skins[players[consoleplayer].skin].realname) > 8)
 			{
 				strcpy(data.coop.passed1, skins[players[consoleplayer].skin].realname);
-				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "PASSED ACT" : "PASSED THE ACT");
+				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "passed act" : "passed the act");
 			}
 			// length is okay for normal use
 			else
 			{
-				snprintf(data.coop.passed1, sizeof data.coop.passed1, "%s GOT",
+				snprintf(data.coop.passed1, sizeof data.coop.passed1, "%s got",
 					skins[players[consoleplayer].skin].realname);
-				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "THROUGH ACT" : "THROUGH THE ACT");
+				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "through act" : "through the act");
 			}
 
 			// set X positions
@@ -1418,40 +1286,8 @@ void Y_StartIntermission(void)
 			break;
 		}
 
-		case int_nightsspec:
-			if (modeattacking && stagefailed)
-			{
-				// Nuh-uh.  Get out of here.
-				Y_EndIntermission();
-				Y_FollowIntermission();
-				break;
-			}
-			if (!stagefailed)
-				G_SetNightsRecords();
-
-			// Check records
-			{
-				UINT8 earnedEmblems = M_CheckLevelEmblems();
-				if (earnedEmblems)
-					CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for NiGHTS records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
-			}
-
-			// fall back into the special stage intermission for now
-			intertype = int_spec;
-			/* FALLTHRU */
 		case int_spec: // coop or single player, special stage
 		{
-			// Update visitation flags?
-			if ((!modifiedgame || savemoddata) && !multiplayer && !demoplayback)
-			{
-				if (!stagefailed)
-					mapvisited[gamemap-1] |= MV_BEATEN;
-
-				// all emeralds/ultimate/perfect emblems won't be possible in ss, oh well?
-				if (completionEmblems)
-					CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)completionEmblems, completionEmblems > 1 ? "s" : "");
-			}
-
 			// give out ring bonuses
 			Y_AwardSpecialStageBonus();
 
@@ -1498,7 +1334,7 @@ void Y_StartIntermission(void)
 			// set up the "got through act" message according to skin name
 			if (stagefailed)
 			{
-				strcpy(data.spec.passed2, "SPECIAL STAGE");
+				strcpy(data.spec.passed2, "Special Stage");
 				data.spec.passed1[0] = '\0';
 			}
 			else if (ALL7EMERALDS(emeralds))
@@ -1507,13 +1343,13 @@ void Y_StartIntermission(void)
 					sizeof data.spec.passed1, "%s",
 					skins[players[consoleplayer].skin].realname);
 				data.spec.passed1[sizeof data.spec.passed1 - 1] = '\0';
-				strcpy(data.spec.passed2, "GOT THEM ALL!");
+				strcpy(data.spec.passed2, "got them all!");
 
 				if (players[consoleplayer].charflags & SF_SUPER)
 				{
-					strcpy(data.spec.passed3, "CAN NOW BECOME");
+					strcpy(data.spec.passed3, "can now become");
 					snprintf(data.spec.passed4,
-						sizeof data.spec.passed4, "SUPER %s",
+						sizeof data.spec.passed4, "Super %s",
 						skins[players[consoleplayer].skin].realname);
 					data.spec.passed4[sizeof data.spec.passed4 - 1] = '\0';
 				}
@@ -1523,13 +1359,13 @@ void Y_StartIntermission(void)
 				if (strlen(skins[players[consoleplayer].skin].realname) <= SKINNAMESIZE-5)
 				{
 					snprintf(data.spec.passed1,
-						sizeof data.spec.passed1, "%s GOT",
+						sizeof data.spec.passed1, "%s got",
 						skins[players[consoleplayer].skin].realname);
 					data.spec.passed1[sizeof data.spec.passed1 - 1] = '\0';
 				}
 				else
-					strcpy(data.spec.passed1, "YOU GOT");
-				strcpy(data.spec.passed2, "A CHAOS EMERALD");
+					strcpy(data.spec.passed1, "You got");
+				strcpy(data.spec.passed2, "a Chaos Emerald");
 				if (P_GetNextEmerald() > 6)
 				{
 					data.spec.passed2[15] = '?';
@@ -1637,7 +1473,7 @@ void Y_StartIntermission(void)
 			break;
 		}
 
-		case int_classicrace: // classic (full race)
+		case int_comp: // classic (full race)
 		{
 			// find out who won
 			Y_CalculateCompetitionWinners();
@@ -2192,23 +2028,6 @@ void Y_EndIntermission(void)
 	usebuffer = false;
 }
 
-//
-// Y_FollowIntermission
-//
-static void Y_FollowIntermission(void)
-{
-	if (modeattacking)
-	{
-		M_EndModeAttackRun();
-		return;
-	}
-
-	// This handles whether to play a post-level cutscene, end the game,
-	// or simply go to the next level.
-	// No need to duplicate the code here!
-	G_AfterIntermission();
-}
-
 #define UNLOAD(x) Z_ChangeTag(x, PU_CACHE); x = NULL
 
 //
@@ -2221,8 +2040,6 @@ static void Y_UnloadData(void)
 	if (rendermode != render_soft)
 		return;
 
-	Y_CleanupScreenBuffer();
-
 	// unload the background patches
 	UNLOAD(bgpatch);
 	UNLOAD(widebgpatch);
@@ -2258,7 +2075,7 @@ static void Y_UnloadData(void)
 			break;
 		default:
 			//without this default,
-			//int_none, int_tag, int_chaos, and int_classicrace
+			//int_none, int_tag, int_chaos, and int_comp
 			//are not handled
 			break;
 	}
diff --git a/src/y_inter.h b/src/y_inter.h
index ccb48dbd4c3ed5cd43273a3971724bb9b6c897b7..b47f3b157e8d3eb0b03ca408c0818aa873b97a81 100644
--- a/src/y_inter.h
+++ b/src/y_inter.h
@@ -16,6 +16,7 @@ void Y_Ticker(void);
 void Y_StartIntermission(void);
 void Y_EndIntermission(void);
 void Y_ConsiderScreenBuffer(void);
+void Y_CleanupScreenBuffer(void);
 
 typedef enum
 {
@@ -26,9 +27,7 @@ typedef enum
 //	int_tag,      // Tag
 	int_ctf,      // CTF
 	int_spec,     // Special Stage
-	int_nights,   // NiGHTS into Dreams
-	int_nightsspec,// NiGHTS special stage
 	int_race,     // Race
-	int_classicrace, // Competition
+	int_comp,     // Competition
 } intertype_t;
 extern intertype_t intertype;