From 512792e240bf831a2021f0485741ba0c48c9ef5d Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Tue, 27 Jun 2023 15:08:25 -0300
Subject: [PATCH] Refactor world loading

---
 src/d_main.c           |   2 +-
 src/d_netcmd.c         |   2 +-
 src/g_game.c           |  31 ++-
 src/hardware/hw_main.c |  25 ---
 src/p_local.h          |   5 +-
 src/p_mobj.c           |  40 +---
 src/p_mobj.h           |  14 --
 src/p_saveg.c          |   6 +-
 src/p_setup.c          | 489 +++++++++++++++++++++--------------------
 src/p_setup.h          |  17 +-
 src/p_tick.c           |   5 +-
 src/p_world.c          |  49 ++++-
 src/p_world.h          |   7 +
 src/r_fps.c            |  11 +-
 src/r_fps.h            |   2 +-
 15 files changed, 343 insertions(+), 362 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index adfc82d064..9d7ae43791 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1739,7 +1739,7 @@ void D_SRB2Main(void)
 	{
 		levelstarttic = gametic;
 		G_SetGamestate(GS_LEVEL);
-		if (!P_LoadLevel(&players[consoleplayer], false, false, false))
+		if (!P_LoadLevel(false, false))
 			I_Quit(); // fail so reset game stuff
 	}
 }
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index e933c733c2..9e67df833e 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -2136,7 +2136,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	{
 		DEBFILE(va("Warping to %s [resetplayer=%d lastgametype=%d gametype=%d cpnd=%d]\n",
 			mapname, resetplayer, lastgametype, gametype, chmappending));
-		if (!addworld || (addworld && playernum == consoleplayer))
+		if (!addworld)
 			CONS_Printf(M_GetText("Speeding off to level...\n"));
 	}
 
diff --git a/src/g_game.c b/src/g_game.c
index 837d4afba6..159080b64a 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1839,13 +1839,17 @@ void G_DoLoadLevel(player_t *player, boolean addworld, boolean resetplayer)
 
 		P_UnloadWorldList();
 	}
-	else if (resetplayer)
-		player->playerstate = PST_REBORN;
 
 	// Setup the level.
-	if (!P_LoadLevel(player, addworld, false, false)) // this never returns false?
+	boolean success;
+	if (addworld)
+		success = P_LoadWorld(false);
+	else
+		success = P_LoadLevel(false, false); // this never returns false? (yes it can)
+
+	// fail so reset game stuff
+	if (!success)
 	{
-		// fail so reset game stuff
 		Command_ExitGame_f();
 		return;
 	}
@@ -1877,14 +1881,16 @@ void G_DoLoadLevel(player_t *player, boolean addworld, boolean resetplayer)
 			G_ResetCamera(1);
 	}
 
-	if (player == &players[consoleplayer])
+	if (!addworld && player == &players[consoleplayer])
 	{
 		G_ResetCamera(consoleplayer);
 
 		// clear hud messages remains (usually from game startup)
 		CON_ClearHUD();
 	}
-	else if (addworld && !splitscreen) // change world back to yours
+
+	// change world back to yours, for consistency
+	if (addworld)
 		P_SetWorld(localworld);
 }
 
@@ -5026,13 +5032,7 @@ void G_InitNew(player_t *player,
 		numgameovers = tokenlist = token = sstimer = redscore = bluescore = lastmap = 0;
 		countdown = countdown2 = exitfadestarted = 0;
 
-		if (addworld)
-		{
-			P_DetachPlayerWorld(player);
-			P_UnloadWorldPlayer(player);
-			G_ResetPlayer(player, pultmode, FLS);
-		}
-		else
+		if (!addworld)
 		{
 			for (i = 0; i < MAXPLAYERS; i++)
 				G_ResetPlayer(&players[i], pultmode, FLS);
@@ -5082,11 +5082,8 @@ void G_InitNew(player_t *player,
 	else
 		G_DoLoadLevel(player, addworld, resetplayer);
 
-	if (addworld && player != &players[consoleplayer])
-		return;
-
 	// current world is hopefully the newly loaded world at this point
-	if (netgame)
+	if (netgame && !addworld)
 	{
 		char *title = G_BuildMapTitle(gamemap);
 
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 3b51fc4124..561577b99b 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -132,7 +132,6 @@ ps_metric_t ps_hw_batchsorttime = {0};
 ps_metric_t ps_hw_batchdrawtime = {0};
 
 boolean gl_init = false;
-boolean gl_maploaded = false;
 boolean gl_sessioncommandsadded = false;
 boolean gl_shadersavailable = true;
 
@@ -5548,23 +5547,6 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	HWD.pfnGClipRect(0, 0, vid.width, vid.height, NZCLIP_PLANE);
 }
 
-void HWR_LoadLevel(void)
-{
-#ifdef ALAM_LIGHTING
-	// BP: reset light between levels (we draw preview frame lights on current frame)
-	HWR_ResetLights();
-#endif
-
-	if (world->extrasubsectors == NULL)
-		HWR_CreatePlanePolygons((INT32)world->numnodes - 1);
-
-	// Build the sky dome
-	HWR_ClearSkyDome(world);
-	HWR_BuildSkyDome(world);
-
-	gl_maploaded = true;
-}
-
 // ==========================================================================
 //                                                         3D ENGINE COMMANDS
 // ==========================================================================
@@ -5705,13 +5687,6 @@ void HWR_Switch(void)
 	// Load textures
 	if (!gl_maptexturesloaded)
 		HWR_LoadMapTextures(numtextures);
-
-	// Create plane polygons
-	if (!gl_maploaded && (gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
-	{
-		HWR_ClearAllTextures();
-		HWR_LoadLevel();
-	}
 }
 
 // --------------------------------------------------------------------------
diff --git a/src/p_local.h b/src/p_local.h
index 8f3bc9edc3..2cc777058d 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -72,9 +72,10 @@ typedef enum
 	THINK_PRECIP,
 	NUM_THINKERLISTS
 } thinklistnum_t; /**< Thinker lists. */
-extern thinker_t *thlist;
 
-void P_InitThinkers(void);
+extern thinker_t *thlist; // the current thinker list
+
+void P_InitThinkers(world_t *w);
 void P_AddThinker(const thinklistnum_t n, thinker_t *thinker);
 void P_RemoveThinker(thinker_t *thinker);
 void P_MoveThinkerToWorld(world_t *w, const thinklistnum_t n, thinker_t *thinker);
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 02308680e2..0940bf1ac3 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -19,6 +19,7 @@
 #include "hu_stuff.h"
 #include "p_local.h"
 #include "p_setup.h"
+#include "p_world.h"
 #include "r_fps.h"
 #include "r_main.h"
 #include "r_skins.h"
@@ -40,41 +41,6 @@
 static CV_PossibleValue_t CV_BobSpeed[] = {{0, "MIN"}, {4*FRACUNIT, "MAX"}, {0, NULL}};
 consvar_t cv_movebob = CVAR_INIT ("movebob", "1.0", CV_FLOAT|CV_SAVE, CV_BobSpeed, NULL);
 
-actioncache_t actioncachehead;
-
-void P_InitCachedActions(void)
-{
-	actioncachehead.prev = actioncachehead.next = &actioncachehead;
-}
-
-void P_RunCachedActions(void)
-{
-	actioncache_t *ac;
-	actioncache_t *next;
-
-	for (ac = actioncachehead.next; ac != &actioncachehead; ac = next)
-	{
-		var1 = states[ac->statenum].var1;
-		var2 = states[ac->statenum].var2;
-		astate = &states[ac->statenum];
-		if (ac->mobj && !P_MobjWasRemoved(ac->mobj)) // just in case...
-			states[ac->statenum].action.acp1(ac->mobj);
-		next = ac->next;
-		Z_Free(ac);
-	}
-}
-
-void P_AddCachedAction(mobj_t *mobj, INT32 statenum)
-{
-	actioncache_t *newaction = Z_Calloc(sizeof(actioncache_t), PU_LEVEL, NULL);
-	newaction->mobj = mobj;
-	newaction->statenum = statenum;
-	actioncachehead.prev->next = newaction;
-	newaction->next = &actioncachehead;
-	newaction->prev = actioncachehead.prev;
-	actioncachehead.prev = newaction;
-}
-
 //
 // P_SetupStateAnimation
 //
@@ -10977,13 +10943,13 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 	// Call action functions when the state is set
 	if (st->action.acp1 && (mobj->flags & MF_RUNSPAWNFUNC))
 	{
-		if (levelloading)
+		if (world->loading)
 		{
 			// Cache actions in a linked list
 			// with function pointer, and
 			// var1 & var2, which will be executed
 			// when the level finishes loading.
-			P_AddCachedAction(mobj, mobj->info->spawnstate);
+			P_AddCachedAction(world, mobj, mobj->info->spawnstate);
 		}
 		else
 		{
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 86bb07676b..3e597c8e5f 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -472,20 +472,6 @@ typedef struct precipmobj_s
 	INT32 flags; // flags from mobjinfo tables
 } precipmobj_t;
 
-typedef struct actioncache_s
-{
-	struct actioncache_s *next;
-	struct actioncache_s *prev;
-	struct mobj_s *mobj;
-	INT32 statenum;
-} actioncache_t;
-
-extern actioncache_t actioncachehead;
-
-void P_InitCachedActions(void);
-void P_RunCachedActions(void);
-void P_AddCachedAction(mobj_t *mobj, INT32 statenum);
-
 // check mobj against water content, before movement code
 void P_MobjCheckWater(mobj_t *mobj);
 
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 53076ade5e..835ea19123 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -3717,7 +3717,7 @@ static void P_NetUnArchiveThinkers(void)
 
 	// we don't want the removed mobjs to come back
 	unarchiveworld->iquetail = unarchiveworld->iquehead = 0;
-	P_InitThinkers();
+	P_InitThinkers(unarchiveworld);
 
 	// clear sector thinker pointers so they don't point to non-existant thinkers for all of eternity
 	for (i = 0; i < numsectors; i++)
@@ -4400,7 +4400,7 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 
 	tokenlist = READUINT32(save_p);
 
-	if (!P_LoadLevel(&players[consoleplayer], false, true, reloading))
+	if (!P_LoadLevel(true, reloading))
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("Can't load the level!\n"));
 		return false;
@@ -4927,7 +4927,7 @@ static void P_NetUnArchiveWorlds(boolean reloading)
 		// Don't load the first world (because it already is loaded at this point)
 		if (i != 0)
 		{
-			if (!P_LoadLevel(player, true, true, reloading))
+			if (!P_LoadWorld(true))
 				I_Error("P_NetUnArchiveWorlds: failed loading world");
 		}
 
diff --git a/src/p_setup.c b/src/p_setup.c
index 81ac86be08..baf5c093dd 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -115,7 +115,6 @@ side_t *spawnsides;
 INT32 numstarposts;
 UINT16 bossdisabled;
 boolean stoppedclock;
-boolean levelloading;
 UINT8 levelfadecol;
 
 // BLOCKMAP
@@ -6832,9 +6831,9 @@ static void P_MakeMapMD5(virtres_t *virt, void *dest)
 	M_Memcpy(dest, &resmd5, 16);
 }
 
-static boolean P_LoadMapFromFile(void)
+static boolean P_LoadMapFromFile(lumpnum_t maplumpnum)
 {
-	virtres_t *virt = vres_GetMap(lastloadedmaplumpnum);
+	virtres_t *virt = vres_GetMap(maplumpnum);
 	virtlump_t *textmap = vres_Find(virt, "TEXTMAP");
 	size_t i;
 	udmf = textmap != NULL;
@@ -6911,9 +6910,6 @@ void P_SetupSkyTexture(INT32 skynum)
 	world->skytexture = R_TextureNumForName(skytexname);
 }
 
-static const char *maplumpname;
-lumpnum_t lastloadedmaplumpnum; // for comparative savegame
-
 //
 // Some player initialization for map start.
 //
@@ -6986,7 +6982,7 @@ static void P_InitWorldSettings(mapheader_t *mapheader)
 	stagefailed = G_IsSpecialStage(gamemap);
 }
 
-static void P_InitLevelSettings(mapheader_t *mapheader, player_t *player, boolean addworld, boolean fromnetsave)
+static void P_InitLevelSettings(mapheader_t *mapheader, boolean addworld)
 {
 	INT32 i;
 	boolean canresetlives = true;
@@ -7010,18 +7006,12 @@ static void P_InitLevelSettings(mapheader_t *mapheader, player_t *player, boolea
 	}
 
 	if (!addworld)
+	{
 		countdown = countdown2 = exitfadestarted = 0;
 
-	if (!addworld)
-	{
 		for (i = 0; i < MAXPLAYERS; i++)
 			P_InitPlayerSettings(i, canresetlives);
 	}
-	else if (player && !fromnetsave)
-	{
-		P_DetachPlayerWorld(player);
-		P_InitPlayerSettings((INT32)(player - players), canresetlives);
-	}
 
 	if (botingame)
 		CV_SetValue(&cv_analog[1], true);
@@ -7051,7 +7041,7 @@ void P_RespawnThings(void)
 		P_RemoveMobj((mobj_t *)think);
 	}
 
-	P_InitLevelSettings(worldmapheader, NULL, false, false);
+	P_InitLevelSettings(worldmapheader, false);
 
 	localaiming = 0;
 	localaiming2 = 0;
@@ -7531,7 +7521,7 @@ static void P_WriteLetter(void)
 	Z_Free(buf);
 }
 
-static void P_InitGametype(player_t *player, boolean addworld)
+static void P_InitGametype(boolean addworld)
 {
 	UINT8 i;
 
@@ -7552,8 +7542,6 @@ static void P_InitGametype(player_t *player, boolean addworld)
 			leveltime = maxstarposttime;
 		}
 	}
-	else
-		P_InitPlayer((INT32)(player - players));
 
 	P_WriteLetter();
 
@@ -7571,20 +7559,170 @@ static void P_InitGametype(player_t *player, boolean addworld)
 			: nextmapheader->numlaps);
 }
 
+static world_t *P_InitWorldFromMap(INT16 mapnumber, mapheader_t *mapheader, boolean addworld, boolean fromnetsave)
+{
+	world_t *curworld = world;
+	world_t *w = P_InitNewWorld();
+
+	w->loading = true;
+
+	P_SetWorld(w);
+
+	if (!addworld && !fromnetsave)
+	{
+		for (INT32 i = 0; i < MAXPLAYERS; i++)
+		{
+			if (!playeringame[i])
+				continue;
+
+			player_t *p = &players[i];
+			p->world = NULL;
+			P_SwitchPlayerWorld(p, w);
+		}
+
+		w->players = D_NumPlayers();
+	}
+
+	R_InitializeLevelInterpolators(w);
+	P_InitThinkers(w);
+	R_InitMobjInterpolators(w);
+
+	// internal game map
+	const char *maplumpname = G_BuildMapName(mapnumber);
+	lumpnum_t maplumpnum = W_CheckNumForMap(maplumpname);
+	if (maplumpnum == LUMPERROR)
+	{
+		P_SetWorld(curworld);
+		return NULL;
+	}
+
+	R_ReInitColormaps(mapheader->palette);
+
+	// Init Boom colormaps.
+	if (!addworld)
+		R_ClearColormaps();
+
+	// SRB2 determines the sky texture to be used depending on the map header.
+	P_InitLevelSky(mapheader->skynum);
+
+	P_ResetSpawnpoints();
+
+	P_ResetWaypoints();
+
+	P_MapStart(); // tmthing can be used starting from this point
+
+	P_InitSlopes();
+
+	if (!P_LoadMapFromFile(maplumpnum))
+		return false;
+
+	// init anything that P_SpawnSlopes/P_SpawnMapThings needs to know
+	P_InitSpecials();
+
+	// actually do that now
+	P_SpawnSlopes(fromnetsave);
+	P_SpawnMapThings(!fromnetsave);
+
+	// Init skybox objects
+	w->skyboxmo[0] = w->skyboxviewpnts[0];
+	w->skyboxmo[1] = w->skyboxcenterpnts[0];
+
+	// Count Co-Op starts
+	for (w->numcoopstarts = 0; w->numcoopstarts < MAXPLAYERS; w->numcoopstarts++)
+		if (!w->playerstarts[w->numcoopstarts])
+			break;
+
+	// set up world state
+	P_SpawnSpecials(fromnetsave);
+
+	//  ugly hack for P_NetUnArchiveMisc (and P_LoadNetGame)
+	if (!fromnetsave)
+		P_SpawnPrecipitation();
+
+#ifdef HWRENDER
+	if (rendermode == render_opengl && world->extrasubsectors == NULL)
+		HWR_CreatePlanePolygons((INT32)world->numnodes - 1);
+#endif
+
+	// oh god I hope this helps
+	// (addendum: apparently it does!
+	//  none of this needs to be done because it's not the beginning of the map when
+	//  a netgame save is being loaded, and could actively be harmful by messing with
+	//  the client's view of the data.)
+	if (!fromnetsave)
+		P_InitGametype(addworld);
+
+	// clear special respawning que
+	w->iquehead = w->iquetail = 0;
+
+	if (precache || dedicated)
+		R_PrecacheLevel();
+
+	if (!demoplayback)
+	{
+		clientGamedata->mapvisited[mapnumber-1] |= MV_VISITED;
+		serverGamedata->mapvisited[mapnumber-1] |= MV_VISITED;
+	}
+
+	w->loading = false;
+
+	P_RunCachedActions(w);
+
+	P_MapEnd(); // tmthing is no longer needed from this point onwards
+
+	P_SetWorld(curworld);
+
+	return w;
+}
+
+static void P_DoLevelProgression(void)
+{
+	// Took me 3 hours to figure out why my progression kept on getting overwritten with the titlemap...
+	if (titlemapinaction)
+		return;
+
+	if (!lastmaploaded) // Start a new game?
+	{
+		// 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)
+			&& !usedCheats && cursaveslot > 0)
+		{
+			G_SaveGame((UINT32)cursaveslot, gamemap);
+		}
+		// If you're looking for saving sp file progression (distinct from G_SaveGameOver), check G_DoCompleted.
+	}
+	lastmaploaded = gamemap; // HAS to be set after saving!!
+}
+
+static void P_FinishMapLoad(boolean fromnetsave)
+{
+	if (!fromnetsave) // uglier hack
+	{
+		// to make a newly loaded level start on the second frame.
+		INT32 buf = gametic % BACKUPTICS;
+		for (INT32 	i = 0; i < MAXPLAYERS; i++)
+		{
+			if (playeringame[i])
+				G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
+		}
+		P_PreTicker(2);
+		P_MapStart(); // just in case MapLoad modifies tmthing
+		LUA_HookInt(gamemap, HOOK(MapLoad));
+		P_MapEnd(); // just in case MapLoad modifies tmthing
+	}
+}
+
 /** Loads a level from a lump or external wad.
   *
   * \param fromnetsave If true, skip some stuff because we're loading a netgame snapshot.
   * \todo Clean up, refactor, split up; get rid of the bloat.
   */
-boolean P_LoadLevel(player_t *player, boolean addworld, boolean fromnetsave, boolean reloadinggamestate)
+boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 {
 	// use gamemap to get map number.
 	// 99% of the things already did, so.
 	// Map header should always be in place at this point
-	INT32 i, ranspecialwipe = 0;
-	boolean runforself = (!addworld || (addworld && player == &players[consoleplayer]));
-
-	levelloading = true;
+	INT32 ranspecialwipe = 0;
 
 	// This is needed. Don't touch.
 	maptol = nextmapheader->typeoflevel;
@@ -7615,7 +7753,7 @@ boolean P_LoadLevel(player_t *player, boolean addworld, boolean fromnetsave, boo
 	if (cv_runscripts.value && nextmapheader->scriptname[0] != '#')
 		P_RunLevelScript(nextmapheader->scriptname);
 
-	P_InitLevelSettings(nextmapheader, player, addworld, fromnetsave);
+	P_InitLevelSettings(nextmapheader, false);
 
 	postimgtype = postimgtype2 = postimg_none;
 
@@ -7647,127 +7785,80 @@ boolean P_LoadLevel(player_t *player, boolean addworld, boolean fromnetsave, boo
 	players[consoleplayer].viewz = 1;
 
 	// Cancel all d_main.c fadeouts (keep fade in though).
-	if (addworld || reloadinggamestate)
+	if (reloadinggamestate)
 		wipegamestate = gamestate; // Don't fade if reloading the gamestate
 	else
 		wipegamestate = FORCEWIPEOFF;
 	wipestyleflags = 0;
 
-	if (!addworld)
+	// Special stage & record attack retry fade to white
+	// This is handled BEFORE sounds are stopped.
+	if (G_GetModeAttackRetryFlag())
 	{
-		// Special stage & record attack retry fade to white
-		// This is handled BEFORE sounds are stopped.
-		if (G_GetModeAttackRetryFlag())
-		{
-			if (modeattacking && !demoplayback)
-			{
-				ranspecialwipe = 2;
-				wipestyleflags |= (WSF_FADEOUT|WSF_TOWHITE);
-			}
-			G_ClearModeAttackRetryFlag();
-		}
-		else if (rendermode != render_none && G_IsSpecialStage(gamemap))
-		{
-			P_RunSpecialStageWipe();
-			ranspecialwipe = 1;
-		}
-
-		// Make sure all sounds are stopped before Z_FreeTags.
-		S_StopSounds();
-		S_ClearSfx();
-
-		// Fade out music here. Deduct 2 tics so the fade volume actually reaches 0.
-		// But don't halt the music! S_Start will take care of that. This dodges a MIDI crash bug.
-		if (!(reloadinggamestate || titlemapinaction) && (S_ShouldResetMusic(nextmapheader) ||
-			strnicmp(S_MusicName(),
-				(mapmusflags & MUSIC_RELOADRESET) ? nextmapheader->musname : mapmusname, 7)))
-		{
-			S_FadeMusic(0, FixedMul(
-				FixedDiv((F_GetWipeLength(wipedefs[wipe_level_toblack])-2)*NEWTICRATERATIO, NEWTICRATE), MUSICRATE));
-		}
-
-		// Let's fade to black here
-		// But only if we didn't do the special stage wipe
-		if (rendermode != render_none && !(ranspecialwipe || reloadinggamestate))
-			P_RunLevelWipe();
-
-		if (!(reloadinggamestate || titlemapinaction))
+		if (modeattacking && !demoplayback)
 		{
-			if (ranspecialwipe == 2)
-			{
-				pausedelay = -3; // preticker plus one
-				S_StartSound(NULL, sfx_s3k73);
-			}
-
-			// Print "SPEEDING OFF TO [ZONE] [ACT 1]..."
-			if (rendermode != render_none)
-			{
-				// Don't include these in the fade!
-				char tx[64];
-				V_DrawSmallString(1, 191, V_ALLOWLOWERCASE|V_TRANSLUCENT|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Speeding off to..."));
-				snprintf(tx, 63, "%s%s%s",
-					nextmapheader->lvlttl,
-					(nextmapheader->levelflags & LF_NOZONE) ? "" : " Zone",
-					(nextmapheader->actnum > 0) ? va(" %d",nextmapheader->actnum) : "");
-				V_DrawSmallString(1, 195, V_ALLOWLOWERCASE|V_TRANSLUCENT|V_SNAPTOLEFT|V_SNAPTOBOTTOM, tx);
-				I_UpdateNoVsync();
-			}
-
-			// As oddly named as this is, this handles music only.
-			// We should be fine starting it here.
-			// Don't do this during titlemap, because the menu code handles music by itself.
-			S_Start(nextmapheader);
-
-			levelfadecol = (ranspecialwipe) ? 0 : 31;
-
-			// Close text prompt before freeing the old level
-			F_EndTextPrompt(false, true);
+			ranspecialwipe = 2;
+			wipestyleflags |= (WSF_FADEOUT|WSF_TOWHITE);
 		}
+		G_ClearModeAttackRetryFlag();
 	}
-	else
+	else if (rendermode != render_none && G_IsSpecialStage(gamemap))
 	{
-		S_SetMapMusic(nextmapheader);
-		S_StopMusic();
-		S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
+		P_RunSpecialStageWipe();
+		ranspecialwipe = 1;
 	}
 
-	if (player && !titlemapinaction && !fromnetsave)
-		P_UnloadWorldPlayer(player);
-
-	// Initialize the world
-	world = P_InitNewWorld();
-	thlist = world->thlist;
+	// Make sure all sounds are stopped before Z_FreeTags.
+	S_StopSounds();
+	S_ClearSfx();
 
-	if (!fromnetsave)
+	// Fade out music here. Deduct 2 tics so the fade volume actually reaches 0.
+	// But don't halt the music! S_Start will take care of that. This dodges a MIDI crash bug.
+	if (!(reloadinggamestate || titlemapinaction) && (S_ShouldResetMusic(nextmapheader) ||
+		strnicmp(S_MusicName(),
+			(mapmusflags & MUSIC_RELOADRESET) ? nextmapheader->musname : mapmusname, 7)))
 	{
-		if (addworld)
-			P_SwitchPlayerWorld(player, world);
-		else
-		{
-			for (i = 0; i < MAXPLAYERS; i++)
-			{
-				player_t *p;
+		S_FadeMusic(0, FixedMul(
+			FixedDiv((F_GetWipeLength(wipedefs[wipe_level_toblack])-2)*NEWTICRATERATIO, NEWTICRATE), MUSICRATE));
+	}
 
-				if (!playeringame[i])
-					continue;
+	// Let's fade to black here
+	// But only if we didn't do the special stage wipe
+	if (rendermode != render_none && !(ranspecialwipe || reloadinggamestate))
+		P_RunLevelWipe();
 
-				p = &players[i];
-				p->world = NULL;
-				P_SwitchPlayerWorld(p, world);
-			}
+	if (!(reloadinggamestate || titlemapinaction))
+	{
+		if (ranspecialwipe == 2)
+		{
+			pausedelay = -3; // preticker plus one
+			S_StartSound(NULL, sfx_s3k73);
+		}
 
-			world->players = D_NumPlayers();
+		// Print "SPEEDING OFF TO [ZONE] [ACT 1]..."
+		if (rendermode != render_none)
+		{
+			// Don't include these in the fade!
+			char tx[64];
+			V_DrawSmallString(1, 191, V_ALLOWLOWERCASE|V_TRANSLUCENT|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Speeding off to..."));
+			snprintf(tx, 63, "%s%s%s",
+				nextmapheader->lvlttl,
+				(nextmapheader->levelflags & LF_NOZONE) ? "" : " Zone",
+				(nextmapheader->actnum > 0) ? va(" %d",nextmapheader->actnum) : "");
+			V_DrawSmallString(1, 195, V_ALLOWLOWERCASE|V_TRANSLUCENT|V_SNAPTOLEFT|V_SNAPTOBOTTOM, tx);
+			I_UpdateNoVsync();
 		}
-	}
 
-	if (!addworld || player == &players[consoleplayer])
-		localworld = world;
+		// As oddly named as this is, this handles music only.
+		// We should be fine starting it here.
+		// Don't do this during titlemap, because the menu code handles music by itself.
+		S_Start(nextmapheader);
 
-	R_InitializeLevelInterpolators(world);
+		levelfadecol = (ranspecialwipe) ? 0 : 31;
 
-	P_InitThinkers();
-	R_InitMobjInterpolators();
-	P_InitCachedActions();
+		// Close text prompt before freeing the old level
+		F_EndTextPrompt(false, true);
+	}
 
 	if (!fromnetsave && savedata.lives > 0)
 	{
@@ -7781,139 +7872,33 @@ boolean P_LoadLevel(player_t *player, boolean addworld, boolean fromnetsave, boo
 		savedata.lives = 0;
 	}
 
-	// internal game map
-	maplumpname = G_BuildMapName(gamemap);
-	lastloadedmaplumpnum = W_CheckNumForMap(maplumpname);
-	if (lastloadedmaplumpnum == LUMPERROR)
-		I_Error("Map %s not found.\n", maplumpname);
-
-	R_ReInitColormaps(worldmapheader->palette);
-	if (!addworld)
-	{
-		// Init Boom colormaps.
-		R_ClearColormaps();
-	}
 	CON_SetupBackColormap();
 
-	// SRB2 determines the sky texture to be used depending on the map header.
-	P_InitLevelSky(worldmapheader->skynum);
-
-	P_ResetSpawnpoints();
-
-	P_ResetWaypoints();
-
-	P_MapStart(); // tmthing can be used starting from this point
-
-	P_InitSlopes();
-
-	if (!P_LoadMapFromFile())
-		return false;
-
-	// init anything that P_SpawnSlopes/P_LoadThings needs to know
-	P_InitSpecials();
-
 	// Defaults in case levels don't have them set.
 	sstimer = worldmapheader->sstimer*TICRATE + 6;
 	ssspheres = worldmapheader->ssspheres;
 
-	P_SpawnSlopes(fromnetsave);
-
-	P_SpawnMapThings(!fromnetsave);
-	world->skyboxmo[0] = world->skyboxviewpnts[0];
-	world->skyboxmo[1] = world->skyboxcenterpnts[0];
-
-	for (world->numcoopstarts = 0; world->numcoopstarts < MAXPLAYERS; world->numcoopstarts++)
-		if (!world->playerstarts[world->numcoopstarts])
-			break;
-
-	// set up world state
-	P_SpawnSpecials(fromnetsave);
+	world_t *w = P_InitWorldFromMap(gamemap, worldmapheader, false, fromnetsave);
+	if (w == NULL)
+		I_Error("Map %s not found.\n", G_BuildMapName(gamemap));
 
-	//  ugly hack for P_NetUnArchiveMisc (and P_LoadNetGame)
-	if (!fromnetsave && runforself)
-		P_SpawnPrecipitation();
-
-#ifdef HWRENDER // not win32 only 19990829 by Kin
-	gl_maploaded = false;
-
-	// Create plane polygons.
-	if (rendermode == render_opengl)
-		HWR_LoadLevel();
-#endif
-
-	// oh god I hope this helps
-	// (addendum: apparently it does!
-	//  none of this needs to be done because it's not the beginning of the map when
-	//  a netgame save is being loaded, and could actively be harmful by messing with
-	//  the client's view of the data.)
-	if (!fromnetsave)
-		P_InitGametype(player, addworld);
-
-	if (runforself && !reloadinggamestate)
+	if (!reloadinggamestate)
 	{
 		P_InitCamera();
 		localaiming = 0;
 		localaiming2 = 0;
 	}
 
-	// clear special respawning que
-	world->iquehead = world->iquetail = 0;
-
 	// Remove the loading shit from the screen
-	if (rendermode != render_none && !(titlemapinaction || reloadinggamestate) && runforself)
+	if (rendermode != render_none && !(titlemapinaction || reloadinggamestate))
 		F_WipeColorFill(levelfadecol);
 
-	if (precache || dedicated)
-		R_PrecacheLevel();
-
 	nextmapoverride = 0;
 	skipstats = 0;
 
-	if (!demoplayback)
-	{
-		clientGamedata->mapvisited[gamemap-1] |= MV_VISITED;
-		serverGamedata->mapvisited[gamemap-1] |= MV_VISITED;
-	}
-
-	levelloading = false;
-
-	P_RunCachedActions();
-
-	P_MapEnd(); // tmthing is no longer needed from this point onwards
-
-	// Took me 3 hours to figure out why my progression kept on getting overwritten with the titlemap...
-	if (!titlemapinaction)
-	{
-		if (!lastmaploaded) // Start a new game?
-		{
-			// 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)
-				&& !usedCheats && cursaveslot > 0)
-			{
-				G_SaveGame((UINT32)cursaveslot, gamemap);
-			}
-			// If you're looking for saving sp file progression (distinct from G_SaveGameOver), check G_DoCompleted.
-		}
-		lastmaploaded = gamemap; // HAS to be set after saving!!
-	}
-
-	if (!fromnetsave) // uglier hack
-	{ // to make a newly loaded level start on the second frame.
-		INT32 buf = gametic % BACKUPTICS;
-		for (i = 0; i < MAXPLAYERS; i++)
-		{
-			if (playeringame[i])
-				G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
-		}
-		P_PreTicker(2);
-		P_MapStart(); // just in case MapLoad modifies tmthing
-		LUA_HookInt(gamemap, HOOK(MapLoad));
-		P_MapEnd(); // just in case MapLoad modifies tmthing
-	}
+	P_DoLevelProgression();
 
-	// Done here
-	if (addworld)
-		return true;
+	P_FinishMapLoad(fromnetsave);
 
 	// No render mode or reloading gamestate, stop here.
 	if (rendermode == render_none || reloadinggamestate)
@@ -7938,6 +7923,36 @@ boolean P_LoadLevel(player_t *player, boolean addworld, boolean fromnetsave, boo
 	return true;
 }
 
+boolean P_LoadWorld(boolean fromnetsave)
+{
+	// Initialize sector node list.
+	P_Initsecnode();
+
+	if (nextmapheader->runsoc[0] != '#')
+		P_RunSOC(nextmapheader->runsoc);
+
+	if (cv_runscripts.value && nextmapheader->scriptname[0] != '#')
+		P_RunLevelScript(nextmapheader->scriptname);
+
+	P_InitLevelSettings(nextmapheader, true);
+
+	// Don't cause a wipe here
+	if (wipegamestate != gamestate)
+	{
+		wipegamestate = gamestate;
+		wipestyleflags = 0;
+	}
+
+	world_t *w = P_InitWorldFromMap(gamemap, worldmapheader, true, fromnetsave);
+	if (w == NULL)
+		I_Error("Map %s not found.\n", G_BuildMapName(gamemap));
+
+	P_FinishMapLoad(fromnetsave);
+
+	return true;
+}
+
+
 //
 // P_RunSOC
 //
diff --git a/src/p_setup.h b/src/p_setup.h
index b541e7c9e0..b293d4e65d 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -21,11 +21,8 @@
 // map md5, sent to players via PT_SERVERINFO
 extern unsigned char mapmd5[16];
 
-extern boolean levelloading;
 extern UINT8 levelfadecol;
 
-extern lumpnum_t lastloadedmaplumpnum; // for comparative savegame
-
 /* for levelflat type */
 enum
 {
@@ -83,6 +80,14 @@ INT32 P_AddLevelFlat(const char *flatname, levelflat_t *levelflat);
 INT32 P_AddLevelFlatRuntime(const char *flatname);
 INT32 P_CheckLevelFlat(const char *flatname);
 
+typedef struct actioncache_s
+{
+	struct actioncache_s *next;
+	struct actioncache_s *prev;
+	struct mobj_s *mobj;
+	INT32 statenum;
+} actioncache_t;
+
 extern size_t nummapthings;
 extern mapthing_t *mapthings;
 
@@ -92,10 +97,8 @@ void P_SetupSkyTexture(INT32 skynum);
 void P_ScanThings(INT16 mapnum, INT16 wadnum, INT16 lumpnum);
 #endif
 void P_RespawnThings(void);
-boolean P_LoadLevel(player_t *player, boolean addworld, boolean fromnetsave, boolean reloadinggamestate);
-#ifdef HWRENDER
-void HWR_LoadLevel(void);
-#endif
+boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate);
+boolean P_LoadWorld(boolean fromnetsave);
 
 boolean P_AddWadFile(const char *wadfilename);
 boolean P_AddFolder(const char *folderpath);
diff --git a/src/p_tick.c b/src/p_tick.c
index e9236cd60d..c24f0236d2 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -191,10 +191,9 @@ void Command_CountMobjs_f(void)
 //
 // P_InitThinkers
 //
-void P_InitThinkers(void)
+void P_InitThinkers(world_t *w)
 {
-	UINT8 i;
-	for (i = 0; i < NUM_THINKERLISTS; i++)
+	for (UINT8 i = 0; i < NUM_THINKERLISTS; i++)
 		thlist[i].prev = thlist[i].next = &thlist[i];
 }
 
diff --git a/src/p_world.c b/src/p_world.c
index dd9fa553c2..114d56e4fa 100644
--- a/src/p_world.c
+++ b/src/p_world.c
@@ -59,10 +59,11 @@ world_t *P_InitWorld(void)
 {
 	world_t *w = Z_Calloc(sizeof(world_t), PU_STATIC, NULL);
 	w->gamemap = gamemap;
-	if (!mapheaderinfo[w->gamemap-1])
-		P_AllocMapHeader(w->gamemap-1);
-	w->header = mapheaderinfo[w->gamemap-1];
+	if (!mapheaderinfo[gamemap-1])
+		P_AllocMapHeader(gamemap-1);
+	w->header = mapheaderinfo[gamemap-1];
 	w->thlist = Z_Calloc(sizeof(thinker_t) * NUM_THINKERLISTS, PU_STATIC, NULL);
+	P_InitCachedActions(w);
 	return w;
 }
 
@@ -309,11 +310,6 @@ void P_SwitchWorld(player_t *player, world_t *w)
 		P_ResetPlayer(player);
 	P_MapEnd();
 
-#ifdef HWRENDER
-	if (rendermode == render_opengl)
-		HWR_LoadLevel();
-#endif
-
 	if (local || splitscreen)
 	{
 		S_SetMapMusic(worldmapheader);
@@ -496,3 +492,40 @@ world_t *P_GetMobjWorld(mobj_t *mobj)
 {
 	return (world_t *)mobj->world;
 }
+
+void P_InitCachedActions(world_t *w)
+{
+	actioncache_t *head = &w->actioncachehead;
+
+	memset(head, 0x00, sizeof(actioncache_t));
+
+	w->actioncachehead.prev = w->actioncachehead.next = head;
+}
+
+void P_RunCachedActions(world_t *w)
+{
+	actioncache_t *ac;
+	actioncache_t *next;
+
+	for (ac = w->actioncachehead.next; ac != &w->actioncachehead; ac = next)
+	{
+		var1 = states[ac->statenum].var1;
+		var2 = states[ac->statenum].var2;
+		astate = &states[ac->statenum];
+		if (ac->mobj && !P_MobjWasRemoved(ac->mobj)) // just in case...
+			states[ac->statenum].action.acp1(ac->mobj);
+		next = ac->next;
+		Z_Free(ac);
+	}
+}
+
+void P_AddCachedAction(world_t *w, mobj_t *mobj, INT32 statenum)
+{
+	actioncache_t *newaction = Z_Calloc(sizeof(actioncache_t), PU_LEVEL, NULL);
+	newaction->mobj = mobj;
+	newaction->statenum = statenum;
+	w->actioncachehead.prev->next = newaction;
+	newaction->next = &w->actioncachehead;
+	newaction->prev = w->actioncachehead.prev;
+	w->actioncachehead.prev = newaction;
+}
diff --git a/src/p_world.h b/src/p_world.h
index 151c279ee9..6f71c649f5 100644
--- a/src/p_world.h
+++ b/src/p_world.h
@@ -36,6 +36,7 @@ typedef struct
 	thinker_t *thlist;
 
 	INT32 players;
+	boolean loading;
 
 	size_t numvertexes, numsegs, numsectors, numsubsectors, numnodes, numlines, numsides, nummapthings;
 	vertex_t *vertexes;
@@ -65,6 +66,8 @@ typedef struct
 
 	mobj_t *overlaycap;
 
+	actioncache_t actioncachehead;
+
 	mapheader_t *header;
 
 	fixed_t gravity;
@@ -168,6 +171,10 @@ void P_SetWorld(world_t *w);
 void P_RoamIntoWorld(player_t *player, INT32 mapnum);
 void P_SwitchWorld(player_t *player, world_t *w);
 
+void P_InitCachedActions(world_t *w);
+void P_RunCachedActions(world_t *w);
+void P_AddCachedAction(world_t *w, mobj_t *mobj, INT32 statenum);
+
 world_t *P_GetPlayerWorld(player_t *player);
 
 void P_DetachPlayerWorld(player_t *player);
diff --git a/src/r_fps.c b/src/r_fps.c
index 32d8718ed8..f2f9a15aa0 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -748,13 +748,12 @@ void R_RemoveMobjInterpolator(mobj_t *mobj)
 	}
 }
 
-void R_InitMobjInterpolators(void)
+void R_InitMobjInterpolators(void *wptr)
 {
-	// apparently it's not acceptable to free something already unallocated
-	// Z_Free(world->interpolated_mobjs);
-	world->interpolated_mobjs = NULL;
-	world->interpolated_mobjs_len = 0;
-	world->interpolated_mobjs_capacity = 0;
+	world_t *w = (world_t *)wptr;
+	w->interpolated_mobjs = NULL;
+	w->interpolated_mobjs_len = 0;
+	w->interpolated_mobjs_capacity = 0;
 }
 
 void R_UpdateMobjInterpolators(void *wptr)
diff --git a/src/r_fps.h b/src/r_fps.h
index 9e074cd71f..3e4d5b64a9 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -150,7 +150,7 @@ void R_RestoreLevelInterpolators(void *wptr);
 void R_DestroyLevelInterpolators(void *wptr, thinker_t *thinker);
 
 // Initialize internal mobj interpolator list (e.g. during level loading)
-void R_InitMobjInterpolators(void);
+void R_InitMobjInterpolators(void *wptr);
 // Add interpolation state for the given mobj
 void R_AddMobjInterpolator(mobj_t *mobj);
 // Remove the interpolation state for the given mobj
-- 
GitLab