From 14bf79f5ec1d2ad740addf9cbda5a2d7c245b232 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Wed, 12 Feb 2025 16:20:32 -0300
Subject: [PATCH 01/17] Implement Hub gametype

---
 src/d_main.c           |  6 ++-
 src/doomstat.h         |  6 ++-
 src/g_game.c           | 74 +++++++++++++++++++++++++-------
 src/g_game.h           |  1 +
 src/m_menu.c           | 23 ++--------
 src/netcode/d_netcmd.c | 96 +++++++++++++++++++++++++++---------------
 src/netcode/d_netcmd.h |  2 +-
 src/p_inter.c          |  2 +-
 src/st_stuff.c         |  2 +-
 src/y_inter.c          | 39 ++++++++---------
 10 files changed, 156 insertions(+), 95 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index d75a4d5013..40d9319fd8 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1377,6 +1377,9 @@ void D_SRB2Main(void)
 	CONS_Printf("Z_Init(): Init zone memory allocation daemon. \n");
 	Z_Init();
 
+	gametype = -1;
+	G_SetGametype(GT_COOP);
+
 	clientGamedata = M_NewGameDataStruct();
 	serverGamedata = M_NewGameDataStruct();
 
@@ -1728,9 +1731,8 @@ void D_SRB2Main(void)
 
 			if (newgametype != -1)
 			{
-				j = gametype;
 				G_SetGametype(newgametype);
-				D_GameTypeChanged(j);
+				D_GameTypeChanged();
 			}
 		}
 
diff --git a/src/doomstat.h b/src/doomstat.h
index b5b2984407..d05620582a 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -96,7 +96,7 @@ extern boolean addedtogame; // true after the server has added you
 // Only true if >1 player. netgame => multiplayer but not (multiplayer=>netgame)
 extern boolean multiplayer;
 
-extern INT16 gametype;
+extern INT16 gametype, lastgametype;
 extern UINT32 gametyperules;
 extern INT16 gametypecount;
 
@@ -407,6 +407,8 @@ enum GameType
 
 	GT_CTF, // capture the flag
 
+	GT_HUB,
+
 	GT_FIRSTFREESLOT,
 	GT_LASTFREESLOT = GT_FIRSTFREESLOT + NUMGAMETYPEFREESLOTS - 1,
 	NUMGAMETYPES
@@ -474,7 +476,7 @@ enum TypeOfLevel
 	TOL_CTF         = 0x40, ///< Capture the Flag
 // CTF default = 64
 
-	// 0x80 was here
+	TOL_HUB         = 0x80, ///< Hub
 
 	TOL_2D     = 0x0100, ///< 2D
 	TOL_MARIO  = 0x0200, ///< Mario
diff --git a/src/g_game.c b/src/g_game.c
index 1c186ae031..ea29d30916 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2353,7 +2353,7 @@ void G_Ticker(boolean run)
 
 					memset(&luabanks, 0, sizeof(luabanks));
 				}
-				else if (G_GametypeUsesLives() && players[consoleplayer].playerstate == PST_LIVE && players[consoleplayer].lives != INFLIVES)
+				else if (G_CanLoseLivesInGametype() && players[consoleplayer].playerstate == PST_LIVE && players[consoleplayer].lives != INFLIVES)
 					players[consoleplayer].lives -= 1;
 
 				G_DoReborn(consoleplayer);
@@ -3133,7 +3133,7 @@ void G_DoReborn(INT32 playernum)
 		return; //Exit function to avoid proccing other SP related mechanics
 	}
 
-	if (countdowntimeup || (!(netgame || multiplayer) && (gametyperules & GTR_CAMPAIGN)))
+	if (countdowntimeup || (!(netgame || multiplayer) && ((gametyperules & GTR_CAMPAIGN) || (maptol & TOL_HUB))))
 		resetlevel = true;
 	else if ((G_GametypeUsesCoopLives() || G_GametypeUsesCoopStarposts()) && (netgame || multiplayer) && !G_IsSpecialStage(gamemap))
 	{
@@ -3420,23 +3420,27 @@ const char *Gametype_Names[NUMGAMETYPES] =
 	"Tag", // GT_TAG
 	"Hide & Seek", // GT_HIDEANDSEEK
 
-	"CTF" // GT_CTF
+	"CTF", // GT_CTF
+
+	"Hub" // GT_HUB
 };
 
 // For dehacked
 const char *Gametype_ConstantNames[NUMGAMETYPES] =
 {
-	"GT_COOP", // GT_COOP
-	"GT_COMPETITION", // GT_COMPETITION
-	"GT_RACE", // GT_RACE
+	"GT_COOP",
+	"GT_COMPETITION",
+	"GT_RACE",
+
+	"GT_MATCH",
+	"GT_TEAMMATCH",
 
-	"GT_MATCH", // GT_MATCH
-	"GT_TEAMMATCH", // GT_TEAMMATCH
+	"GT_TAG",
+	"GT_HIDEANDSEEK",
 
-	"GT_TAG", // GT_TAG
-	"GT_HIDEANDSEEK", // GT_HIDEANDSEEK
+	"GT_CTF",
 
-	"GT_CTF" // GT_CTF
+	"GT_HUB"
 };
 
 // Gametype rules
@@ -3461,15 +3465,26 @@ UINT32 gametypedefaultrules[NUMGAMETYPES] =
 
 	// CTF
 	GTR_RINGSLINGER|GTR_FIRSTPERSON|GTR_SPECTATORS|GTR_TEAMS|GTR_TEAMFLAGS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_POWERSTONES|GTR_DEATHMATCHSTARTS|GTR_SPAWNINVUL|GTR_RESPAWNDELAY|GTR_PITYSHIELD,
+
+	// Hub
+	GTR_LIVES|GTR_FRIENDLY|GTR_SPAWNENEMIES|GTR_ALLOWEXIT|GTR_CUTSCENES
 };
 
 //
-// Sets a new gametype.
+// Sets a new gametype, also setting gametype rules accordingly.
 //
 void G_SetGametype(INT16 gtype)
 {
-	gametype = gtype;
-	gametyperules = gametypedefaultrules[gametype];
+	if (gametype != gtype)
+	{
+		if (gametype == -1)
+			lastgametype = gtype;
+		else
+			lastgametype = gametype;
+
+		gametype = gtype;
+		gametyperules = gametypedefaultrules[gametype];
+	}
 }
 
 //
@@ -3627,6 +3642,8 @@ UINT32 gametypetol[NUMGAMETYPES] =
 	TOL_TAG, // Hide and Seek
 
 	TOL_CTF, // CTF
+
+	TOL_HUB // Hub
 };
 
 tolinfo_t TYPEOFLEVEL[NUMTOLNAMES] = {
@@ -3645,6 +3662,8 @@ tolinfo_t TYPEOFLEVEL[NUMTOLNAMES] = {
 	{"TAG",TOL_TAG},
 	{"CTF",TOL_CTF},
 
+	{"HUB",TOL_HUB},
+
 	{"2D",TOL_2D},
 	{"MARIO",TOL_MARIO},
 	{"NIGHTS",TOL_NIGHTS},
@@ -3736,6 +3755,20 @@ boolean G_GametypeUsesLives(void)
 	return false;
 }
 
+//
+// G_CanLoseLivesInGametype
+//
+// Returns true if player can lose lives in
+// the current gametype.  False otherwise.
+//
+boolean G_CanLoseLivesInGametype(void)
+{
+	if (maptol & TOL_HUB)
+		return false;
+
+	return G_GametypeUsesLives();
+}
+
 //
 // G_GametypeUsesCoopLives
 //
@@ -3840,7 +3873,11 @@ boolean G_CompetitionGametype(void)
 UINT32 G_TOLFlag(INT32 pgametype)
 {
 	if (!multiplayer)
-		return TOL_SP;
+	{
+		if (pgametype != GT_HUB)
+			return TOL_SP;
+	}
+
 	return gametypetol[pgametype];
 }
 
@@ -4051,6 +4088,13 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 
 			memset(visitedmap, 0, sizeof (visitedmap));
 
+			// Allow warping to non-hub levels if in a hub level.
+			if (gametype_to_use == GT_HUB && lastgametype != -1)
+				tolflag |= G_TOLFlag(lastgametype);
+			// Allow warping to hub levels if in a non-hub level.
+			else if (gametype_to_use != GT_HUB)
+				tolflag |= G_TOLFlag(GT_HUB);
+
 			while (!mapheaderinfo[cm] || !(mapheaderinfo[cm]->typeoflevel & tolflag))
 			{
 				visitedmap[cm/8] |= (1<<(cm&7));
diff --git a/src/g_game.h b/src/g_game.h
index f72ea6b41b..d9436b69ce 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -206,6 +206,7 @@ void G_SetGametypeDescription(INT16 gtype, char *descriptiontext, UINT8 leftcolo
 INT32 G_GetGametypeByName(const char *gametypestr);
 boolean G_IsSpecialStage(INT32 mapnum);
 boolean G_GametypeUsesLives(void);
+boolean G_CanLoseLivesInGametype(void);
 boolean G_GametypeUsesCoopLives(void);
 boolean G_GametypeUsesCoopStarposts(void);
 boolean G_GametypeHasTeams(void);
diff --git a/src/m_menu.c b/src/m_menu.c
index 37d191a0df..3e597682c5 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -646,6 +646,7 @@ gtdesc_t gametypedesc[NUMGAMETYPES] =
 	{{123, 123}, "Whoever's IT has to hunt down everyone else. If you get caught, you have to turn on your former friends!"},
 	{{150, 150}, "Try and find a good hiding place in these maps - we dare you."},
 	{{ 37, 153}, "Steal the flag from the enemy's base and bring it back to your own, but watch out - they could just as easily steal yours!"},
+	{{182, 182}, "Walk through a selection of levels."}
 };
 
 static menuitem_t MISC_ChangeLevelMenu[] =
@@ -5089,24 +5090,6 @@ static boolean M_CanShowLevelOnPlatter(INT32 mapnum, INT32 gt)
 			if (G_IsSpecialStage(mapnum+1))
 				return false;
 
-			if (gt == GT_COOP && (mapheaderinfo[mapnum]->typeoflevel & TOL_COOP))
-				return true;
-
-			if (gt == GT_COMPETITION && (mapheaderinfo[mapnum]->typeoflevel & TOL_COMPETITION))
-				return true;
-
-			if (gt == GT_CTF && (mapheaderinfo[mapnum]->typeoflevel & TOL_CTF))
-				return true;
-
-			if ((gt == GT_MATCH || gt == GT_TEAMMATCH) && (mapheaderinfo[mapnum]->typeoflevel & TOL_MATCH))
-				return true;
-
-			if ((gt == GT_TAG || gt == GT_HIDEANDSEEK) && (mapheaderinfo[mapnum]->typeoflevel & TOL_TAG))
-				return true;
-
-			if (gt == GT_RACE && (mapheaderinfo[mapnum]->typeoflevel & TOL_RACE))
-				return true;
-
 			if (gt >= 0 && gt < gametypecount && (mapheaderinfo[mapnum]->typeoflevel & gametypetol[gt]))
 				return true;
 
@@ -9567,7 +9550,9 @@ static void M_Statistics(INT32 choice)
 		if (!mapheaderinfo[i] || mapheaderinfo[i]->lvlttl[0] == '\0')
 			continue;
 
-		if (!(mapheaderinfo[i]->typeoflevel & TOL_SP) || (mapheaderinfo[i]->menuflags & LF2_HIDEINSTATS))
+		if (!(mapheaderinfo[i]->typeoflevel & TOL_SP)
+		|| (mapheaderinfo[i]->typeoflevel & TOL_HUB)
+		|| (mapheaderinfo[i]->menuflags & LF2_HIDEINSTATS))
 			continue;
 
 		if (!(clientGamedata->mapvisited[i] & MV_MAX))
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index 94170fa0df..4214425c45 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -406,9 +406,10 @@ boolean timedemo_csv;
 char timedemo_csv_id[256];
 boolean timedemo_quit;
 
-INT16 gametype = GT_COOP;
+INT16 gametype = -1;
+INT16 lastgametype = -1;
 UINT32 gametyperules = 0;
-INT16 gametypecount = (GT_CTF + 1);
+INT16 gametypecount = (GT_HUB + 1);
 
 boolean splitscreen = false;
 boolean circuitmap = false;
@@ -1731,6 +1732,22 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pultmode, boolean rese
 				SendNameAndColor2();
 			}
 		}
+
+		if (gametype == newgametype && mapheaderinfo[mapnum-1])
+		{
+			// Change gametype to Hub if warping into a hub level
+			if ((gametyperules & GTR_CAMPAIGN) && (mapheaderinfo[mapnum-1]->typeoflevel & TOL_HUB))
+			{
+				newgametype = GT_HUB;
+			}
+			// Change gametype back to the previous one if warping out of a hub level
+			else if ((maptol & TOL_HUB)
+				&& lastgametype != -1
+				&& (mapheaderinfo[mapnum-1]->typeoflevel & TOL_HUB) == 0)
+			{
+				newgametype = lastgametype;
+			}
+		}
 	}
 	CONS_Debug(DBG_GAMELOGIC, "Map change: mapnum=%d gametype=%d ultmode=%d resetplayers=%d delay=%d skipprecutscene=%d\n",
 	           mapnum, newgametype, pultmode, resetplayers, delay, skipprecutscene);
@@ -1838,6 +1855,8 @@ static void Command_Map_f(void)
 
 	INT32 newgametype = gametype;
 
+	UINT32 tolflags;
+
 	INT32 d;
 
 	if (client && !IsPlayerAdmin(consoleplayer))
@@ -1965,15 +1984,35 @@ static void Command_Map_f(void)
 
 	// don't use a gametype the map doesn't support
 	// G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer
+	tolflags = G_TOLFlag(newgametype);
+
+	// Allow warping to non-hub levels if in a hub level.
+	if (newgametype == GT_HUB && lastgametype != -1)
+		tolflags |= G_TOLFlag(lastgametype);
+	// Allow warping to hub levels if in a non-hub level.
+	else if (newgametype != GT_HUB)
+		tolflags |= G_TOLFlag(GT_HUB);
+
 	if (!(
 			mapheaderinfo[newmapnum-1] &&
-			mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)
+			mapheaderinfo[newmapnum-1]->typeoflevel & tolflags
 	))
 	{
 		if (prevent_cheat && !cv_skipmapcheck.value)
 		{
-			CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum),
-				(multiplayer ? gametype_cons_t[newgametype].strvalue : "Single Player"));
+			const char *gametype_name;
+
+			if (!multiplayer && newgametype == GT_COOP)
+			{
+				gametype_name = M_GetText("Single Player");
+			}
+			else
+			{
+				gametype_name = Gametype_Names[newgametype];
+			}
+
+			CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"),
+				realmapname, G_BuildMapName(newmapnum), gametype_name);
 			Z_Free(realmapname);
 			Z_Free(mapname);
 			return;
@@ -2044,7 +2083,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 {
 	char mapname[MAX_WADPATH+1];
 	UINT8 flags;
-	INT32 resetplayer = 1, lastgametype;
+	INT32 resetplayer = 1;
 	UINT8 skipprecutscene, FLS;
 	INT16 mapnumber;
 
@@ -2076,7 +2115,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 		G_SetGametype(gametype);
 
 	if (gametype != lastgametype)
-		D_GameTypeChanged(lastgametype); // emulate consvar_t behavior for gametype
+		D_GameTypeChanged();
 
 	skipprecutscene = ((flags & (1<<2)) != 0);
 
@@ -3955,22 +3994,13 @@ static void Command_ModDetails_f(void)
 //
 static void Command_ShowGametype_f(void)
 {
-	const char *gametypestr = NULL;
-
-	if (!(netgame || multiplayer)) // print "Single player" instead of "Co-op"
+	if (!(netgame || multiplayer) && gametype == GT_COOP) // print "Single player" instead of "Co-op"
 	{
-		CONS_Printf(M_GetText("Current gametype is %s\n"), M_GetText("Single player"));
+		CONS_Printf(M_GetText("Current gametype is %s\n"), M_GetText("Single Player"));
 		return;
 	}
 
-	// get name string for current gametype
-	if (gametype >= 0 && gametype < gametypecount)
-		gametypestr = Gametype_Names[gametype];
-
-	if (gametypestr)
-		CONS_Printf(M_GetText("Current gametype is %s\n"), gametypestr);
-	else // string for current gametype was not found above (should never happen)
-		CONS_Printf(M_GetText("Unknown gametype set (%d)\n"), gametype);
+	CONS_Printf(M_GetText("Current gametype is %s\n"), Gametype_Names[gametype]);
 }
 
 /** Plays the intro.
@@ -4210,7 +4240,14 @@ static void TimeLimit_OnChange(void)
 		// Some people might like to use them together. It works.
 	}
 	else if (netgame || multiplayer)
-		CONS_Printf(M_GetText("Time limit disabled\n"));
+	{
+		if (timelimitintics != 0)
+		{
+			CONS_Printf(M_GetText("Time limit disabled\n"));
+		}
+
+		timelimitintics = 0;
+	}
 }
 
 /** Adjusts certain settings to match a changed gametype.
@@ -4220,20 +4257,13 @@ static void TimeLimit_OnChange(void)
   * \author Graue <graue@oceanbase.org>
   * \todo Get rid of the hardcoded stuff, ugly stuff, etc.
   */
-void D_GameTypeChanged(INT32 lastgametype)
+void D_GameTypeChanged(void)
 {
-	if (netgame)
+	if (netgame && lastgametype != -1)
 	{
-		const char *oldgt = NULL, *newgt = NULL;
-
-		if (lastgametype >= 0 && lastgametype < gametypecount)
-			oldgt = Gametype_Names[lastgametype];
-		if (gametype >= 0 && lastgametype < gametypecount)
-			newgt = Gametype_Names[gametype];
-
-		if (oldgt && newgt)
-			CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), oldgt, newgt);
+		CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), Gametype_Names[lastgametype], Gametype_Names[gametype]);
 	}
+
 	// Only do the following as the server, not as remote admin.
 	// There will always be a server, and this only needs to be done once.
 	if (server && (multiplayer || netgame))
@@ -4293,10 +4323,6 @@ void D_GameTypeChanged(INT32 lastgametype)
 				break;
 		}
 	}
-	else if (!multiplayer && !netgame)
-	{
-		G_SetGametype(GT_COOP);
-	}
 
 	// reset timelimit and pointlimit in race/coop, prevent stupid cheats
 	if (server)
diff --git a/src/netcode/d_netcmd.h b/src/netcode/d_netcmd.h
index c11575575f..a9a91d8b22 100644
--- a/src/netcode/d_netcmd.h
+++ b/src/netcode/d_netcmd.h
@@ -203,7 +203,7 @@ boolean EnsurePlayerNameIsGood(char *name, INT32 playernum);
 void D_SendPlayerConfig(void);
 void Command_ExitGame_f(void);
 void Command_Retry_f(void);
-void D_GameTypeChanged(INT32 lastgametype); // not a real _OnChange function anymore
+void D_GameTypeChanged(void);
 void D_MapChange(INT32 pmapnum, INT32 pgametype, boolean pultmode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pfromlevelselect);
 boolean IsPlayerAdmin(INT32 playernum);
 void SetAdminPlayer(INT32 playernum);
diff --git a/src/p_inter.c b/src/p_inter.c
index 27e612154d..06884beef6 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2691,7 +2691,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 		if ((target->player->lives <= 1) && (netgame || multiplayer) && G_GametypeUsesCoopLives() && (cv_cooplives.value == 0))
 			;
 		else if ((!target->player->bot || target->player->bot == BOT_MPAI) && !target->player->spectator && (target->player->lives != INFLIVES)
-		 && G_GametypeUsesLives())
+		 && G_CanLoseLivesInGametype())
 		{
 			if (!(target->player->pflags & PF_FINISHED))
 				target->player->lives -= 1; // Lose a life Tails 03-11-2000
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 4fdacd51ad..4541b0adb3 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -968,7 +968,7 @@ static void ST_drawLivesArea(void)
 			}
 			else
 			{
-				if (stplyr->playerstate == PST_DEAD && !(stplyr->spectator) && (livescount || stplyr->deadtimer < (TICRATE<<1)) && !(stplyr->pflags & PF_FINISHED))
+				if (G_CanLoseLivesInGametype() && stplyr->playerstate == PST_DEAD && !(stplyr->spectator) && (livescount || stplyr->deadtimer < (TICRATE<<1)) && !(stplyr->pflags & PF_FINISHED))
 					livescount++;
 				if (livescount > 99)
 					livescount = 99;
diff --git a/src/y_inter.c b/src/y_inter.c
index d7e644567e..e2343b530d 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -165,7 +165,22 @@ static INT32 tallydonetic = -1;
 static INT32 endtic = -1;
 
 intertype_t intertype = int_none;
-intertype_t intermissiontypes[NUMGAMETYPES];
+intertype_t intermissiontypes[NUMGAMETYPES] =
+{
+	int_coop, // GT_COOP
+	int_comp, // GT_COMPETITION
+	int_race, // GT_RACE
+
+	int_match, // GT_MATCH
+	int_teammatch, // GT_TEAMMATCH
+
+	int_match, // GT_TAG
+	int_match, // GT_HIDEANDSEEK
+
+	int_ctf, // GT_CTF
+
+	int_none // GT_HUB
+};
 
 static huddrawlist_h luahuddrawlist_intermission;
 
@@ -1275,25 +1290,11 @@ void Y_Ticker(void)
 //
 void Y_DetermineIntermissionType(void)
 {
-	// set to int_none initially
-	intertype = int_none;
+	intertype = intermissiontypes[gametype];
 
-	if (intermissiontypes[gametype] != int_none)
-		intertype = intermissiontypes[gametype];
-	else if (gametype == GT_COOP)
-		intertype = (G_IsSpecialStage(gamemap)) ? int_spec : int_coop;
-	else if (gametype == GT_TEAMMATCH)
-		intertype = int_teammatch;
-	else if (gametype == GT_MATCH
-	 || gametype == GT_TAG
-	 || gametype == GT_HIDEANDSEEK)
-		intertype = int_match;
-	else if (gametype == GT_RACE)
-		intertype = int_race;
-	else if (gametype == GT_COMPETITION)
-		intertype = int_comp;
-	else if (gametype == GT_CTF)
-		intertype = int_ctf;
+	// Special case for special stages
+	if (intertype == int_coop && G_IsSpecialStage(gamemap))
+		intertype = int_spec;
 }
 
 //
-- 
GitLab


From c3cb628b2db8e2e190b4cb89c6ca91bb3b4a380a Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Fri, 14 Feb 2025 15:07:02 -0300
Subject: [PATCH 02/17] Add "Return to Hub" menu selection

---
 src/m_menu.c | 89 ++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 83 insertions(+), 6 deletions(-)

diff --git a/src/m_menu.c b/src/m_menu.c
index 3e597682c5..60477bc15f 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -254,6 +254,7 @@ static void M_SinglePlayerMenu(INT32 choice);
 static void M_Options(INT32 choice);
 static void M_SelectableClearMenus(INT32 choice);
 static void M_Retry(INT32 choice);
+static void M_ReturnToHub(INT32 choice);
 static void M_EndGame(INT32 choice);
 static void M_MapChange(INT32 choice);
 static void M_ChangeLevel(INT32 choice);
@@ -545,6 +546,7 @@ typedef enum
 // ---------------------
 // Pause Menu MP Edition
 // ---------------------
+// Caution: M_StartControlPanel recalculates the positions of all items after mpause_returntohub.
 static menuitem_t MPauseMenu[] =
 {
 	{IT_STRING | IT_CALL,    NULL, "Add-ons...",                M_Addons,               8},
@@ -552,6 +554,7 @@ static menuitem_t MPauseMenu[] =
 	{IT_STRING | IT_CALL,    NULL, "Emblem Hints...",           M_EmblemHints,         24},
 	{IT_STRING | IT_CALL,    NULL, "Switch Gametype/Level...",  M_MapChange,           32},
 
+	{IT_STRING | IT_CALL,    NULL, "Return to Hub",             M_ReturnToHub,         48},
 	{IT_STRING | IT_CALL,    NULL, "Continue",                  M_SelectableClearMenus,48},
 
 	{IT_STRING | IT_CALL,    NULL, "Player 1 Setup",            M_SetupMultiPlayer,    56}, // splitscreen
@@ -575,6 +578,7 @@ typedef enum
 	mpause_hints,
 	mpause_switchmap,
 
+	mpause_returntohub,
 	mpause_continue,
 	mpause_psetupsplit,
 	mpause_psetupsplit2,
@@ -591,13 +595,14 @@ typedef enum
 // ---------------------
 // Pause Menu SP Edition
 // ---------------------
+// Caution: M_StartControlPanel recalculates the positions of all items after spause_returntohub.
 static menuitem_t SPauseMenu[] =
 {
-	// Pandora's Box will be shifted up if both options are available
 	{IT_CALL | IT_STRING,    NULL, "Pandora's Box...",     M_PandorasBox,         16},
 	{IT_CALL | IT_STRING,    NULL, "Emblem Hints...",      M_EmblemHints,         24},
 	{IT_CALL | IT_STRING,    NULL, "Level Select...",      M_PauseLevelSelect,    32},
 
+	{IT_CALL | IT_STRING,    NULL, "Return to Hub",        M_ReturnToHub,         48},
 	{IT_CALL | IT_STRING,    NULL, "Continue",             M_SelectableClearMenus,48},
 	{IT_CALL | IT_STRING,    NULL, "Retry",                M_Retry,               56},
 	{IT_CALL | IT_STRING,    NULL, "Options",              M_Options,             64},
@@ -611,6 +616,7 @@ typedef enum
 	spause_pandora = 0,
 	spause_hints,
 	spause_levelselect,
+	spause_returntohub,
 
 	spause_continue,
 	spause_retry,
@@ -3639,6 +3645,8 @@ void M_Drawer(void)
 //
 void M_StartControlPanel(void)
 {
+	INT32 offset;
+
 	// time attack HACK
 	if (modeattacking && demoplayback)
 	{
@@ -3711,12 +3719,26 @@ void M_StartControlPanel(void)
 		// And emblem hints.
 		SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
-		// Shift up Pandora's Box if both pandora and levelselect are active
-		/*if (SPauseMenu[spause_pandora].status != (IT_DISABLED)
-		 && SPauseMenu[spause_levelselect].status != (IT_DISABLED))
-			SPauseMenu[spause_pandora].alphaKey = 24;
+		// Shift down some selections if "Return to Hub" is available.
+		if (true) // TODO: Implement.
+		{
+			SPauseMenu[spause_returntohub].status = IT_STRING | IT_CALL;
+
+			offset = 8;
+		}
 		else
-			SPauseMenu[spause_pandora].alphaKey = 32;*/
+		{
+			SPauseMenu[spause_returntohub].status = IT_DISABLED;
+
+			offset = 0;
+		}
+
+		SPauseMenu[spause_continue].alphaKey = 48 + offset;
+		SPauseMenu[spause_retry].alphaKey = 56 + offset;
+		SPauseMenu[spause_options].alphaKey  = 64 + offset;
+
+		SPauseMenu[spause_title].alphaKey = 80 + offset;
+		SPauseMenu[spause_quit].alphaKey = 88 + offset;
 
 		currentMenu = &SPauseDef;
 		itemOn = spause_continue;
@@ -3758,6 +3780,38 @@ void M_StartControlPanel(void)
 				MPauseMenu[mpause_spectate].status = IT_GRAYEDOUT;
 		}
 
+		// Shift down some selections if "Return to Hub" is available.
+		if (server) // TODO: Implement.
+		{
+			MPauseMenu[mpause_returntohub].status = IT_STRING | IT_CALL;
+
+			offset = 8;
+		}
+		else
+		{
+			MPauseMenu[mpause_returntohub].status = IT_DISABLED;
+
+			offset = 0;
+		}
+
+		if (splitscreen)
+		{
+			MPauseMenu[mpause_psetupsplit].alphaKey = 56 + offset;
+			MPauseMenu[mpause_psetupsplit2].alphaKey = 64 + offset;
+		}
+		else
+		{
+			MPauseMenu[mpause_switchteam].alphaKey =
+			MPauseMenu[mpause_entergame].alphaKey =
+			MPauseMenu[mpause_spectate].alphaKey = 56 + offset;
+			MPauseMenu[mpause_psetup].alphaKey = 64 + offset;
+		}
+
+		MPauseMenu[mpause_continue].alphaKey = 48 + offset;
+		MPauseMenu[mpause_options].alphaKey = 72 + offset;
+		MPauseMenu[mpause_title].alphaKey = 88 + offset;
+		MPauseMenu[mpause_quit].alphaKey = 96 + offset;
+
 		MPauseMenu[mpause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata) && G_CoopGametype()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		currentMenu = &MPauseDef;
@@ -6914,6 +6968,29 @@ static void M_Retry(INT32 choice)
 	M_StartMessage(M_GetText("Retry this act from the last starpost?\n\n(Press 'Y' to confirm)\n"),M_RetryResponse,MM_YESNO);
 }
 
+static void M_ReturnToHubResponse(INT32 ch)
+{
+	if (ch != 'y' && ch != KEY_ENTER)
+		return;
+
+	// TODO: Implement.
+	INT32 currenthubmap = 1;
+
+#if 0
+	if (currenthubmap == 0)
+		return;
+#endif
+
+	M_ClearMenus(true);
+	D_MapChange(currenthubmap, gametype, false, false, 0, false, false);
+}
+
+static void M_ReturnToHub(INT32 choice)
+{
+	(void)choice;
+	M_StartMessage(M_GetText("Return to the hub?\nAny progress in the\ncurrent act will be lost.\n\n(Press 'Y' to confirm)\n"),M_ReturnToHubResponse,MM_YESNO);
+}
+
 static void M_SelectableClearMenus(INT32 choice)
 {
 	(void)choice;
-- 
GitLab


From 906e0411583a7f3f7eafa258b354939782fd1cfa Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 00:42:35 -0300
Subject: [PATCH 03/17] Implement DEFAULTHUBMAP

---
 src/d_main.c   |  4 ++++
 src/deh_soc.c  | 13 +++++++++++++
 src/doomstat.h |  4 ++++
 src/g_game.c   | 16 ++++++++++++++++
 src/g_game.h   |  3 +++
 src/m_menu.c   | 35 +++++++++++++++++++++++++----------
 6 files changed, 65 insertions(+), 10 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index 40d9319fd8..0b6e215d48 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -984,6 +984,8 @@ void D_StartTitle(void)
 	memset(&luabanks, 0, sizeof(luabanks));
 	lastmaploaded = 0;
 	pickedchar = R_SkinAvailable(cv_skin.string);
+	currenthubmap = defaulthubmap;
+	hubmapdisabled = false;
 
 	// In case someone exits out at the same time they start a time attack run,
 	// reset modeattacking
@@ -1524,6 +1526,8 @@ void D_SRB2Main(void)
 	wipegamestate = gamestate;
 
 	savedata.lives = 0; // flag this as not-used
+	currenthubmap = defaulthubmap;
+	hubmapdisabled = false;
 
 	//------------------------------------------------ COMMAND LINE PARAMS
 
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 343beb3012..b94b79932d 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -3981,6 +3981,19 @@ void readmaincfg(MYFILE *f)
 
 				tutorialmap = (INT16)value;
 			}
+			else if (fastcmp(word, "DEFAULTHUBMAP"))
+			{
+				// Support using the actual map name,
+				// i.e., Level AB, Level FZ, etc.
+
+				// Convert to map number
+				if (word2[0] >= 'A' && word2[0] <= 'Z')
+					value = M_MapNumber(word2[0], word2[1]);
+				else
+					value = get_number(word2);
+
+				defaulthubmap = (INT16)value;
+			}
 			else
 				deh_warning("Maincfg: unknown word '%s'", word);
 		}
diff --git a/src/doomstat.h b/src/doomstat.h
index d05620582a..66bae1b67e 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -145,6 +145,10 @@ extern INT16 titlemap;
 extern boolean hidetitlepics;
 extern INT16 bootmap; //bootmap for loading a map on startup
 
+extern INT16 defaulthubmap; // default map number of hub
+extern INT16 currenthubmap; // current map number of hub
+extern boolean hubmapdisabled; // hub map is disabled
+
 extern INT16 tutorialmap; // map to load for tutorial
 extern boolean tutorialmode; // are we in a tutorial right now?
 extern INT32 tutorialgcs; // which control scheme is loaded?
diff --git a/src/g_game.c b/src/g_game.c
index ea29d30916..88bc86d017 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -136,6 +136,10 @@ INT16 titlemap = 0;
 boolean hidetitlepics = false;
 INT16 bootmap; //bootmap for loading a map on startup
 
+INT16 defaulthubmap = 0; // default map number of hub
+INT16 currenthubmap = 0; // current map number of hub
+boolean hubmapdisabled = false; // hub map is disabled
+
 INT16 tutorialmap = 0; // map to load for tutorial
 boolean tutorialmode = false; // are we in a tutorial right now?
 INT32 tutorialgcs = gcs_custom; // which control scheme is loaded?
@@ -3864,6 +3868,15 @@ boolean G_CompetitionGametype(void)
 	return ((gametyperules & GTR_RACE) && (gametyperules & GTR_LIVES));
 }
 
+// Returns true if the current hub level can be warped to.
+boolean G_IsHubAvailable(void)
+{
+	if (currenthubmap == 0 || hubmapdisabled || M_MapLocked(currenthubmap, serverGamedata))
+		return false;
+
+	return mapheaderinfo[currenthubmap-1] && mapheaderinfo[currenthubmap-1]->typeoflevel & TOL_HUB;
+}
+
 /** Get the typeoflevel flag needed to indicate support of a gametype.
   * In single-player, this always returns TOL_SP.
   * \param gametype The gametype for which support is desired.
@@ -5062,6 +5075,9 @@ void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 character, bo
 		SplitScreen_OnChange();
 	}
 
+	currenthubmap = defaulthubmap;
+	hubmapdisabled = false;
+
 	SetPlayerSkinByNum(consoleplayer, character);
 
 	if (mapname)
diff --git a/src/g_game.h b/src/g_game.h
index d9436b69ce..890fcd71e1 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -178,10 +178,13 @@ void G_SpawnPlayer(INT32 playernum);
 // A normal game starts at map 1, but a warp test can start elsewhere
 void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 character, boolean SSSG, boolean FLS);
 void G_DoLoadLevel(boolean resetplayer);
+
 void G_StartTitleCard(void);
 void G_PreLevelTitleCard(void);
 boolean G_IsTitleCardAvailable(void);
 
+boolean G_IsHubAvailable(void);
+
 // Can be called by the startup code or M_Responder, calls P_SetupLevel.
 void G_LoadGame(UINT32 slot, INT16 mapoverride);
 
diff --git a/src/m_menu.c b/src/m_menu.c
index 60477bc15f..15a0102e2e 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -408,6 +408,9 @@ static void M_HandleVideoMode(INT32 choice);
 
 static void M_ResetCvars(void);
 
+static boolean M_CanShowReturnToHub(void);
+static boolean M_CanSelectReturnToHub(void);
+
 // Consvar onchange functions
 static void Newgametype_OnChange(void);
 static void Dummymares_OnChange(void);
@@ -3720,9 +3723,9 @@ void M_StartControlPanel(void)
 		SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		// Shift down some selections if "Return to Hub" is available.
-		if (true) // TODO: Implement.
+		if (M_CanShowReturnToHub())
 		{
-			SPauseMenu[spause_returntohub].status = IT_STRING | IT_CALL;
+			SPauseMenu[spause_returntohub].status = M_CanSelectReturnToHub() ? (IT_STRING | IT_CALL) : IT_GRAYEDOUT;
 
 			offset = 8;
 		}
@@ -3781,9 +3784,9 @@ void M_StartControlPanel(void)
 		}
 
 		// Shift down some selections if "Return to Hub" is available.
-		if (server) // TODO: Implement.
+		if (M_CanShowReturnToHub())
 		{
-			MPauseMenu[mpause_returntohub].status = IT_STRING | IT_CALL;
+			MPauseMenu[mpause_returntohub].status = M_CanSelectReturnToHub() ? (IT_STRING | IT_CALL) : IT_GRAYEDOUT;
 
 			offset = 8;
 		}
@@ -3828,6 +3831,23 @@ void M_EndModeAttackRun(void)
 	M_ModeAttackEndGame(0);
 }
 
+// Hides "Return to Hub" if there is no current hub map, or it's locked.
+// Also, hides it if you aren't the server.
+static boolean M_CanShowReturnToHub(void)
+{
+	if (netgame && !server)
+		return false;
+
+	// M_MapLocked doesn't do bounds checking (oops!)
+	return currenthubmap != 0 && !M_MapLocked(currenthubmap, serverGamedata);
+}
+
+// Returns true if you are able to select "Return to Hub".
+static boolean M_CanSelectReturnToHub(void)
+{
+	return G_IsHubAvailable() && gamemap != currenthubmap;
+}
+
 //
 // M_ClearMenus
 //
@@ -6973,13 +6993,8 @@ static void M_ReturnToHubResponse(INT32 ch)
 	if (ch != 'y' && ch != KEY_ENTER)
 		return;
 
-	// TODO: Implement.
-	INT32 currenthubmap = 1;
-
-#if 0
-	if (currenthubmap == 0)
+	if (!M_CanSelectReturnToHub())
 		return;
-#endif
 
 	M_ClearMenus(true);
 	D_MapChange(currenthubmap, gametype, false, false, 0, false, false);
-- 
GitLab


From de1e84513459b94b1333be5d24604e810970f746 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 00:47:28 -0300
Subject: [PATCH 04/17] Set currenthubmap if loading a map with TOL_HUB

---
 src/p_setup.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/p_setup.c b/src/p_setup.c
index c2b8f2db2e..31c621acb4 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -7853,6 +7853,10 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	maptol = mapheaderinfo[gamemap-1]->typeoflevel;
 	gametyperules = gametypedefaultrules[gametype];
 
+	// Set the current hub map
+	if (maptol & TOL_HUB)
+		currenthubmap = gamemap;
+
 	// clear the target on map change, since the object will be invalidated
 	P_SetTarget(&ticcmd_ztargetfocus[0], NULL);
 	P_SetTarget(&ticcmd_ztargetfocus[1], NULL);
-- 
GitLab


From 6dc451c4b35f81f90ec225d7235b5881a1751117 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 00:48:10 -0300
Subject: [PATCH 05/17] Serialize currenthubmap and hubmapdisabled

---
 src/p_saveg.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/p_saveg.c b/src/p_saveg.c
index 650622f59f..ccc5c7011c 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -4788,6 +4788,8 @@ static void P_NetArchiveMisc(save_t *save_p, boolean resending)
 	else
 		P_WriteINT16(save_p, gamestate);
 	P_WriteINT16(save_p, gametype);
+	P_WriteINT16(save_p, currenthubmap);
+	P_WriteUINT8(save_p, hubmapdisabled);
 
 	{
 		UINT32 pig = 0;
@@ -4886,6 +4888,8 @@ static inline boolean P_NetUnArchiveMisc(save_t *save_p, boolean reloading)
 	G_SetGamestate(P_ReadINT16(save_p));
 
 	gametype = P_ReadINT16(save_p);
+	currenthubmap = P_ReadINT16(save_p);
+	hubmapdisabled = P_ReadUINT8(save_p);
 
 	{
 		UINT32 pig = P_ReadUINT32(save_p);
-- 
GitLab


From dd50bf6f258bfc1cf75d4b8803c5f3a25e2b8ccc Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 15:54:00 -0300
Subject: [PATCH 06/17] Add defaulthubmap, currenthubmap, hubmapenabled to Lua

---
 src/lua_script.c | 36 +++++++++++++++++++++++++++++++++++-
 1 file changed, 35 insertions(+), 1 deletion(-)

diff --git a/src/lua_script.c b/src/lua_script.c
index 686555a16d..15db1b0a67 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -180,6 +180,25 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"stoppedclock")) {
 		lua_pushboolean(L, stoppedclock);
 		return 1;
+	} else if (fastcmp(word,"defaulthubmap")) {
+		if (defaulthubmap == 0) {
+			lua_pushnil(L);
+		}
+		else {
+			lua_pushinteger(L, defaulthubmap);
+		}
+		return 1;
+	} else if (fastcmp(word, "currenthubmap")) {
+		if (currenthubmap == 0) {
+			lua_pushnil(L);
+		}
+		else {
+			lua_pushinteger(L, currenthubmap);
+		}
+		return 1;
+	} else if (fastcmp(word, "hubmapenabled")) {
+		lua_pushboolean(L, !hubmapdisabled);
+		return 1;
 	} else if (fastcmp(word,"netgame")) {
 		lua_pushboolean(L, netgame);
 		return 1;
@@ -444,7 +463,22 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 // See the above.
 int LUA_CheckGlobals(lua_State *L, const char *word)
 {
-	if (fastcmp(word, "redscore"))
+	if (fastcmp(word, "currenthubmap")) {
+		if (lua_isnoneornil(L, 2)) {
+			currenthubmap = 0;
+		}
+		else {
+			int mapnum = luaL_checkinteger(L, 2);
+			if (mapnum < 1 || mapnum > NUMMAPS) {
+				luaL_error(L, "map number %d out of range (1 - %d)", mapnum, NUMMAPS);
+				return 1;
+			}
+			currenthubmap = (INT16)mapnum;
+		}
+	}
+	else if (fastcmp(word, "hubmapenabled"))
+		hubmapdisabled = !luaL_checkboolean(L, 2);
+	else if (fastcmp(word, "redscore"))
 		redscore = (UINT32)luaL_checkinteger(L, 2);
 	else if (fastcmp(word, "bluescore"))
 		bluescore = (UINT32)luaL_checkinteger(L, 2);
-- 
GitLab


From 510ccb4c178649c524870a4464390305d06c17d8 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 15:57:45 -0300
Subject: [PATCH 07/17] Invert behavior of hubmapdisabled

---
 src/d_main.c     | 4 ++--
 src/deh_soc.c    | 1 +
 src/doomstat.h   | 2 +-
 src/g_game.c     | 6 +++---
 src/lua_script.c | 4 ++--
 src/p_saveg.c    | 4 ++--
 6 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index 0b6e215d48..1507b36680 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -985,7 +985,7 @@ void D_StartTitle(void)
 	lastmaploaded = 0;
 	pickedchar = R_SkinAvailable(cv_skin.string);
 	currenthubmap = defaulthubmap;
-	hubmapdisabled = false;
+	hubmapenabled = currenthubmap != 0;
 
 	// In case someone exits out at the same time they start a time attack run,
 	// reset modeattacking
@@ -1527,7 +1527,7 @@ void D_SRB2Main(void)
 
 	savedata.lives = 0; // flag this as not-used
 	currenthubmap = defaulthubmap;
-	hubmapdisabled = false;
+	hubmapenabled = currenthubmap != 0;
 
 	//------------------------------------------------ COMMAND LINE PARAMS
 
diff --git a/src/deh_soc.c b/src/deh_soc.c
index b94b79932d..e0b71fea95 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -3993,6 +3993,7 @@ void readmaincfg(MYFILE *f)
 					value = get_number(word2);
 
 				defaulthubmap = (INT16)value;
+				hubmapenabled = defaulthubmap != 0;
 			}
 			else
 				deh_warning("Maincfg: unknown word '%s'", word);
diff --git a/src/doomstat.h b/src/doomstat.h
index 66bae1b67e..4b1ab80d71 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -147,7 +147,7 @@ extern INT16 bootmap; //bootmap for loading a map on startup
 
 extern INT16 defaulthubmap; // default map number of hub
 extern INT16 currenthubmap; // current map number of hub
-extern boolean hubmapdisabled; // hub map is disabled
+extern boolean hubmapenabled; // hub map is enabled
 
 extern INT16 tutorialmap; // map to load for tutorial
 extern boolean tutorialmode; // are we in a tutorial right now?
diff --git a/src/g_game.c b/src/g_game.c
index 88bc86d017..2a256d045e 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -138,7 +138,7 @@ INT16 bootmap; //bootmap for loading a map on startup
 
 INT16 defaulthubmap = 0; // default map number of hub
 INT16 currenthubmap = 0; // current map number of hub
-boolean hubmapdisabled = false; // hub map is disabled
+boolean hubmapenabled = false; // hub map is enabled
 
 INT16 tutorialmap = 0; // map to load for tutorial
 boolean tutorialmode = false; // are we in a tutorial right now?
@@ -3871,7 +3871,7 @@ boolean G_CompetitionGametype(void)
 // Returns true if the current hub level can be warped to.
 boolean G_IsHubAvailable(void)
 {
-	if (currenthubmap == 0 || hubmapdisabled || M_MapLocked(currenthubmap, serverGamedata))
+	if (!hubmapenabled || currenthubmap == 0 || M_MapLocked(currenthubmap, serverGamedata))
 		return false;
 
 	return mapheaderinfo[currenthubmap-1] && mapheaderinfo[currenthubmap-1]->typeoflevel & TOL_HUB;
@@ -5076,7 +5076,7 @@ void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 character, bo
 	}
 
 	currenthubmap = defaulthubmap;
-	hubmapdisabled = false;
+	hubmapenabled = currenthubmap != 0;
 
 	SetPlayerSkinByNum(consoleplayer, character);
 
diff --git a/src/lua_script.c b/src/lua_script.c
index 15db1b0a67..94578fb990 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -197,7 +197,7 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		}
 		return 1;
 	} else if (fastcmp(word, "hubmapenabled")) {
-		lua_pushboolean(L, !hubmapdisabled);
+		lua_pushboolean(L, hubmapenabled);
 		return 1;
 	} else if (fastcmp(word,"netgame")) {
 		lua_pushboolean(L, netgame);
@@ -477,7 +477,7 @@ int LUA_CheckGlobals(lua_State *L, const char *word)
 		}
 	}
 	else if (fastcmp(word, "hubmapenabled"))
-		hubmapdisabled = !luaL_checkboolean(L, 2);
+		hubmapenabled = luaL_checkboolean(L, 2);
 	else if (fastcmp(word, "redscore"))
 		redscore = (UINT32)luaL_checkinteger(L, 2);
 	else if (fastcmp(word, "bluescore"))
diff --git a/src/p_saveg.c b/src/p_saveg.c
index ccc5c7011c..e37c2713ae 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -4789,7 +4789,7 @@ static void P_NetArchiveMisc(save_t *save_p, boolean resending)
 		P_WriteINT16(save_p, gamestate);
 	P_WriteINT16(save_p, gametype);
 	P_WriteINT16(save_p, currenthubmap);
-	P_WriteUINT8(save_p, hubmapdisabled);
+	P_WriteUINT8(save_p, hubmapenabled);
 
 	{
 		UINT32 pig = 0;
@@ -4889,7 +4889,7 @@ static inline boolean P_NetUnArchiveMisc(save_t *save_p, boolean reloading)
 
 	gametype = P_ReadINT16(save_p);
 	currenthubmap = P_ReadINT16(save_p);
-	hubmapdisabled = P_ReadUINT8(save_p);
+	hubmapenabled = P_ReadUINT8(save_p);
 
 	{
 		UINT32 pig = P_ReadUINT32(save_p);
-- 
GitLab


From eb71c1eb61cebd7d944f6eac7ba33566106f6226 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 16:01:05 -0300
Subject: [PATCH 08/17] Add G_IsHubAvailable to Lua

---
 src/lua_baselib.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index ecd1ee55e6..af0c3c5759 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -4298,6 +4298,14 @@ static int lib_gCompetitionGametype(lua_State *L)
 	return 1;
 }
 
+static int lib_gIsHubAvailable(lua_State *L)
+{
+	//HUDSAFE
+	INLEVEL
+	lua_pushboolean(L, G_IsHubAvailable());
+	return 1;
+}
+
 static int lib_gTicsToHours(lua_State *L)
 {
 	tic_t rtic = luaL_checkinteger(L, 1);
@@ -4645,6 +4653,7 @@ static luaL_Reg lib[] = {
 	{"G_CoopGametype",lib_gCoopGametype},
 	{"G_TagGametype",lib_gTagGametype},
 	{"G_CompetitionGametype",lib_gCompetitionGametype},
+	{"G_IsHubAvailable",lib_gIsHubAvailable},
 	{"G_TicsToHours",lib_gTicsToHours},
 	{"G_TicsToMinutes",lib_gTicsToMinutes},
 	{"G_TicsToSeconds",lib_gTicsToSeconds},
-- 
GitLab


From 74d1871079e8955fc434235d6d3853eecc30d343 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 18:57:12 -0300
Subject: [PATCH 09/17] Support HUB as a valid NEXTLEVEL

---
 src/deh_soc.c |  1 +
 src/g_game.c  | 21 ++++++++++++++++++++-
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/deh_soc.c b/src/deh_soc.c
index e0b71fea95..30b2b42b84 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1584,6 +1584,7 @@ void readlevelheader(MYFILE *f, INT32 num)
 				else if (fastcmp(word2, "EVALUATION")) i = 1101;
 				else if (fastcmp(word2, "CREDITS"))    i = 1102;
 				else if (fastcmp(word2, "ENDING"))     i = 1103;
+				else if (fastcmp(word2, "HUB"))        i = 1104;
 				else
 				// Support using the actual map name,
 				// i.e., Nextlevel = AB, Nextlevel = FZ, etc.
diff --git a/src/g_game.c b/src/g_game.c
index 2a256d045e..b831c5dcd2 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3871,6 +3871,9 @@ boolean G_CompetitionGametype(void)
 // Returns true if the current hub level can be warped to.
 boolean G_IsHubAvailable(void)
 {
+	if (marathonmode || modeattacking)
+		return false;
+
 	if (!hubmapenabled || currenthubmap == 0 || M_MapLocked(currenthubmap, serverGamedata))
 		return false;
 
@@ -4076,6 +4079,22 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 	else
 	{
 		newmapnum = (INT16)(mapheaderinfo[gamemap-1]->nextlevel-1);
+
+		if (newmapnum == 1104-1) // Going back to the hub
+		{
+			if (G_IsHubAvailable()) // Should handle Marathon Mode.
+				newmapnum = currenthubmap-1;
+			else
+			{
+				// The hub map isn't valid... Naughty!
+				// Just use the starting map instead.
+				if (marathonmode)
+					newmapnum = spmarathon_start-1; // Handled below
+				else
+					newmapnum = spstage_start-1;
+			}
+		}
+
 		if (marathonmode && newmapnum == spmarathon_start-1)
 			newmapnum = 1100-1; // No infinite loop for you
 	}
@@ -4149,7 +4168,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 		if (newmapnum >= 1100-1 && newmapnum <= 1102-1 && !(gametyperules & GTR_CAMPAIGN))
 			newmapnum = (INT16)(spstage_start-1);
 
-		if (newmapnum < 0 || (newmapnum >= NUMMAPS && newmapnum < 1100-1) || newmapnum > 1103-1)
+		if (newmapnum < 0 || (newmapnum >= NUMMAPS && newmapnum < 1100-1) || newmapnum > 1104-1)
 			I_Error("Followed map %d to invalid map %d\n", prevmap + 1, newmapnum + 1);
 
 		if (!spec)
-- 
GitLab


From e5753a13a221dd229a82ee5b3451fb3e14bd7b38 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 18:57:32 -0300
Subject: [PATCH 10/17] Allow use of the Custom Exit special while in a hub

---
 src/p_spec.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/p_spec.c b/src/p_spec.c
index 93809cbb4b..7d0a51b2fa 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -4550,7 +4550,7 @@ static void P_ProcessExitSector(player_t *player, mtag_t sectag)
 
 	P_SetupSignExit(player);
 
-	if (!G_CoopGametype())
+	if (!(G_CoopGametype() || (maptol & TOL_HUB)))
 		return;
 
 	// Custom exit!
-- 
GitLab


From c5a46a83330ff5f675f2cf577f79559df3ca63f3 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 20:26:46 -0300
Subject: [PATCH 11/17] Allow using the hub as the next map even if not
 unlocked

---
 src/g_game.c | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/src/g_game.c b/src/g_game.c
index b831c5dcd2..75f40c8ee2 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3868,16 +3868,24 @@ boolean G_CompetitionGametype(void)
 	return ((gametyperules & GTR_RACE) && (gametyperules & GTR_LIVES));
 }
 
+static boolean G_IsHubMapValid(INT16 mapnum)
+{
+	if (!hubmapenabled || mapnum == 0)
+		return false;
+
+	return mapheaderinfo[mapnum-1] && mapheaderinfo[mapnum-1]->typeoflevel & TOL_HUB;
+}
+
 // Returns true if the current hub level can be warped to.
 boolean G_IsHubAvailable(void)
 {
 	if (marathonmode || modeattacking)
 		return false;
 
-	if (!hubmapenabled || currenthubmap == 0 || M_MapLocked(currenthubmap, serverGamedata))
+	if (M_MapLocked(currenthubmap, serverGamedata))
 		return false;
 
-	return mapheaderinfo[currenthubmap-1] && mapheaderinfo[currenthubmap-1]->typeoflevel & TOL_HUB;
+	return G_IsHubMapValid(currenthubmap);
 }
 
 /** Get the typeoflevel flag needed to indicate support of a gametype.
@@ -4061,6 +4069,16 @@ static void G_HandleSaveLevel(void)
 	}
 }
 
+// Returns true if the hub is a valid next map.
+static boolean G_IsHubValidNextMap(void)
+{
+	if (marathonmode || modeattacking)
+		return false;
+
+	// The hub is a valid next map even if it's not unlocked.
+	return G_IsHubMapValid(currenthubmap);
+}
+
 //
 // G_GetNextMap
 //
@@ -4082,7 +4100,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 
 		if (newmapnum == 1104-1) // Going back to the hub
 		{
-			if (G_IsHubAvailable()) // Should handle Marathon Mode.
+			if (G_IsHubValidNextMap()) // Should handle Marathon Mode.
 				newmapnum = currenthubmap-1;
 			else
 			{
-- 
GitLab


From c05f109caa9b5ca2931c7bcf9014ff36e548d2b2 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 20:28:13 -0300
Subject: [PATCH 12/17] Disallow returning to the hub if not in a level

---
 src/m_menu.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/m_menu.c b/src/m_menu.c
index 15a0102e2e..1ad9bf5bd3 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -3845,6 +3845,9 @@ static boolean M_CanShowReturnToHub(void)
 // Returns true if you are able to select "Return to Hub".
 static boolean M_CanSelectReturnToHub(void)
 {
+	if (gamestate != GS_LEVEL)
+		return false;
+
 	return G_IsHubAvailable() && gamemap != currenthubmap;
 }
 
-- 
GitLab


From e858b8e6b68e55fbf4b5452c79794a56a880ca69 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 20:28:41 -0300
Subject: [PATCH 13/17] Don't update current hub if the map is not unlocked

---
 src/p_setup.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/p_setup.c b/src/p_setup.c
index 31c621acb4..36d7d48d28 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -7853,8 +7853,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	maptol = mapheaderinfo[gamemap-1]->typeoflevel;
 	gametyperules = gametypedefaultrules[gametype];
 
-	// Set the current hub map
-	if (maptol & TOL_HUB)
+	// Set the current hub map if it was unlocked
+	if ((maptol & TOL_HUB) && !M_MapLocked(gamemap, serverGamedata))
 		currenthubmap = gamemap;
 
 	// clear the target on map change, since the object will be invalidated
-- 
GitLab


From fcb98c9a96811e39dd75730691ee840a2a288689 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 20:29:06 -0300
Subject: [PATCH 14/17] Remove unnecessary assignment

---
 src/netcode/d_netcmd.c | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index 4214425c45..4b44aaa9af 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -2105,8 +2105,6 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 		ultimatemode = false;
 
 	resetplayer = ((flags & (1<<1)) == 0);
-
-	lastgametype = gametype;
 	gametype = READUINT8(*cp);
 
 	if (gametype < 0 || gametype >= gametypecount)
-- 
GitLab


From 445dc5639cb97a00d4042233cbc97ec4e7b0f1f1 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 21:38:41 -0300
Subject: [PATCH 15/17] Add HUBLEVEL for maps

---
 src/d_main.c           |  1 +
 src/deh_soc.c          |  8 ++++++++
 src/doomstat.h         |  1 +
 src/g_game.c           | 30 +++++++++++++++++++++++-------
 src/g_game.h           |  3 ++-
 src/hu_stuff.c         |  3 +--
 src/lua_maplib.c       |  5 +++++
 src/netcode/d_clisrv.c |  1 +
 src/netcode/d_netcmd.c | 14 +++++---------
 src/p_setup.c          |  5 +----
 10 files changed, 48 insertions(+), 23 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index 1507b36680..4d740cca34 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -997,6 +997,7 @@ void D_StartTitle(void)
 
 	gameaction = ga_nothing;
 	displayplayer = consoleplayer = 0;
+	gametype = -1;
 	G_SetGametype(GT_COOP);
 	paused = false;
 	advancedemo = false;
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 30b2b42b84..b6dfdb7ca6 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1611,6 +1611,14 @@ void readlevelheader(MYFILE *f, INT32 num)
 
 				mapheaderinfo[num-1]->marathonnext = (INT16)i;
 			}
+			else if (fastcmp(word, "HUBLEVEL"))
+			{
+				// Convert to map number
+				if (word2[0] >= 'A' && word2[0] <= 'Z' && word2[2] == '\0')
+					i = M_MapNumber(word2[0], word2[1]);
+
+				mapheaderinfo[num-1]->hublevel = (INT16)i;
+			}
 			else if (fastcmp(word, "TYPEOFLEVEL"))
 			{
 				if (i) // it's just a number
diff --git a/src/doomstat.h b/src/doomstat.h
index 4b1ab80d71..74e3e52b94 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -306,6 +306,7 @@ typedef struct
 	UINT32 typeoflevel;         ///< Combination of typeoflevel flags.
 	INT16 nextlevel;            ///< Map number of next level, or 1100-1102 to end.
 	INT16 marathonnext;         ///< See nextlevel, but for Marathon mode. Necessary to support hub worlds ala SUGOI.
+	INT16 hublevel;             ///< Map number of the hub associated with this level.
 	char keywords[32+1];        ///< Keywords separated by space to search for. 32 characters.
 	char musname[6+1];          ///< Music track to play. "" for no music.
 	UINT16 mustrack;            ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
diff --git a/src/g_game.c b/src/g_game.c
index 75f40c8ee2..a61202b4cd 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3477,16 +3477,16 @@ UINT32 gametypedefaultrules[NUMGAMETYPES] =
 //
 // Sets a new gametype, also setting gametype rules accordingly.
 //
-void G_SetGametype(INT16 gtype)
+void G_SetGametype(INT16 newgametype)
 {
-	if (gametype != gtype)
+	if (newgametype >= 0 && newgametype < gametypecount && gametype != newgametype)
 	{
 		if (gametype == -1)
-			lastgametype = gtype;
+			lastgametype = newgametype;
 		else
 			lastgametype = gametype;
 
-		gametype = gtype;
+		gametype = newgametype;
 		gametyperules = gametypedefaultrules[gametype];
 	}
 }
@@ -3888,6 +3888,21 @@ boolean G_IsHubAvailable(void)
 	return G_IsHubMapValid(currenthubmap);
 }
 
+// Updates the current hub map. Reading the function explains the function.
+void G_UpdateCurrentHubMap(void)
+{
+	if (maptol & TOL_HUB)
+	{
+		if (!M_MapLocked(gamemap, serverGamedata))
+			currenthubmap = gamemap;
+	}
+	else if (mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->hublevel)
+	{
+		if (!M_MapLocked(mapheaderinfo[gamemap-1]->hublevel, serverGamedata))
+			currenthubmap = mapheaderinfo[gamemap-1]->hublevel;
+	}
+}
+
 /** Get the typeoflevel flag needed to indicate support of a gametype.
   * In single-player, this always returns TOL_SP.
   * \param gametype The gametype for which support is desired.
@@ -4086,6 +4101,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 {
 	INT32 i;
 	INT16 newmapnum;
+	INT16 gametype_to_use;
 	boolean spec = G_IsSpecialStage(gamemap);
 
 	// go to next level
@@ -4117,8 +4133,6 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 			newmapnum = 1100-1; // No infinite loop for you
 	}
 
-	INT16 gametype_to_use;
-
 	if (nextgametype >= 0 && nextgametype < gametypecount)
 		gametype_to_use = nextgametype;
 	else
@@ -4139,7 +4153,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 			memset(visitedmap, 0, sizeof (visitedmap));
 
 			// Allow warping to non-hub levels if in a hub level.
-			if (gametype_to_use == GT_HUB && lastgametype != -1)
+			if (gametype_to_use == GT_HUB)
 				tolflag |= G_TOLFlag(lastgametype);
 			// Allow warping to hub levels if in a non-hub level.
 			else if (gametype_to_use != GT_HUB)
@@ -5219,6 +5233,8 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 	maptol = mapheaderinfo[gamemap-1]->typeoflevel;
 	globalweather = mapheaderinfo[gamemap-1]->weather;
 
+	G_UpdateCurrentHubMap();
+
 	// Don't carry over custom music change to another map.
 	mapmusflags |= MUSIC_RELOADRESET;
 
diff --git a/src/g_game.h b/src/g_game.h
index 890fcd71e1..3c0878858f 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -184,6 +184,7 @@ void G_PreLevelTitleCard(void);
 boolean G_IsTitleCardAvailable(void);
 
 boolean G_IsHubAvailable(void);
+void G_UpdateCurrentHubMap(void);
 
 // Can be called by the startup code or M_Responder, calls P_SetupLevel.
 void G_LoadGame(UINT32 slot, INT16 mapoverride);
@@ -198,7 +199,7 @@ extern UINT32 gametypedefaultrules[NUMGAMETYPES];
 extern UINT32 gametypetol[NUMGAMETYPES];
 extern INT16 gametyperankings[NUMGAMETYPES];
 
-void G_SetGametype(INT16 gametype);
+void G_SetGametype(INT16 newgametype);
 INT16 G_AddGametype(UINT32 rules);
 void G_AddGametypeConstant(INT16 gtype, const char *newgtconst);
 void G_UpdateGametypeSelections(void);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 7f2560bcc4..c4f8697c47 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -2536,8 +2536,7 @@ static void HU_DrawRankings(void)
 	UINT32 whiteplayer;
 
 	// draw the current gametype in the lower right
-	if (gametype >= 0 && gametype < gametypecount)
-		V_DrawString(4, splitscreen ? 184 : 192, 0, Gametype_Names[gametype]);
+	V_DrawString(4, splitscreen ? 184 : 192, 0, Gametype_Names[gametype]);
 
 	if (gametyperules & (GTR_TIMELIMIT|GTR_POINTLIMIT))
 	{
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index c946b10ce2..3c4f899fac 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -2801,6 +2801,7 @@ enum mapheaderinfo_e
 	mapheaderinfo_typeoflevel,
 	mapheaderinfo_nextlevel,
 	mapheaderinfo_marathonnext,
+	mapheaderinfo_hublevel,
 	mapheaderinfo_keywords,
 	mapheaderinfo_musname,
 	mapheaderinfo_mustrack,
@@ -2849,6 +2850,7 @@ static const char *const mapheaderinfo_opt[] = {
 	"typeoflevel",
 	"nextlevel",
 	"marathonnext",
+	"hublevel",
 	"keywords",
 	"musname",
 	"mustrack",
@@ -2919,6 +2921,9 @@ static int mapheaderinfo_get(lua_State *L)
 	case mapheaderinfo_marathonnext:
 		lua_pushinteger(L, header->marathonnext);
 		break;
+	case mapheaderinfo_hublevel:
+		lua_pushinteger(L, header->hublevel);
+		break;
 	case mapheaderinfo_keywords:
 		lua_pushstring(L, header->keywords);
 		break;
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index 6702e25912..5f7ad68140 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -815,6 +815,7 @@ void SV_StartSinglePlayerServer(void)
 	server = true;
 	netgame = false;
 	multiplayer = false;
+	gametype = -1;
 	G_SetGametype(GT_COOP);
 
 	// no more tic the game with this settings!
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index 4b44aaa9af..897e6dc27c 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -1741,9 +1741,7 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pultmode, boolean rese
 				newgametype = GT_HUB;
 			}
 			// Change gametype back to the previous one if warping out of a hub level
-			else if ((maptol & TOL_HUB)
-				&& lastgametype != -1
-				&& (mapheaderinfo[mapnum-1]->typeoflevel & TOL_HUB) == 0)
+			else if ((maptol & TOL_HUB) && (mapheaderinfo[mapnum-1]->typeoflevel & TOL_HUB) == 0)
 			{
 				newgametype = lastgametype;
 			}
@@ -2086,6 +2084,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	INT32 resetplayer = 1;
 	UINT8 skipprecutscene, FLS;
 	INT16 mapnumber;
+	UINT8 newgametype;
 
 	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
 	{
@@ -2105,12 +2104,9 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 		ultimatemode = false;
 
 	resetplayer = ((flags & (1<<1)) == 0);
-	gametype = READUINT8(*cp);
+	newgametype = READUINT8(*cp);
 
-	if (gametype < 0 || gametype >= gametypecount)
-		gametype = lastgametype;
-	else
-		G_SetGametype(gametype);
+	G_SetGametype(newgametype);
 
 	if (gametype != lastgametype)
 		D_GameTypeChanged();
@@ -4257,7 +4253,7 @@ static void TimeLimit_OnChange(void)
   */
 void D_GameTypeChanged(void)
 {
-	if (netgame && lastgametype != -1)
+	if (netgame)
 	{
 		CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), Gametype_Names[lastgametype], Gametype_Names[gametype]);
 	}
diff --git a/src/p_setup.c b/src/p_setup.c
index 36d7d48d28..cac70b703e 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -355,6 +355,7 @@ static void P_ClearSingleMapHeaderInfo(INT16 i)
 	mapheaderinfo[num]->actnum = 0;
 	mapheaderinfo[num]->typeoflevel = 0;
 	mapheaderinfo[num]->nextlevel = (INT16)(i + 1);
+	mapheaderinfo[num]->hublevel = 0;
 	mapheaderinfo[num]->marathonnext = 0;
 	mapheaderinfo[num]->startrings = 0;
 	mapheaderinfo[num]->sstimer = 90;
@@ -7853,10 +7854,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	maptol = mapheaderinfo[gamemap-1]->typeoflevel;
 	gametyperules = gametypedefaultrules[gametype];
 
-	// Set the current hub map if it was unlocked
-	if ((maptol & TOL_HUB) && !M_MapLocked(gamemap, serverGamedata))
-		currenthubmap = gamemap;
-
 	// clear the target on map change, since the object will be invalidated
 	P_SetTarget(&ticcmd_ztargetfocus[0], NULL);
 	P_SetTarget(&ticcmd_ztargetfocus[1], NULL);
-- 
GitLab


From 182991c0127c7a5002e49d5dcb9740780b777619 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 21:44:51 -0300
Subject: [PATCH 16/17] Rename HUBLEVEL to HUBMAP

---
 src/deh_soc.c    |  4 ++--
 src/doomstat.h   |  2 +-
 src/g_game.c     | 12 ++++++------
 src/lua_maplib.c |  8 ++++----
 src/p_setup.c    |  2 +-
 5 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/src/deh_soc.c b/src/deh_soc.c
index b6dfdb7ca6..5f741744be 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1611,13 +1611,13 @@ void readlevelheader(MYFILE *f, INT32 num)
 
 				mapheaderinfo[num-1]->marathonnext = (INT16)i;
 			}
-			else if (fastcmp(word, "HUBLEVEL"))
+			else if (fastcmp(word, "HUBMAP"))
 			{
 				// Convert to map number
 				if (word2[0] >= 'A' && word2[0] <= 'Z' && word2[2] == '\0')
 					i = M_MapNumber(word2[0], word2[1]);
 
-				mapheaderinfo[num-1]->hublevel = (INT16)i;
+				mapheaderinfo[num-1]->hubmap = (INT16)i;
 			}
 			else if (fastcmp(word, "TYPEOFLEVEL"))
 			{
diff --git a/src/doomstat.h b/src/doomstat.h
index 74e3e52b94..e6acdaa784 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -306,7 +306,7 @@ typedef struct
 	UINT32 typeoflevel;         ///< Combination of typeoflevel flags.
 	INT16 nextlevel;            ///< Map number of next level, or 1100-1102 to end.
 	INT16 marathonnext;         ///< See nextlevel, but for Marathon mode. Necessary to support hub worlds ala SUGOI.
-	INT16 hublevel;             ///< Map number of the hub associated with this level.
+	INT16 hubmap;               ///< Map number of the hub associated with this level.
 	char keywords[32+1];        ///< Keywords separated by space to search for. 32 characters.
 	char musname[6+1];          ///< Music track to play. "" for no music.
 	UINT16 mustrack;            ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
diff --git a/src/g_game.c b/src/g_game.c
index a61202b4cd..837b95eef1 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3870,7 +3870,7 @@ boolean G_CompetitionGametype(void)
 
 static boolean G_IsHubMapValid(INT16 mapnum)
 {
-	if (!hubmapenabled || mapnum == 0)
+	if (mapnum < 1 || mapnum > NUMMAPS)
 		return false;
 
 	return mapheaderinfo[mapnum-1] && mapheaderinfo[mapnum-1]->typeoflevel & TOL_HUB;
@@ -3879,7 +3879,7 @@ static boolean G_IsHubMapValid(INT16 mapnum)
 // Returns true if the current hub level can be warped to.
 boolean G_IsHubAvailable(void)
 {
-	if (marathonmode || modeattacking)
+	if (!hubmapenabled || marathonmode || modeattacking)
 		return false;
 
 	if (M_MapLocked(currenthubmap, serverGamedata))
@@ -3896,10 +3896,10 @@ void G_UpdateCurrentHubMap(void)
 		if (!M_MapLocked(gamemap, serverGamedata))
 			currenthubmap = gamemap;
 	}
-	else if (mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->hublevel)
+	else if (mapheaderinfo[gamemap-1] && G_IsHubMapValid(mapheaderinfo[gamemap-1]->hubmap))
 	{
-		if (!M_MapLocked(mapheaderinfo[gamemap-1]->hublevel, serverGamedata))
-			currenthubmap = mapheaderinfo[gamemap-1]->hublevel;
+		if (!M_MapLocked(mapheaderinfo[gamemap-1]->hubmap, serverGamedata))
+			currenthubmap = mapheaderinfo[gamemap-1]->hubmap;
 	}
 }
 
@@ -4087,7 +4087,7 @@ static void G_HandleSaveLevel(void)
 // Returns true if the hub is a valid next map.
 static boolean G_IsHubValidNextMap(void)
 {
-	if (marathonmode || modeattacking)
+	if (!hubmapenabled || marathonmode || modeattacking)
 		return false;
 
 	// The hub is a valid next map even if it's not unlocked.
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 3c4f899fac..171244742e 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -2801,7 +2801,7 @@ enum mapheaderinfo_e
 	mapheaderinfo_typeoflevel,
 	mapheaderinfo_nextlevel,
 	mapheaderinfo_marathonnext,
-	mapheaderinfo_hublevel,
+	mapheaderinfo_hubmap,
 	mapheaderinfo_keywords,
 	mapheaderinfo_musname,
 	mapheaderinfo_mustrack,
@@ -2850,7 +2850,7 @@ static const char *const mapheaderinfo_opt[] = {
 	"typeoflevel",
 	"nextlevel",
 	"marathonnext",
-	"hublevel",
+	"hubmap",
 	"keywords",
 	"musname",
 	"mustrack",
@@ -2921,8 +2921,8 @@ static int mapheaderinfo_get(lua_State *L)
 	case mapheaderinfo_marathonnext:
 		lua_pushinteger(L, header->marathonnext);
 		break;
-	case mapheaderinfo_hublevel:
-		lua_pushinteger(L, header->hublevel);
+	case mapheaderinfo_hubmap:
+		lua_pushinteger(L, header->hubmap);
 		break;
 	case mapheaderinfo_keywords:
 		lua_pushstring(L, header->keywords);
diff --git a/src/p_setup.c b/src/p_setup.c
index cac70b703e..9ede7adfe0 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -355,7 +355,7 @@ static void P_ClearSingleMapHeaderInfo(INT16 i)
 	mapheaderinfo[num]->actnum = 0;
 	mapheaderinfo[num]->typeoflevel = 0;
 	mapheaderinfo[num]->nextlevel = (INT16)(i + 1);
-	mapheaderinfo[num]->hublevel = 0;
+	mapheaderinfo[num]->hubmap = 0;
 	mapheaderinfo[num]->marathonnext = 0;
 	mapheaderinfo[num]->startrings = 0;
 	mapheaderinfo[num]->sstimer = 90;
-- 
GitLab


From 1bbfe31708ddd1e6c7de6f03d3f97136e1146275 Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 15 Feb 2025 21:46:34 -0300
Subject: [PATCH 17/17] Make Lua defaulthubmap and currenthubmap consistent
 with other map vars

---
 src/lua_script.c | 29 +++++++----------------------
 1 file changed, 7 insertions(+), 22 deletions(-)

diff --git a/src/lua_script.c b/src/lua_script.c
index 94578fb990..92ae3d89a6 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -181,20 +181,10 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		lua_pushboolean(L, stoppedclock);
 		return 1;
 	} else if (fastcmp(word,"defaulthubmap")) {
-		if (defaulthubmap == 0) {
-			lua_pushnil(L);
-		}
-		else {
-			lua_pushinteger(L, defaulthubmap);
-		}
+		lua_pushinteger(L, defaulthubmap);
 		return 1;
 	} else if (fastcmp(word, "currenthubmap")) {
-		if (currenthubmap == 0) {
-			lua_pushnil(L);
-		}
-		else {
-			lua_pushinteger(L, currenthubmap);
-		}
+		lua_pushinteger(L, currenthubmap);
 		return 1;
 	} else if (fastcmp(word, "hubmapenabled")) {
 		lua_pushboolean(L, hubmapenabled);
@@ -464,17 +454,12 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 int LUA_CheckGlobals(lua_State *L, const char *word)
 {
 	if (fastcmp(word, "currenthubmap")) {
-		if (lua_isnoneornil(L, 2)) {
-			currenthubmap = 0;
-		}
-		else {
-			int mapnum = luaL_checkinteger(L, 2);
-			if (mapnum < 1 || mapnum > NUMMAPS) {
-				luaL_error(L, "map number %d out of range (1 - %d)", mapnum, NUMMAPS);
-				return 1;
-			}
-			currenthubmap = (INT16)mapnum;
+		int mapnum = luaL_checkinteger(L, 2);
+		if (mapnum < 0 || mapnum > NUMMAPS) {
+			luaL_error(L, "map number %d out of range (1 - %d)", mapnum, NUMMAPS);
+			return 1;
 		}
+		currenthubmap = (INT16)mapnum;
 	}
 	else if (fastcmp(word, "hubmapenabled"))
 		hubmapenabled = luaL_checkboolean(L, 2);
-- 
GitLab