diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 4cd6333c5e6f1b8a831041d36927b0ee82bc4a1d..f95b952f39d2f8b0a09683cbd86e4d88531ce6ee 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -1448,6 +1448,7 @@ static boolean SV_SendServerConfig(INT32 node)
 	netbuffer->u.servercfg.gamestate = (UINT8)gamestate;
 	netbuffer->u.servercfg.gametype = (UINT8)gametype;
 	netbuffer->u.servercfg.modifiedgame = (UINT8)modifiedgame;
+	netbuffer->u.servercfg.usedCheats = (UINT8)usedCheats;
 
 	memcpy(netbuffer->u.servercfg.server_context, server_context, 8);
 
@@ -4341,6 +4342,8 @@ static void HandlePacketFromAwayNode(SINT8 node)
 				maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic);
 				G_SetGametype(netbuffer->u.servercfg.gametype);
 				modifiedgame = netbuffer->u.servercfg.modifiedgame;
+				if (netbuffer->u.servercfg.usedCheats)
+					G_SetUsedCheats(true);
 				memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
 			}
 
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index e07864122455bf0960e2299ba5ef7c3d9b2ac39f..f3896c7ea20db8fefc5c20444bcba9d07c7553f7 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -158,6 +158,7 @@ typedef struct
 
 	UINT8 gametype;
 	UINT8 modifiedgame;
+	UINT8 usedCheats;
 
 	char server_context[8]; // Unique context id, generated at server startup.
 } ATTRPACK serverconfig_pak;
diff --git a/src/d_main.c b/src/d_main.c
index b1f09aaa89d10ef481d2aba6c699efc0ff31074c..9df1d8fab9433a9c80d7e751588bfea446d366d8 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1506,7 +1506,7 @@ void D_SRB2Main(void)
 		else
 		{
 			if (!M_CheckParm("-server"))
-				G_SetGameModified(true);
+				G_SetUsedCheats(true);
 			autostart = true;
 		}
 	}
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 73bfe9e174cba91d425109190d1a4d0bfe3c8a6d..847f97341985f50498f43316a9adb289e2dc4f21 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -1877,7 +1877,7 @@ static void Command_Map_f(void)
 	const char *gametypename;
 	boolean newresetplayers;
 
-	boolean mustmodifygame;
+	boolean wouldSetCheats;
 
 	INT32 newmapnum;
 
@@ -1898,11 +1898,11 @@ static void Command_Map_f(void)
 	option_gametype =   COM_CheckPartialParm("-g");
 	newresetplayers = ! COM_CheckParm("-noresetplayers");
 
-	mustmodifygame =
-		!( netgame     || multiplayer ) &&
-		(!modifiedgame || savemoddata );
+	wouldSetCheats =
+		!( netgame || multiplayer ) &&
+		!( usedCheats );
 
-	if (mustmodifygame && !option_force)
+	if (wouldSetCheats && !option_force)
 	{
 		/* May want to be more descriptive? */
 		CONS_Printf(M_GetText("Sorry, level change disabled in single player.\n"));
@@ -1956,9 +1956,9 @@ static void Command_Map_f(void)
 		return;
 	}
 
-	if (mustmodifygame && option_force)
+	if (wouldSetCheats && option_force)
 	{
-		G_SetGameModified(false);
+		G_SetUsedCheats(false);
 	}
 
 	// new gametype value
@@ -4259,7 +4259,7 @@ static void Ringslinger_OnChange(void)
 	}
 
 	if (cv_ringslinger.value) // Only if it's been turned on
-		G_SetGameModified(multiplayer);
+		G_SetUsedCheats(false);
 }
 
 static void Gravity_OnChange(void)
@@ -4280,7 +4280,7 @@ static void Gravity_OnChange(void)
 #endif
 
 	if (!CV_IsSetToDefault(&cv_gravity))
-		G_SetGameModified(multiplayer);
+		G_SetUsedCheats(false);
 	gravity = cv_gravity.value;
 }
 
@@ -4596,7 +4596,7 @@ static void Fishcake_OnChange(void)
 	// so don't make modifiedgame always on!
 	if (cv_debug)
 	{
-		G_SetGameModified(multiplayer);
+		G_SetUsedCheats(false);
 	}
 
 	else if (cv_debug != cv_fishcake.value)
diff --git a/src/doomstat.h b/src/doomstat.h
index bce43416b840e7704a870857708ae1e019062868..490054cf97bc1153ae8d0ec25e21b6b357db2a09 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -75,6 +75,7 @@ extern SINT8 startinglivesbalance[maxgameovers+1];
 extern boolean modifiedgame;
 extern UINT16 mainwads;
 extern boolean savemoddata; // This mod saves time/emblem data.
+extern boolean usedCheats;
 extern boolean disableSpeedAdjust; // Don't alter the duration of player states if true
 extern boolean imcontinuing; // Temporary flag while continuing
 extern boolean metalrecording;
diff --git a/src/g_game.c b/src/g_game.c
index 892802d910448434bf69c2eba2323dbb6d44743d..e75bb433ea8d393f69d8486b354784bb2958804a 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -96,6 +96,7 @@ SINT8 startinglivesbalance[maxgameovers+1] = {3, 5, 7, 9, 12, 15, 20, 25, 30, 40
 UINT16 mainwads = 0;
 boolean modifiedgame; // Set if homebrew PWAD stuff has been added.
 boolean savemoddata = false;
+boolean usedCheats = false; // Set when a gamedata-preventing cheat command is used.
 UINT8 paused;
 UINT8 modeattacking = ATTACKING_NONE;
 boolean disableSpeedAdjust = false;
@@ -764,6 +765,23 @@ void G_SetGameModified(boolean silent)
 		Command_ExitGame_f();
 }
 
+void G_SetUsedCheats(boolean silent)
+{
+	if (usedCheats)
+		return;
+
+	usedCheats = true;
+
+	if (!silent)
+		CONS_Alert(CONS_NOTICE, M_GetText("Game must be restarted to save progress.\n"));
+
+	// If in record attack recording, cancel it.
+	if (modeattacking)
+		M_EndModeAttackRun();
+	else if (marathonmode)
+		Command_ExitGame_f();
+}
+
 /** Builds an original game map name from a map number.
   * The complexity is due to MAPA0-MAPZZ.
   *
@@ -3894,7 +3912,7 @@ static void G_HandleSaveLevel(void)
 					remove(liveeventbackup);
 				cursaveslot = 0;
 			}
-			else if (!(netgame || multiplayer || ultimatemode || demorecording || metalrecording || modeattacking))
+			else if (!usedCheats && !(netgame || multiplayer || ultimatemode || demorecording || metalrecording || modeattacking))
 			{
 				G_SaveGame((UINT32)cursaveslot, spstage_start);
 			}
@@ -3902,7 +3920,7 @@ static void G_HandleSaveLevel(void)
 	}
 	// and doing THIS here means you don't lose your progress if you close the game mid-intermission
 	else if (!(ultimatemode || netgame || multiplayer || demoplayback || demorecording || metalrecording || modeattacking)
-		&& cursaveslot > 0 && CanSaveLevel(lastmap+1))
+		&& !usedCheats && cursaveslot > 0 && CanSaveLevel(lastmap+1))
 	{
 		G_SaveGame((UINT32)cursaveslot, lastmap+1); // not nextmap+1 to route around special stages
 	}
@@ -4181,7 +4199,7 @@ static void G_DoContinued(void)
 	tokenlist = 0;
 	token = 0;
 
-	if (!(netgame || multiplayer || demoplayback || demorecording || metalrecording || modeattacking) && cursaveslot > 0)
+	if (!(netgame || multiplayer || demoplayback || demorecording || metalrecording || modeattacking) && !usedCheats && cursaveslot > 0)
 	{
 		G_SaveGameOver((UINT32)cursaveslot, true);
 	}
@@ -4480,6 +4498,13 @@ void G_SaveGameData(void)
 		return;
 	}
 
+	if (usedCheats)
+	{
+		free(savebuffer);
+		save_p = savebuffer = NULL;
+		return;
+	}
+
 	// Version test
 	WRITEUINT32(save_p, GAMEDATA_ID);
 
diff --git a/src/g_game.h b/src/g_game.h
index dca043f2e0fae899ec86ac55255f2613dfdde0eb..a781e23f991a24af6deed1e2723c9279e383354e 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -243,6 +243,7 @@ void G_LoadGameData(void);
 void G_LoadGameSettings(void);
 
 void G_SetGameModified(boolean silent);
+void G_SetUsedCheats(boolean silent);
 
 void G_SetGamestate(gamestate_t newstate);
 
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 29adb478abfffd58a5d564769382c6290c6acefd..031a155d23d90de8106cc7435d9a8ebf314e9a68 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -3612,6 +3612,16 @@ static int lib_gRemovePlayer(lua_State *L)
 }
 
 
+static int lib_gSetUsedCheats(lua_State *L)
+{
+	// Let large-scale level packs using Lua be able to add cheat commands.
+	boolean silent = lua_optboolean(L, 1);
+	//NOHUD
+	//INLEVEL
+	G_SetUsedCheats(silent);
+	return 0;
+}
+
 static int Lcheckmapnumber (lua_State *L, int idx, const char *fun)
 {
 	if (ISINLEVEL)
@@ -4213,6 +4223,7 @@ static luaL_Reg lib[] = {
 	{"G_AddGametype", lib_gAddGametype},
 	{"G_AddPlayer", lib_gAddPlayer},
 	{"G_RemovePlayer", lib_gRemovePlayer},
+	{"G_SetUsedCheats", lib_gSetUsedCheats},
 	{"G_BuildMapName",lib_gBuildMapName},
 	{"G_BuildMapTitle",lib_gBuildMapTitle},
 	{"G_FindMap",lib_gFindMap},
diff --git a/src/lua_script.c b/src/lua_script.c
index 4d407154505fc5a9bbfd4942b5e8147d840edc37..5f16bca8a72e91a82603560bcfee77150f780e7b 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -204,6 +204,9 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"modifiedgame")) {
 		lua_pushboolean(L, modifiedgame && !savemoddata);
 		return 1;
+	} else if (fastcmp(word,"usedCheats")) {
+		lua_pushboolean(L, usedCheats);
+		return 1;
 	} else if (fastcmp(word,"menuactive")) {
 		lua_pushboolean(L, menuactive);
 		return 1;
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 89c8009aed321a0535d3e9921432fe59b09a68ee..78fb3a50578a8975c726f0c1d13142cbe1465163 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -81,7 +81,7 @@ static UINT8 cheatf_warp(void)
 	S_StartSound(0, sfx_itemup);
 
 	// Temporarily unlock stuff.
-	G_SetGameModified(false);
+	G_SetUsedCheats(false);
 	unlockables[31].unlocked = true; // credits
 	unlockables[30].unlocked = true; // sound test
 	unlockables[28].unlocked = true; // level select
@@ -106,7 +106,7 @@ static UINT8 cheatf_devmode(void)
 	S_StartSound(0, sfx_itemup);
 
 	// Just unlock all the things and turn on -debug and console devmode.
-	G_SetGameModified(false);
+	G_SetUsedCheats(false);
 	for (i = 0; i < MAXUNLOCKABLES; i++)
 		unlockables[i].unlocked = true;
 	devparm = true;
@@ -275,7 +275,7 @@ void Command_CheatNoClip_f(void)
 	plyr->pflags ^= PF_NOCLIP;
 	CONS_Printf(M_GetText("No Clipping %s\n"), plyr->pflags & PF_NOCLIP ? M_GetText("On") : M_GetText("Off"));
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 void Command_CheatGod_f(void)
@@ -290,7 +290,7 @@ void Command_CheatGod_f(void)
 	plyr->pflags ^= PF_GODMODE;
 	CONS_Printf(M_GetText("Cheese Mode %s\n"), plyr->pflags & PF_GODMODE ? M_GetText("On") : M_GetText("Off"));
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 void Command_CheatNoTarget_f(void)
@@ -305,7 +305,7 @@ void Command_CheatNoTarget_f(void)
 	plyr->pflags ^= PF_INVIS;
 	CONS_Printf(M_GetText("SEP Field %s\n"), plyr->pflags & PF_INVIS ? M_GetText("On") : M_GetText("Off"));
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 void Command_Scale_f(void)
@@ -879,7 +879,7 @@ void Command_Devmode_f(void)
 		return;
 	}
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 void Command_Setrings_f(void)
@@ -905,7 +905,7 @@ void Command_Setrings_f(void)
 			// no totalsphere addition to revert
 		}
 
-		G_SetGameModified(multiplayer);
+		G_SetUsedCheats(false);
 	}
 }
 
@@ -928,7 +928,7 @@ void Command_Setlives_f(void)
 			P_GivePlayerLives(&players[consoleplayer], atoi(COM_Argv(1)));
 		}
 
-		G_SetGameModified(multiplayer);
+		G_SetUsedCheats(false);
 	}
 }
 
@@ -955,7 +955,7 @@ void Command_Setcontinues_f(void)
 
 		players[consoleplayer].continues = numcontinues;
 
-		G_SetGameModified(multiplayer);
+		G_SetUsedCheats(false);
 	}
 }
 
@@ -1446,7 +1446,7 @@ void Command_ObjectPlace_f(void)
 	REQUIRE_SINGLEPLAYER;
 	REQUIRE_NOULTIMATE;
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 
 	silent = COM_CheckParm("-silent");
 
diff --git a/src/m_menu.c b/src/m_menu.c
index 55733e552a65e29d0273e3fd46d6e7e29cb59a8c..81567662ad7a754beeb2c653ac7793bc5f01fff3 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -7082,7 +7082,7 @@ static void M_AllowSuper(INT32 choice)
 	M_StartMessage(M_GetText("You are now capable of turning super.\nRemember to get all the emeralds!\n"),NULL,MM_NOTHING);
 	SR_PandorasBox[6].status = IT_GRAYEDOUT;
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 static void M_GetAllEmeralds(INT32 choice)
@@ -7093,7 +7093,7 @@ static void M_GetAllEmeralds(INT32 choice)
 	M_StartMessage(M_GetText("You now have all 7 emeralds.\nUse them wisely.\nWith great power comes great ring drain.\n"),NULL,MM_NOTHING);
 	SR_PandorasBox[7].status = IT_GRAYEDOUT;
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 static void M_DestroyRobotsResponse(INT32 ch)
@@ -7104,7 +7104,7 @@ static void M_DestroyRobotsResponse(INT32 ch)
 	// Destroy all robots
 	P_DestroyRobots();
 
-	G_SetGameModified(multiplayer);
+	G_SetUsedCheats(false);
 }
 
 static void M_DestroyRobots(INT32 choice)
@@ -8703,6 +8703,12 @@ static void M_DrawLoad(void)
 		loadgameoffset = 0;
 
 	M_DrawLoadGameData();
+
+	if (usedCheats)
+	{
+		V_DrawCenteredThinString(BASEVIDWIDTH/2, 184, 0, "\x85WARNING:\x80 Cheats have been activated.");
+		V_DrawCenteredThinString(BASEVIDWIDTH/2, 192, 0, "Progress will not be saved.");
+	}
 }
 
 //
@@ -9004,12 +9010,18 @@ static void M_HandleLoadSave(INT32 choice)
 			break;
 
 		case KEY_ENTER:
-			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT)
+			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT && !usedCheats)
 			{
 				loadgamescroll = 0;
 				S_StartSound(NULL, sfx_skid);
 				M_StartMessage("Are you sure you want to play\n\x85ultimate mode\x80? It isn't remotely fair,\nand you don't even get an emblem for it.\n\n(Press 'Y' to confirm)\n",M_SaveGameUltimateResponse,MM_YESNO);
 			}
+			else if (saveSlotSelected != NOSAVESLOT && savegameinfo[saveSlotSelected-1].lives == -42 && usedCheats)
+			{
+				loadgamescroll = 0;
+				S_StartSound(NULL, sfx_skid);
+				M_StartMessage(M_GetText("This cannot be done in a cheated game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+			}
 			else if (saveSlotSelected == NOSAVESLOT || savegameinfo[saveSlotSelected-1].lives != -666) // don't allow loading of "bad saves"
 			{
 				loadgamescroll = 0;
diff --git a/src/p_inter.c b/src/p_inter.c
index 1094c3045938e0b91bb0608ad4dacfd9b41de955..b86bb39a68d8e2d9be84900b4dbebb343e3f1fa5 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2583,7 +2583,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 				if (!(netgame || multiplayer || demoplayback || demorecording || metalrecording || modeattacking) && numgameovers < maxgameovers)
 				{
 					numgameovers++;
-					if (cursaveslot > 0)
+					if (!usedCheats && cursaveslot > 0)
 						G_SaveGameOver((UINT32)cursaveslot, (target->player->continues <= 0));
 				}
 			}
diff --git a/src/p_setup.c b/src/p_setup.c
index c41a5d94e20a617b8fbf0a6e7dce8613bd004a92..a965c5142c97c41edc633e67366ddaa478f8ddf5 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -7764,7 +7764,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 		{
 			// I'd love to do this in the menu code instead of here, but everything's a mess and I can't guarantee saving proper player struct info before the first act's started. You could probably refactor it, but it'd be a lot of effort. Easier to just work off known good code. ~toast 22/06/2020
 			if (!(ultimatemode || netgame || multiplayer || demoplayback || demorecording || metalrecording || modeattacking || marathonmode)
-				&& cursaveslot > 0)
+				&& !usedCheats && cursaveslot > 0)
 			{
 				G_SaveGame((UINT32)cursaveslot, gamemap);
 			}