From bd5542c182723b211a49b3382dbf26ec75171b1b Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sat, 5 Aug 2023 20:14:44 -0300
Subject: [PATCH] Implement adding new teams

---
 src/deh_lua.c          |  29 ++++
 src/deh_soc.c          |  51 +++++-
 src/doomstat.h         |  13 +-
 src/g_game.c           |  81 +++++++--
 src/g_game.h           |   2 +
 src/hu_stuff.c         |  12 +-
 src/lua_baselib.c      |  41 ++++-
 src/lua_infolib.c      | 371 ++++++++++++++++++++++++++++++++++++++++-
 src/lua_libs.h         |   2 +
 src/lua_script.c       |  30 +++-
 src/m_menu.c           |  15 +-
 src/netcode/d_netcmd.c |   2 +-
 src/p_inter.c          |  34 +++-
 src/p_saveg.c          |  10 --
 14 files changed, 628 insertions(+), 65 deletions(-)

diff --git a/src/deh_lua.c b/src/deh_lua.c
index feb312095f..3571212f7f 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -131,6 +131,22 @@ static inline int lib_freeslot(lua_State *L)
 			if (i == NUMCOLORFREESLOTS)
 				CONS_Alert(CONS_WARNING, "Ran out of free skincolor slots!\n");
 		}
+		else if (fastcmp(type, "TEAM"))
+		{
+			UINT8 i;
+			for (i = 0; i < MAXTEAMS; i++)
+				if (!teamnames[i]) {
+					CONS_Printf("Team TEAM_%s allocated.\n",word);
+					teamnames[i] = Z_Malloc(strlen(word)+1, PU_STATIC, NULL);
+					strcpy(teamnames[i],word);
+					lua_pushinteger(L, i);
+					numteams++;
+					r++;
+					break;
+				}
+			if (i == MAXTEAMS)
+				CONS_Alert(CONS_WARNING, "Ran out of free team slots!\n");
+		}
 		else if (fastcmp(type, "SPR2"))
 		{
 			// Search if we already have an SPR2 by that name...
@@ -561,6 +577,19 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 			}
 		return luaL_error(L, "skincolor '%s' could not be found.\n", word);
 	}
+	else if (fastncmp("TEAM_",word,5)) {
+		p = word+5;
+		for (i = 0; i < MAXTEAMS; i++)
+		{
+			if (!teamnames[i])
+				break;
+			if (fastcmp(p, teamnames[i])) {
+				CacheAndPushConstant(L, word, i);
+				return 1;
+			}
+		}
+		return luaL_error(L, "team '%s' could not be found.\n", word);
+	}
 	else if (fastncmp("GRADE_",word,6))
 	{
 		p = word+6;
diff --git a/src/deh_soc.c b/src/deh_soc.c
index d37ff11cfc..0130b9a7a4 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -473,6 +473,16 @@ void readfreeslots(MYFILE *f)
 						break;
 					}
 			}
+			else if (fastcmp(type, "TEAM"))
+			{
+				for (i = 0; i < MAXTEAMS; i++)
+					if (!teamnames[i]) {
+						teamnames[i] = Z_Malloc(strlen(word)+1, PU_STATIC, NULL);
+						strcpy(teamnames[i],word);
+						numteams++;
+						break;
+					}
+			}
 			else if (fastcmp(type, "SPR2"))
 			{
 				// Search if we already have an SPR2 by that name...
@@ -1139,15 +1149,20 @@ void readgametype(MYFILE *f, INT32 num)
 	INT32 i, j;
 
 	char *gtname = gametypes[num].name;
-	char gtconst[32];
+	char gtconst[MAXLINELEN];
 	char gtdescription[441];
 
+	UINT8 teamcount = 0;
+	UINT8 teamlist[MAXTEAMS];
+
 	UINT8 newgtleftcolor, newgtrightcolor;
 	boolean has_desc_colors[2] = { false, false };
 
 	memset(gtconst, 0, sizeof(gtconst));
 	memset(gtdescription, 0, sizeof(gtconst));
 
+	strcpy(gtdescription, "???");
+
 	do
 	{
 		if (myfgets(s, MAXLINELEN, f))
@@ -1307,6 +1322,35 @@ void readgametype(MYFILE *f, INT32 num)
 					gametypes[num].typeoflevel = tol;
 				}
 			}
+			// Teams
+			else if (fastcmp(word, "TEAMLIST"))
+			{
+				tmp = strtok(word2,",");
+				do {
+					if (teamcount == MAXTEAMS)
+					{
+						deh_warning("readgametype %s: too many teams\n", gtname);
+						break;
+					}
+					UINT8 team_id = TEAM_NONE;
+					for (i = 1; i < MAXTEAMS; i++)
+					{
+						if (!teamnames[i])
+							break;
+						if (fasticmp(tmp, teamnames[i]))
+						{
+							team_id = i;
+							break;
+						}
+					}
+					if (team_id == TEAM_NONE)
+						deh_warning("readgametype %s: unknown team %s\n", gtname, tmp);
+					else
+					{
+						teamlist[teamcount++] = team_id;
+					}
+				} while((tmp = strtok(NULL,",")) != NULL);
+			}
 			// This SOC probably provided gametype rules as words, instead of using the RULES keyword.
 			// (For example, "NOSPECTATORSPAWN = TRUE")
 			else
@@ -1339,6 +1383,11 @@ void readgametype(MYFILE *f, INT32 num)
 	if (has_desc_colors[1])
 		G_SetGametypeDescriptionRightColor(num, newgtrightcolor);
 
+	// Copy the teams
+	gametypes[num].teams.num = teamcount;
+	if (teamcount)
+		memcpy(gametypes[num].teams.list, teamlist, sizeof(teamlist[0]) * teamcount);
+
 	// Write the constant name.
 	if (gametypes[num].constant_name == NULL)
 	{
diff --git a/src/doomstat.h b/src/doomstat.h
index a0cc83c1a1..5b30f4bb36 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -407,7 +407,7 @@ typedef struct
 {
 	char *name;
 	char *flag_name;
-	UINT8 flag;
+	UINT16 flag;
 	UINT32 flag_mobj_type;
 	UINT16 color;
 	UINT16 weapon_color;
@@ -418,6 +418,8 @@ typedef struct
 extern team_t teams[MAXTEAMS];
 extern UINT8 numteams;
 
+extern char *teamnames[MAXTEAMS];
+
 #define NUMGAMETYPEFREESLOTS 128
 
 // Gametypes
@@ -485,6 +487,12 @@ enum
 	RANKINGS_RACE
 };
 
+typedef struct
+{
+	UINT8 num;
+	UINT8 list[MAXTEAMS];
+} teamlist_t;
+
 typedef struct
 {
 	char *name;
@@ -495,8 +503,7 @@ typedef struct
 	INT16 rankings_type;
 	INT32 pointlimit;
 	INT32 timelimit;
-	UINT8 numteams;
-	UINT8 teams[MAXTEAMS];
+	teamlist_t teams;
 } gametype_t;
 
 extern gametype_t gametypes[NUMGAMETYPES];
diff --git a/src/g_game.c b/src/g_game.c
index 5bf91c1b92..ad7866c8ae 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3460,8 +3460,10 @@ gametype_t gametypes[NUMGAMETYPES] = {
 		// default settings for match: timelimit 10 mins, no pointlimit
 		.timelimit = 10,
 		.pointlimit = 0,
-		.numteams = 2,
-		.teams = { TEAM_RED, TEAM_BLUE }
+		.teams = {
+			.num = 2,
+			.list = { TEAM_RED, TEAM_BLUE }
+		}
 	},
 	// GT_TAG
 	{
@@ -3494,8 +3496,10 @@ gametype_t gametypes[NUMGAMETYPES] = {
 		// default settings for CTF: no timelimit, pointlimit 5
 		.timelimit = 0,
 		.pointlimit = 5,
-		.numteams = 2,
-		.teams = { TEAM_RED, TEAM_BLUE }
+		.teams = {
+			.num = 2,
+			.list = { TEAM_RED, TEAM_BLUE }
+		}
 	},
 };
 
@@ -3520,6 +3524,8 @@ team_t teams[MAXTEAMS] = {
 	}
 };
 
+char *teamnames[MAXTEAMS];
+
 static void G_InitTeams(void)
 {
 	numteams = 3;
@@ -3527,6 +3533,7 @@ static void G_InitTeams(void)
 
 	teams[TEAM_NONE].name = Z_StrDup("None");
 	teams[TEAM_NONE].flag_name = Z_StrDup("Thingmabob");
+	teamnames[TEAM_NONE] = Z_StrDup("NONE");
 
 	teams[TEAM_RED].name = Z_StrDup("Red");
 	teams[TEAM_RED].flag_name = Z_StrDup("Red Flag");
@@ -3534,6 +3541,7 @@ static void G_InitTeams(void)
 	teams[TEAM_RED].icons[TEAM_ICON_FLAG] = Z_StrDup("RFLAGICO");
 	teams[TEAM_RED].icons[TEAM_ICON_GOT_FLAG] = Z_StrDup("GOTRFLAG");
 	teams[TEAM_RED].icons[TEAM_ICON_MISSING_FLAG] = Z_StrDup("NONICON2");
+	teamnames[TEAM_RED] = Z_StrDup("RED");
 
 	teams[TEAM_BLUE].name = Z_StrDup("Blue");
 	teams[TEAM_BLUE].flag_name = Z_StrDup("Blue Flag");
@@ -3541,6 +3549,7 @@ static void G_InitTeams(void)
 	teams[TEAM_BLUE].icons[TEAM_ICON_FLAG] = Z_StrDup("BFLAGICO");
 	teams[TEAM_BLUE].icons[TEAM_ICON_GOT_FLAG] = Z_StrDup("GOTBFLAG");
 	teams[TEAM_BLUE].icons[TEAM_ICON_MISSING_FLAG] = Z_StrDup("NONICON");
+	teamnames[TEAM_BLUE] = Z_StrDup("BLUE");
 
 	G_UpdateTeamSelection();
 }
@@ -3567,11 +3576,20 @@ void G_UpdateTeamSelection(void)
 		i++;
 	}
 
-	for (UINT8 j = 1; j < teamsingame; j++, i++)
+	if (G_GametypeHasTeams())
+	{
+		for (UINT8 j = 1; j < teamsingame; j++, i++)
+		{
+			UINT8 team = G_GetTeam(j);
+			dummyteam_cons_t[i].value = team;
+			dummyteam_cons_t[i].strvalue = teams[team].name;
+		}
+	}
+	else
 	{
-		UINT8 team = G_GetTeam(j);
-		dummyteam_cons_t[i].value = team;
-		dummyteam_cons_t[i].strvalue = teams[team].name;
+		dummyteam_cons_t[i].value = 1;
+		dummyteam_cons_t[i].strvalue = "Playing";
+		i++;
 	}
 
 	dummyteam_cons_t[i].value = 0;
@@ -3579,6 +3597,7 @@ void G_UpdateTeamSelection(void)
 
 	cv_dummyteam.defaultvalue = dummyteam_cons_t[0].strvalue;
 	cv_dummyteam.value = 0;
+	cv_dummyteam.string = cv_dummyteam.defaultvalue;
 }
 
 //
@@ -3590,9 +3609,9 @@ void G_SetGametype(INT16 gtype)
 	gametyperules = gametypes[gametype].rules;
 
 	if (G_GametypeHasTeams())
-		teamsingame = gametypes[gametype].numteams + 1;
+		teamsingame = gametypes[gametype].teams.num + 1;
 	else
-		teamsingame = 3;
+		teamsingame = 0;
 
 	G_UpdateTeamSelection();
 }
@@ -3803,7 +3822,10 @@ boolean G_GametypeUsesCoopStarposts(void)
 //
 boolean G_GametypeHasTeams(void)
 {
-	return (gametyperules & GTR_TEAMS);
+	if (gametyperules & GTR_TEAMS)
+		return gametypes[gametype].teams.num > 0;
+
+	return false;
 }
 
 //
@@ -3884,10 +3906,10 @@ UINT32 G_TOLFlag(INT32 pgametype)
 
 UINT8 G_GetGametypeTeam(UINT8 gtype, UINT8 team)
 {
-	if (team == TEAM_NONE || team >= gametypes[gtype].numteams + 1)
+	if (team == TEAM_NONE || team >= gametypes[gtype].teams.num + 1)
 		return TEAM_NONE;
 
-	return gametypes[gtype].teams[team - 1] % MAXTEAMS;
+	return gametypes[gtype].teams.list[team - 1] % MAXTEAMS;
 }
 
 UINT8 G_GetTeam(UINT8 team)
@@ -3981,6 +4003,39 @@ boolean G_HasTeamIcon(UINT8 team, UINT8 icon_type)
 	return true;
 }
 
+void G_SetTeamIcon(UINT8 team, UINT8 icon_type, const char *icon)
+{
+	if (team >= numteams || icon_type >= TEAM_ICON_MAX)
+		return;
+
+	Z_Free(teams[team].icons[icon_type]);
+	teams[team].icons[icon_type] = NULL;
+	if (icon)
+		teams[team].icons[icon_type] = Z_StrDup(icon);
+}
+
+void G_FreeTeamData(UINT8 team)
+{
+	if (team >= numteams)
+		return;
+
+	team_t *team_ptr = &teams[team];
+
+	if (team_ptr->name)
+		Z_Free(team_ptr->name);
+	if (team_ptr->flag_name)
+		Z_Free(team_ptr->flag_name);
+
+	for (UINT8 i = 0; i < TEAM_ICON_MAX; i++)
+	{
+		Z_Free(team_ptr->icons[i]);
+		team_ptr->icons[i] = NULL;
+	}
+
+	team_ptr->name = NULL;
+	team_ptr->flag_name = NULL;
+}
+
 /** Select a random map with the given typeoflevel flags.
   * If no map has those flags, this arbitrarily gives you map 1.
   * \param tolflags The typeoflevel flags to insist on. Other bits may
diff --git a/src/g_game.h b/src/g_game.h
index 0f5533f892..cb97de6ff7 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -234,6 +234,8 @@ UINT16 G_GetTeamWeaponColor(UINT8 team);
 UINT16 G_GetTeamMissileColor(UINT8 team);
 const char *G_GetTeamIcon(UINT8 team, UINT8 icon_type);
 boolean G_HasTeamIcon(UINT8 team, UINT8 icon_type);
+void G_SetTeamIcon(UINT8 team, UINT8 icon_type, const char *icon);
+void G_FreeTeamData(UINT8 team);
 
 void G_Ticker(boolean run);
 boolean G_Responder(event_t *ev);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 8649cc5507..4df833e541 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -2294,13 +2294,13 @@ static void HU_Draw32TeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		greycheck = greycheckdef;
 		supercheck = supercheckdef;
 
-		if (tab[i].team == TEAM_RED) //red
+		if (tab[i].team == G_GetTeam(1)) //red
 		{
 			redplayers++;
 			x = 14 + (BASEVIDWIDTH/2);
 			y = (redplayers * 9) + 20;
 		}
-		else if (tab[i].team == TEAM_BLUE) //blue
+		else if (tab[i].team == G_GetTeam(2)) //blue
 		{
 			blueplayers++;
 			x = 14;
@@ -2380,7 +2380,7 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		if (players[tab[i].num].spectator)
 			continue; //ignore them.
 
-		if (tab[i].team == TEAM_RED) //red
+		if (tab[i].team == G_GetTeam(1)) //red
 		{
 			if (redplayers++ > 8)
 			{
@@ -2388,7 +2388,7 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 				break; // don't make more loops than we need to.
 			}
 		}
-		else if (tab[i].team == TEAM_BLUE) //blue
+		else if (tab[i].team == G_GetTeam(2)) //blue
 		{
 			if (blueplayers++ > 8)
 			{
@@ -2419,14 +2419,14 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		if (players[tab[i].num].spectator)
 			continue; //ignore them.
 
-		if (tab[i].team == TEAM_RED) //red
+		if (tab[i].team == G_GetTeam(1)) //red
 		{
 			if (redplayers++ > 8)
 				continue;
 			x = 32 + (BASEVIDWIDTH/2);
 			y = (redplayers * 16) + 16;
 		}
-		else if (tab[i].team == TEAM_BLUE) //blue
+		else if (tab[i].team == G_GetTeam(2)) //blue
 		{
 			if (blueplayers++ > 8)
 				continue;
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index e4aae50a8a..824202273c 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -156,6 +156,8 @@ static const struct {
 	{META_SKINCOLOR,    "skincolor_t"},
 	{META_COLORRAMP,    "skincolor_t.ramp"},
 	{META_GAMETYPE,     "gametype_t"},
+	{META_TEAM,         "team_t"},
+	{META_TEAMLIST,     "teamlist_t"},
 	{META_SPRITEINFO,   "spriteinfo_t"},
 	{META_PIVOTLIST,    "spriteframepivot_t[]"},
 	{META_FRAMEPIVOT,   "spriteframepivot_t"},
@@ -3545,7 +3547,6 @@ static int lib_sResumeMusic(lua_State *L)
 // G_GAME
 ////////////
 
-// Copypasted from lib_cvRegisterVar :]
 static int lib_gAddGametype(lua_State *L)
 {
 	const char *k;
@@ -3562,6 +3563,8 @@ static int lib_gAddGametype(lua_State *L)
 	UINT8 newgtrightcolor = 54;
 	INT16 newgtrankingstype = RANKINGS_DEFAULT;
 	int newgtinttype = 0;
+	UINT8 teamcount = 0;
+	UINT8 teamlist[MAXTEAMS];
 
 	luaL_checktype(L, 1, LUA_TTABLE);
 	lua_settop(L, 1); // Clear out all other possible arguments, leaving only the first one.
@@ -3587,7 +3590,6 @@ static int lib_gAddGametype(lua_State *L)
 		else if (lua_isstring(L, 2))
 			k = lua_tostring(L, 2);
 
-		// Sorry, no gametype rules as key names.
 		if (i == 1 || (k && fasticmp(k, "name"))) {
 			if (!lua_isstring(L, 3))
 				TYPEERROR("name", LUA_TSTRING)
@@ -3632,6 +3634,36 @@ static int lib_gAddGametype(lua_State *L)
 			if (!lua_isnumber(L, 3))
 				TYPEERROR("headerrightcolor", LUA_TNUMBER)
 			newgtrightcolor = (UINT8)lua_tointeger(L, 3);
+		} else if (i == 12 || (k && fasticmp(k, "teams"))) {
+			if (lua_istable(L, 3))
+			{
+				lua_pushnil(L);
+
+				while (lua_next(L, 3)) {
+					lua_Integer idx = luaL_checkinteger(L, -2) - 1;
+					if (idx >= 0 && idx < MAXTEAMS)
+					{
+						int team_index = luaL_checkinteger(L, -1);
+						if (team_index < 0 || team_index >= numteams)
+							luaL_error(L, "team index %d out of range (0 - %d)", team_index, numteams-1);
+
+						teamlist[idx] = (UINT8)team_index;
+
+						if ((lua_Integer)teamcount < idx + 1)
+							teamcount = (UINT8)idx + 1;
+					}
+
+					lua_pop(L, 1);
+				}
+			}
+			else if (lua_isuserdata(L, 3))
+			{
+				teamlist_t *tl = *((teamlist_t **)luaL_checkudata(L, 3, META_TEAMLIST));
+				teamcount = tl->num;
+				memcpy(teamlist, tl->list, sizeof(teamlist[0]) * teamcount);
+			}
+			else
+				TYPEERROR("teams", LUA_TTABLE)
 		// Key name specified
 		} else if ((!i) && (k && fasticmp(k, "headercolor"))) {
 			if (!lua_isnumber(L, 3))
@@ -3667,6 +3699,11 @@ static int lib_gAddGametype(lua_State *L)
 
 	gt->name = gtname;
 
+	// Copy the teams
+	gt->teams.num = teamcount;
+	if (teamcount)
+		memcpy(gt->teams.list, teamlist, sizeof(teamlist[0]) * teamcount);
+
 	if (gtconst)
 		G_AddGametypeConstant(gtype, gtconst);
 	else
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 08f7f86004..4f62e50cb8 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1933,7 +1933,8 @@ enum gametype_e
 	gametype_intermission_type,
 	gametype_rankings_type,
 	gametype_pointlimit,
-	gametype_timelimit
+	gametype_timelimit,
+	gametype_teams
 };
 
 const char *const gametype_opt[] = {
@@ -1944,6 +1945,7 @@ const char *const gametype_opt[] = {
 	"rankings_type",
 	"point_limit",
 	"time_limit",
+	"teams",
 	NULL,
 };
 
@@ -1980,6 +1982,11 @@ static int gametype_get(lua_State *L)
 	case gametype_timelimit:
 		lua_pushinteger(L, gt->timelimit);
 		break;
+	case gametype_teams:
+		LUA_PushUserdata(L, &gt->teams, META_TEAMLIST);
+		break;
+	default:
+		return luaL_error(L, LUA_QL("gametype_t") " has no field named " LUA_QS, gametype_opt[field]);
 	}
 	return 1;
 }
@@ -1989,6 +1996,8 @@ static int gametype_set(lua_State *L)
 	gametype_t *gt = *((gametype_t **)luaL_checkudata(L, 1, META_GAMETYPE));
 	enum gametype_e field = Lua_optoption(L, 2, -1, gametype_fields_ref);
 
+	if (!lua_lumploading)
+		return luaL_error(L, "Do not alter gametype data from within a hook or coroutine!");
 	if (hud_running)
 		return luaL_error(L, "Do not alter gametype data in HUD rendering code!");
 	if (hook_cmd_running)
@@ -1997,6 +2006,8 @@ static int gametype_set(lua_State *L)
 	I_Assert(gt != NULL);
 	I_Assert(gt >= gametypes);
 
+	INT16 gametype_id = gt - gametypes;
+
 	switch (field)
 	{
 	case gametype_name:
@@ -2022,6 +2033,52 @@ static int gametype_set(lua_State *L)
 	case gametype_timelimit:
 		gt->timelimit = luaL_checkinteger(L, 3);
 		break;
+	case gametype_teams:
+		if (lua_istable(L, 3))
+		{
+			gt->teams.num = 0;
+			memset(gt->teams.list, TEAM_NONE, sizeof(gt->teams.list));
+
+			lua_pushnil(L);
+
+			while (lua_next(L, 3)) {
+				lua_Integer i = luaL_checkinteger(L, -2) - 1;
+				if (i >= 0 && i < MAXTEAMS)
+				{
+					int team_index = luaL_checkinteger(L, -1);
+					if (team_index < 0 || team_index >= numteams)
+						luaL_error(L, "team index %d out of range (0 - %d)", team_index, numteams-1);
+
+					gt->teams.list[i] = (UINT8)team_index;
+
+					if ((lua_Integer)gt->teams.num < i + 1)
+						gt->teams.num = (UINT8)i + 1;
+				}
+
+				lua_pop(L, 1);
+			}
+
+			if (gametype == gametype_id)
+			{
+				teamsingame = gt->teams.num;
+				G_UpdateTeamSelection();
+			}
+		}
+		else
+		{
+			teamlist_t *teamlist = *((teamlist_t **)luaL_checkudata(L, 3, META_TEAMLIST));
+
+			memcpy(&gt->teams, teamlist, sizeof(teamlist_t));
+
+			if (gametype == gametype_id)
+			{
+				teamsingame = gt->teams.num;
+				G_UpdateTeamSelection();
+			}
+		}
+		break;
+	default:
+		return luaL_error(L, LUA_QL("gametype_t") " has no field named " LUA_QS, gametype_opt[field]);
 	}
 	return 0;
 }
@@ -2037,6 +2094,313 @@ static int gametype_num(lua_State *L)
 	return 1;
 }
 
+///////////
+// TEAMS //
+///////////
+
+enum team_e
+{
+	team_name,
+	team_flag_name,
+	team_flag,
+	team_flag_mobj_type,
+	team_color,
+	team_weapon_color,
+	team_missile_color,
+	team_icon,
+	team_icon_flag,
+	team_icon_got_flag,
+	team_icon_missing_flag
+};
+
+const char *const team_opt[] = {
+	"name",
+	"flag_name",
+	"flag",
+	"flag_mobj_type",
+	"color",
+	"weapon_color",
+	"missile_color",
+	"icon",
+	"flag_icon",
+	"captured_flag_icon",
+	"missing_flag_icon",
+	NULL,
+};
+
+static int team_fields_ref = LUA_NOREF;
+
+static int lib_getTeams(lua_State *L)
+{
+	INT16 i;
+	lua_remove(L, 1);
+
+	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);
+	LUA_PushUserdata(L, &teams[i], META_GAMETYPE);
+	return 1;
+}
+
+static int set_team_field(lua_State *L, team_t *team, enum team_e field)
+{
+	switch (field)
+	{
+	case team_name:
+		Z_Free(team->name);
+		team->name = Z_StrDup(luaL_checkstring(L, 3));
+		G_UpdateTeamSelection();
+		break;
+	case team_flag_name:
+		Z_Free(team->flag_name);
+		team->flag_name = Z_StrDup(luaL_checkstring(L, 3));
+		break;
+	case team_flag:
+		team->flag = (UINT16)luaL_checkinteger(L, 3);
+		break;
+	case team_flag_mobj_type:
+	{
+		mobjtype_t type = luaL_checkinteger(L, 3);
+		if (type >= NUMMOBJTYPES)
+		{
+			luaL_error(L, "mobj type %d out of range (0 - %d)", type, NUMMOBJTYPES-1);
+			return 0;
+		}
+		team->flag_mobj_type = type;
+		break;
+	}
+	case team_color:
+	{
+		UINT16 newcolor = (UINT16)luaL_checkinteger(L, 3);
+		if (newcolor >= numskincolors)
+		{
+			luaL_error(L, "skincolor %d out of range (0 - %d).", newcolor, numskincolors-1);
+			return 0;
+		}
+		team->color = newcolor;
+		break;
+	}
+	case team_weapon_color:
+	{
+		UINT16 newcolor = (UINT16)luaL_checkinteger(L, 3);
+		if (newcolor >= numskincolors)
+		{
+			luaL_error(L, "skincolor %d out of range (0 - %d).", newcolor, numskincolors-1);
+			return 0;
+		}
+		team->weapon_color = newcolor;
+		break;
+	}
+	case team_missile_color:
+	{
+		UINT16 newcolor = (UINT16)luaL_checkinteger(L, 3);
+		if (newcolor >= numskincolors)
+		{
+			luaL_error(L, "skincolor %d out of range (0 - %d).", newcolor, numskincolors-1);
+			return 0;
+		}
+		team->missile_color = newcolor;
+		break;
+	}
+	case team_icon:
+		G_SetTeamIcon(team - teams, TEAM_ICON, luaL_checkstring(L, 3));
+		ST_LoadTeamIcons();
+		break;
+	case team_icon_flag:
+		G_SetTeamIcon(team - teams, TEAM_ICON_FLAG, luaL_checkstring(L, 3));
+		ST_LoadTeamIcons();
+		break;
+	case team_icon_got_flag:
+		G_SetTeamIcon(team - teams, TEAM_ICON_GOT_FLAG, luaL_checkstring(L, 3));
+		ST_LoadTeamIcons();
+		break;
+	case team_icon_missing_flag:
+		G_SetTeamIcon(team - teams, TEAM_ICON_MISSING_FLAG, luaL_checkstring(L, 3));
+		ST_LoadTeamIcons();
+		break;
+	default:
+		return -1;
+	}
+	return 1;
+}
+
+static int lib_setTeams(lua_State *L)
+{
+	UINT32 teamnum;
+	team_t *team;
+	lua_remove(L, 1);
+	{
+		teamnum = luaL_checkinteger(L, 1);
+		if (teamnum >= numteams)
+			return luaL_error(L, "teams[] index %d out of range (0 - %d)", teamnum, numteams-1);
+		team = &teams[teamnum];
+	}
+	luaL_checktype(L, 2, LUA_TTABLE);
+	lua_remove(L, 1);
+	lua_settop(L, 1);
+
+	if (hud_running)
+		return luaL_error(L, "Do not alter team data in HUD rendering code!");
+	if (hook_cmd_running)
+		return luaL_error(L, "Do not alter team data in CMD building code!");
+
+	G_FreeTeamData(teamnum);
+
+	memset(team, 0, sizeof(team_t));
+
+	lua_pushnil(L);
+	while (lua_next(L, 1)) {
+		const char *str = luaL_checkstring(L, 2);
+		int field = -1;
+
+		for (int i = 0; team_opt[i]; i++) {
+			if (fastcmp(str, team_opt[i]))
+			{
+				field = i;
+				break;
+			}
+		}
+
+		if (field != -1)
+			set_team_field(L, team, field);
+		else
+			luaL_error(L, LUA_QL("team_t") " has no field named " LUA_QS, str);
+
+		lua_pop(L, 1);
+	}
+	return 0;
+}
+
+// #teams -> numteams
+static int lib_teamslen(lua_State *L)
+{
+	lua_pushinteger(L, numteams);
+	return 1;
+}
+
+static int team_get(lua_State *L)
+{
+	team_t *team = *((team_t **)luaL_checkudata(L, 1, META_GAMETYPE));
+	enum team_e field = Lua_optoption(L, 2, team_name, team_fields_ref);
+
+	I_Assert(team != NULL);
+	I_Assert(team >= teams);
+
+	switch (field)
+	{
+	case team_name:
+		lua_pushstring(L, team->name);
+		break;
+	case team_flag_name:
+		lua_pushstring(L, team->flag_name);
+		break;
+	case team_flag:
+		lua_pushinteger(L, team->flag);
+		break;
+	case team_flag_mobj_type:
+		lua_pushinteger(L, team->flag_mobj_type);
+		break;
+	case team_color:
+		lua_pushinteger(L, team->color);
+		break;
+	case team_weapon_color:
+		lua_pushinteger(L, team->weapon_color);
+		break;
+	case team_missile_color:
+		lua_pushinteger(L, team->missile_color);
+		break;
+	case team_icon:
+		if (G_HasTeamIcon(team - teams, TEAM_ICON))
+			lua_pushstring(L, G_GetTeamIcon(team - teams, TEAM_ICON));
+		else
+			lua_pushnil(L);
+		break;
+	case team_icon_flag:
+		if (G_HasTeamIcon(team - teams, TEAM_ICON_FLAG))
+			lua_pushstring(L, G_GetTeamIcon(team - teams, TEAM_ICON_FLAG));
+		else
+			lua_pushnil(L);
+		break;
+	case team_icon_got_flag:
+		if (G_HasTeamIcon(team - teams, TEAM_ICON_GOT_FLAG))
+			lua_pushstring(L, G_GetTeamIcon(team - teams, TEAM_ICON_GOT_FLAG));
+		else
+			lua_pushnil(L);
+		break;
+	case team_icon_missing_flag:
+		if (G_HasTeamIcon(team - teams, TEAM_ICON_MISSING_FLAG))
+			lua_pushstring(L, G_GetTeamIcon(team - teams, TEAM_ICON_MISSING_FLAG));
+		else
+			lua_pushnil(L);
+		break;
+	default:
+		return luaL_error(L, LUA_QL("team_t") " has no field named " LUA_QS, lua_tostring(L, 2));
+	}
+	return 1;
+}
+
+static int team_set(lua_State *L)
+{
+	team_t *team = *((team_t **)luaL_checkudata(L, 1, META_GAMETYPE));
+	enum team_e field = Lua_optoption(L, 2, -1, team_fields_ref);
+
+	if (!lua_lumploading)
+		return luaL_error(L, "Do not alter team data from within a hook or coroutine!");
+	if (hud_running)
+		return luaL_error(L, "Do not alter team data in HUD rendering code!");
+	if (hook_cmd_running)
+		return luaL_error(L, "Do not alter team data in CMD building code!");
+
+	I_Assert(team != NULL);
+	I_Assert(team >= teams);
+
+	if (set_team_field(L, team, field) < 0)
+		return luaL_error(L, LUA_QL("team_t") " has no field named " LUA_QS, lua_tostring(L, 2));
+
+	return 0;
+}
+
+static int team_num(lua_State *L)
+{
+	team_t *team = *((team_t **)luaL_checkudata(L, 1, META_GAMETYPE));
+
+	I_Assert(team != NULL);
+	I_Assert(team >= teams);
+
+	lua_pushinteger(L, team-teams);
+	return 1;
+}
+
+static int teamlist_len(lua_State *L)
+{
+	teamlist_t *teamlist = *((teamlist_t **)luaL_checkudata(L, 1, META_TEAMLIST));
+	lua_pushinteger(L, teamlist->num);
+	return 1;
+}
+
+static int teamlist_get(lua_State *L)
+{
+	teamlist_t *teamlist = *((teamlist_t **)luaL_checkudata(L, 1, META_TEAMLIST));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i > teamlist->num)
+		return luaL_error(L, "list index %d out of range (1 - %d)", i, teamlist->num);
+	lua_pushinteger(L, teamlist->list[i - 1]);
+	return 1;
+}
+
+static int teamlist_set(lua_State *L)
+{
+	teamlist_t *teamlist = *((teamlist_t **)luaL_checkudata(L, 1, META_TEAMLIST));
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i > teamlist->num)
+		return luaL_error(L, "list index %d out of range (1 - %d)", i, teamlist->num);
+	int team = luaL_checkinteger(L, 3);
+	if (team < 0 || team >= numteams)
+		return luaL_error(L, "team index %d out of range (0 - %d)", i, numteams - 1);
+	teamlist->list[i - 1] = (UINT8)team;
+	return 0;
+}
+
 //////////////////////////////
 //
 // Now push all these functions into the Lua state!
@@ -2055,6 +2419,8 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_STATE, state_get, state_set, state_num);
 	LUA_RegisterUserdataMetatable(L, META_MOBJINFO, mobjinfo_get, mobjinfo_set, mobjinfo_num);
 	LUA_RegisterUserdataMetatable(L, META_GAMETYPE, gametype_get, gametype_set, gametype_num);
+	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_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);
@@ -2064,6 +2430,7 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_LUABANKS, lib_getluabanks, lib_setluabanks, lib_luabankslen);
 
 	mobjinfo_fields_ref = Lua_CreateFieldTable(L, mobjinfo_opt);
+	team_fields_ref = Lua_CreateFieldTable(L, team_opt);
 
 	LUA_RegisterGlobalUserdata(L, "sprnames", lib_getSprname, NULL, lib_sprnamelen);
 	LUA_RegisterGlobalUserdata(L, "spr2names", lib_getSpr2name, NULL, lib_spr2namelen);
@@ -2071,6 +2438,8 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterGlobalUserdata(L, "states", lib_getState, lib_setState, lib_statelen);
 	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, "gametypes", lib_getGametypes, NULL, lib_gametypeslen);
 	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 41e119fcc5..8a6af9f8b4 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -30,6 +30,8 @@ extern boolean ignoregameinputs;
 #define META_COLORRAMP "SKINCOLOR_T*RAMP"
 #define META_SPRITEINFO "SPRITEINFO_T*"
 #define META_GAMETYPE "GAMETYPE_T*"
+#define META_TEAM "TEAM_T*"
+#define META_TEAMLIST "TEAMLIST_T*"
 #define META_PIVOTLIST "SPRITEFRAMEPIVOT_T[]"
 #define META_FRAMEPIVOT "SPRITEFRAMEPIVOT_T*"
 
diff --git a/src/lua_script.c b/src/lua_script.c
index 40c6af3e09..084b9ce6d4 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -214,10 +214,10 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		lua_pushboolean(L, paused);
 		return 1;
 	} else if (fastcmp(word,"bluescore")) {
-		lua_pushinteger(L, teamscores[TEAM_BLUE]);
+		lua_pushinteger(L, teamscores[G_GetTeam(2)]);
 		return 1;
 	} else if (fastcmp(word,"redscore")) {
-		lua_pushinteger(L, teamscores[TEAM_RED]);
+		lua_pushinteger(L, teamscores[G_GetTeam(1)]);
 		return 1;
 	} else if (fastcmp(word,"timelimit")) {
 		lua_pushinteger(L, cv_timelimit.value);
@@ -226,16 +226,16 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		lua_pushinteger(L, cv_pointlimit.value);
 		return 1;
 	} else if (fastcmp(word, "redflag")) {
-		LUA_PushUserdata(L, flagmobjs[TEAM_RED], META_MOBJ);
+		LUA_PushUserdata(L, flagmobjs[G_GetTeam(1)], META_MOBJ);
 		return 1;
 	} else if (fastcmp(word, "blueflag")) {
-		LUA_PushUserdata(L, flagmobjs[TEAM_BLUE], META_MOBJ);
+		LUA_PushUserdata(L, flagmobjs[G_GetTeam(2)], META_MOBJ);
 		return 1;
 	} else if (fastcmp(word, "rflagpoint")) {
-		LUA_PushUserdata(L, flagpoints[TEAM_RED], META_MAPTHING);
+		LUA_PushUserdata(L, flagpoints[G_GetTeam(1)], META_MAPTHING);
 		return 1;
 	} else if (fastcmp(word, "bflagpoint")) {
-		LUA_PushUserdata(L, flagpoints[TEAM_BLUE], META_MAPTHING);
+		LUA_PushUserdata(L, flagpoints[G_GetTeam(2)], META_MAPTHING);
 		return 1;
 	// begin map vars
 	} else if (fastcmp(word,"spstage_start")) {
@@ -271,6 +271,12 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"tutorialmode")) {
 		lua_pushboolean(L, tutorialmode);
 		return 1;
+	} else if (fastcmp(word,"numteams")) {
+		lua_pushinteger(L, max(numteams - 1, 0));
+		return 1;
+	} else if (fastcmp(word,"teamsingame")) {
+		lua_pushinteger(L, max(teamsingame - 1, 0));
+		return 1;
 	// end map vars
 	// begin CTF colors
 	} else if (fastcmp(word,"skincolor_redteam")) {
@@ -992,6 +998,7 @@ enum
 	ARCH_MOUSE,
 	ARCH_SKIN,
 	ARCH_GAMETYPE,
+	ARCH_TEAM,
 
 	ARCH_TEND=0xFF,
 };
@@ -1022,6 +1029,7 @@ static const struct {
 	{META_MOUSE,    ARCH_MOUSE},
 	{META_SKIN,     ARCH_SKIN},
 	{META_GAMETYPE, ARCH_GAMETYPE},
+	{META_TEAM,     ARCH_TEAM},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1357,6 +1365,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			WRITEUINT8(save_p, gt - gametypes);
 			break;
 		}
+		case ARCH_TEAM:
+		{
+			team_t *team = *((team_t **)lua_touserdata(gL, myindex));
+			WRITEUINT8(save_p, ARCH_TEAM);
+			WRITEUINT8(save_p, team - teams);
+			break;
+		}
 		default:
 			WRITEUINT8(save_p, ARCH_NULL);
 			return 2;
@@ -1609,6 +1624,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_GAMETYPE:
 		LUA_PushUserdata(gL, &gametypes[READUINT8(save_p)], META_GAMETYPE);
 		break;
+	case ARCH_TEAM:
+		LUA_PushUserdata(gL, &teams[READUINT8(save_p)], META_TEAM);
+		break;
 	case ARCH_TEND:
 		return 1;
 	}
diff --git a/src/m_menu.c b/src/m_menu.c
index 26366adb7d..136e3e8430 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -465,7 +465,7 @@ static CV_PossibleValue_t dummymares_cons_t[] = {
 	{-1, "END"}, {0,"Overall"}, {1,"Mare 1"}, {2,"Mare 2"}, {3,"Mare 3"}, {4,"Mare 4"}, {5,"Mare 5"}, {6,"Mare 6"}, {7,"Mare 7"}, {8,"Mare 8"}, {0,NULL}
 };
 
-CV_PossibleValue_t dummyteam_cons_t[MAXTEAMS + 1];
+CV_PossibleValue_t dummyteam_cons_t[MAXTEAMS + 2];
 
 consvar_t cv_dummyteam = CVAR_INIT ("dummyteam", "Spectator", CV_HIDEN, dummyteam_cons_t, NULL);
 
@@ -6954,18 +6954,7 @@ static void M_ConfirmTeamChange(INT32 choice)
 
 	M_ClearMenus(true);
 
-	switch (cv_dummyteam.value)
-	{
-		case 0:
-			COM_ImmedExecute("changeteam spectator");
-			break;
-		case 1:
-			COM_ImmedExecute("changeteam red");
-			break;
-		case 2:
-			COM_ImmedExecute("changeteam blue");
-			break;
-	}
+	COM_ImmedExecute(va("changeteam %d", cv_dummyteam.value));
 }
 
 static void M_Options(INT32 choice)
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index e9ec1dbd88..bf694425d2 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -4208,7 +4208,7 @@ void D_GameTypeChanged(INT32 lastgametype)
 	// When swapping to a gametype that supports spectators,
 	// make everyone a spectator initially.
 	// Averted with GTR_NOSPECTATORSPAWN.
-	if (!splitscreen && (G_GametypeHasSpectators()))
+	if (!splitscreen && G_GametypeHasSpectators())
 	{
 		INT32 i;
 		for (i = 0; i < MAXPLAYERS; i++)
diff --git a/src/p_inter.c b/src/p_inter.c
index 09ac61d0d3..88baf3de61 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2283,8 +2283,20 @@ void P_CheckTimeLimit(void)
 			}
 			else
 			{
-				//In team match and CTF, determining a tie is much simpler. =P
-				if (teamscores[TEAM_RED] == teamscores[TEAM_BLUE])
+				boolean is_tied = true;
+				UINT32 lastscore = teamscores[G_GetTeam(1)];
+
+				for (UINT8 j = 2; j < teamsingame; j++)
+				{
+					if (teamscores[G_GetTeam(j)] != lastscore)
+					{
+						is_tied = false;
+						break;
+					}
+					lastscore = teamscores[G_GetTeam(j)];
+				}
+
+				if (is_tied)
 					return;
 			}
 		}
@@ -2304,7 +2316,8 @@ void P_CheckTimeLimit(void)
   */
 void P_CheckPointLimit(void)
 {
-	INT32 i;
+	if (!server)
+		return;
 
 	if (!cv_pointlimit.value)
 		return;
@@ -2315,19 +2328,22 @@ void P_CheckPointLimit(void)
 	if (!(gametyperules & GTR_POINTLIMIT))
 		return;
 
-	// pointlimit is nonzero, check if it's been reached by this player
 	if (G_GametypeHasTeams())
 	{
-		// Just check both teams
-		if ((UINT32)cv_pointlimit.value <= teamscores[TEAM_RED] || (UINT32)cv_pointlimit.value <= teamscores[TEAM_BLUE])
+		for (UINT8 i = 1; i < teamsingame; i++)
 		{
-			if (server)
-				D_SendExitLevel(false);
+			if (teamscores[G_GetTeam(i)] >= (UINT32)cv_pointlimit.value)
+			{
+				if (server)
+					D_SendExitLevel(false);
+				return;
+			}
 		}
 	}
 	else
 	{
-		for (i = 0; i < MAXPLAYERS; i++)
+		// pointlimit is nonzero, check if it's been reached by this player
+		for (INT32 i = 0; i < MAXPLAYERS; i++)
 		{
 			if (!playeringame[i] || players[i].spectator)
 				continue;
diff --git a/src/p_saveg.c b/src/p_saveg.c
index f2b32f2992..a92a0fa31c 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -4340,12 +4340,7 @@ static void P_NetArchiveMisc(boolean resending)
 
 	WRITEUINT8(save_p, teamsingame);
 	for (i = 0; i < MAXTEAMS; i++)
-	{
 		WRITEUINT32(save_p, teamscores[i]);
-		WRITEUINT16(save_p, teams[i].color);
-		WRITEUINT16(save_p, teams[i].weapon_color);
-		WRITEUINT16(save_p, teams[i].missile_color);
-	}
 
 	WRITEINT32(save_p, modulothing);
 
@@ -4440,12 +4435,7 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 
 	teamsingame = READUINT8(save_p);
 	for (i = 0; i < MAXTEAMS; i++)
-	{
 		teamscores[i] = READUINT32(save_p);
-		teams[i].color = READUINT16(save_p);
-		teams[i].weapon_color = READUINT16(save_p);
-		teams[i].missile_color = READUINT16(save_p);
-	}
 
 	modulothing = READINT32(save_p);
 
-- 
GitLab