diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index e5321d225d0e0b1d03a72b0a71061e6b9020ee9b..b03eb478ea16a014fd8c33ec515ece903bdee582 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -3599,10 +3599,6 @@ boolean SV_SpawnServer(void)
 		if (netgame && I_NetOpenSocket)
 		{
 			I_NetOpenSocket();
-#ifdef MASTERSERVER
-			if (cv_advertise.value)
-				RegisterServer();
-#endif
 		}
 
 		// non dedicated server just connect to itself
diff --git a/src/d_player.h b/src/d_player.h
index 81c70c1306ee1dc40455d10f9d6f1f7a67d2acae..4908d849088c2162d49f53d9ca97aacfe22177f9 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -328,7 +328,6 @@ typedef struct player_s
 	skybox_t skybox;
 
 	angle_t viewrollangle;
-	angle_t old_viewrollangle;
 	// camera tilt
 	// TODO: expose to lua
 	angle_t tilt;
diff --git a/src/d_ticcmd.h b/src/d_ticcmd.h
index 4b264a68ae4293633ba76236ef6fe2949b479366..4994cc206e3e8bad18a11aa1e1c202162fa26315 100644
--- a/src/d_ticcmd.h
+++ b/src/d_ticcmd.h
@@ -50,6 +50,9 @@ typedef enum
 // ticcmd turning bits
 #define TICCMD_REDUCE 16
 
+// ticcmd latency mask
+#define TICCMD_LATENCYMASK 0xFF
+
 // ticcmd flags
 #define TICCMD_RECEIVED 1
 #define TICCMD_TYPING 2/* chat window or console open */
diff --git a/src/g_game.c b/src/g_game.c
index 7915c06480fa6e928dd2fd20b16c4a8e50bf961d..21467e592a5f23beb5d706bca87cbfdbab1f7825 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -871,6 +871,78 @@ static void G_HandleAxisDeadZone(UINT8 splitnum, joystickvector2_t *joystickvect
 INT32 localaiming[MAXSPLITSCREENPLAYERS];
 angle_t localangle[MAXSPLITSCREENPLAYERS];
 
+INT32 localsteering[MAXSPLITSCREENPLAYERS];
+INT32 localdelta[MAXSPLITSCREENPLAYERS];
+INT32 localstoredeltas[MAXSPLITSCREENPLAYERS][TICCMD_LATENCYMASK + 1];
+UINT8 localtic;
+
+void G_ResetAnglePrediction(player_t *player)
+{
+	UINT16 i, j;
+
+	for (i = 0; i <= r_splitscreen; i++)
+	{
+		if (&players[displayplayers[i]] == player)
+		{
+			localdelta[i] = 0;
+			for (j = 0; j < TICCMD_LATENCYMASK; j++)
+			{
+				localstoredeltas[i][j] = 0;
+			}
+			break;
+		}
+	}
+}
+
+// Turning was removed from G_BuildTiccmd to prevent easy client hacking.
+// This brings back the camera prediction that was lost.
+static void G_DoAnglePrediction(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer, player_t *player)
+{
+	angle_t angleChange = 0;
+	angle_t destAngle = player->angleturn;
+	angle_t diff = 0;
+
+	localtic = cmd->latency;
+
+	if (player->pflags & PF_DRIFTEND)
+	{
+		// Otherwise, your angle slingshots off to the side violently...
+		G_ResetAnglePrediction(player);
+	}
+	else
+	{
+		while (realtics > 0)
+		{
+			localsteering[ssplayer - 1] = K_UpdateSteeringValue(localsteering[ssplayer - 1], cmd->turning);
+			angleChange = K_GetKartTurnValue(player, localsteering[ssplayer - 1]) << TICCMD_REDUCE;
+
+			// Store the angle we applied to this tic, so we can revert it later.
+			// If we trust the camera to do all of the work, then it can get out of sync fast.
+			localstoredeltas[ssplayer - 1][cmd->latency] += angleChange;
+			localdelta[ssplayer - 1] += angleChange;
+
+			realtics--;
+		}
+	}
+
+	// We COULD set it to destAngle directly...
+	// but this causes incredible jittering when the prediction turns out to be wrong. So we ease into it.
+	// Slight increased camera lag in all scenarios > Mostly lagless camera but with jittering
+	destAngle = player->angleturn + localdelta[ssplayer - 1];
+	diff = destAngle - localangle[ssplayer - 1];
+
+	if (diff > ANGLE_180)
+	{
+		diff = InvAngle(InvAngle(diff) / 2);
+	}
+	else
+	{
+		diff /= 2;
+	}
+
+	localangle[ssplayer - 1] += diff;
+}
+
 void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 {
 	const UINT8 forplayer = ssplayer-1;
@@ -896,8 +968,6 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	boolean *rd = &resetdown[forplayer];
 	const boolean mouseaiming = player->spectator;
 
-	(void)realtics;
-
 	if (demo.playback) return;
 
 	// Is there any reason this can't just be I_BaseTiccmd?
@@ -1153,7 +1223,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 		// Send leveltime when this tic was generated to the server for control lag calculations.
 		// Only do this when in a level. Also do this after the hook, so that it can't overwrite this.
-		cmd->latency = (leveltime & 0xFF);
+		cmd->latency = (leveltime & TICCMD_LATENCYMASK);
 	}
 
 	if (cmd->forwardmove > MAXPLMOVE)
@@ -1171,6 +1241,8 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	else if (cmd->throwdir < -KART_FULLTURN)
 		cmd->throwdir = -KART_FULLTURN;
 
+	G_DoAnglePrediction(cmd, realtics, ssplayer, player);
+
 	// Reset away view if a command is given.
 	if ((cmd->forwardmove || cmd->buttons)
 		&& !r_splitscreen && displayplayers[0] != consoleplayer && ssplayer == 1)
@@ -1936,7 +2008,7 @@ void G_Ticker(boolean run)
 				else
 				{
 					//@TODO add a cvar to allow setting this max
-					cmd->latency = min(((leveltime & 0xFF) - cmd->latency) & 0xFF, MAXPREDICTTICS-1);
+					cmd->latency = min(((leveltime & TICCMD_LATENCYMASK) - cmd->latency) & TICCMD_LATENCYMASK, MAXPREDICTTICS-1);
 				}
 			}
 		}
diff --git a/src/g_game.h b/src/g_game.h
index cb0cae127126c88c9bc5b23b0759bc1d55c161ab..52c7c7a0d2ee8c080326a03371996c964f7e4423 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -84,6 +84,7 @@ extern consvar_t cv_resume;
 // build an internal map name MAPxx from map number
 const char *G_BuildMapName(INT32 map);
 
+void G_ResetAnglePrediction(player_t *player);
 void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer);
 
 // copy ticcmd_t to and fro the normal way
@@ -117,6 +118,10 @@ INT32 PlayerJoyAxis(UINT8 player, axis_input_e axissel);
 
 extern angle_t localangle[MAXSPLITSCREENPLAYERS];
 extern INT32 localaiming[MAXSPLITSCREENPLAYERS]; // should be an angle_t but signed
+extern INT32 localsteering[MAXSPLITSCREENPLAYERS];
+extern INT32 localdelta[MAXSPLITSCREENPLAYERS];
+extern INT32 localstoredeltas[MAXSPLITSCREENPLAYERS][TICCMD_LATENCYMASK + 1];
+extern UINT8 localtic;
 
 //
 // GAME
diff --git a/src/k_hud.c b/src/k_hud.c
index 00537f81b78c6668ecde0af923149bab094aa4ab..3861f2e0876d99f79d52b73997c56634a08677b2 100644
--- a/src/k_hud.c
+++ b/src/k_hud.c
@@ -825,16 +825,11 @@ void K_AdjustXYWithSnap(INT32 *x, INT32 *y, UINT32 options, INT32 dupx, INT32 du
 }
 
 // This version of the function was prototyped in Lua by Nev3r ... a HUGE thank you goes out to them!
-// TODO: This should probably support view rolling if we're adding that soon...
-void K_ObjectTracking(trackingResult_t *result, vector3_t *point, UINT8 cameraNum, angle_t angleOffset)
+void K_ObjectTracking(trackingResult_t *result, vector3_t *point, boolean reverse)
 {
 #define NEWTAN(x) FINETANGENT(((x + ANGLE_90) >> ANGLETOFINESHIFT) & 4095) // tan function used by Lua
 #define NEWCOS(x) FINECOSINE((x >> ANGLETOFINESHIFT) & FINEMASK)
 
-	camera_t *cam;
-	player_t *player;
-
-	fixed_t viewpointX, viewpointY, viewpointZ;
 	angle_t viewpointAngle, viewpointAiming, viewpointRoll;
 
 	INT32 screenWidth, screenHeight;
@@ -846,6 +841,8 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, UINT8 cameraNu
 	fixed_t h;
 	INT32 da;
 
+	UINT8 cameraNum = R_GetViewNumber();
+
 	I_Assert(result != NULL);
 	I_Assert(point != NULL);
 
@@ -854,49 +851,21 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, UINT8 cameraNu
 	result->scale = FRACUNIT;
 	result->onScreen = false;
 
-	if (cameraNum > r_splitscreen)
-	{
-		// Invalid camera ID.
-		return;
-	}
-
-	cam = &camera[cameraNum];
-	player = &players[displayplayers[cameraNum]];
-
-	if (cam == NULL || player == NULL || player->mo == NULL || P_MobjWasRemoved(player->mo) == true)
-	{
-		// Shouldn't be possible?
-		return;
-	}
-
-	// TODO: parts need interp
-	if (cam->chase == true && !player->spectator)
+	// Take the view's properties as necessary.
+	if (reverse)
 	{
-		// Use the camera's properties.
-		viewpointX = R_InterpolateFixed(cam->old_x, cam->x);
-		viewpointY = R_InterpolateFixed(cam->old_y, cam->y);
-		viewpointZ = R_InterpolateFixed(cam->old_z, cam->z) - point->z;
-		viewpointAngle = (INT32)R_InterpolateAngle(cam->old_angle, cam->angle);
-		viewpointAiming = (INT32)R_InterpolateAngle(cam->old_aiming, cam->aiming);
-		viewpointRoll = (INT32)R_InterpolateAngle(player->old_viewrollangle, player->viewrollangle);
+		viewpointAngle = (INT32)(viewangle + ANGLE_180);
+		viewpointAiming = (INT32)InvAngle(aimingangle);
+		viewpointRoll = (INT32)viewroll;
 	}
 	else
 	{
-		// Use player properties.
-		viewpointX = R_InterpolateFixed(player->mo->old_x, player->mo->x);
-		viewpointY = R_InterpolateFixed(player->mo->old_y, player->mo->y);
-		viewpointZ = R_InterpolateFixed(player->mo->old_z, player->mo->z) - point->z; //player->old_viewz
-		viewpointAngle = (INT32)R_InterpolateAngle(player->mo->old_angle, player->mo->angle);
-		viewpointAiming = (INT32)player->aiming;
-		viewpointRoll = (INT32)R_InterpolateAngle(player->old_viewrollangle, player->viewrollangle);
+		viewpointAngle = (INT32)viewangle;
+		viewpointAiming = (INT32)aimingangle;
+		viewpointRoll = (INT32)InvAngle(viewroll);
 	}
 
-	viewpointAngle += (INT32)angleOffset;
-
-	(void)viewpointRoll; // will be used later...
-
 	// Calculate screen size adjustments.
-	// TODO: Anyone want to make this support non-green resolutions somehow? :V
 	screenWidth = vid.width/vid.dupx;
 	screenHeight = vid.height/vid.dupy;
 
@@ -917,7 +886,7 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, UINT8 cameraNu
 
 	// Calculate FOV adjustments.
 	fovDiff = cv_fov[cameraNum].value - baseFov;
-	fov = ((baseFov - fovDiff) / 2) - (player->fovadd / 2);
+	fov = ((baseFov - fovDiff) / 2) - (stplyr->fovadd / 2);
 	fovTangent = NEWTAN(FixedAngle(fov));
 
 	if (r_splitscreen == 1)
@@ -929,23 +898,36 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, UINT8 cameraNu
 	fg = (screenWidth >> 1) * fovTangent;
 
 	// Determine viewpoint factors.
-	h = R_PointToDist2(point->x, point->y, viewpointX, viewpointY);
-	da = AngleDeltaSigned(viewpointAngle, R_PointToAngle2(point->x, point->y, viewpointX, viewpointY));
-
-	// Set results!
-	result->x = screenHalfW + FixedMul(NEWTAN(da), fg);
-	result->y = screenHalfH + FixedMul((NEWTAN(viewpointAiming) - FixedDiv(viewpointZ, 1 + FixedMul(NEWCOS(da), h))), fg);
+	h = R_PointToDist2(point->x, point->y, viewx, viewy);
+	da = AngleDeltaSigned(viewpointAngle, R_PointToAngle2(point->x, point->y, viewx, viewy));
 
-	result->scale = FixedDiv(screenHalfW, h+1);
+	// Set results relative to top left!
+	result->x = FixedMul(NEWTAN(da), fg);
+	result->y = FixedMul((NEWTAN(viewpointAiming) - FixedDiv((viewz - point->z), 1 + FixedMul(NEWCOS(da), h))), fg);
 
-	result->onScreen = ((abs(da) > ANG60) || (abs(AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, viewpointZ))) > ANGLE_45));
+	// Rotate for screen roll...
+	if (viewpointRoll)
+	{
+		fixed_t tempx = result->x;
+		viewpointRoll >>= ANGLETOFINESHIFT;
+		result->x = FixedMul(FINECOSINE(viewpointRoll), tempx) - FixedMul(FINESINE(viewpointRoll), result->y);
+		result->y = FixedMul(FINESINE(viewpointRoll), tempx) + FixedMul(FINECOSINE(viewpointRoll), result->y);
+	}
 
+	// Flipped screen?
 	if (encoremode)
 	{
-		// Flipped screen
-		result->x = (screenWidth << FRACBITS) - result->x;
+		result->x = -result->x;
 	}
 
+	// Center results.
+	result->x += screenHalfW;
+	result->y += screenHalfH;
+
+	result->scale = FixedDiv(screenHalfW, h+1);
+
+	result->onScreen = ((abs(da) > ANG60) || (abs(AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, (viewz - point->z)))) > ANGLE_45));
+
 	// Cheap dirty hacks for some split-screen related cases
 	if (result->x < 0 || result->x > (screenWidth << FRACBITS))
 	{
@@ -2841,7 +2823,6 @@ static void K_drawKartWanted(void)
 static void K_drawKartPlayerCheck(void)
 {
 	const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed));
-	UINT8 cnum = 0;
 	UINT8 i;
 	INT32 splitflags = V_SNAPTOBOTTOM|V_SPLITSCREEN;
 	fixed_t y = CHEK_Y * FRACUNIT;
@@ -2861,20 +2842,6 @@ static void K_drawKartPlayerCheck(void)
 		return;
 	}
 
-	if (r_splitscreen)
-	{
-		y /= 2;
-
-		for (i = 1; i <= r_splitscreen; i++)
-		{
-			if (stplyr == &players[displayplayers[i]])
-			{
-				cnum = i;
-				break;
-			}
-		}
-	}
-
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		player_t *checkplayer = &players[i];
@@ -2933,7 +2900,7 @@ static void K_drawKartPlayerCheck(void)
 			pnum += 2;
 		}
 
-		K_ObjectTracking(&result, &v, cnum, ANGLE_180);
+		K_ObjectTracking(&result, &v, true);
 
 		if (result.onScreen == true)
 		{
@@ -3008,7 +2975,7 @@ static void K_DrawTypingNotifier(fixed_t x, fixed_t y, player_t *p)
 	}
 }
 
-static void K_DrawNameTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT8 cnum)
+static void K_DrawNameTagForPlayer(fixed_t x, fixed_t y, player_t *p)
 {
 	const INT32 clr = skincolors[p->skincolor].chatcolor;
 	const INT32 namelen = V_ThinStringWidth(player_names[p - players], V_6WIDTHSPACE|V_ALLOWLOWERCASE);
@@ -3016,6 +2983,8 @@ static void K_DrawNameTagForPlayer(fixed_t x, fixed_t y, player_t *p, UINT8 cnum
 	UINT8 *colormap = V_GetStringColormap(clr);
 	INT32 barx = 0, bary = 0, barw = 0;
 
+	UINT8 cnum = R_GetViewNumber();
+
 	// Since there's no "V_DrawFixedFill", and I don't feel like making it,
 	// fuck it, we're gonna just V_NOSCALESTART hack it
 	if (cnum & 1)
@@ -3100,9 +3069,8 @@ static void K_DrawWeakSpot(weakspotdraw_t *ws)
 static void K_drawKartNameTags(void)
 {
 	const fixed_t maxdistance = 8192*mapobjectscale;
-	camera_t *thiscam;
 	vector3_t c;
-	UINT8 cnum = 0;
+	UINT8 cnum = R_GetViewNumber();
 	UINT8 tobesorted[MAXPLAYERS];
 	fixed_t sortdist[MAXPLAYERS];
 	UINT8 sortlen = 0;
@@ -3118,32 +3086,9 @@ static void K_drawKartNameTags(void)
 		return;
 	}
 
-	if (r_splitscreen)
-	{
-		for (i = 1; i <= r_splitscreen; i++)
-		{
-			if (stplyr == &players[displayplayers[i]])
-			{
-				cnum = i;
-				break;
-			}
-		}
-	}
-
-	thiscam = &camera[cnum];
-
-	if (thiscam->chase == true)
-	{
-		c.x = R_InterpolateFixed(thiscam->old_x, thiscam->x);
-		c.y = R_InterpolateFixed(thiscam->old_y, thiscam->y);
-		c.z = R_InterpolateFixed(thiscam->old_z, thiscam->z);
-	}
-	else
-	{
-		c.x = R_InterpolateFixed(stplyr->mo->old_x, stplyr->mo->x);
-		c.y = R_InterpolateFixed(stplyr->mo->old_y, stplyr->mo->y);
-		c.z = R_InterpolateFixed(stplyr->mo->old_z, stplyr->mo->z);
-	}
+	c.x = viewx;
+	c.y = viewy;
+	c.z = viewz;
 
 	// Maybe shouldn't be handling this here... but the camera info is too good.
 	if (bossinfo.boss)
@@ -3175,7 +3120,7 @@ static void K_drawKartNameTags(void)
 
 			v.z += (bossinfo.weakspots[i].spot->height / 2);
 
-			K_ObjectTracking(&result, &v, cnum, 0);
+			K_ObjectTracking(&result, &v, false);
 			if (result.onScreen == false)
 			{
 				continue;
@@ -3328,7 +3273,7 @@ static void K_drawKartNameTags(void)
 				v.z += headOffset;
 			}
 
-			K_ObjectTracking(&result, &v, cnum, 0);
+			K_ObjectTracking(&result, &v, false);
 
 			if (result.onScreen == true)
 			{
@@ -3363,7 +3308,7 @@ static void K_drawKartNameTags(void)
 				{
 					if (K_ShowPlayerNametag(ntplayer) == true)
 					{
-						K_DrawNameTagForPlayer(result.x, result.y, ntplayer, cnum);
+						K_DrawNameTagForPlayer(result.x, result.y, ntplayer);
 						K_DrawTypingNotifier(result.x, result.y, ntplayer);
 					}
 				}
diff --git a/src/k_hud.h b/src/k_hud.h
index fff718ef5bb4fe150c93752a473fee0c2657cdbe..683425e8a7cd6a433c9f70ca722598f4efa49b86 100644
--- a/src/k_hud.h
+++ b/src/k_hud.h
@@ -28,7 +28,7 @@ typedef struct trackingResult_s
 	boolean onScreen;
 } trackingResult_t;
 
-void K_ObjectTracking(trackingResult_t *result, vector3_t *point, UINT8 cameraNum, angle_t angleOffset);
+void K_ObjectTracking(trackingResult_t *result, vector3_t *point, boolean reverse);
 
 const char *K_GetItemPatch(UINT8 item, boolean tiny);
 void K_LoadKartHUDGraphics(void);
diff --git a/src/k_kart.c b/src/k_kart.c
index 74b6f42fde332768825a4387c74147514ba38042..b2fa1b6278a7c273f656710116e4a14d6a33a039 100644
--- a/src/k_kart.c
+++ b/src/k_kart.c
@@ -8236,31 +8236,34 @@ static INT16 K_GetKartDriftValue(player_t *player, fixed_t countersteer)
 	return basedrift + (FixedMul(driftadjust * FRACUNIT, countersteer) / FRACUNIT);
 }
 
-void K_UpdateSteeringValue(player_t *player, INT16 destSteering)
+INT16 K_UpdateSteeringValue(INT16 inputSteering, INT16 destSteering)
 {
 	// player->steering is the turning value, but with easing applied.
 	// Keeps micro-turning from old easing, but isn't controller dependent.
 
 	const INT16 amount = KART_FULLTURN/4;
-	INT16 diff = destSteering - player->steering;
+	INT16 diff = destSteering - inputSteering;
+	INT16 outputSteering = inputSteering;
 
 	if (abs(diff) <= amount)
 	{
 		// Reached the intended value, set instantly.
-		player->steering = destSteering;
+		outputSteering = destSteering;
 	}
 	else
 	{
 		// Go linearly towards the value we wanted.
 		if (diff < 0)
 		{
-			player->steering -= amount;
+			outputSteering -= amount;
 		}
 		else
 		{
-			player->steering += amount;
+			outputSteering += amount;
 		}
 	}
+
+	return outputSteering;
 }
 
 INT16 K_GetKartTurnValue(player_t *player, INT16 turnvalue)
diff --git a/src/k_kart.h b/src/k_kart.h
index c0bd71462e56e9b7f1efd6f268cb15d53fbf13da..20c8cd33f07f838db0830d3ae120f74ccfe4be84 100644
--- a/src/k_kart.h
+++ b/src/k_kart.h
@@ -98,7 +98,7 @@ player_t *K_FindJawzTarget(mobj_t *actor, player_t *source);
 INT32 K_GetKartRingPower(player_t *player, boolean boosted);
 void K_UpdateDistanceFromFinishLine(player_t *const player);
 boolean K_CheckPlayersRespawnColliding(INT32 playernum, fixed_t x, fixed_t y);
-void K_UpdateSteeringValue(player_t *player, INT16 destSteering);
+INT16 K_UpdateSteeringValue(INT16 inputSteering, INT16 destSteering);
 INT16 K_GetKartTurnValue(player_t *player, INT16 turnvalue);
 INT32 K_GetUnderwaterTurnAdjust(player_t *player);
 INT32 K_GetKartDriftSparkValue(player_t *player);
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 8245cb1e82ea154678e34a35662296bae79abab7..ad65555413e2d7157d5a9684452589fcae0a455d 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -14,6 +14,7 @@
 #include "fastcmp.h"
 #include "r_defs.h"
 #include "r_local.h"
+#include "r_fps.h"
 #include "st_stuff.h"
 #include "g_game.h"
 #include "i_video.h" // rendermode
@@ -1394,26 +1395,9 @@ void LUAh_GameHUD(player_t *stplayr, huddrawlist_h list)
 	lua_remove(gL, -3); // pop HUD
 	LUA_PushUserdata(gL, stplayr, META_PLAYER);
 
-	if (r_splitscreen > 2 && stplayr == &players[displayplayers[3]])
-	{
-		LUA_PushUserdata(gL, &camera[3], META_CAMERA);
-		camnum = 4;
-	}
-	else if (r_splitscreen > 1 && stplayr == &players[displayplayers[2]])
-	{
-		LUA_PushUserdata(gL, &camera[2], META_CAMERA);
-		camnum = 3;
-	}
-	else if (r_splitscreen && stplayr == &players[displayplayers[1]])
-	{
-		LUA_PushUserdata(gL, &camera[1], META_CAMERA);
-		camnum = 2;
-	}
-	else
-	{
-		LUA_PushUserdata(gL, &camera[0], META_CAMERA);
-		camnum = 1;
-	}
+	camnum = R_GetViewNumber();
+	LUA_PushUserdata(gL, &camera[camnum], META_CAMERA);
+	camnum++; // for compatibility
 
 	lua_pushnil(gL);
 	while (lua_next(gL, -5) != 0) {
diff --git a/src/p_setup.c b/src/p_setup.c
index baba85ee87878e451c5256cea8411a5c810f19b1..c1da43eef71ce99b5fd5b0990aac8d20f1f057cb 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -4509,6 +4509,10 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	if (rendermode == render_none || reloadinggamestate)
 		return true;
 
+	R_ResetViewInterpolation(0);
+	R_ResetViewInterpolation(0);
+	R_UpdateMobjInterpolators();
+
 	// Title card!
 	G_StartTitleCard();
 
diff --git a/src/p_user.c b/src/p_user.c
index 145835e481c5fe785f6b53d42f05ae0105257e86..dbf7169257b1f12df9ee12ac6602d1f2cacd2638 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2003,13 +2003,46 @@ static void P_3dMovement(player_t *player)
 static void P_UpdatePlayerAngle(player_t *player)
 {
 	angle_t angleChange = ANGLE_MAX;
+	UINT8 p = UINT8_MAX;
 	UINT8 i;
 
-	K_UpdateSteeringValue(player, player->cmd.turning);
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (player == &players[g_localplayers[i]])
+		{
+			p = i;
+			break;
+		}
+	}
+
+	player->steering = K_UpdateSteeringValue(player->steering, player->cmd.turning);
 	angleChange = K_GetKartTurnValue(player, player->steering) << TICCMD_REDUCE;
 
-	P_SetPlayerAngle(player, player->angleturn + angleChange);
-	player->mo->angle = player->angleturn;
+	if (p == UINT8_MAX)
+	{
+		// When F12ing players, set local angle directly.
+		P_SetPlayerAngle(player, player->angleturn + angleChange);
+		player->mo->angle = player->angleturn;
+	}
+	else
+	{
+		UINT8 lateTic = ((leveltime - player->cmd.latency) & TICCMD_LATENCYMASK);
+		UINT8 clearTic = ((localtic + 1) & TICCMD_LATENCYMASK);
+
+		player->angleturn += angleChange;
+		player->mo->angle = player->angleturn;
+
+		// Undo the ticcmd's old emulated angle,
+		// now that we added the actual game logic angle.
+
+		while (lateTic != clearTic)
+		{
+			localdelta[p] -= localstoredeltas[p][lateTic];
+			localstoredeltas[p][lateTic] = 0;
+
+			lateTic = (lateTic - 1) & TICCMD_LATENCYMASK;
+		}
+	}
 
 	if (!cv_allowmlook.value || player->spectator == false)
 	{
@@ -2021,13 +2054,9 @@ static void P_UpdatePlayerAngle(player_t *player)
 		player->aiming = G_ClipAimingPitch((INT32 *)&player->aiming);
 	}
 
-	for (i = 0; i <= r_splitscreen; i++)
+	if (p != UINT8_MAX)
 	{
-		if (player == &players[displayplayers[i]])
-		{
-			localaiming[i] = player->aiming;
-			break;
-		}
+		localaiming[p] = player->aiming;
 	}
 }
 
@@ -3593,7 +3622,11 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	}
 
 	if (lookbackdown)
+	{
 		P_MoveChaseCamera(player, thiscam, false);
+		R_ResetViewInterpolation(num + 1);
+		R_ResetViewInterpolation(num + 1);
+	}
 
 	return (x == thiscam->x && y == thiscam->y && z == thiscam->z && angle == thiscam->aiming);
 
@@ -4328,7 +4361,6 @@ void P_PlayerThink(player_t *player)
 	}
 
 	player->old_drawangle = player->drawangle;
-	player->old_viewrollangle = player->viewrollangle;
 
 	player->pflags &= ~PF_HITFINISHLINE;
 
@@ -4866,6 +4898,7 @@ void P_ForceLocalAngle(player_t *player, angle_t angle)
 		if (player == &players[displayplayers[i]])
 		{
 			localangle[i] = angle;
+			G_ResetAnglePrediction(player);
 			break;
 		}
 	}
diff --git a/src/r_fps.c b/src/r_fps.c
index 6f4281687646ad98de5ba397cd6b75cddf284a89..aff4819105ec34757290d1be181feed02c7bf015 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -163,22 +163,7 @@ void R_InterpolateView(fixed_t frac)
 	if (frac > FRACUNIT)
 		frac = FRACUNIT;
 
-	if (viewcontext == VIEWCONTEXT_SKY1 || viewcontext == VIEWCONTEXT_PLAYER1)
-	{
-		i = 0;
-	}
-	else if (viewcontext == VIEWCONTEXT_SKY2 || viewcontext == VIEWCONTEXT_PLAYER2)
-	{
-		i = 1;
-	}
-	else if (viewcontext == VIEWCONTEXT_SKY3 || viewcontext == VIEWCONTEXT_PLAYER3)
-	{
-		i = 2;
-	}
-	else
-	{
-		i = 3;
-	}
+	i = R_GetViewNumber();
 
 	if (oldview_invalid[i] != 0)
 	{
diff --git a/src/r_fps.h b/src/r_fps.h
index a91f7004eb2e8f6c2356f1ca59a7d5babdf7d0fc..41fc65af05d87bf38a936a3afd5c6e985d2a757e 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -36,6 +36,10 @@ enum viewcontext_e
 	VIEWCONTEXT_SKY4
 };
 
+extern enum viewcontext_e viewcontext;
+
+#define R_GetViewNumber() ((viewcontext - VIEWCONTEXT_PLAYER1) & 3)
+
 typedef struct {
 	fixed_t x;
 	fixed_t y;
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 9b98f0b6837262efcc9f55ad98cab2dd1c0066fa..bb1cea9ff601689eb1e81feebef7fa29e5b78fc0 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -33,6 +33,7 @@
 #include "p_setup.h" // NiGHTS grading
 #include "k_grandprix.h"	// we need to know grandprix status for titlecards
 #include "k_boss.h"
+#include "r_fps.h"
 
 //random index
 #include "m_random.h"
@@ -1002,8 +1003,17 @@ static void ST_overlayDrawer(void)
 {
 	// hu_showscores = auto hide score/time/rings when tab rankings are shown
 	if (!(hu_showscores && (netgame || multiplayer)))
+	{
 		K_drawKartHUD();
 
+		if (renderisnewtic)
+		{
+			LUA_HUD_ClearDrawList(luahuddrawlist_game);
+			LUAh_GameHUD(stplyr, luahuddrawlist_game);
+		}
+		LUA_HUD_DrawList(luahuddrawlist_game);
+	}
+
 	if (!hu_showscores) // hide the following if TAB is held
 	{
 		if (cv_showviewpointtext.value)
@@ -1031,16 +1041,6 @@ static void ST_overlayDrawer(void)
 		}
 	}
 
-	if (!(netgame || multiplayer) || !hu_showscores)
-	{
-		if (renderisnewtic)
-		{
-			LUA_HUD_ClearDrawList(luahuddrawlist_game);
-			LUAh_GameHUD(stplyr, luahuddrawlist_game);
-		}
-		LUA_HUD_DrawList(luahuddrawlist_game);
-	}
-
 	if (!hu_showscores && netgame && !mapreset)
 	{
 		if (stplyr->spectator && LUA_HudEnabled(hud_textspectator))
@@ -1081,32 +1081,6 @@ static void ST_overlayDrawer(void)
 			}
 		}
 	}
-
-	// Replay manual-save stuff
-	if (demo.recording && multiplayer && demo.savebutton && demo.savebutton + 3*TICRATE < leveltime)
-	{
-		switch (demo.savemode)
-		{
-		case DSM_NOTSAVING:
-			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|((gametyperules & GTR_BUMPERS) ? V_REDMAP : V_SKYMAP), "Look Backward: Save replay");
-			break;
-
-		case DSM_WILLAUTOSAVE:
-			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|((gametyperules & GTR_BUMPERS) ? V_REDMAP : V_SKYMAP), "Replay will be saved. (Look Backward: Change title)");
-			break;
-
-		case DSM_WILLSAVE:
-			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|((gametyperules & GTR_BUMPERS) ? V_REDMAP : V_SKYMAP), "Replay will be saved.");
-			break;
-
-		case DSM_TITLEENTRY:
-			ST_DrawDemoTitleEntry();
-			break;
-
-		default: // Don't render anything
-			break;
-		}
-	}
 }
 
 void ST_DrawDemoTitleEntry(void)
@@ -1217,6 +1191,8 @@ void ST_Drawer(void)
 		for (i = 0; i <= r_splitscreen; i++)
 		{
 			stplyr = &players[displayplayers[i]];
+			R_SetViewContext(VIEWCONTEXT_PLAYER1 + i);
+			R_InterpolateView(rendertimefrac); // to assist with object tracking
 			ST_overlayDrawer();
 		}
 
@@ -1232,5 +1208,31 @@ void ST_Drawer(void)
 	if (stagetitle)
 		ST_drawTitleCard();
 
+	// Replay manual-save stuff
+	if (demo.recording && multiplayer && demo.savebutton && demo.savebutton + 3*TICRATE < leveltime)
+	{
+		switch (demo.savemode)
+		{
+		case DSM_NOTSAVING:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|((gametyperules & GTR_BUMPERS) ? V_REDMAP : V_SKYMAP), "Look Backward: Save replay");
+			break;
+
+		case DSM_WILLAUTOSAVE:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|((gametyperules & GTR_BUMPERS) ? V_REDMAP : V_SKYMAP), "Replay will be saved. (Look Backward: Change title)");
+			break;
+
+		case DSM_WILLSAVE:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|((gametyperules & GTR_BUMPERS) ? V_REDMAP : V_SKYMAP), "Replay will be saved.");
+			break;
+
+		case DSM_TITLEENTRY:
+			ST_DrawDemoTitleEntry();
+			break;
+
+		default: // Don't render anything
+			break;
+		}
+	}
+
 	ST_drawDebugInfo();
 }