diff --git a/src/d_main.c b/src/d_main.c
index 274e4ceb366511fe9b9e9d77a792b51f4f51bbba..697e9570d8659c2c696dff2ffd964c90848e82fb 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1375,6 +1375,8 @@ void D_SRB2Main(void)
 	CONS_Printf("Z_Init(): Init zone memory allocation daemon. \n");
 	Z_Init();
 
+	G_InitGametypes();
+
 	clientGamedata = M_NewGameDataStruct();
 	serverGamedata = M_NewGameDataStruct();
 
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 260e64f823b2c7f6ae355da4b083f1780ca615c8..5ec9816d635f6b566408b8c1c5ae5e35b7222a46 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1325,12 +1325,12 @@ void readgametype(MYFILE *f, char *gtname)
 	gametypes[newgtidx].timelimit = newgttimelimit;
 
 	// Write the new gametype name.
-	gametypes[newgtidx].name = Z_StrDup((const char *)gtname);
+	gametypes[newgtidx].name = Z_StrDup(gtname);
 
 	// Write the constant name.
 	if (gtconst[0] == '\0')
 		strncpy(gtconst, gtname, MAXLINELEN);
-	G_AddGametypeConstant(newgtidx, (const char *)gtconst);
+	G_AddGametypeConstant(newgtidx, gtconst);
 
 	// Update gametype_cons_t accordingly.
 	G_UpdateGametypeSelections();
diff --git a/src/doomstat.h b/src/doomstat.h
index 00db0cf3eaf5fd5a985b640c5911e6c7d036b279..7dbe3e7780c80de346605101352545a5218fd00b 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -389,8 +389,9 @@ typedef struct
 
 extern mapheader_t* mapheaderinfo[NUMMAPS];
 
-// Gametypes
 #define NUMGAMETYPEFREESLOTS 128
+
+// Gametypes
 enum GameType
 {
 	GT_COOP = 0, // also used in single player
@@ -457,8 +458,8 @@ enum
 
 typedef struct
 {
-	const char *name;
-	const char *constant_name;
+	char *name;
+	char *constant_name;
 	UINT32 rules;
 	UINT32 typeoflevel;
 	UINT8 intermission_type;
diff --git a/src/g_game.c b/src/g_game.c
index 04c8d9150dc01f33634bbe8a570b6264efc0b739..42491048150d00356f104916e18b88daad3dba59 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3392,11 +3392,40 @@ void G_ExitLevel(void)
 	}
 }
 
+// See also the enum GameType in doomstat.h
+static const char *Gametype_Names[NUMGAMETYPES] =
+{
+	"Co-op", // GT_COOP
+	"Competition", // GT_COMPETITION
+	"Race", // GT_RACE
+
+	"Match", // GT_MATCH
+	"Team Match", // GT_TEAMMATCH
+
+	"Tag", // GT_TAG
+	"Hide & Seek", // GT_HIDEANDSEEK
+
+	"CTF" // GT_CTF
+};
+
+static const char *Gametype_ConstantNames[NUMGAMETYPES] =
+{
+	"GT_COOP", // GT_COOP
+	"GT_COMPETITION", // GT_COMPETITION
+	"GT_RACE", // GT_RACE
+
+	"GT_MATCH", // GT_MATCH
+	"GT_TEAMMATCH", // GT_TEAMMATCH
+
+	"GT_TAG", // GT_TAG
+	"GT_HIDEANDSEEK", // GT_HIDEANDSEEK
+
+	"GT_CTF" // GT_CTF
+};
+
 gametype_t gametypes[NUMGAMETYPES] = {
 	// GT_COOP
 	{
-		.name = "Co-op",
-		.constant_name = "GT_COOP",
 		.rules = GTR_CAMPAIGN|GTR_LIVES|GTR_FRIENDLY|GTR_SPAWNENEMIES|GTR_ALLOWEXIT|GTR_EMERALDHUNT|GTR_EMERALDTOKENS|GTR_SPECIALSTAGES|GTR_CUTSCENES,
 		.typeoflevel = TOL_COOP,
 		.intermission_type = int_coop,
@@ -3404,8 +3433,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_COMPETITION
 	{
-		.name = "Competition",
-		.constant_name = "GT_COMPETITION",
 		.rules = GTR_RACE|GTR_LIVES|GTR_SPAWNENEMIES|GTR_EMERALDTOKENS|GTR_SPAWNINVUL|GTR_ALLOWEXIT,
 		.typeoflevel = TOL_COMPETITION,
 		.intermission_type = int_comp,
@@ -3413,8 +3440,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_RACE
 	{
-		.name = "Race",
-		.constant_name = "GT_RACE",
 		.rules = GTR_RACE|GTR_SPAWNENEMIES|GTR_SPAWNINVUL|GTR_ALLOWEXIT,
 		.typeoflevel = TOL_RACE,
 		.intermission_type = int_race,
@@ -3422,8 +3447,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_MATCH
 	{
-		.name = "Match",
-		.constant_name = "GT_MATCH",
 		.rules = GTR_RINGSLINGER|GTR_FIRSTPERSON|GTR_SPECTATORS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_POWERSTONES|GTR_DEATHMATCHSTARTS|GTR_SPAWNINVUL|GTR_RESPAWNDELAY|GTR_PITYSHIELD|GTR_DEATHPENALTY,
 		.typeoflevel = TOL_MATCH,
 		.intermission_type = int_match,
@@ -3434,8 +3457,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_TEAMMATCH
 	{
-		.name = "Team Match",
-		.constant_name = "GT_TEAMMATCH",
 		.rules = GTR_RINGSLINGER|GTR_FIRSTPERSON|GTR_SPECTATORS|GTR_TEAMS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_DEATHMATCHSTARTS|GTR_SPAWNINVUL|GTR_RESPAWNDELAY|GTR_PITYSHIELD,
 		.typeoflevel = TOL_MATCH,
 		.intermission_type = int_teammatch,
@@ -3446,8 +3467,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_TAG
 	{
-		.name = "Tag",
-		.constant_name = "GT_TAG",
 		.rules = GTR_RINGSLINGER|GTR_FIRSTPERSON|GTR_TAG|GTR_SPECTATORS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_STARTCOUNTDOWN|GTR_BLINDFOLDED|GTR_DEATHMATCHSTARTS|GTR_SPAWNINVUL|GTR_RESPAWNDELAY,
 		.typeoflevel = TOL_TAG,
 		.intermission_type = int_match,
@@ -3459,8 +3478,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_HIDEANDSEEK
 	{
-		.name = "Hide & Seek",
-		.constant_name = "GT_HIDEANDSEEK",
 		.rules = GTR_RINGSLINGER|GTR_FIRSTPERSON|GTR_TAG|GTR_SPECTATORS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_STARTCOUNTDOWN|GTR_HIDEFROZEN|GTR_BLINDFOLDED|GTR_DEATHMATCHSTARTS|GTR_SPAWNINVUL|GTR_RESPAWNDELAY,
 		.typeoflevel = TOL_TAG,
 		.intermission_type = int_match,
@@ -3472,8 +3489,6 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 	// GT_CTF
 	{
-		.name = "CTF",
-		.constant_name = "GT_CTF",
 		.rules = GTR_RINGSLINGER|GTR_FIRSTPERSON|GTR_SPECTATORS|GTR_TEAMS|GTR_TEAMFLAGS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_POWERSTONES|GTR_DEATHMATCHSTARTS|GTR_SPAWNINVUL|GTR_RESPAWNDELAY|GTR_PITYSHIELD,
 		.typeoflevel = TOL_CTF,
 		.intermission_type = int_ctf,
@@ -3484,6 +3499,15 @@ gametype_t gametypes[NUMGAMETYPES] = {
 	},
 };
 
+void G_InitGametypes(void)
+{
+	for (unsigned i = 0; i <= GT_CTF; i++)
+	{
+		gametypes[i].name = Z_StrDup(Gametype_Names[i]);
+		gametypes[i].constant_name = Z_StrDup(Gametype_ConstantNames[i]);
+	}
+}
+
 //
 // Sets a new gametype, also setting gametype rules accordingly.
 //
@@ -3501,7 +3525,7 @@ INT16 G_AddGametype(UINT32 rules)
 	INT16 newgtype = gametypecount;
 	gametypecount++;
 
-	gametypes[newgtype].name = "???";
+	gametypes[newgtype].name = Z_StrDup("???");
 	gametypes[newgtype].rules = rules;
 
 	// Update gametype_cons_t accordingly.
diff --git a/src/g_game.h b/src/g_game.h
index 6fbfdd67873725fef26c012d4388a432b9eda553..97e38d88a131de729f2c9d3c2d280dae2d0b1d31 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -192,6 +192,7 @@ void G_SaveGame(UINT32 slot, INT16 mapnum);
 void G_SaveGameOver(UINT32 slot, boolean modifylives);
 
 void G_SetGametype(INT16 gametype);
+void G_InitGametypes(void);
 INT16 G_AddGametype(UINT32 rules);
 void G_AddGametypeConstant(INT16 gtype, const char *newgtconst);
 void G_UpdateGametypeSelections(void);
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index cbd93fb3c72a63787107f6c6fe83feb47f28d780..5fcd0f9b22bfe63d94f13e726e1f7784e5199ad0 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -155,6 +155,7 @@ static const struct {
 	{META_SFXINFO,      "sfxinfo_t"},
 	{META_SKINCOLOR,    "skincolor_t"},
 	{META_COLORRAMP,    "skincolor_t.ramp"},
+	{META_GAMETYPE,     "gametype_t"},
 	{META_SPRITEINFO,   "spriteinfo_t"},
 	{META_PIVOTLIST,    "spriteframepivot_t[]"},
 	{META_FRAMEPIVOT,   "spriteframepivot_t"},
@@ -3550,8 +3551,8 @@ static int lib_gAddGametype(lua_State *L)
 	const char *k;
 	lua_Integer i;
 
-	const char *gtname = NULL;
-	const char *gtconst = NULL;
+	char *gtname = NULL;
+	char *gtconst = NULL;
 	const char *gtdescription = NULL;
 	INT16 newgtidx = 0;
 	UINT32 newgtrules = 0;
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index ed78811ce4867642e660dd433c3153958e68797a..d337b053f4efbbb25953c5a32f5bd6f96e0fc3d2 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1902,6 +1902,140 @@ static int colorramp_len(lua_State *L)
 	return 1;
 }
 
+///////////////
+// GAMETYPES //
+///////////////
+
+static int lib_getGametypes(lua_State *L)
+{
+	INT16 i;
+	lua_remove(L, 1);
+
+	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);
+	LUA_PushUserdata(L, &gametypes[i], META_GAMETYPE);
+	return 1;
+}
+
+// #gametypes -> gametypecount
+static int lib_gametypeslen(lua_State *L)
+{
+	lua_pushinteger(L, gametypecount);
+	return 1;
+}
+
+enum gametype_e
+{
+	gametype_name,
+	gametype_rules,
+	gametype_typeoflevel,
+	gametype_intermission_type,
+	gametype_rankings_type,
+	gametype_pointlimit,
+	gametype_timelimit
+};
+
+const char *const gametype_opt[] = {
+	"name",
+	"rules",
+	"type_of_level",
+	"intermission_type",
+	"rankings_type",
+	"point_limit",
+	"time_limit",
+	NULL,
+};
+
+static int gametype_fields_ref = LUA_NOREF;
+
+static int gametype_get(lua_State *L)
+{
+	gametype_t *gt = *((gametype_t **)luaL_checkudata(L, 1, META_GAMETYPE));
+	enum gametype_e field = Lua_optoption(L, 2, gametype_name, gametype_fields_ref);
+
+	I_Assert(gt != NULL);
+	I_Assert(gt >= gametypes);
+
+	switch (field)
+	{
+	case gametype_name:
+		lua_pushstring(L, gt->name);
+		break;
+	case gametype_rules:
+		lua_pushinteger(L, gt->rules);
+		break;
+	case gametype_typeoflevel:
+		lua_pushinteger(L, gt->typeoflevel);
+		break;
+	case gametype_intermission_type:
+		lua_pushinteger(L, gt->intermission_type);
+		break;
+	case gametype_rankings_type:
+		lua_pushinteger(L, gt->rankings_type);
+		break;
+	case gametype_pointlimit:
+		lua_pushinteger(L, gt->pointlimit);
+		break;
+	case gametype_timelimit:
+		lua_pushinteger(L, gt->timelimit);
+		break;
+	}
+	return 1;
+}
+
+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 (hud_running)
+		return luaL_error(L, "Do not alter gametype data in HUD rendering code!");
+	if (hook_cmd_running)
+		return luaL_error(L, "Do not alter gametype data in CMD building code!");
+
+	I_Assert(gt != NULL);
+	I_Assert(gt >= gametypes);
+
+	switch (field)
+	{
+	case gametype_name:
+		Z_Free(gt->name);
+		gt->name = Z_StrDup(luaL_checkstring(L, 3));
+		break;
+	case gametype_rules:
+		gt->rules = luaL_checkinteger(L, 3);
+		break;
+	case gametype_typeoflevel:
+		gt->typeoflevel = luaL_checkinteger(L, 3);
+		break;
+	case gametype_intermission_type:
+		gt->intermission_type = luaL_checkinteger(L, 3);
+		break;
+	case gametype_rankings_type:
+		gt->rankings_type = luaL_checkinteger(L, 3);
+		break;
+	case gametype_pointlimit:
+		gt->pointlimit = luaL_checkinteger(L, 3);
+		break;
+	case gametype_timelimit:
+		gt->timelimit = luaL_checkinteger(L, 3);
+		break;
+	}
+	return 0;
+}
+
+static int gametype_num(lua_State *L)
+{
+	gametype_t *gt = *((gametype_t **)luaL_checkudata(L, 1, META_GAMETYPE));
+
+	I_Assert(gt != NULL);
+	I_Assert(gt >= gametypes);
+
+	lua_pushinteger(L, gt-gametypes);
+	return 1;
+}
+
 //////////////////////////////
 //
 // Now push all these functions into the Lua state!
@@ -1919,6 +2053,7 @@ 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_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);
@@ -1934,6 +2069,7 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterGlobalUserdata(L, "spr2defaults", lib_getSpr2default, lib_setSpr2default, lib_spr2namelen);
 	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, "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 2e3c706528f24dd127f77b0e1256463643c59654..41e119fcc56759b1a636422b0de5c20239490b9a 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -29,6 +29,7 @@ extern boolean ignoregameinputs;
 #define META_SKINCOLOR "SKINCOLOR_T*"
 #define META_COLORRAMP "SKINCOLOR_T*RAMP"
 #define META_SPRITEINFO "SPRITEINFO_T*"
+#define META_GAMETYPE "GAMETYPE_T*"
 #define META_PIVOTLIST "SPRITEFRAMEPIVOT_T[]"
 #define META_FRAMEPIVOT "SPRITEFRAMEPIVOT_T*"
 
diff --git a/src/lua_script.c b/src/lua_script.c
index 5cacc3f01bcad77d41c33ac101a7b48795695fde..e352673c0ac8a44e89dbfc9137ab25438b69cb6f 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -991,6 +991,7 @@ enum
 	ARCH_SKINCOLOR,
 	ARCH_MOUSE,
 	ARCH_SKIN,
+	ARCH_GAMETYPE,
 
 	ARCH_TEND=0xFF,
 };
@@ -1020,6 +1021,7 @@ static const struct {
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
 	{META_MOUSE,    ARCH_MOUSE},
 	{META_SKIN,     ARCH_SKIN},
+	{META_GAMETYPE, ARCH_GAMETYPE},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1348,6 +1350,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			WRITEUINT8(save_p, skin - skins); // UINT8 because MAXSKINS is only 32
 			break;
 		}
+		case ARCH_GAMETYPE:
+		{
+			gametype_t *gt = *((gametype_t **)lua_touserdata(gL, myindex));
+			WRITEUINT8(save_p, ARCH_GAMETYPE);
+			WRITEUINT8(save_p, gt - gametypes);
+			break;
+		}
 		default:
 			WRITEUINT8(save_p, ARCH_NULL);
 			return 2;
@@ -1597,6 +1606,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_SKIN:
 		LUA_PushUserdata(gL, &skins[READUINT8(save_p)], META_SKIN);
 		break;
+	case ARCH_GAMETYPE:
+		LUA_PushUserdata(gL, &gametypes[READUINT8(save_p)], META_GAMETYPE);
+		break;
 	case ARCH_TEND:
 		return 1;
 	}