diff --git a/src/doomstat.h b/src/doomstat.h
index 7ca69e9605d79f946764ce6e52fe2bc178d22183..c141aa946f24367668da9d78abaa3176e1b73d30 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -634,14 +634,33 @@ extern tic_t hidetime;
 //  WAD, partly set at startup time.
 
 extern tic_t gametic;
+
 #define localgametic leveltime
 
+enum
+{
+	PLAYER_START_TYPE_COOP,
+	PLAYER_START_TYPE_MATCH,
+	PLAYER_START_TYPE_TEAM
+};
+
+typedef struct
+{
+	size_t count;
+	size_t capacity;
+	mapthing_t **list;
+} playerstarts_t;
+
+#define MAX_DM_STARTS 64
+
 // Player spawn spots.
-extern mapthing_t *playerstarts[MAXPLAYERS]; // Cooperative
-extern mapthing_t *teamstarts[MAXTEAMS][MAXPLAYERS]; // CTF
+extern playerstarts_t playerstarts;
+extern playerstarts_t deathmatchstarts;
+extern playerstarts_t teamstarts[MAXTEAMS];
 
 #define WAYPOINTSEQUENCESIZE 256
 #define NUMWAYPOINTSEQUENCES 256
+
 extern mobj_t *waypoints[NUMWAYPOINTSEQUENCES][WAYPOINTSEQUENCESIZE];
 extern UINT16 numwaypoints[NUMWAYPOINTSEQUENCES];
 
diff --git a/src/f_finale.c b/src/f_finale.c
index 68e9c3216948704d2e7f6ff0716edcdda08e2291..454bcf2600596683f093d857b4ab29a3359f7f29 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -2444,13 +2444,8 @@ void F_StartTitleScreen(void)
 
 		players[displayplayer].playerstate = PST_DEAD; // Don't spawn the player in dummy (I'm still a filthy cheater)
 
-		// Set Default Position
-		if (playerstarts[0])
-			startpos = playerstarts[0];
-		else if (deathmatchstarts[0])
-			startpos = deathmatchstarts[0];
-		else
-			startpos = NULL;
+		// Set initial camera position
+		startpos = G_GetInitialSpawnPoint();
 
 		if (startpos)
 		{
diff --git a/src/g_demo.c b/src/g_demo.c
index 7026c3391648e6619c3d76a48251c757b555e11b..3700fa5b601526afbcf87743219c75d68f3acdf9 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -2530,7 +2530,7 @@ void G_AddGhost(char *defdemoname)
 	ghosts = gh;
 
 	gh->version = ghostversion;
-	mthing = playerstarts[0];
+	mthing = G_GetPlayerStart(0);
 	I_Assert(mthing);
 	{ // A bit more complex than P_SpawnPlayer because ghosts aren't solid and won't just push themselves out of the ceiling.
 		fixed_t z,f,c;
diff --git a/src/g_game.c b/src/g_game.c
index 9dd63e0b7ae4cb081ddb5616ea086df9694c8185..505714b9ac726b675ecd284caef6d99333bcc5ad 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -157,10 +157,13 @@ INT16 nextmapoverride;
 UINT8 skipstats;
 INT16 nextgametype = -1;
 
-// Pointers to each CTF flag
-mobj_t *flagmobjs[MAXTEAMS];
-// Pointers to CTF spawn location
-mapthing_t *flagpoints[MAXTEAMS];
+// Maintain single and multi player starting spots.
+playerstarts_t playerstarts;
+playerstarts_t deathmatchstarts;
+playerstarts_t teamstarts[MAXTEAMS];
+
+mobj_t *flagmobjs[MAXTEAMS]; // Pointers to each CTF flag
+mapthing_t *flagpoints[MAXTEAMS]; // Pointers to CTF flag spawn locations
 
 struct quake quake;
 
@@ -2855,29 +2858,134 @@ void G_MovePlayerToSpawnOrStarpost(INT32 playernum)
 		P_ResetCamera(&players[playernum], &camera2);
 }
 
-enum
-{
-	PLAYER_START_TYPE_COOP,
-	PLAYER_START_TYPE_MATCH,
-	PLAYER_START_TYPE_TEAM
-};
-
 static boolean G_AreCoopStartsAvailable(void)
 {
-	return numcoopstarts != 0;
+	return playerstarts.count != 0;
 }
 
 static boolean G_AreMatchStartsAvailable(void)
 {
-	return numdmstarts != 0;
+	return deathmatchstarts.count != 0;
 }
 
 static boolean G_AreTeamStartsAvailable(UINT8 team)
 {
-	if (team >= numteams)
+	if (team == TEAM_NONE || team >= numteams)
 		return false;
 
-	return numteamstarts[team] != 0;
+	return teamstarts[team].count != 0;
+}
+
+mapthing_t *G_GetPlayerStart(INT32 i)
+{
+	if (i < 0 || i >= (signed)playerstarts.count || playerstarts.list == NULL)
+		return NULL;
+
+	return playerstarts.list[i];
+}
+
+mapthing_t *G_GetMatchPlayerStart(INT32 i)
+{
+	if (i < 0 || i >= (signed)deathmatchstarts.count || deathmatchstarts.list == NULL)
+		return NULL;
+
+	return deathmatchstarts.list[i];
+}
+
+mapthing_t *G_GetTeamPlayerStart(UINT8 team, INT32 i)
+{
+	if (team == TEAM_NONE || team >= numteams)
+		return NULL;
+
+	if (i < 0 || i >= (signed)teamstarts[team].count || teamstarts[team].list == NULL)
+		return NULL;
+
+	return teamstarts[team].list[i];
+}
+
+mapthing_t *G_GetInitialSpawnPoint(void)
+{
+	if (gametyperules & GTR_DEATHMATCHSTARTS)
+		return G_GetMatchPlayerStart(0);
+	else
+		return G_GetPlayerStart(0);
+}
+
+void G_InitSpawnPointList(playerstarts_t *starts, size_t capacity)
+{
+	Z_Free(starts->list);
+	starts->list = NULL;
+	starts->capacity = capacity;
+	starts->count = 0;
+}
+
+void G_AddSpawnPointToList(playerstarts_t *starts, mapthing_t *mthing)
+{
+	if (starts->list == NULL || starts->count == starts->capacity)
+	{
+		if (starts->capacity == 0 || starts->list != NULL)
+			starts->capacity += 16;
+
+		if (starts->list != NULL)
+			starts->list = Z_Realloc(starts->list, sizeof(mapthing_t *) * starts->capacity, PU_STATIC, NULL);
+		else
+			starts->list = Z_Calloc(sizeof(mapthing_t *) * starts->capacity, PU_STATIC, NULL);
+	}
+
+	starts->list[starts->count++] = mthing;
+}
+
+void G_InitSpawnPoints(void)
+{
+	G_InitSpawnPointList(&playerstarts, MAXPLAYERS);
+	G_InitSpawnPointList(&deathmatchstarts, MAX_DM_STARTS);
+
+	for (UINT8 i = 1; i < MAXTEAMS; i++)
+		G_InitSpawnPointList(&teamstarts[i], MAXPLAYERS);
+}
+
+void G_AddPlayerStart(int index, mapthing_t *mthing)
+{
+	if (playerstarts.list == NULL)
+	{
+		if (playerstarts.capacity == 0)
+			playerstarts.capacity = MAXPLAYERS;
+
+		playerstarts.list = Z_Calloc(sizeof(mapthing_t *) * playerstarts.capacity, PU_STATIC, NULL);
+	}
+
+	if (index >= 0 && index < (signed)playerstarts.capacity)
+		playerstarts.list[index] = mthing;
+}
+
+void G_AddMatchPlayerStart(mapthing_t *mthing)
+{
+	G_AddSpawnPointToList(&deathmatchstarts, mthing);
+}
+
+void G_AddTeamPlayerStart(UINT8 team, mapthing_t *mthing)
+{
+	if (team == TEAM_NONE || team >= numteams)
+		return;
+
+	G_AddSpawnPointToList(&teamstarts[team], mthing);
+}
+
+void G_CountPlayerStarts(void)
+{
+	playerstarts.count = 0;
+
+	if (playerstarts.list == NULL)
+		return;
+
+	for (; playerstarts.count < playerstarts.capacity; playerstarts.count++)
+		if (!playerstarts.list[playerstarts.count])
+			break;
+}
+
+boolean G_IsSpawnPointThingType(UINT16 mthingtype)
+{
+	return mthingtype >= 1 && mthingtype <= 35;
 }
 
 static boolean G_AreTeamStartsAvailableForPlayer(INT32 playernum)
@@ -2891,13 +2999,14 @@ static boolean G_AreTeamStartsAvailableForPlayer(INT32 playernum)
 mapthing_t *G_FindTeamStart(INT32 playernum)
 {
 	UINT8 team = players[playernum].ctfteam;
+
 	if (team != TEAM_NONE && G_AreTeamStartsAvailable(team))
 	{
 		for (INT32 j = 0; j < MAXPLAYERS; j++)
 		{
-			INT32 i = P_RandomKey(numteamstarts[team]);
-			if (G_CheckSpot(playernum, teamstarts[team][i]))
-				return teamstarts[team][i];
+			INT32 i = P_RandomKey(teamstarts[team].count);
+			if (G_CheckSpot(playernum, teamstarts[team].list[i]))
+				return teamstarts[team].list[i];
 		}
 	}
 
@@ -2906,15 +3015,13 @@ mapthing_t *G_FindTeamStart(INT32 playernum)
 
 mapthing_t *G_FindMatchStart(INT32 playernum)
 {
-	INT32 i, j;
-
 	if (G_AreMatchStartsAvailable())
 	{
-		for (j = 0; j < 64; j++)
+		for (INT32 j = 0; j < 64; j++)
 		{
-			i = P_RandomKey(numdmstarts);
-			if (G_CheckSpot(playernum, deathmatchstarts[i]))
-				return deathmatchstarts[i];
+			INT32 i = P_RandomKey(deathmatchstarts.count);
+			if (G_CheckSpot(playernum, deathmatchstarts.list[i]))
+				return deathmatchstarts.list[i];
 		}
 	}
 
@@ -2925,13 +3032,14 @@ mapthing_t *G_FindCoopStart(INT32 playernum)
 {
 	if (G_AreCoopStartsAvailable())
 	{
-		//if there's 6 players in a map with 3 player starts, this spawns them 1/2/3/1/2/3.
-		if (G_CheckSpot(playernum, playerstarts[playernum % numcoopstarts]))
-			return playerstarts[playernum % numcoopstarts];
+		// if there's 6 players in a map with 3 player starts, this spawns them 1/2/3/1/2/3.
+		mapthing_t *spawnpoint = G_GetPlayerStart(playernum % playerstarts.count);
+		if (G_CheckSpot(playernum, spawnpoint))
+			return spawnpoint;
 
-		//Don't bother checking to see if the player 1 start is open.
-		//Just spawn there.
-		return playerstarts[0];
+		// Don't bother checking to see if the player 1 start is open.
+		// Just spawn there.
+		return G_GetPlayerStart(0);
 	}
 
 	return NULL;
diff --git a/src/g_game.h b/src/g_game.h
index 5801b06a4a8d2e2f50ab83bf01c9dbc408adfe5d..b35225dc6ebf9dbbb9b3563e9fe028bfd99fd685 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -166,6 +166,21 @@ void G_FreeMapSearch(mapsearchfreq_t *freq, INT32 freqc);
 /* Match map name by search + 2 digit map code or map number. */
 INT32 G_FindMapByNameOrCode(const char *query, char **foundmapnamep);
 
+void G_InitSpawnPointList(playerstarts_t *starts, size_t capacity);
+void G_AddSpawnPointToList(playerstarts_t *starts, mapthing_t *mthing);
+boolean G_IsSpawnPointThingType(UINT16 mthingtype);
+mapthing_t *G_GetInitialSpawnPoint(void);
+
+mapthing_t *G_GetPlayerStart(INT32 i);
+mapthing_t *G_GetMatchPlayerStart(INT32 i);
+mapthing_t *G_GetTeamPlayerStart(UINT8 team, INT32 i);
+
+void G_InitSpawnPoints(void);
+void G_AddPlayerStart(int index, mapthing_t *mthing);
+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);
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 5c1a9415df696c32289a26dcc290a650eabb5239..65c9bd3980ac5e6c1d8c80275f32af3d0aa31365 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -158,6 +158,8 @@ static const struct {
 	{META_GAMETYPE,     "gametype_t"},
 	{META_TEAM,         "team_t"},
 	{META_TEAMLIST,     "teamlist_t"},
+	{META_TEAMSCORES,   "teamscores"},
+	{META_PLAYERSTARTS, "playerstarts"},
 	{META_SPRITEINFO,   "spriteinfo_t"},
 	{META_PIVOTLIST,    "spriteframepivot_t[]"},
 	{META_FRAMEPIVOT,   "spriteframepivot_t"},
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 8c8de99f48e49625ccfa1fe0bb7184ca9eedef3e..cc856faab1b0f894f6f58d48bf50b6f386b056e6 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1913,7 +1913,7 @@ static int lib_getGametypes(lua_State *L)
 
 	i = luaL_checkinteger(L, 1);
 	if (i < 0 || i >= gametypecount)
-		return luaL_error(L, "gametypes[] index %d out of range (0 - %d)", i, gametypecount-1);
+		return luaL_error(L, "gametypes[] index %d out of range (0 - %d)", i, gametypecount - 1);
 	LUA_PushUserdata(L, &gametypes[i], META_GAMETYPE);
 	return 1;
 }
@@ -2141,7 +2141,7 @@ static int lib_getTeams(lua_State *L)
 
 	i = luaL_checkinteger(L, 1);
 	if (i < 0 || i >= numteams)
-		return luaL_error(L, "teams[] index %d out of range (0 - %d)", i, numteams-1);
+		return luaL_error(L, "teams[] index %d out of range (0 - %d)", i, max(0, (int)numteams - 1));
 	LUA_PushUserdata(L, &teams[i], META_GAMETYPE);
 	return 1;
 }
@@ -2258,7 +2258,7 @@ static int lib_setTeams(lua_State *L)
 	{
 		teamnum = luaL_checkinteger(L, 1);
 		if (teamnum >= numteams)
-			return luaL_error(L, "teams[] index %d out of range (0 - %d)", teamnum, numteams-1);
+			return luaL_error(L, "teams[] index %d out of range (0 - %d)", teamnum, max(0, (int)numteams - 1));
 		team = &teams[teamnum];
 	}
 	luaL_checktype(L, 2, LUA_TTABLE);
@@ -2458,6 +2458,61 @@ static int teamscores_len(lua_State *L)
 	return 1;
 }
 
+///////////////////
+// PLAYER STARTS //
+///////////////////
+
+static int playerstarts_get(lua_State *L)
+{
+	playerstarts_t *starts = *((playerstarts_t **)luaL_checkudata(L, 1, META_PLAYERSTARTS));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= (signed)starts->count)
+		return luaL_error(L, "player start index %d out of range (0 - %d)", i, max(0, (int)starts->count - 1));
+	LUA_PushUserdata(L, starts->list[i], META_MAPTHING);
+	return 1;
+}
+
+static int playerstarts_set(lua_State *L)
+{
+	playerstarts_t *starts = *((playerstarts_t **)luaL_checkudata(L, 1, META_PLAYERSTARTS));
+	int i = luaL_checkinteger(L, 2);
+	mapthing_t *mthing = *((mapthing_t **)luaL_checkudata(L, 3, META_MAPTHING));
+	if (i < 0 || i >= (signed)starts->count)
+		return luaL_error(L, "player start index %d out of range (0 - %d)", i, max(0, (int)starts->count - 1));
+	if (!mthing)
+		return LUA_ErrInvalid(L, "mapthing_t");
+	starts->list[i] = mthing;
+	return 0;
+}
+
+static int playerstarts_len(lua_State *L)
+{
+	playerstarts_t *starts = *((playerstarts_t **)luaL_checkudata(L, 1, META_PLAYERSTARTS));
+	lua_pushinteger(L, starts->count);
+	return 1;
+}
+
+static int lib_getTeamstarts(lua_State *L)
+{
+	int i;
+	lua_remove(L, 1);
+
+	i = luaL_checkinteger(L, 1);
+	if (i <= 0 || i >= numteams)
+		return luaL_error(L, "team index %d out of range (1 - %d)", i, numteams - 1);
+
+	LUA_PushUserdata(L, &teamstarts[i], META_PLAYERSTARTS);
+
+	return 1;
+}
+
+// #teamstarts -> MAXTEAMS
+static int lib_teamstartslen(lua_State *L)
+{
+	lua_pushinteger(L, MAXTEAMS);
+	return 1;
+}
+
 //////////////////////////////
 //
 // Now push all these functions into the Lua state!
@@ -2479,6 +2534,7 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_TEAM, team_get, team_set, team_num);
 	LUA_RegisterUserdataMetatable(L, META_TEAMLIST, teamlist_get, teamlist_set, teamlist_len);
 	LUA_RegisterUserdataMetatable(L, META_TEAMSCORES, teamscores_get, teamscores_set, teamscores_len);
+	LUA_RegisterUserdataMetatable(L, META_PLAYERSTARTS, playerstarts_get, playerstarts_set, playerstarts_len);
 	LUA_RegisterUserdataMetatable(L, META_SKINCOLOR, skincolor_get, skincolor_set, skincolor_num);
 	LUA_RegisterUserdataMetatable(L, META_COLORRAMP, colorramp_get, colorramp_set, colorramp_len);
 	LUA_RegisterUserdataMetatable(L, META_SFXINFO, sfxinfo_get, sfxinfo_set, sfxinfo_num);
@@ -2497,6 +2553,7 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterGlobalUserdata(L, "mobjinfo", lib_getMobjInfo, lib_setMobjInfo, lib_mobjinfolen);
 	LUA_RegisterGlobalUserdata(L, "gametypes", lib_getGametypes, NULL, lib_gametypeslen);
 	LUA_RegisterGlobalUserdata(L, "teams", lib_getTeams, lib_setTeams, lib_teamslen);
+	LUA_RegisterGlobalUserdata(L, "teamstarts", lib_getTeamstarts, NULL, lib_teamstartslen);
 	LUA_RegisterGlobalUserdata(L, "skincolors", lib_getSkinColor, lib_setSkinColor, lib_skincolorslen);
 	LUA_RegisterGlobalUserdata(L, "spriteinfo", lib_getSpriteInfo, lib_setSpriteInfo, lib_spriteinfolen);
 	LUA_RegisterGlobalUserdata(L, "sfxinfo", lib_getSfxInfo, lib_setSfxInfo, lib_sfxlen);
diff --git a/src/lua_libs.h b/src/lua_libs.h
index f611019df9bc35da8b115e979588e72738f24267..78a8a8da985ddf74450863d821664723b3b10bd1 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -33,6 +33,7 @@ extern boolean ignoregameinputs;
 #define META_TEAM "TEAM_T*"
 #define META_TEAMLIST "TEAMLIST_T*"
 #define META_TEAMSCORES "TEAMSCORES"
+#define META_PLAYERSTARTS "PLAYERSTARTS"
 #define META_PIVOTLIST "SPRITEFRAMEPIVOT_T[]"
 #define META_FRAMEPIVOT "SPRITEFRAMEPIVOT_T*"
 
diff --git a/src/lua_script.c b/src/lua_script.c
index bca8197709edb5dd169cd58a58c3ee9fa5815ac3..c60f434f41b26feaf3607404387b5ef13103412e 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -213,6 +213,12 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"paused")) {
 		lua_pushboolean(L, paused);
 		return 1;
+	} else if (fastcmp(word, "playerstarts")) {
+		LUA_PushUserdata(L, &playerstarts, META_PLAYERSTARTS);
+		return 1;
+	} else if (fastcmp(word, "matchstarts")) {
+		LUA_PushUserdata(L, &deathmatchstarts, META_PLAYERSTARTS);
+		return 1;
 	} else if (fastcmp(word,"bluescore")) {
 		lua_pushinteger(L, teamscores[G_GetTeam(2)]);
 		return 1;
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 0cb8eed2fe352b2db9486b3863a16e1a8e6894e3..022b62f3ba792d6c3c2d008634a2d36c49b036ca 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -513,13 +513,14 @@ void Command_Teleport_f(void)
 			mapthing_t *mt;
 			fixed_t offset;
 
-			if (starpostpath >= numcoopstarts)
+			int numstarts = (signed)playerstarts.count;
+			if (starpostpath >= numstarts)
 			{
-				CONS_Alert(CONS_NOTICE, M_GetText("Player %d spawnpoint not found (%d max).\n"), starpostpath+1, numcoopstarts-1);
+				CONS_Alert(CONS_NOTICE, M_GetText("Player %d spawnpoint not found (%d max).\n"), starpostpath+1, numstarts-1);
 				return;
 			}
 
-			mt = playerstarts[starpostpath]; // Given above check, should never be NULL.
+			mt = G_GetPlayerStart(starpostpath); // Given above check, should never be NULL.
 			intx = mt->x<<FRACBITS;
 			inty = mt->y<<FRACBITS;
 			offset = mt->z<<FRACBITS;
diff --git a/src/p_mobj.c b/src/p_mobj.c
index b06b529138b254fbed9b0d484368308b52e7a5b4..7df0454fb8cf1709188bbcd0d0a0183aa9435bd1 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -45,6 +45,8 @@ actioncache_t actioncachehead;
 
 static mobj_t *overlaycap = NULL;
 
+static mobj_t *P_RespawnMapThing(mapthing_t *mthing);
+
 #define MAXHUNTEMERALDS 64
 mapthing_t *huntemeralds[MAXHUNTEMERALDS];
 INT32 numhuntemeralds;
@@ -11560,7 +11562,7 @@ void P_RespawnSpecials(void)
 #endif
 
 	if (mthing)
-		P_SpawnMapThing(mthing);
+		P_RespawnMapThing(mthing);
 
 	// pull it from the que
 	iquetail = (iquetail+1)&(ITEMQUESIZE-1);
@@ -11963,17 +11965,6 @@ fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mt
 	return P_GetMobjSpawnHeight(mobjtype, x, y, dz, offset, flip, mthing->scale, absolutez);
 }
 
-static void P_SetTeamStart(UINT8 team, mapthing_t *mthing)
-{
-	if (team != TEAM_NONE && team < numteams && numteamstarts[team] < MAXPLAYERS)
-	{
-		teamstarts[team][numteamstarts[team]] = mthing;
-		numteamstarts[team]++;
-		numteamstarts[0]++;
-		mthing->type = 0;
-	}
-}
-
 static boolean P_SpawnNonMobjMapThing(mapthing_t *mthing)
 {
 #if MAXPLAYERS > 32
@@ -11983,33 +11974,28 @@ static boolean P_SpawnNonMobjMapThing(mapthing_t *mthing)
 	{
 		// save spots for respawning in network games
 		if (!metalrecording)
-			playerstarts[mthing->type - 1] = mthing;
+			G_AddPlayerStart(mthing->type - 1, mthing);
 		return true;
 	}
 	else if (mthing->type == 33) // Match starts
 	{
-		if (numdmstarts < MAX_DM_STARTS)
-		{
-			deathmatchstarts[numdmstarts] = mthing;
-			mthing->type = 0;
-			numdmstarts++;
-		}
+		G_AddMatchPlayerStart(mthing);
 		return true;
 	}
 	else if (mthing->type == 34) // Red CTF starts
 	{
-		P_SetTeamStart(G_GetTeam(TEAM_RED), mthing);
+		G_AddTeamPlayerStart(G_GetTeam(TEAM_RED), mthing);
 		return true;
 	}
 	else if (mthing->type == 35) // Blue CTF starts
 	{
-		P_SetTeamStart(G_GetTeam(TEAM_BLUE), mthing);
+		G_AddTeamPlayerStart(G_GetTeam(TEAM_BLUE), mthing);
 		return true;
 	}
 	else if (metalrecording && mthing->type == mobjinfo[MT_METALSONIC_RACE].doomednum)
 	{
 		// If recording, you ARE Metal Sonic. Do not spawn it, do not save normal spawnpoints.
-		playerstarts[0] = mthing;
+		G_AddPlayerStart(0, mthing);
 		return true;
 	}
 	else if (mthing->type == 750 // Slope vertex point (formerly chaos spawn)
@@ -13435,11 +13421,6 @@ static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y,
 	return mobj;
 }
 
-//
-// P_SpawnMapThing
-// The fields of the mapthing should
-// already be in host byte order.
-//
 mobj_t *P_SpawnMapThing(mapthing_t *mthing)
 {
 	mobjtype_t i;
@@ -13476,6 +13457,14 @@ mobj_t *P_SpawnMapThing(mapthing_t *mthing)
 	return P_SpawnMobjFromMapThing(mthing, x, y, z, i);
 }
 
+static mobj_t *P_RespawnMapThing(mapthing_t *mthing)
+{
+	if (G_IsSpawnPointThingType(mthing->type))
+		return NULL;
+
+	return P_SpawnMapThing(mthing);
+}
+
 void P_SpawnHoop(mapthing_t *mthing)
 {
 	if (metalrecording)
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 4c54001fe7adf4b2a7e05161dda3c1b6a734a33b..73eda425a7676ad78e9e97e9f312794197afcad9 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -4300,6 +4300,101 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride)
 	playeringame[consoleplayer] = true;
 }
 
+static void ArchiveSpawnPoints(void)
+{
+	// Write player starts
+	WRITEUINT8(save_p, playerstarts.capacity);
+	for (size_t i = 0; i < playerstarts.capacity; i++)
+	{
+		if (playerstarts.list[i] == NULL)
+			WRITEUINT16(save_p, 0xFFFF);
+		else
+		{
+			size_t mapthingnum = playerstarts.list[i] - mapthings;
+			WRITEUINT16(save_p, mapthingnum);
+		}
+	}
+
+	// Write Match starts
+	WRITEUINT8(save_p, deathmatchstarts.count);
+	for (size_t i = 0; i < deathmatchstarts.count; i++)
+	{
+		size_t mapthingnum = deathmatchstarts.list[i] - mapthings;
+		WRITEUINT16(save_p, mapthingnum);
+	}
+
+	// Write team starts
+	for (UINT8 team = 1; team < numteams; team++)
+	{
+		WRITEUINT8(save_p, teamstarts[team].count);
+		for (size_t i = 0; i < teamstarts[team].count; i++)
+		{
+			size_t mapthingnum = teamstarts[team].list[i] - mapthings;
+			WRITEUINT16(save_p, mapthingnum);
+		}
+	}
+}
+
+static boolean ReadPlayerStarts(playerstarts_t *starts)
+{
+	size_t count = (size_t)READUINT8(save_p);
+
+	for (size_t i = 0; i < count; i++)
+	{
+		UINT16 mapthingnum = READUINT16(save_p);
+		if (mapthingnum < nummapthings)
+			G_AddSpawnPointToList(starts, &mapthings[mapthingnum]);
+		else
+		{
+			CONS_Alert(CONS_ERROR, "Player start %d has invalid map thing number %d\n", (int)i, mapthingnum);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static boolean RestoreSpawnPoints(void)
+{
+	// Read player starts
+	playerstarts.capacity = READUINT8(save_p);
+
+	for (size_t i = 0; i < playerstarts.capacity; i++)
+	{
+		UINT16 mapthingnum = READUINT16(save_p);
+		if (mapthingnum == 0xFFFF)
+			G_AddPlayerStart(i, NULL);
+		else if (mapthingnum < nummapthings)
+			G_AddPlayerStart(i, &mapthings[mapthingnum]);
+		else
+		{
+			CONS_Alert(CONS_ERROR, "Player start %d has invalid map thing number %d\n", (int)i, mapthingnum);
+			return false;
+		}
+	}
+
+	G_CountPlayerStarts();
+
+	// Read Match starts
+	if (!ReadPlayerStarts(&deathmatchstarts))
+	{
+		CONS_Alert(CONS_ERROR, "Failed to read Match player starts\n");
+		return false;
+	}
+
+	// Read team starts
+	for (UINT8 team = 1; team < numteams; team++)
+	{
+		if (!ReadPlayerStarts(&teamstarts[team]))
+		{
+			CONS_Alert(CONS_ERROR, "Failed to read team %d player starts\n", team);
+			return false;
+		}
+	}
+
+	return true;
+}
+
 static void P_NetArchiveMisc(boolean resending)
 {
 	INT32 i;
@@ -4349,6 +4444,8 @@ static void P_NetArchiveMisc(boolean resending)
 	for (i = 0; i < MAXTEAMS; i++)
 		WRITEUINT32(save_p, teamscores[i]);
 
+	ArchiveSpawnPoints();
+
 	WRITEINT32(save_p, modulothing);
 
 	WRITEINT16(save_p, autobalance);
@@ -4444,6 +4541,9 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 	for (i = 0; i < MAXTEAMS; i++)
 		teamscores[i] = READUINT32(save_p);
 
+	if (!RestoreSpawnPoints())
+		I_Error("Savegame corrupted");
+
 	modulothing = READINT32(save_p);
 
 	autobalance = READINT16(save_p);
diff --git a/src/p_setup.c b/src/p_setup.c
index 4ba07397e732e47c31f098eb99d0af041e68c5b2..c9cbc7380635013cb87c117fa043fced23963465 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -146,13 +146,6 @@ mobj_t **blocklinks;
 //
 UINT8 *rejectmatrix;
 
-// Maintain single and multi player starting spots.
-INT32 numdmstarts, numcoopstarts, numteamstarts[MAXTEAMS];
-
-mapthing_t *deathmatchstarts[MAX_DM_STARTS];
-mapthing_t *playerstarts[MAXPLAYERS];
-mapthing_t *teamstarts[MAXTEAMS][MAXPLAYERS];
-
 // Maintain waypoints
 mobj_t *waypoints[NUMWAYPOINTSEQUENCES][WAYPOINTSEQUENCESIZE];
 UINT16 numwaypoints[NUMWAYPOINTSEQUENCES];
@@ -873,7 +866,7 @@ static void P_SpawnEmeraldHunt(void)
 	}
 }
 
-static void P_SpawnMapThings(boolean spawnemblems)
+static void P_SpawnMapThings(boolean respawning, boolean fromnetsave)
 {
 	size_t i;
 	mapthing_t *mt;
@@ -898,22 +891,31 @@ static void P_SpawnMapThings(boolean spawnemblems)
 
 	for (i = 0, mt = mapthings; i < nummapthings; i++, mt++)
 	{
-		if (mt->type == 1700 // MT_AXIS
-			|| mt->type == 1701 // MT_AXISTRANSFER
-			|| mt->type == 1702) // MT_AXISTRANSFERLINE
+		UINT16 type = mt->type;
+
+		if (type == 1700 // MT_AXIS
+			|| type == 1701 // MT_AXISTRANSFER
+			|| type == 1702) // MT_AXISTRANSFERLINE
 			continue; // These were already spawned
 
-		if (!spawnemblems && mt->type == mobjinfo[MT_EMBLEM].doomednum)
+		if (fromnetsave && type == mobjinfo[MT_EMBLEM].doomednum)
 			continue;
 
 		mt->mobj = NULL;
 
-		if (mt->type >= 600 && mt->type <= 611) // item patterns
+		if (type >= 600 && type <= 611) // item patterns
 			P_SpawnItemPattern(mt, false);
-		else if (mt->type == 1713) // hoops
+		else if (type == 1713) // hoops
 			P_SpawnHoop(mt);
 		else // Everything else
+		{
+			// Don't respawn player starts if the game is either respawning all things,
+			// or if the game is loading a savegame.
+			if (G_IsSpawnPointThingType(mt->type) && (respawning || fromnetsave))
+				continue;
+
 			P_SpawnMapThing(mt);
+		}
 	}
 
 	// random emeralds for hunt
@@ -7178,7 +7180,7 @@ void P_RespawnThings(void)
 	localaiming = 0;
 	localaiming2 = 0;
 
-	P_SpawnMapThings(true);
+	P_SpawnMapThings(true, false);
 
 	// restore skybox viewpoint/centerpoint if necessary, set them to defaults if we can't do that
 	skyboxmo[0] = skyboxviewpnts[(viewid >= 0) ? viewid : 0];
@@ -7237,24 +7239,9 @@ static void P_ForceCharacter(const char *forcecharskin)
 
 static void P_ResetSpawnpoints(void)
 {
-	UINT8 i, j;
-
-	numdmstarts = 0;
-
-	// reset the player starts
-	for (i = 0; i < MAXPLAYERS; i++)
-		playerstarts[i] = NULL;
-
-	for (i = 0; i < MAX_DM_STARTS; i++)
-		deathmatchstarts[i] = NULL;
-
-	for (i = 0; i < MAXTEAMS; i++)
-	{
-		numteamstarts[i] = 0;
+	UINT8 i;
 
-		for (j = 0; j < MAXPLAYERS; j++)
-			teamstarts[i][j] = NULL;
-	}
+	G_InitSpawnPoints();
 
 	for (i = 0; i < 2; i++)
 		skyboxmo[i] = NULL;
@@ -7441,12 +7428,7 @@ static void P_SetupCamera(void)
 	}
 	else
 	{
-		mapthing_t *thing;
-
-		if (gametyperules & GTR_DEATHMATCHSTARTS)
-			thing = deathmatchstarts[0];
-		else
-			thing = playerstarts[0];
+		mapthing_t *thing = G_GetInitialSpawnPoint();
 
 		if (thing)
 		{
@@ -7883,13 +7865,12 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 	P_SpawnSlopes(fromnetsave);
 
-	P_SpawnMapThings(!fromnetsave);
+	P_SpawnMapThings(false, fromnetsave);
 	skyboxmo[0] = skyboxviewpnts[0];
 	skyboxmo[1] = skyboxcenterpnts[0];
 
-	for (numcoopstarts = 0; numcoopstarts < MAXPLAYERS; numcoopstarts++)
-		if (!playerstarts[numcoopstarts])
-			break;
+	if (!fromnetsave)
+		G_CountPlayerStarts();
 
 	// set up world state
 	P_SpawnSpecials(fromnetsave);
diff --git a/src/p_setup.h b/src/p_setup.h
index 6e00a5b5cba59385e3fe4713213292c694401e2b..63bd90117e90b66606f169d928175cac7dd524ad 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -21,11 +21,6 @@
 // map md5, sent to players via PT_SERVERINFO
 extern unsigned char mapmd5[16];
 
-// Player spawn spots for deathmatch.
-#define MAX_DM_STARTS 64
-extern mapthing_t *deathmatchstarts[MAX_DM_STARTS];
-extern INT32 numdmstarts, numcoopstarts, numteamstarts[MAXTEAMS];
-
 extern boolean levelloading;
 extern UINT8 levelfadecol;