diff --git a/src/deh_soc.c b/src/deh_soc.c
index 0e904dda40dd010fa6677f5b56aa6ab290213e87..5960ac33693a8a307cb5e829d6e689c31d16d43a 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1340,10 +1340,10 @@ void readgametype(MYFILE *f, char *gtname)
 
 static INT32 ParseNextLevelName(const char *name)
 {
-	if      (fastcmp(name, "TITLE"))      return MAP_TITLE;
-	else if (fastcmp(name, "EVALUATION")) return MAP_EVALUATION;
-	else if (fastcmp(name, "CREDITS"))    return MAP_CREDITS;
-	else if (fastcmp(name, "ENDING"))     return MAP_ENDING;
+	if      (fastcmp(name, "TITLE"))      return NEXTMAP_TITLE;
+	else if (fastcmp(name, "EVALUATION")) return NEXTMAP_EVALUATION;
+	else if (fastcmp(name, "CREDITS"))    return NEXTMAP_CREDITS;
+	else if (fastcmp(name, "ENDING"))     return NEXTMAP_ENDING;
 	else
 	{
 		// Support using the actual map name,
@@ -1356,16 +1356,19 @@ static INT32 ParseNextLevelName(const char *name)
 
 static INT32 ConvertLevelHeaderMapNum(INT32 mapnum)
 {
-	if (mapnum == 1100)
-		return MAP_TITLE;
-	else if (mapnum == 1101)
-		return MAP_EVALUATION;
-	else if (mapnum == 1102)
-		return MAP_CREDITS;
-	else if (mapnum == 1103)
-		return MAP_ENDING;
-	else
+	switch (mapnum)
+	{
+	case 1100:
+		return NEXTMAP_TITLE;
+	case 1101:
+		return NEXTMAP_EVALUATION;
+	case 1102:
+		return NEXTMAP_CREDITS;
+	case 1103:
+		return NEXTMAP_ENDING;
+	default:
 		return mapnum;
+	}
 }
 
 void readlevelheader(MYFILE *f, INT32 num)
diff --git a/src/doomdata.h b/src/doomdata.h
index 33051da656b9237a4ae11c3fb43398422dd4cec6..0b6210d2fcb768c2c8bf210f0fd0dc9e70e450b4 100644
--- a/src/doomdata.h
+++ b/src/doomdata.h
@@ -223,15 +223,4 @@ typedef struct
 
 #define ZSHIFT 4
 
-#define MAXMAPS 16386
-
-#define MAX_MAP_NAME_SIZE 256 // This is an arbitrary limit to prevent exceedingly long map names.
-
-#define MAP_TITLE (MAXMAPS)
-#define MAP_EVALUATION (MAXMAPS+2)
-#define MAP_CREDITS (MAXMAPS+3)
-#define MAP_ENDING (MAXMAPS+4)
-
-#define NUMBASEMAPS 1035 // MAP01 to MAPZZ
-
 #endif // __DOOMDATA__
diff --git a/src/doomdef.h b/src/doomdef.h
index 2626e8f54d6e4e7e27b44227ee83fda3a24ddeb6..415854aa6c42772294f2933c16204d4bb90fca2d 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -237,6 +237,19 @@ extern char logfilename[1024];
 #define PLAYERSMASK (MAXPLAYERS-1)
 #define MAXPLAYERNAME 21
 
+#define MAXMAPS 16386
+
+#define MAX_MAP_NAME_SIZE 256 // This is an arbitrary limit to prevent exceedingly long map names.
+
+#define NEXTMAP_TITLE (MAXMAPS)
+#define NEXTMAP_EVALUATION (MAXMAPS+2)
+#define NEXTMAP_CREDITS (MAXMAPS+3)
+#define NEXTMAP_ENDING (MAXMAPS+4)
+
+#define NUM_NEXTMAPS 4
+
+#define NUMBASEMAPS 1035 // MAP01 to MAPZZ
+
 #define COLORRAMPSIZE 16
 #define MAXCOLORNAME 32
 #define NUMCOLORFREESLOTS 1024
diff --git a/src/doomstat.h b/src/doomstat.h
index 3141e7cc4446098ed3b5aed16bd4237969e7c90c..3ad304452ca1e4df0705d21efa6e10ae8c162851 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -281,7 +281,13 @@ extern struct quake
 typedef struct
 {
 	UINT32 hash;
-	char *name;
+	size_t length;
+	char *chars;
+} mapname_t;
+
+typedef struct
+{
+	mapname_t name;
 	char *thumbnail;
 	char *thumbnail_wide;
 	char *music;
@@ -311,7 +317,7 @@ typedef struct
 	char subttl[33];       ///< Subtitle for level
 	UINT8 actnum;          ///< Act number or 0 for none.
 	UINT32 typeoflevel;    ///< Combination of typeoflevel flags.
-	INT16 nextlevel;       ///< Map number of next level, or 1100-1102 to end.
+	INT16 nextlevel;       ///< Map number of next level, or NEXTMAP_* to end.
 	INT16 marathonnext;    ///< See nextlevel, but for Marathon mode. Necessary to support hub worlds ala SUGOI.
 	char keywords[33];     ///< Keywords separated by space to search for. 32 characters.
 	char musname[MAX_MUSIC_NAME+1]; ///< Music track to play. "" for no music.
diff --git a/src/g_game.c b/src/g_game.c
index 28e7646a8b7fd1b5b0ad437848364e41be39ae28..d11cbedbaff09ebec4888434214aed0a15a044a0 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -178,6 +178,9 @@ mapheader_t* mapheaderinfo[MAXMAPS] = {NULL};
 gamemap_t gamemaps[MAXMAPS];
 UINT16 numgamemaps = 0;
 
+static mapname_t nextmapnames[NUM_NEXTMAPS];
+static INT16 nextmapids[NUM_NEXTMAPS] = { NEXTMAP_TITLE, NEXTMAP_EVALUATION, NEXTMAP_CREDITS, NEXTMAP_ENDING };
+
 static boolean exitgame = false;
 static boolean retrying = false;
 static boolean retryingmodeattack = false;
@@ -818,7 +821,7 @@ void G_SetUsedCheats(boolean silent)
 const char *G_BuildMapName(INT32 map)
 {
 	if (map > 0 && map <= numgamemaps)
-		return gamemaps[map - 1].name;
+		return gamemaps[map - 1].name.chars;
 
 	return "UNKNOWN";
 }
@@ -852,6 +855,19 @@ const char *G_BuildClassicMapName(INT32 map)
 	return mapname;
 }
 
+static UINT32 G_HashMapNameString(const char *name, size_t name_length)
+{
+	return quickncasehash(name, name_length);
+}
+
+static void G_MakeMapName(mapname_t *name, const char *string)
+{
+	name->length = strlen(string);
+	name->hash = G_HashMapNameString(string, name->length);
+	name->chars = Z_StrDup(string);
+	strupr(name->chars);
+}
+
 void G_InitMaps(void)
 {
 	for (UINT16 i = 0; i < NUMBASEMAPS; i++)
@@ -859,29 +875,53 @@ void G_InitMaps(void)
 		const char *name = G_BuildClassicMapName(i + 1);
 		G_AddMap(name);
 	}
+
+	G_MakeMapName(&nextmapnames[0], "SCENE_TITLE");
+	G_MakeMapName(&nextmapnames[1], "SCENE_EVALUATION");
+	G_MakeMapName(&nextmapnames[2], "SCENE_CREDITS");
+	G_MakeMapName(&nextmapnames[3], "SCENE_ENDING");
 }
 
-UINT16 G_GetMapNumber(const char *name)
+static UINT16 MapIDForHashedString(const char *name, size_t name_length, UINT32 name_hash)
 {
-	size_t name_len = strlen(name);
-
 	// Special case
-	if (name_len == 2 && name[0] >= 'A' && name[0] <= 'Z')
-	{
+	if (name_length == 2 && name[0] >= 'A' && name[0] <= 'Z')
 		return M_MapNumber(name[0], name[1]);
-	}
-
-	UINT32 hash = quickncasehash(name, name_len);
 
 	for (UINT16 i = 0; i < numgamemaps; i++)
 	{
-		if (hash == gamemaps[i].hash && stricmp(gamemaps[i].name, name) == 0)
+		if (gamemaps[i].name.length == name_length
+		&& gamemaps[i].name.hash == name_hash
+		&& stricmp(name, gamemaps[i].name.chars) == 0)
 			return i + 1;
 	}
 
 	return 0;
 }
 
+UINT16 G_GetMapNumber(const char *name)
+{
+	size_t name_length = strlen(name);
+
+	return MapIDForHashedString(name, name_length, G_HashMapNameString(name, name_length));
+}
+
+UINT16 G_GetNextMapNumber(const char *name)
+{
+	size_t name_length = strlen(name);
+	UINT32 name_hash = G_HashMapNameString(name, name_length);
+
+	for (UINT16 i = 0; i < NUM_NEXTMAPS; i++)
+	{
+		if (nextmapnames[i].length == name_length
+		&& nextmapnames[i].hash == name_hash
+		&& stricmp(name, nextmapnames[i].chars) == 0)
+			return nextmapids[i];
+	}
+
+	return MapIDForHashedString(name, name_length, name_hash);
+}
+
 UINT16 G_AddMap(const char *name)
 {
 	if (numgamemaps == MAXMAPS)
@@ -898,10 +938,7 @@ UINT16 G_AddMap(const char *name)
 	if (name_len > MAX_MAP_NAME_SIZE)
 		return 0;
 
-	gamemaps[numgamemaps].hash = quickncasehash(name, name_len);
-	gamemaps[numgamemaps].name = Z_StrDup(name);
-
-	strupr(gamemaps[numgamemaps].name);
+	G_MakeMapName(&gamemaps[numgamemaps].name, name);
 
 	numgamemaps++;
 
@@ -925,7 +962,7 @@ boolean G_IsValidMapName(const char *name)
 
 static char *BuildCombinedMapString(INT16 map, const char *newfmt, const char *oldfmt)
 {
-	const char *mapname = gamemaps[map].name;
+	const char *mapname = gamemaps[map].name.chars;
 	const char *fmt = (map < NUMBASEMAPS) ? oldfmt : newfmt;
 
 	size_t len = strlen(mapname) + strlen(fmt) + 1;
@@ -994,10 +1031,10 @@ boolean G_IsGameEndMap(INT16 mapnum)
 {
 	switch (mapnum)
 	{
-		case MAP_TITLE:
-		case MAP_EVALUATION:
-		case MAP_CREDITS:
-		case MAP_ENDING:
+		case NEXTMAP_TITLE:
+		case NEXTMAP_EVALUATION:
+		case NEXTMAP_CREDITS:
+		case NEXTMAP_ENDING:
 			return true;
 		default:
 			return false;
@@ -4144,7 +4181,7 @@ static void G_HandleSaveLevel(void)
 	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)
+	if (nextmap >= NEXTMAP_TITLE-1)
 	{
 		if (!gamecomplete)
 			gamecomplete = 2; // special temporary mode to prevent using SP level select in pause menu until the intermission is over without restricting it in every intermission
@@ -4215,7 +4252,7 @@ static void G_DoCompleted(void)
 	{
 		nextmap = (INT16)(mapheaderinfo[gamemap-1]->nextlevel-1);
 		if (marathonmode && nextmap == spmarathon_start-1)
-			nextmap = 1100-1; // No infinite loop for you
+			nextmap = NEXTMAP_TITLE-1; // No infinite loop for you
 	}
 
 	INT16 gametype_to_use;
@@ -4247,7 +4284,7 @@ static void G_DoCompleted(void)
 				else
 					cm = (INT16)(mapheaderinfo[cm]->nextlevel-1);
 
-				if (cm >= numgamemaps || cm < 0) // out of range (either 1100ish or error)
+				if (cm >= numgamemaps || cm < 0) // out of range (either NEXTMAP_* or error)
 				{
 					cm = nextmap; //Start the loop again so that the error checking below is executed.
 
@@ -4487,24 +4524,21 @@ void G_EndGame(void)
 	// Only do evaluation and credits in coop games.
 	if (gametyperules & GTR_CUTSCENES)
 	{
-		if (nextmap+1 == MAP_ENDING) // end game with ending
+		switch (nextmap+1)
 		{
+		case NEXTMAP_ENDING:
 			F_StartEnding();
 			return;
-		}
-		else if (nextmap+1 == MAP_CREDITS) // end game with credits
-		{
+		case NEXTMAP_CREDITS:
 			F_StartCredits();
 			return;
-		}
-		else if (nextmap+1 == MAP_EVALUATION) // end game with evaluation
-		{
+		case NEXTMAP_EVALUATION:
 			F_StartGameEvaluation();
 			return;
 		}
 	}
 
-	// 1100 or competitive multiplayer, so go back to title screen.
+	// NEXTMAP_TITLE or competitive multiplayer, so go back to title screen.
 	D_StartTitle();
 }
 
diff --git a/src/g_game.h b/src/g_game.h
index 6e59b2cd2473b416512d5775c7b5f4bb9e01ee33..930bc0b990d079668476f415263c27c98b5f12e9 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -119,6 +119,7 @@ const char *G_BuildMapName(INT32 map);
 
 void G_InitMaps(void);
 UINT16 G_GetMapNumber(const char *name);
+UINT16 G_GetNextMapNumber(const char *name);
 UINT16 G_AddMap(const char *name);
 boolean G_MapFileExists(const char *name);
 boolean G_IsValidMapName(const char *name);
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 87afc772c04a6c7fabce14de17ae4ad87a1d90df..c4d4dea0609cee168f32054b8bade54e048e493b 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -3846,6 +3846,25 @@ static int GetMapNameOrNumber(lua_State *L, int idx)
 		return luaL_checkinteger(L, idx);
 }
 
+static int GetNextMapNameOrNumber(lua_State *L, int idx)
+{
+	if (lua_type(L, idx) == LUA_TSTRING)
+	{
+		const char *mapname = luaL_checkstring(L, idx);
+		INT16 mapnum = G_GetNextMapNumber(mapname);
+		if (mapnum == 0)
+		{
+			return luaL_error(L,
+					"%s is not a valid game map.",
+					mapname
+			);
+		}
+		return mapnum;
+	}
+	else
+		return luaL_checkinteger(L, idx);
+}
+
 static int Lcheckmapnumber (lua_State *L, int idx, const char *fun)
 {
 	if (ISINLEVEL)
@@ -4078,8 +4097,8 @@ static int lib_gSetCustomExitVars(lua_State *L)
 	{
 		if (!lua_isnoneornil(L, 1))
 		{
-			INT16 mapnum = GetMapNameOrNumber(L, 1);
-			if (mapnum < 1 || mapnum > numgamemaps)
+			INT16 mapnum = GetNextMapNameOrNumber(L, 1);
+			if (mapnum < 1 || (mapnum > numgamemaps && !G_IsGameEndMap(mapnum)))
 			{
 				return luaL_error(L,
 						"map number %d out of range (1 - %d)",
diff --git a/src/m_menu.c b/src/m_menu.c
index 7dc5056ac221ecad45199ff23e87f87241d68e83..170ff482746e2fdd896615097dc09b3efe3b4422 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -8235,7 +8235,7 @@ static void M_SinglePlayerMenu(INT32 choice)
 	if (mapheaderinfo[spmarathon_start-1]
 		&& !mapheaderinfo[spmarathon_start-1]->marathonnext
 		&& (mapheaderinfo[spmarathon_start-1]->nextlevel == spmarathon_start
-			|| mapheaderinfo[spmarathon_start-1]->nextlevel >= 1100))
+			|| G_IsGameEndMap(mapheaderinfo[spmarathon_start-1]->nextlevel)))
 	{
 		SP_MainMenu[spmarathon].status = IT_NOTHING|IT_DISABLED; // Hide and disable the Marathon Run option...
 		// ...and lower the above options' display positions by 8 pixels to close the gap
diff --git a/src/p_setup.c b/src/p_setup.c
index 40be9babab87d4c715874202df838634eefffd25..0ab8974073c1338972915e031f3eecab13894c52 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -8119,7 +8119,7 @@ static lumpinfo_t* FindFolder(const char *folName, UINT16 *start, UINT16 *end, l
 	return lumpinfo;
 }
 
-void P_LoadMapsFromFile(UINT16 wadnum, boolean is_pwad)
+void P_LoadMapsFromFile(UINT16 wadnum, boolean added_ingame)
 {
 	boolean mapsadded = false;
 
@@ -8158,10 +8158,10 @@ void P_LoadMapsFromFile(UINT16 wadnum, boolean is_pwad)
 				break;
 			}
 			//If you replaced the map you're on, end the level when done.
-			else if (num == gamemap)
+			else if (gamestate == GS_LEVEL && num == gamemap)
 				replacedcurrentmap = true;
 
-			if (is_pwad)
+			if (added_ingame)
 				CONS_Printf("%s\n", name);
 			mapsadded = true;
 		}
@@ -8179,17 +8179,17 @@ void P_LoadMapsFromFile(UINT16 wadnum, boolean is_pwad)
 				num = (INT16)M_MapNumber(name[3], name[4]);
 
 				//If you replaced the map you're on, end the level when done.
-				if (num == gamemap)
+				if (gamestate == GS_LEVEL && num == gamemap)
 					replacedcurrentmap = true;
 
-				if (is_pwad)
+				if (added_ingame)
 					CONS_Printf("%s\n", name);
 				mapsadded = true;
 			}
 		}
 	}
 
-	if (!mapsadded && is_pwad)
+	if (!mapsadded && added_ingame)
 		CONS_Printf(M_GetText("No maps added\n"));
 }
 
@@ -8365,7 +8365,7 @@ static boolean P_LoadAddon(UINT16 numlumps)
 
 		if (gamestate == GS_LEVEL && (netgame || multiplayer))
 		{
-			CONS_Printf(M_GetText("Current map %d replaced by added file, ending the level to ensure consistency.\n"), gamemap);
+			CONS_Printf(M_GetText("Current map %s replaced by added file, ending the level to ensure consistency.\n"), G_BuildMapName(gamemap));
 			if (server)
 				D_SendExitLevel(false);
 		}
diff --git a/src/p_setup.h b/src/p_setup.h
index 23c6d4b1c8883675e6100e4664ad3197097d40d4..a3f850ff6b66de423d936ab2af1cc62e299fd46e 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -107,7 +107,7 @@ boolean P_AddFolder(const char *folderpath);
 boolean P_RunSOC(const char *socfilename);
 void P_LoadSoundsRange(UINT16 wadnum, UINT16 first, UINT16 num);
 void P_LoadMusicsRange(UINT16 wadnum, UINT16 first, UINT16 num);
-void P_LoadMapsFromFile(UINT16 wadnum, boolean is_pwad);
+void P_LoadMapsFromFile(UINT16 wadnum, boolean added_ingame);
 //void P_WriteThings(void);
 size_t P_PrecacheLevelFlats(void);
 void P_AllocMapHeader(INT16 i);