diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index c9a4be3ff0ae344858a44274f77834c085b81b1b..72d020c22a3be45b94c15fb6dd22f74de47f16fd 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -2340,7 +2340,7 @@ static void Got_Clearscores(UINT8 **cp, INT32 playernum)
 	}
 
 	for (i = 0; i < MAXPLAYERS; i++)
-		players[i].score = 0;
+		players[i].score = players[i].recordscore = 0;
 
 	CONS_Printf(M_GetText("Scores have been reset by the server.\n"));
 }
diff --git a/src/d_player.h b/src/d_player.h
index f407c29e9be5cad4d59a1319743cf83951af3bfb..7ad5b9f811962cb56ffcf55030f2ff1d2aea331d 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -453,7 +453,8 @@ typedef struct player_s
 	INT32 skin;
 	UINT32 availabilities;
 
-	UINT32 score; // player score
+	UINT32 score; // player score (total)
+	UINT32 recordscore; // player score (per life / map)
 	fixed_t dashspeed; // dashing speed
 
 	fixed_t normalspeed; // Normal ground
diff --git a/src/g_game.c b/src/g_game.c
index 6cedd35deb62ea003ed074c9789d8d9bdea79fb0..15b1e10816318ffef1f1ed530a6c9cc0751b2c46 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -519,140 +519,154 @@ UINT8 G_GetBestNightsGrade(INT16 map, UINT8 mare, gamedata_t *data)
 }
 
 // For easy adding of NiGHTS records
-void G_AddTempNightsRecords(UINT32 pscore, tic_t ptime, UINT8 mare)
+void G_AddTempNightsRecords(player_t *player, UINT32 pscore, tic_t ptime, UINT8 mare)
 {
-	ntemprecords.score[mare] = pscore;
-	ntemprecords.grade[mare] = P_GetGrade(pscore, gamemap, mare - 1);
-	ntemprecords.time[mare] = ptime;
+	const UINT8 playerID = player - players;
+
+	I_Assert(player != NULL);
+
+	ntemprecords[playerID].score[mare] = pscore;
+	ntemprecords[playerID].grade[mare] = P_GetGrade(pscore, gamemap, mare - 1);
+	ntemprecords[playerID].time[mare] = ptime;
 
 	// Update nummares
 	// Note that mare "0" is overall, mare "1" is the first real mare
-	if (ntemprecords.nummares < mare)
-		ntemprecords.nummares = mare;
+	if (ntemprecords[playerID].nummares < mare)
+		ntemprecords[playerID].nummares = mare;
 }
 
 //
-// G_UpdateRecordReplays
+// G_SetMainRecords
 //
 // Update replay files/data, etc. for Record Attack
 // See G_SetNightsRecords for NiGHTS Attack.
 //
-static void G_UpdateRecordReplays(gamedata_t *data)
+static void G_SetMainRecords(gamedata_t *data, player_t *player)
 {
-	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;
 
+	I_Assert(player != NULL);
+
 	// Record new best time
 	if (!data->mainrecords[gamemap-1])
 		G_AllocMainRecordData(gamemap-1, data);
 
-	if (players[consoleplayer].score > data->mainrecords[gamemap-1]->score)
-		data->mainrecords[gamemap-1]->score = players[consoleplayer].score;
+	if (player->recordscore > data->mainrecords[gamemap-1]->score)
+		data->mainrecords[gamemap-1]->score = player->recordscore;
 
-	if ((data->mainrecords[gamemap-1]->time == 0) || (players[consoleplayer].realtime < data->mainrecords[gamemap-1]->time))
-		data->mainrecords[gamemap-1]->time = players[consoleplayer].realtime;
+	if ((data->mainrecords[gamemap-1]->time == 0) || (player->realtime < data->mainrecords[gamemap-1]->time))
+		data->mainrecords[gamemap-1]->time = player->realtime;
 
-	if ((UINT16)(players[consoleplayer].rings) > data->mainrecords[gamemap-1]->rings)
-		data->mainrecords[gamemap-1]->rings = (UINT16)(players[consoleplayer].rings);
+	if ((UINT16)(player->rings) > data->mainrecords[gamemap-1]->rings)
+		data->mainrecords[gamemap-1]->rings = (UINT16)(player->rings);
 
-	// Save demo!
-	bestdemo[255] = '\0';
-	lastdemo[255] = '\0';
-	G_SetDemoTime(players[consoleplayer].realtime, players[consoleplayer].score, (UINT16)(players[consoleplayer].rings));
-	G_CheckDemoStatus();
+	if (modeattacking)
+	{
+		const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
+		char *gpath;
+		char lastdemo[256], bestdemo[256];
 
-	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
-	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
+		// Save demo!
+		bestdemo[255] = '\0';
+		lastdemo[255] = '\0';
+		G_SetDemoTime(player->realtime, player->recordscore, (UINT16)(player->rings));
+		G_CheckDemoStatus();
 
-	if ((gpath = malloc(glen)) == NULL)
-		I_Error("Out of memory for replay filepath\n");
+		I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
+		I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
 
-	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 ((gpath = malloc(glen)) == NULL)
+			I_Error("Out of memory for replay filepath\n");
 
-	if (FIL_FileExists(lastdemo))
-	{
-		UINT8 *buf;
-		size_t len = FIL_ReadFile(lastdemo, &buf);
+		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);
 
-		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);
-		}
+		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-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);
-		}
+			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);
+			//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
 
-		Z_Free(buf);
+			Z_Free(buf);
+		}
+		free(gpath);
 	}
-	free(gpath);
 
 	// Check emblems when level data is updated
 	if ((earnedEmblems = M_CheckLevelEmblems(data)))
+	{
 		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(gamedata_t *data)
+static void G_SetNightsRecords(gamedata_t *data, player_t *player)
 {
-	INT32 i;
+	nightsdata_t *const ntemprecord = &ntemprecords[player - players];
 	UINT32 totalscore = 0;
 	tic_t totaltime = 0;
-	UINT8 earnedEmblems;
+	INT32 i;
 
-	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;
 
-	if (!ntemprecords.nummares)
+	if (!ntemprecord->nummares)
+	{
 		return;
+	}
 
 	// Set overall
 	{
 		UINT8 totalrank = 0, realrank = 0;
 
-		for (i = 1; i <= ntemprecords.nummares; ++i)
+		for (i = 1; i <= ntemprecord->nummares; ++i)
 		{
-			totalscore += ntemprecords.score[i];
-			totalrank += ntemprecords.grade[i];
-			totaltime += ntemprecords.time[i];
+			totalscore += ntemprecord->score[i];
+			totalrank += ntemprecord->grade[i];
+			totaltime += ntemprecord->time[i];
 		}
 
 		// Determine overall grade
-		realrank = (UINT8)((FixedDiv((fixed_t)totalrank << FRACBITS, ntemprecords.nummares << FRACBITS) + (FRACUNIT/2)) >> FRACBITS);
+		realrank = (UINT8)((FixedDiv((fixed_t)totalrank << FRACBITS, ntemprecord->nummares << FRACBITS) + (FRACUNIT/2)) >> FRACBITS);
 
 		// You need ALL rainbow As to get a rainbow A overall
-		if (realrank == GRADE_S && (totalrank / ntemprecords.nummares) != GRADE_S)
+		if (realrank == GRADE_S && (totalrank / ntemprecord->nummares) != GRADE_S)
+		{
 			realrank = GRADE_A;
+		}
 
-		ntemprecords.score[0] = totalscore;
-		ntemprecords.grade[0] = realrank;
-		ntemprecords.time[0] = totaltime;
+		ntemprecord->score[0] = totalscore;
+		ntemprecord->grade[0] = realrank;
+		ntemprecord->time[0] = totaltime;
 	}
 
 	// Now take all temp records and put them in the actual records
@@ -660,71 +674,85 @@ void G_SetNightsRecords(gamedata_t *data)
 		nightsdata_t *maprecords;
 
 		if (!data->nightsrecords[gamemap-1])
+		{
 			G_AllocNightsRecordData(gamemap-1, data);
+		}
+
 		maprecords = data->nightsrecords[gamemap-1];
 
-		if (maprecords->nummares != ntemprecords.nummares)
-			maprecords->nummares = ntemprecords.nummares;
+		if (maprecords->nummares != ntemprecord->nummares)
+		{
+			maprecords->nummares = ntemprecord->nummares;
+		}
 
-		for (i = 0; i < ntemprecords.nummares + 1; ++i)
+		for (i = 0; i < ntemprecord->nummares + 1; ++i)
 		{
-			if (maprecords->score[i] < ntemprecords.score[i])
-				maprecords->score[i] = ntemprecords.score[i];
-			if (maprecords->grade[i] < ntemprecords.grade[i])
-				maprecords->grade[i] = ntemprecords.grade[i];
-			if (!maprecords->time[i] || maprecords->time[i] > ntemprecords.time[i])
-				maprecords->time[i] = ntemprecords.time[i];
+			if (maprecords->score[i] < ntemprecord->score[i])
+				maprecords->score[i] = ntemprecord->score[i];
+			if (maprecords->grade[i] < ntemprecord->grade[i])
+				maprecords->grade[i] = ntemprecord->grade[i];
+			if (!maprecords->time[i] || maprecords->time[i] > ntemprecord->time[i])
+				maprecords->time[i] = ntemprecord->time[i];
 		}
 	}
 
-	memset(&ntemprecords, 0, sizeof(nightsdata_t));
+	memset(&ntemprecords[player - players], 0, sizeof(nightsdata_t));
 
-	// Save demo!
-	bestdemo[255] = '\0';
-	lastdemo[255] = '\0';
-	G_SetDemoTime(totaltime, totalscore, 0);
-	G_CheckDemoStatus();
+	if (modeattacking)
+	{
+		const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
+		char *gpath;
+		char lastdemo[256], bestdemo[256];
 
-	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
-	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
+		// Save demo!
+		bestdemo[255] = '\0';
+		lastdemo[255] = '\0';
+		G_SetDemoTime(totaltime, totalscore, 0);
+		G_CheckDemoStatus();
 
-	if ((gpath = malloc(glen)) == NULL)
-		I_Error("Out of memory for replay filepath\n");
+		I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
+		I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
 
-	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 ((gpath = malloc(glen)) == NULL)
+			I_Error("Out of memory for replay filepath\n");
 
-	if (FIL_FileExists(lastdemo))
-	{
-		UINT8 *buf;
-		size_t len = FIL_ReadFile(lastdemo, &buf);
+		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);
 
-		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);
-		}
+		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-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);
+			}
 
-		//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
+			//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
 
-		Z_Free(buf);
+			Z_Free(buf);
+		}
+		free(gpath);
 	}
-	free(gpath);
 
 	if ((earnedEmblems = M_CheckLevelEmblems(data)))
+	{
 		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();
@@ -2288,7 +2316,7 @@ void G_Ticker(boolean run)
 					p->lives = startinglivesbalance[0];
 					p->continues = 1;
 
-					p->score = 0;
+					p->score = p->recordscore = 0;
 
 					// The latter two should clear by themselves, but just in case
 					p->pflags &= ~(PF_TAGIT|PF_GAMETYPEOVER|PF_FULLSTASIS);
@@ -2494,6 +2522,7 @@ static inline void G_PlayerFinishLevel(INT32 player)
 
 	memset(p->powers, 0, sizeof (p->powers));
 	p->ringweapons = 0;
+	p->recordscore = 0;
 
 	p->mo->flags2 &= ~MF2_SHADOW; // cancel invisibility
 	P_FlashPal(p, 0, 0); // Resets
@@ -2517,6 +2546,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 {
 	player_t *p;
 	INT32 score;
+	INT32 recordscore;
 	INT32 lives;
 	INT32 continues;
 	fixed_t camerascale;
@@ -3169,6 +3199,7 @@ void G_DoReborn(INT32 playernum)
 			{
 				if (!playeringame[i])
 					continue;
+				players[i].recordscore = 0;
 				players[i].starpostscale = 0;
 				players[i].starpostangle = 0;
 				players[i].starposttime = 0;
@@ -3848,15 +3879,16 @@ static INT16 RandMap(UINT32 tolflags, INT16 pprevmap)
 //
 // G_UpdateVisited
 //
-static void G_UpdateVisited(gamedata_t *data, boolean silent)
+static void G_UpdateVisited(gamedata_t *data, player_t *player, boolean silent)
 {
-	boolean spec = G_IsSpecialStage(gamemap);
 	// Update visitation flags?
 	if (!demoplayback
 		&& G_CoopGametype() // Campaign mode
 		&& !stagefailed) // Did not fail the stage
 	{
 		UINT8 earnedEmblems;
+		UINT16 totalrings = 0;
+		INT32 i;
 
 		// Update visitation flags
 		data->mapvisited[gamemap-1] |= MV_BEATEN;
@@ -3865,36 +3897,62 @@ static void G_UpdateVisited(gamedata_t *data, boolean silent)
 		if (ultimatemode)
 			data->mapvisited[gamemap-1] |= MV_ULTIMATE;
 
+		for (i = 0; i < MAXPLAYERS; i++)
+		{
+			if (!playeringame[i])
+			{
+				continue;
+			}
+
+			totalrings += players[i].rings;
+		}
+
 		// may seem incorrect but IS possible in what the main game uses as mp special stages, and nummaprings will be -1 in NiGHTS
-		if (nummaprings > 0 && players[consoleplayer].rings >= nummaprings)
+		if (nummaprings > 0 && totalrings >= nummaprings)
 		{
 			data->mapvisited[gamemap-1] |= MV_PERFECT;
 			if (modeattacking)
 				data->mapvisited[gamemap-1] |= MV_PERFECTRA;
 		}
 
-		if (!spec)
+		if (!G_IsSpecialStage(gamemap))
 		{
 			// 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))
 				data->mapvisited[gamemap-1] |= MV_ALLEMERALDS;
 		}
 
+		if ((earnedEmblems = M_CompletionEmblems(data)) && !silent)
+		{
+			CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+		}
+
 		if (silent)
 		{
-			if (modeattacking)
-				M_CheckLevelEmblems(data);
+			M_CheckLevelEmblems(data);
 		}
 		else
 		{
-			if (modeattacking == ATTACKING_RECORD)
-				G_UpdateRecordReplays(data);
-			else if (modeattacking == ATTACKING_NIGHTS)
-				G_SetNightsRecords(data);
+			if (mapheaderinfo[gamemap-1]->menuflags & LF2_RECORDATTACK)
+				G_SetMainRecords(data, player);
+			else if (mapheaderinfo[gamemap-1]->menuflags & LF2_NIGHTSATTACK)
+				G_SetNightsRecords(data, player);
 		}
+	}
+}
 
-		if ((earnedEmblems = M_CompletionEmblems(data)) && !silent)
-			CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+static void G_UpdateAllVisited(void)
+{
+	// Update server
+	G_UpdateVisited(serverGamedata, &players[serverplayer], true);
+
+	// Update client
+	G_UpdateVisited(clientGamedata, &players[consoleplayer], false);
+
+	if (splitscreen)
+	{
+		// Allow P2 to get emblems too, why not :)
+		G_UpdateVisited(clientGamedata, &players[secondarydisplayplayer], false);
 	}
 }
 
@@ -3918,6 +3976,9 @@ static boolean CanSaveLevel(INT32 mapnum)
 
 static void G_HandleSaveLevel(void)
 {
+	// Update records & emblems
+	G_UpdateAllVisited();
+
 	// do this before running the intermission or custom cutscene, mostly for the sake of marathon mode but it also massively reduces redundant file save events in f_finale.c
 	if (nextmap >= 1100-1)
 	{
@@ -4096,8 +4157,6 @@ static void G_DoCompleted(void)
 
 	if ((skipstats && !modeattacking) || (modeattacking && stagefailed) || (intertype == int_none))
 	{
-		G_UpdateVisited(serverGamedata, true);
-		G_UpdateVisited(clientGamedata, false);
 		G_HandleSaveLevel();
 		G_AfterIntermission();
 	}
@@ -4106,8 +4165,6 @@ static void G_DoCompleted(void)
 		G_SetGamestate(GS_INTERMISSION);
 		Y_StartIntermission();
 		Y_LoadIntermissionData();
-		G_UpdateVisited(serverGamedata, true);
-		G_UpdateVisited(clientGamedata, false);
 		G_HandleSaveLevel();
 	}
 }
@@ -4996,6 +5053,7 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 			players[i].playerstate = PST_REBORN;
 			players[i].starpostscale = players[i].starpostangle = players[i].starpostnum = players[i].starposttime = 0;
 			players[i].starpostx = players[i].starposty = players[i].starpostz = 0;
+			players[i].recordscore = 0;
 
 			if (netgame || multiplayer)
 			{
diff --git a/src/g_game.h b/src/g_game.h
index 6cda7ca9ccd8e4d7a7cba990fb2b9ffd58983103..a8c285f79f442b04a266f7e4ccbdd34997029833 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -260,8 +260,7 @@ UINT32 G_GetBestNightsScore(INT16 map, UINT8 mare, gamedata_t *data);
 tic_t G_GetBestNightsTime(INT16 map, UINT8 mare, gamedata_t *data);
 UINT8 G_GetBestNightsGrade(INT16 map, UINT8 mare, gamedata_t *data);
 
-void G_AddTempNightsRecords(UINT32 pscore, tic_t ptime, UINT8 mare);
-void G_SetNightsRecords(gamedata_t *data);
+void G_AddTempNightsRecords(player_t *player, UINT32 pscore, tic_t ptime, UINT8 mare);
 
 FUNCMATH INT32 G_TicsToHours(tic_t tics);
 FUNCMATH INT32 G_TicsToMinutes(tic_t tics, boolean full);
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 510e481e66412569d22b55e077deee38291db985..827e5a405fffb7fa1dda25f58aea049dc244bf38 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -114,6 +114,7 @@ enum player_e
 	player_skin,
 	player_availabilities,
 	player_score,
+	player_recordscore,
 	player_dashspeed,
 	player_normalspeed,
 	player_runspeed,
@@ -261,6 +262,7 @@ static const char *const player_opt[] = {
 	"skin",
 	"availabilities",
 	"score",
+	"recordscore",
 	"dashspeed",
 	"normalspeed",
 	"runspeed",
@@ -497,6 +499,9 @@ static int player_get(lua_State *L)
 	case player_score:
 		lua_pushinteger(L, plr->score);
 		break;
+	case player_recordscore:
+		lua_pushinteger(L, plr->recordscore);
+		break;
 	case player_dashspeed:
 		lua_pushfixed(L, plr->dashspeed);
 		break;
@@ -962,6 +967,9 @@ static int player_set(lua_State *L)
 	case player_score:
 		plr->score = (UINT32)luaL_checkinteger(L, 3);
 		break;
+	case player_recordscore:
+		plr->recordscore = (UINT32)luaL_checkinteger(L, 3);
+		break;
 	case player_dashspeed:
 		plr->dashspeed = luaL_checkfixed(L, 3);
 		break;
diff --git a/src/m_cond.c b/src/m_cond.c
index a54988f67accbb8f790fbfc06e9a9055b2362f7c..b21778c697042c4b96fed6126447df803121b077 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -45,7 +45,7 @@ INT32 numemblems = 0;
 INT32 numextraemblems = 0;
 
 // Temporary holding place for nights data for the current map
-nightsdata_t ntemprecords;
+nightsdata_t ntemprecords[MAXPLAYERS];
 
 // Create a new gamedata_t, for start-up
 gamedata_t *M_NewGameDataStruct(void)
diff --git a/src/m_cond.h b/src/m_cond.h
index 95c3e1ebeb0345fe102acf2ab4af4f1331031178..2be8d564be46f0c6bba94a427555fc2a50c08d05 100644
--- a/src/m_cond.h
+++ b/src/m_cond.h
@@ -181,7 +181,7 @@ typedef struct
 #define MV_MAX         63 // used in gamedata check, update whenever MV's are added
 
 // Temporary holding place for nights data for the current map
-extern nightsdata_t ntemprecords;
+extern nightsdata_t ntemprecords[MAXPLAYERS];
 
 // GAMEDATA STRUCTURE
 // Everything that would get saved in gamedata.dat
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 295fcbe950133c5371217e5f96516e827a46d460..76f1661acee724c8b710939590a8d4dbb139a303 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -179,6 +179,7 @@ static void P_NetArchivePlayers(void)
 		WRITEINT32(save_p, players[i].skin);
 		WRITEUINT32(save_p, players[i].availabilities);
 		WRITEUINT32(save_p, players[i].score);
+		WRITEUINT32(save_p, players[i].recordscore);
 		WRITEFIXED(save_p, players[i].dashspeed);
 		WRITESINT8(save_p, players[i].lives);
 		WRITESINT8(save_p, players[i].continues);
@@ -407,6 +408,7 @@ static void P_NetUnArchivePlayers(void)
 		players[i].skin = READINT32(save_p);
 		players[i].availabilities = READUINT32(save_p);
 		players[i].score = READUINT32(save_p);
+		players[i].recordscore = READUINT32(save_p);
 		players[i].dashspeed = READFIXED(save_p); // dashing speed
 		players[i].lives = READSINT8(save_p);
 		players[i].continues = READSINT8(save_p); // continues that player has acquired
@@ -4346,8 +4348,6 @@ static void P_NetArchiveMisc(boolean resending)
 
 	WRITEUINT32(save_p, hidetime);
 
-	WRITEUINT32(save_p, unlocktriggers);
-
 	// Is it paused?
 	if (paused)
 		WRITEUINT8(save_p, 0x2f);
@@ -4446,8 +4446,6 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 
 	hidetime = READUINT32(save_p);
 
-	unlocktriggers = READUINT32(save_p);
-
 	// Is it paused?
 	if (READUINT8(save_p) == 0x2f)
 		paused = true;
@@ -4455,7 +4453,6 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 	return true;
 }
 
-
 static inline void P_NetArchiveEmblems(void)
 {
 	gamedata_t *data = serverGamedata;
@@ -4554,6 +4551,27 @@ static inline void P_NetArchiveEmblems(void)
 			WRITEUINT32(save_p, data->nightsrecords[i]->time[curmare]);
 		}
 	}
+
+	// Mid-map stuff
+	WRITEUINT32(save_p, unlocktriggers);
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!ntemprecords[i].nummares)
+		{
+			WRITEUINT8(save_p, 0);
+			continue;
+		}
+
+		WRITEUINT8(save_p, ntemprecords[i].nummares);
+
+		for (curmare = 0; curmare < (ntemprecords[i].nummares + 1); ++curmare)
+		{
+			WRITEUINT32(save_p, ntemprecords[i].score[curmare]);
+			WRITEUINT8(save_p, ntemprecords[i].grade[curmare]);
+			WRITEUINT32(save_p, ntemprecords[i].time[curmare]);
+		}
+	}
 }
 
 static inline void P_NetUnArchiveEmblems(void)
@@ -4573,9 +4591,9 @@ static inline void P_NetUnArchiveEmblems(void)
 	savemoddata = (boolean)READUINT8(save_p); // this one is actually necessary because savemoddata stays false otherwise for some reason.
 
 	if (numemblems != READINT32(save_p))
-		I_Error("numemblems mismatch");
+		I_Error("Bad $$$.sav dearchiving Emblems (numemblems mismatch)");
 	if (numextraemblems != READINT32(save_p))
-		I_Error("numextraemblems mismatch");
+		I_Error("Bad $$$.sav dearchiving Emblems (numextraemblems mismatch)");
 
 	// This shouldn't happen, but if something really fucked up happens and you transfer
 	// the SERVER player's gamedata over your own CLIENT gamedata,
@@ -4594,7 +4612,7 @@ static inline void P_NetUnArchiveEmblems(void)
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
 		if ((data->mapvisited[i] = READUINT8(save_p)) > MV_MAX)
-			I_Error("Bad $$$.sav dearchiving Emblems");
+			I_Error("Bad $$$.sav dearchiving Emblems (invalid visit flags)");
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < MAXEMBLEMS;)
@@ -4638,7 +4656,7 @@ static inline void P_NetUnArchiveEmblems(void)
 		recrings = READUINT16(save_p);
 
 		if (recrings > 10000 || recscore > MAXSCORE)
-			I_Error("Bad $$$.sav dearchiving Emblems");
+			I_Error("Bad $$$.sav dearchiving Emblems (invalid score)");
 
 		if (recscore || rectime || recrings)
 		{
@@ -4665,12 +4683,35 @@ static inline void P_NetUnArchiveEmblems(void)
 
 			if (data->nightsrecords[i]->grade[curmare] > GRADE_S)
 			{
-				I_Error("Bad $$$.sav dearchiving Emblems");
+				I_Error("Bad $$$.sav dearchiving Emblems (invalid grade)");
 			}
 		}
 
 		data->nightsrecords[i]->nummares = recmares;
 	}
+
+	// Mid-map stuff
+	unlocktriggers = READUINT32(save_p);
+
+	for (i = 0; i < MAXPLAYERS; ++i)
+	{
+		if ((recmares = READUINT8(save_p)) == 0)
+			continue;
+
+		for (curmare = 0; curmare < (recmares+1); ++curmare)
+		{
+			ntemprecords[i].score[curmare] = READUINT32(save_p);
+			ntemprecords[i].grade[curmare] = READUINT8(save_p);
+			ntemprecords[i].time[curmare] = (tic_t)READUINT32(save_p);
+
+			if (ntemprecords[i].grade[curmare] > GRADE_S)
+			{
+				I_Error("Bad $$$.sav dearchiving Emblems (invalid temp grade)");
+			}
+		}
+
+		ntemprecords[i].nummares = recmares;
+	}
 }
 
 static inline void P_ArchiveLuabanksAndConsistency(void)
diff --git a/src/p_setup.c b/src/p_setup.c
index 532de7dfd4c171dc29d21a4360d8dc334a1ce4d0..175ab3328b627d7aceabeae3fd865ca71aba7b0b 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -7039,7 +7039,7 @@ static void P_InitLevelSettings(void)
 	stagefailed = G_IsSpecialStage(gamemap);
 
 	// Reset temporary record data
-	memset(&ntemprecords, 0, sizeof(nightsdata_t));
+	memset(&ntemprecords, 0, sizeof(ntemprecords));
 
 	// earthquake camera
 	memset(&quake,0,sizeof(struct quake));
diff --git a/src/p_user.c b/src/p_user.c
index 2aead091200b960c9c76edb42004b99aed29c244..62dd2fcb24e23367f55349fed517ea5c67925c46 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -882,8 +882,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 			}
 
 			// Add score to leaderboards now
-			if (!(netgame||multiplayer) && P_IsLocalPlayer(&players[i]))
-				G_AddTempNightsRecords(players[i].marescore, leveltime - player->marebegunat, players[i].mare + 1);
+			G_AddTempNightsRecords(player, players[i].marescore, leveltime - player->marebegunat, players[i].mare + 1);
 
 			// transfer scores anyway
 			players[i].totalmarescore += players[i].marescore;
@@ -909,8 +908,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 		player->finishedrings = (INT16)(player->rings);
 
 		// Add score to temp leaderboards
-		if (!(netgame||multiplayer) && P_IsLocalPlayer(player))
-			G_AddTempNightsRecords(player->marescore, leveltime - player->marebegunat, (UINT8)(oldmare + 1));
+		G_AddTempNightsRecords(player, player->marescore, leveltime - player->marebegunat, (UINT8)(oldmare + 1));
 
 		// Starting a new mare, transfer scores
 		player->totalmarescore += player->marescore;
@@ -1440,6 +1438,10 @@ void P_AddPlayerScore(player_t *player, UINT32 amount)
 	if (player->score > MAXSCORE)
 		player->score = MAXSCORE;
 
+	player->recordscore += amount;
+	if (player->recordscore > MAXSCORE)
+		player->recordscore = MAXSCORE;
+
 	// check for extra lives every 50000 pts
 	if (!ultimatemode && !modeattacking && player->score > oldscore && player->score % 50000 < amount && (gametyperules & GTR_LIVES))
 	{
@@ -11736,7 +11738,7 @@ void P_PlayerThink(player_t *player)
 	if (player->spectator)
 	{
 		if (!(gametyperules & GTR_CAMPAIGN))
-			player->score = 0;
+			player->score = player->recordscore = 0;
 	}
 	else if ((netgame || multiplayer) && player->lives <= 0 && !G_CoopGametype())
 	{