diff --git a/src/doomstat.h b/src/doomstat.h
index f0bf5d6a9d903834018f30b89e6d117d7bbd39fa..7111806fadd48f418f64f42fd03175bac04b38fe 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -637,6 +637,14 @@ extern tic_t gametic;
 
 #define localgametic leveltime
 
+typedef struct
+{
+	fixed_t x, y, z;
+	angle_t angle;
+	boolean spawn_on_ceiling;
+	boolean spawn_flipped;
+} spawnpoint_t;
+
 enum
 {
 	PLAYER_START_TYPE_COOP,
diff --git a/src/g_game.c b/src/g_game.c
index 5b06381ffd93c5614984792cf706281e246075a8..8eed821290b9cab2b4baf2480c78633d1a586434 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2836,10 +2836,21 @@ void G_SpawnPlayer(INT32 playernum)
 
 void G_MovePlayerToSpawnOrStarpost(INT32 playernum)
 {
-	if (players[playernum].starposttime)
-		P_MovePlayerToStarpost(playernum);
-	else
-		P_MovePlayerToSpawn(playernum, G_FindMapStart(playernum));
+	spawnpoint_t *spawnpoint = LUA_HookPlayerRespawn(&players[playernum]);
+
+	if (spawnpoint == NULL)
+	{
+		if (players[playernum].starposttime)
+			P_MovePlayerToStarpost(playernum);
+		else
+			spawnpoint = G_FindSpawnPoint(playernum);
+	}
+
+	if (spawnpoint)
+	{
+		P_MovePlayerToSpawn(playernum, spawnpoint);
+		Z_Free(spawnpoint);
+	}
 
 	R_ResetMobjInterpolationState(players[playernum].mo);
 
@@ -3174,13 +3185,13 @@ static mapthing_t *G_GetTeamStartForSpawning(INT32 playernum)
 	return spawnpoint;
 }
 
-mapthing_t *G_FindMapStart(INT32 playernum)
+spawnpoint_t *G_FindSpawnPoint(INT32 playernum)
 {
 	if (!playeringame[playernum])
 		return NULL;
 
 	player_t *player = &players[playernum];
-	mapthing_t *spawnpoint = NULL;
+	mapthing_t *mthing = NULL;
 
 	// -- Spectators --
 	// Order in platform gametypes: Coop->DM->Team
@@ -3190,9 +3201,9 @@ mapthing_t *G_FindMapStart(INT32 playernum)
 		// In platform gametypes, spawn in Co-op starts first
 		// Overriden by GTR_DEATHMATCHSTARTS.
 		if (G_PlatformGametype() && !(gametyperules & GTR_DEATHMATCHSTARTS))
-			spawnpoint = G_GetCoopStartForSpawning(playernum);
+			mthing = G_GetCoopStartForSpawning(playernum);
 		else
-			spawnpoint = G_GetMatchStartForSpawning(playernum);
+			mthing = G_GetMatchStartForSpawning(playernum);
 	}
 
 	// -- CTF --
@@ -3200,9 +3211,9 @@ mapthing_t *G_FindMapStart(INT32 playernum)
 	else if (gametyperules & GTR_TEAMFLAGS && G_GametypeHasTeams())
 	{
 		if (player->ctfteam > TEAM_NONE && player->ctfteam < numteams)
-			spawnpoint = G_GetTeamStartForSpawning(playernum);
+			mthing = G_GetTeamStartForSpawning(playernum);
 		else
-			spawnpoint = G_GetMatchStartForSpawning(playernum);
+			mthing = G_GetMatchStartForSpawning(playernum);
 	}
 
 	// -- DM/Tag/etc --
@@ -3213,42 +3224,43 @@ mapthing_t *G_FindMapStart(INT32 playernum)
 		// If the player is not in a team, then this just gets a match start.
 		if (G_GametypeHasTeams() && player->ctfteam > TEAM_NONE && player->ctfteam < numteams)
 		{
-			spawnpoint = G_FindTeamStart(playernum);
+			mthing = G_FindTeamStart(playernum);
 
 			// If no spawn point was returned, but team starts are available, this means there is no good location
 			// for the player to spawn at. In that situation, we display a message telling the player so.
-			if (spawnpoint == NULL && G_AreTeamStartsAvailable(player->ctfteam) && P_IsLocalPlayer(player))
+			if (mthing == NULL && G_AreTeamStartsAvailable(player->ctfteam) && P_IsLocalPlayer(player))
 				CONS_Alert(CONS_WARNING, M_GetText("Could not spawn at any %s starts!\n"), G_GetTeamName(player->ctfteam));
 		}
 
 		// If that failed, no warning is shown. Instead, this will look for a match start, which may
 		// then display a warning if no suitable map start was found.
-		if (spawnpoint == NULL)
-			spawnpoint = G_GetMatchStartForSpawning(playernum);
+		if (mthing == NULL)
+			mthing = G_GetMatchStartForSpawning(playernum);
 	}
 
 	// -- Other game modes --
 	// Order: Coop->DM->Team
 	else
-		spawnpoint = G_GetCoopStartForSpawning(playernum);
+		mthing = G_GetCoopStartForSpawning(playernum);
 
 	//No spawns found. ANYWHERE.
-	if (!spawnpoint)
+	if (!mthing)
 	{
 		if (nummapthings)
 		{
 			if (P_IsLocalPlayer(player))
 				CONS_Alert(CONS_ERROR, M_GetText("No player spawns found, spawning at the first mapthing!\n"));
-			spawnpoint = &mapthings[0];
+			mthing = &mapthings[0];
 		}
 		else
 		{
 			if (P_IsLocalPlayer(player))
 				CONS_Alert(CONS_ERROR, M_GetText("No player spawns found, spawning at the origin!\n"));
+			return NULL;
 		}
 	}
 
-	return spawnpoint;
+	return P_MakeSpawnPointFromMapthing(mthing);
 }
 
 mapthing_t *G_FindBestPlayerStart(INT32 playernum)
diff --git a/src/g_game.h b/src/g_game.h
index bb427d341d31ebebf797f4a49418708bdd9eb8b9..19b9b8fd9e918f0f6bc36b63d624390cbf6400f6 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -179,11 +179,13 @@ void G_AddMatchPlayerStart(mapthing_t *mthing);
 void G_AddTeamPlayerStart(UINT8 team, mapthing_t *mthing);
 void G_CountPlayerStarts(void);
 
-mapthing_t *G_FindMapStart(INT32 playernum);
 mapthing_t *G_FindBestPlayerStart(INT32 playernum);
 mapthing_t *G_FindCoopStart(INT32 playernum);
 mapthing_t *G_FindMatchStart(INT32 playernum);
 mapthing_t *G_FindTeamStart(INT32 playernum);
+
+spawnpoint_t *G_FindSpawnPoint(INT32 playernum);
+
 void G_MovePlayerToSpawnOrStarpost(INT32 playernum);
 void G_SpawnPlayer(INT32 playernum);
 
diff --git a/src/lua_hook.h b/src/lua_hook.h
index cb0e30d870ad3672d1c6076627ff73b90b103d6b..b78a9bbe5b5c5a4f0d9c7dbfedd91e8580014a0e 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -72,6 +72,7 @@ automatically.
 	X (MusicChange),\
 	X (PlayerHeight),/* override player height */\
 	X (PlayerCanEnterSpinGaps),\
+	X (PlayerRespawn),\
 	X (KeyDown),\
 	X (KeyUp),\
 
@@ -148,4 +149,5 @@ int  LUA_HookShouldJingleContinue(player_t *, const char *musname);
 int  LUA_HookPlayerCmd(player_t *, ticcmd_t *);
 int  LUA_HookMusicChange(const char *oldname, struct MusicChange *);
 fixed_t LUA_HookPlayerHeight(player_t *player);
+spawnpoint_t *LUA_HookPlayerRespawn(player_t *player);
 int  LUA_HookPlayerCanEnterSpinGaps(player_t *player);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 039a9677f421dad1ffc4fac29a2335c88c866afa..174c901391114264791ead74e4ef3cab19d38c92 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -264,7 +264,12 @@ typedef struct Hook_State Hook_State;
 typedef void (*Hook_Callback)(Hook_State *);
 
 struct Hook_State {
-	INT32        status;/* return status to calling function */
+	union {
+		int      type_int;
+		fixed_t  type_fixed;
+		boolean  type_bool;
+		void   * type_void_pointer;
+	} status;/* return status to calling function */
 	void       * userdata;
 	int          hook_type;
 	mobjtype_t   mobj_type;/* >0 if mobj hook */
@@ -313,7 +318,7 @@ static boolean init_hook_type
 		const char * string,
 		int          nonzero
 ){
-	hook->status = status;
+	hook->status.type_int = status;
 
 	if (nonzero)
 	{
@@ -531,13 +536,13 @@ static int call_hooks
 static void res_true(Hook_State *hook)
 {
 	if (lua_toboolean(gL, -1))
-		hook->status = true;
+		hook->status.type_bool = true;
 }
 
 static void res_false(Hook_State *hook)
 {
 	if (!lua_isnil(gL, -1) && !lua_toboolean(gL, -1))
-		hook->status = false;
+		hook->status.type_bool = false;
 }
 
 static void res_force(Hook_State *hook)
@@ -545,9 +550,9 @@ static void res_force(Hook_State *hook)
 	if (!lua_isnil(gL, -1))
 	{
 		if (lua_toboolean(gL, -1))
-			hook->status = 1; // Force yes
+			hook->status.type_int = 1; // Force yes
 		else
-			hook->status = 2; // Force no
+			hook->status.type_int = 2; // Force no
 	}
 }
 
@@ -563,7 +568,7 @@ int LUA_HookMobj(mobj_t *mobj, int hook_type)
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_Hook2Mobj(mobj_t *t1, mobj_t *t2, int hook_type)
@@ -575,7 +580,7 @@ int LUA_Hook2Mobj(mobj_t *t1, mobj_t *t2, int hook_type)
 		LUA_PushUserdata(gL, t2, META_MOBJ);
 		call_hooks(&hook, 1, res_force);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 void LUA_HookVoid(int type)
@@ -613,7 +618,7 @@ int LUA_HookPlayer(player_t *player, int hook_type)
 		LUA_PushUserdata(gL, player, META_PLAYER);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
@@ -632,7 +637,7 @@ int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
 		if (hook_type == HOOK(PlayerCmd))
 			hook_cmd_running = false;
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookKey(event_t *event, int hook_type)
@@ -643,7 +648,7 @@ int LUA_HookKey(event_t *event, int hook_type)
 		LUA_PushUserdata(gL, event, META_KEYEVENT);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 void LUA_HookHUD(int hook_type, huddrawlist_h list)
@@ -723,7 +728,7 @@ int LUA_HookMobjLineCollide(mobj_t *mobj, line_t *line)
 		LUA_PushUserdata(gL, line, META_LINE);
 		call_hooks(&hook, 1, res_force);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher)
@@ -735,7 +740,7 @@ int LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher)
 		LUA_PushUserdata(gL, toucher, META_MOBJ);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 static int damage_hook
@@ -759,7 +764,7 @@ static int damage_hook
 		lua_pushinteger(gL, damagetype);
 		call_hooks(&hook, 1, results_handler);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
@@ -790,7 +795,7 @@ int LUA_HookMobjMoveBlocked(mobj_t *t1, mobj_t *t2, line_t *line)
 		LUA_PushUserdata(gL, line, META_LINE);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 typedef struct {
@@ -843,7 +848,7 @@ static void res_botai(Hook_State *hook)
 	B_KeysToTiccmd(botai->tails, botai->cmd,
 			k[0],k[1],k[2],k[3],k[4],k[5],k[6],k[7]);
 
-	hook->status = true;
+	hook->status.type_bool = true;
 }
 
 int LUA_HookBotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
@@ -866,7 +871,7 @@ int LUA_HookBotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 		call_hooks(&hook, 8, res_botai);
 	}
 
-	return hook.status;
+	return hook.status.type_int;
 }
 
 void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
@@ -904,7 +909,7 @@ int LUA_HookPlayerMsg(int source, int target, int flags, char *msg)
 		lua_pushstring(gL, msg); // msg
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookHurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
@@ -918,7 +923,7 @@ int LUA_HookHurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 d
 		lua_pushinteger(gL, damagetype);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 void LUA_HookNetArchive(lua_CFunction archFunc)
@@ -960,7 +965,7 @@ int LUA_HookMapThingSpawn(mobj_t *mobj, mapthing_t *mthing)
 		LUA_PushUserdata(gL, mthing, META_MAPTHING);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookFollowMobj(player_t *player, mobj_t *mobj)
@@ -972,7 +977,7 @@ int LUA_HookFollowMobj(player_t *player, mobj_t *mobj)
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
 		call_hooks(&hook, 1, res_true);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookPlayerCanDamage(player_t *player, mobj_t *mobj)
@@ -984,7 +989,7 @@ int LUA_HookPlayerCanDamage(player_t *player, mobj_t *mobj)
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
 		call_hooks(&hook, 1, res_force);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 void LUA_HookPlayerQuit(player_t *plr, kickreason_t reason)
@@ -1010,7 +1015,7 @@ int LUA_HookTeamSwitch(player_t *player, int newteam, boolean fromspectators, bo
 		lua_pushboolean(gL, tryingscramble);
 		call_hooks(&hook, 1, res_false);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolean forced)
@@ -1026,7 +1031,7 @@ int LUA_HookViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolea
 		call_hooks(&hook, 1, res_force);
 		hud_running = false;
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookSeenPlayer(player_t *player, player_t *seenfriend)
@@ -1041,7 +1046,7 @@ int LUA_HookSeenPlayer(player_t *player, player_t *seenfriend)
 		call_hooks(&hook, 1, res_false);
 		hud_running = false;
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 int LUA_HookShouldJingleContinue(player_t *player, const char *musname)
@@ -1057,7 +1062,7 @@ int LUA_HookShouldJingleContinue(player_t *player, const char *musname)
 		call_hooks(&hook, 1, res_true);
 		hud_running = false;
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
 
 boolean hook_cmd_running = false;
@@ -1090,7 +1095,7 @@ static void res_musicchange(Hook_State *hook)
 	if (lua_isstring(gL, -6))
 		update_music_name(musicchange);
 	else if (lua_isboolean(gL, -6) && lua_toboolean(gL, -6))
-		hook->status = true;
+		hook->status.type_bool = true;
 
 	// output 2: mflags override
 	if (lua_isnumber(gL, -5))
@@ -1145,7 +1150,7 @@ int LUA_HookMusicChange(const char *oldname, struct MusicChange *param)
 		lua_settop(gL, 0);
 	}
 
-	return hook.status;
+	return hook.status.type_int;
 }
 
 static void res_playerheight(Hook_State *hook)
@@ -1157,7 +1162,7 @@ static void res_playerheight(Hook_State *hook)
 		// when an object's height is set to a negative number directly with lua, it's forced to 0 instead.
 		// here, I think it's better to ignore negatives so that they don't replace any results of previous hooks!
 		if (returnedheight >= 0)
-			hook->status = returnedheight;
+			hook->status.type_fixed = returnedheight;
 	}
 }
 
@@ -1169,7 +1174,105 @@ fixed_t LUA_HookPlayerHeight(player_t *player)
 		LUA_PushUserdata(gL, player, META_PLAYER);
 		call_hooks(&hook, 1, res_playerheight);
 	}
-	return hook.status;
+	return hook.status.type_fixed;
+}
+
+static void res_playerrespawn(Hook_State *hook)
+{
+	hook->status.type_void_pointer = NULL;
+
+	if (!lua_isnil(gL, -1))
+	{
+		if (!lua_istable(gL, -1))
+		{
+			CONS_Alert(CONS_WARNING, "table expected in \"PlayerRespawn\" hook, got %s\n", luaL_typename(gL, -1));
+			return;
+		}
+
+		fixed_t height_offset = 0;
+		boolean has_z = false;
+		spawnpoint_t spawnpoint;
+		memset(&spawnpoint, 0, sizeof(spawnpoint));
+
+		lua_pushnil(gL);
+		while (lua_next(gL, -2))
+		{
+			lua_pushvalue(gL, -2);
+
+			const char *key = NULL;
+			if (lua_isstring(gL, -1))
+				key = lua_tostring(gL, -1);
+
+#define TYPEERROR(f, t) \
+	CONS_Alert(CONS_WARNING, \
+		"bad value for \"%s\" in table returned by \"PlayerRespawn\" hook (%s expected, got %s)\n", \
+		f, lua_typename(gL, t), luaL_typename(gL, -2))
+
+#define GETNUMBER(r,f) \
+			if (!strcmp(key, f)) \
+			{ \
+				if (!lua_isnumber(gL, -2)) \
+					TYPEERROR(f, LUA_TNUMBER); \
+				else \
+					r = lua_tonumber(gL, -2); \
+			}
+#define GETNUMBEROPT(r,f,opt) \
+			if (!strcmp(key, f)) \
+			{ \
+				if (!lua_isnumber(gL, -2)) \
+					TYPEERROR(f, LUA_TNUMBER); \
+				else \
+					r = lua_tonumber(gL, -2); \
+				opt = true; \
+			}
+#define GETBOOLEAN(r,f) \
+			if (!strcmp(key, f)) \
+			{ \
+				if (!lua_isboolean(gL, -2)) \
+					TYPEERROR(f, LUA_TBOOLEAN); \
+				else \
+					r = lua_toboolean(gL, -2); \
+			}
+
+			if (key)
+			{
+				GETNUMBER(spawnpoint.x, "x");
+				GETNUMBER(spawnpoint.y, "y");
+				GETNUMBEROPT(spawnpoint.z, "z", has_z);
+				GETNUMBER(height_offset, "height");
+				GETNUMBER(spawnpoint.angle, "angle");
+				GETBOOLEAN(spawnpoint.spawn_on_ceiling, "spawn_on_ceiling");
+				GETBOOLEAN(spawnpoint.spawn_flipped, "spawn_flipped");
+			}
+
+#undef GETNUMBER
+#undef GETNUMBEROPT
+#undef GETBOOLEAN
+#undef TYPEERROR
+
+			lua_pop(gL, 2);
+		}
+
+		if (!has_z)
+			P_SetAbsoluteSpawnPointHeight(&spawnpoint, height_offset);
+		else
+			spawnpoint.z += height_offset;
+
+		spawnpoint_t *result = Z_Calloc(sizeof(spawnpoint_t), PU_STATIC, NULL);
+		memcpy(result, &spawnpoint, sizeof(spawnpoint_t));
+		hook->status.type_void_pointer = result;
+	}
+}
+
+spawnpoint_t *LUA_HookPlayerRespawn(player_t *player)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, -1, HOOK(PlayerRespawn)))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, res_playerrespawn);
+	}
+	return (spawnpoint_t *)hook.status.type_void_pointer;
 }
 
 int LUA_HookPlayerCanEnterSpinGaps(player_t *player)
@@ -1180,5 +1283,5 @@ int LUA_HookPlayerCanEnterSpinGaps(player_t *player)
 		LUA_PushUserdata(gL, player, META_PLAYER);
 		call_hooks(&hook, 1, res_force);
 	}
-	return hook.status;
+	return hook.status.type_int;
 }
diff --git a/src/p_local.h b/src/p_local.h
index f0b0a86e24efd92c503901f678dd216e52d8d65e..a114d43c696998c0490600b040be623abe041c0a 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -15,6 +15,7 @@
 #define __P_LOCAL__
 
 #include "command.h"
+#include "doomstat.h"
 #include "d_player.h"
 #include "d_think.h"
 #include "m_fixed.h"
@@ -126,6 +127,15 @@ void CV_UpdateCam2Dist(void);
 extern fixed_t t_cam_dist, t_cam_height, t_cam_rotate;
 extern fixed_t t_cam2_dist, t_cam2_height, t_cam2_rotate;
 
+// Player spawn points
+void P_SpawnPlayer(INT32 playernum);
+void P_MovePlayerToSpawn(INT32 playernum, spawnpoint_t *mthing);
+void P_MovePlayerToStarpost(INT32 playernum);
+void P_AfterPlayerSpawn(INT32 playernum);
+
+spawnpoint_t *P_MakeSpawnPointFromMapthing(mapthing_t *mthing);
+void P_SetAbsoluteSpawnPointHeight(spawnpoint_t *spawnpoint, fixed_t offset);
+
 INT32 P_GetPlayerControlDirection(player_t *player);
 void P_AddPlayerScore(player_t *player, UINT32 amount);
 void P_StealPlayerScore(player_t *player, UINT32 amount);
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 942edff6529abc3f263950205106ccb2ae30f2ad..0928725c7be5c4898876362c0987b065c0576264 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -11726,51 +11726,46 @@ void P_AfterPlayerSpawn(INT32 playernum)
 }
 
 // spawn it at a playerspawn mapthing
-void P_MovePlayerToSpawn(INT32 playernum, mapthing_t *mthing)
+void P_MovePlayerToSpawn(INT32 playernum, spawnpoint_t *spawnpoint)
 {
-	fixed_t x = 0, y = 0;
-	angle_t angle = 0;
-
-	fixed_t z;
-	sector_t *sector;
-	fixed_t floor, ceiling, ceilingspawn;
+	fixed_t x, y, z;
+	angle_t angle;
 
 	player_t *p = &players[playernum];
 	mobj_t *mobj = p->mo;
 	I_Assert(mobj != NULL);
 
-	if (mthing)
+	if (spawnpoint)
 	{
-		x = mthing->x << FRACBITS;
-		y = mthing->y << FRACBITS;
-		angle = FixedAngle(mthing->angle<<FRACBITS);
+		x = spawnpoint->x;
+		y = spawnpoint->y;
+		angle = spawnpoint->angle;
+	}
+	else
+	{
+		// Spawn at the origin as a desperation move if there is no spawnpoint
+		x = 0;
+		y = 0;
+		angle = 0;
 	}
-	//spawn at the origin as a desperation move if there is no mapthing
 
 	// set Z height
-	sector = R_PointInSubsector(x, y)->sector;
-
-	floor   = P_GetSectorFloorZAt  (sector, x, y);
-	ceiling = P_GetSectorCeilingZAt(sector, x, y);
-	ceilingspawn = ceiling - mobjinfo[MT_PLAYER].height;
+	sector_t *sector = R_PointInSubsector(x, y)->sector;
+	fixed_t floor = P_GetSectorFloorZAt(sector, x, y);
+	fixed_t ceiling = P_GetSectorCeilingZAt(sector, x, y);
+	fixed_t ceilingspawn = ceiling - mobjinfo[MT_PLAYER].height;
 
-	if (mthing)
+	if (spawnpoint)
 	{
-		fixed_t offset = mthing->z << FRACBITS;
+		z = spawnpoint->z;
 
-		// Setting the spawnpoint's args[0] will make the player start on the ceiling
-		// Objectflip inverts
-		if (!!(mthing->args[0]) ^ !!(mthing->options & MTF_OBJECTFLIP))
-			z = ceilingspawn - offset;
-		else
-			z = floor + offset;
-
-		if (mthing->options & MTF_OBJECTFLIP) // flip the player!
+		if (spawnpoint->spawn_flipped) // flip the player!
 		{
 			mobj->eflags |= MFE_VERTICALFLIP;
 			mobj->flags2 |= MF2_OBJECTFLIP;
 		}
-		if (mthing->args[0])
+
+		if (spawnpoint->spawn_on_ceiling)
 			P_SetPlayerMobjState(mobj, S_PLAY_FALL);
 		else if (metalrecording)
 			P_SetPlayerMobjState(mobj, S_PLAY_WAIT);
@@ -11857,6 +11852,40 @@ void P_MovePlayerToStarpost(INT32 playernum)
 		leveltime = p->starposttime;
 }
 
+spawnpoint_t *P_MakeSpawnPointFromMapthing(mapthing_t *mthing)
+{
+	spawnpoint_t *spawnpoint = Z_Malloc(sizeof(spawnpoint_t), PU_STATIC, NULL);
+	spawnpoint->x = mthing->x << FRACBITS;
+	spawnpoint->y = mthing->y << FRACBITS;
+	spawnpoint->angle = FixedAngle(mthing->angle<<FRACBITS);
+
+	// Setting the spawnpoint's args[0] will make the player start on the ceiling
+	// Objectflip inverts
+	spawnpoint->spawn_on_ceiling = mthing->args[0];
+	spawnpoint->spawn_flipped = mthing->options & MTF_OBJECTFLIP;
+
+	P_SetAbsoluteSpawnPointHeight(spawnpoint, mthing->z << FRACBITS);
+
+	return spawnpoint;
+}
+
+void P_SetAbsoluteSpawnPointHeight(spawnpoint_t *spawnpoint, fixed_t offset)
+{
+	sector_t *sector = R_PointInSubsector(spawnpoint->x, spawnpoint->y)->sector;
+	fixed_t floor = P_GetSectorFloorZAt(sector, spawnpoint->x, spawnpoint->y);
+	fixed_t ceilingspawn = P_GetSectorCeilingZAt(sector, spawnpoint->x, spawnpoint->y) - mobjinfo[MT_PLAYER].height;
+
+	if (!!(spawnpoint->spawn_on_ceiling) ^ !!(spawnpoint->spawn_flipped))
+		spawnpoint->z = ceilingspawn - offset;
+	else
+		spawnpoint->z = floor + offset;
+
+	if (spawnpoint->z < floor)
+		spawnpoint->z = floor;
+	else if (spawnpoint->z > ceilingspawn)
+		spawnpoint->z = ceilingspawn;
+}
+
 fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const fixed_t y, const fixed_t dz, const fixed_t offset, const boolean flip, const fixed_t scale)
 {
 	const subsector_t *ss = R_PointInSubsector(x, y);
diff --git a/src/p_mobj.h b/src/p_mobj.h
index fd959588d0651a999dab6814e863a6b0f17867e7..54a160cb171ce1a92a76da144ec64ad5a7ac2c6d 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -486,12 +486,6 @@ void P_AddCachedAction(mobj_t *mobj, INT32 statenum);
 // check mobj against water content, before movement code
 void P_MobjCheckWater(mobj_t *mobj);
 
-// Player spawn points
-void P_SpawnPlayer(INT32 playernum);
-void P_MovePlayerToSpawn(INT32 playernum, mapthing_t *mthing);
-void P_MovePlayerToStarpost(INT32 playernum);
-void P_AfterPlayerSpawn(INT32 playernum);
-
 fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const fixed_t y, const fixed_t dz, const fixed_t offset, const boolean flip, const fixed_t scale);
 fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mthing, const fixed_t x, const fixed_t y);