diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d35e774e934d5b624b4d2bcc5c19b5bc24b6abf6..87a0499b6d088b5bb51cf36658de9238397b3402 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -276,6 +276,7 @@ set(SRB2_LUA_SOURCES
 	lua_hudlib.c
 	lua_infolib.c
 	lua_maplib.c
+	lua_taglib.c
 	lua_mathlib.c
 	lua_mobjlib.c
 	lua_playerlib.c
diff --git a/src/blua/Makefile.cfg b/src/blua/Makefile.cfg
index eae95ba3ae2cf52656cd40eaff40b185a694cf51..3a2962e659e24cbb34f9690afe52348393a31ed3 100644
--- a/src/blua/Makefile.cfg
+++ b/src/blua/Makefile.cfg
@@ -47,6 +47,7 @@ OBJS:=$(OBJS) \
 	$(OBJDIR)/lua_skinlib.o \
 	$(OBJDIR)/lua_thinkerlib.o \
 	$(OBJDIR)/lua_maplib.o \
+	$(OBJDIR)/lua_taglib.o \
 	$(OBJDIR)/lua_polyobjlib.o \
 	$(OBJDIR)/lua_blockmaplib.o \
 	$(OBJDIR)/lua_hudlib.o
diff --git a/src/doomtype.h b/src/doomtype.h
index c239c7b8e91d4f7edc24ad396fb84c106eac8789..950f50856b7679e035c268e036660fe505d8dbab 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -379,6 +379,28 @@ Needed for some lua shenanigans.
 #define FIELDFROM( type, field, have, want ) \
 	(void *)((intptr_t)(field) - offsetof (type, have) + offsetof (type, want))
 
+typedef UINT8 bitarray_t;
+
+#define BIT_ARRAY_SIZE(n) (((n) + 7) >> 3)
+
+static inline int
+in_bit_array (const bitarray_t * const array, const int value)
+{
+	return (array[value >> 3] & (1<<(value & 7)));
+}
+
+static inline void
+set_bit_array (bitarray_t * const array, const int value)
+{
+	array[value >> 3] |= (1<<(value & 7));
+}
+
+static inline void
+unset_bit_array (bitarray_t * const array, const int value)
+{
+	array[value >> 3] &= ~(1<<(value & 7));
+}
+
 #ifdef HAVE_SDL
 typedef UINT64 precise_t;
 #endif
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 515f6f0ba65b00e64e85c7a63d8c45b65868b08b..c5f847be63026831eb2fefd6b71637cfa67b3a80 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -155,6 +155,8 @@ static const struct {
 	{META_PIVOTLIST,    "spriteframepivot_t[]"},
 	{META_FRAMEPIVOT,   "spriteframepivot_t"},
 
+	{META_TAGLIST,      "taglist"},
+
 	{META_MOBJ,         "mobj_t"},
 	{META_MAPTHING,     "mapthing_t"},
 
@@ -186,6 +188,9 @@ static const struct {
 	{META_CVAR,         "consvar_t"},
 
 	{META_SECTORLINES,  "sector_t.lines"},
+#ifdef MUTABLE_TAGS
+	{META_SECTORTAGLIST, "sector_t.taglist"},
+#endif
 	{META_SIDENUM,      "line_t.sidenum"},
 	{META_LINEARGS,     "line_t.args"},
 	{META_LINESTRINGARGS, "line_t.stringargs"},
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 54ac89bcc559b796c3f7348f04c9c53237abc889..fbe8d48780f35009dfb0ca9b8f7209664297017c 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -12,6 +12,8 @@
 
 extern lua_State *gL;
 
+#define MUTABLE_TAGS
+
 #define LREG_VALID "VALID_USERDATA"
 #define LREG_EXTVARS "LUA_VARS"
 #define LREG_STATEACTION "STATE_ACTION"
@@ -27,6 +29,8 @@ extern lua_State *gL;
 #define META_PIVOTLIST "SPRITEFRAMEPIVOT_T[]"
 #define META_FRAMEPIVOT "SPRITEFRAMEPIVOT_T*"
 
+#define META_TAGLIST "TAGLIST"
+
 #define META_MOBJ "MOBJ_T*"
 #define META_MAPTHING "MAPTHING_T*"
 
@@ -58,6 +62,9 @@ extern lua_State *gL;
 #define META_CVAR "CONSVAR_T*"
 
 #define META_SECTORLINES "SECTOR_T*LINES"
+#ifdef MUTABLE_TAGS
+#define META_SECTORTAGLIST "sector_t.taglist"
+#endif
 #define META_SIDENUM "LINE_T*SIDENUM"
 #define META_LINEARGS "LINE_T*ARGS"
 #define META_LINESTRINGARGS "LINE_T*STRINGARGS"
@@ -95,6 +102,7 @@ int LUA_PlayerLib(lua_State *L);
 int LUA_SkinLib(lua_State *L);
 int LUA_ThinkerLib(lua_State *L);
 int LUA_MapLib(lua_State *L);
+int LUA_TagLib(lua_State *L);
 int LUA_PolyObjLib(lua_State *L);
 int LUA_BlockmapLib(lua_State *L);
 int LUA_HudLib(lua_State *L);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 83744c74d90378580f1c34d516e3a82f7b8067ae..6a9091cc949f364c8e01fd19d2b3e0eb740d44c8 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -37,6 +37,7 @@ enum sector_e {
 	sector_lightlevel,
 	sector_special,
 	sector_tag,
+	sector_taglist,
 	sector_thinglist,
 	sector_heightsec,
 	sector_camsec,
@@ -55,6 +56,7 @@ static const char *const sector_opt[] = {
 	"lightlevel",
 	"special",
 	"tag",
+	"taglist",
 	"thinglist",
 	"heightsec",
 	"camsec",
@@ -89,6 +91,7 @@ enum line_e {
 	line_flags,
 	line_special,
 	line_tag,
+	line_taglist,
 	line_args,
 	line_stringargs,
 	line_sidenum,
@@ -113,6 +116,7 @@ static const char *const line_opt[] = {
 	"flags",
 	"special",
 	"tag",
+	"taglist",
 	"args",
 	"stringargs",
 	"sidenum",
@@ -581,6 +585,9 @@ static int sector_get(lua_State *L)
 	case sector_tag:
 		lua_pushinteger(L, Tag_FGet(&sector->tags));
 		return 1;
+	case sector_taglist:
+		LUA_PushUserdata(L, &sector->tags, META_SECTORTAGLIST);
+		return 1;
 	case sector_thinglist: // thinglist
 		lua_pushcfunction(L, lib_iterateSectorThinglist);
 		LUA_PushUserdata(L, sector->thinglist, META_MOBJ);
@@ -682,6 +689,8 @@ static int sector_set(lua_State *L)
 	case sector_tag:
 		Tag_SectorFSet((UINT32)(sector - sectors), (INT16)luaL_checkinteger(L, 3));
 		break;
+	case sector_taglist:
+		return LUA_ErrSetDirectly(L, "sector_t", "taglist");
 	}
 	return 0;
 }
@@ -821,6 +830,9 @@ static int line_get(lua_State *L)
 	case line_tag:
 		lua_pushinteger(L, Tag_FGet(&line->tags));
 		return 1;
+	case line_taglist:
+		LUA_PushUserdata(L, &line->tags, META_TAGLIST);
+		return 1;
 	case line_args:
 		LUA_PushUserdata(L, line->args, META_LINEARGS);
 		return 1;
@@ -1385,25 +1397,15 @@ static int lib_iterateSectors(lua_State *L)
 
 static int lib_getSector(lua_State *L)
 {
-	int field;
 	INLEVEL
-	lua_settop(L, 2);
-	lua_remove(L, 1); // dummy userdata table is unused.
-	if (lua_isnumber(L, 1))
+	if (lua_isnumber(L, 2))
 	{
-		size_t i = lua_tointeger(L, 1);
+		size_t i = lua_tointeger(L, 2);
 		if (i >= numsectors)
 			return 0;
 		LUA_PushUserdata(L, &sectors[i], META_SECTOR);
 		return 1;
 	}
-	field = luaL_checkoption(L, 1, NULL, array_opt);
-	switch(field)
-	{
-	case 0: // iterate
-		lua_pushcfunction(L, lib_iterateSectors);
-		return 1;
-	}
 	return 0;
 }
 
@@ -1489,25 +1491,15 @@ static int lib_iterateLines(lua_State *L)
 
 static int lib_getLine(lua_State *L)
 {
-	int field;
 	INLEVEL
-	lua_settop(L, 2);
-	lua_remove(L, 1); // dummy userdata table is unused.
-	if (lua_isnumber(L, 1))
+	if (lua_isnumber(L, 2))
 	{
-		size_t i = lua_tointeger(L, 1);
+		size_t i = lua_tointeger(L, 2);
 		if (i >= numlines)
 			return 0;
 		LUA_PushUserdata(L, &lines[i], META_LINE);
 		return 1;
 	}
-	field = luaL_checkoption(L, 1, NULL, array_opt);
-	switch(field)
-	{
-	case 0: // iterate
-		lua_pushcfunction(L, lib_iterateLines);
-		return 1;
-	}
 	return 0;
 }
 
@@ -2360,15 +2352,13 @@ int LUA_MapLib(lua_State *L)
 		//lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSector);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsectors);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "sectors");
+	LUA_PushTaggableObjectArray(L, "sectors",
+			lib_iterateSectors,
+			lib_getSector,
+			lib_numsectors,
+			tags_sectors,
+			&numsectors, &sectors,
+			sizeof (sector_t), META_SECTOR);
 
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
@@ -2380,15 +2370,13 @@ int LUA_MapLib(lua_State *L)
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, "subsectors");
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getLine);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numlines);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "lines");
+	LUA_PushTaggableObjectArray(L, "lines",
+			lib_iterateLines,
+			lib_getLine,
+			lib_numlines,
+			tags_lines,
+			&numlines, &lines,
+			sizeof (line_t), META_LINE);
 
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index 7aae18c90a31f43dd43fefc6e33c6085003d20af..65adceb154cb66424c73b3415253891307a9f706 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -22,8 +22,6 @@
 #include "lua_hud.h" // hud_running errors
 #include "lua_hook.h" // hook_cmd_running errors
 
-static const char *const array_opt[] ={"iterate",NULL};
-
 enum mobj_e {
 	mobj_valid = 0,
 	mobj_x,
@@ -904,6 +902,11 @@ static int mapthing_get(lua_State *L)
 		number = mt->extrainfo;
 	else if(fastcmp(field,"tag"))
 		number = Tag_FGet(&mt->tags);
+	else if(fastcmp(field,"taglist"))
+	{
+		LUA_PushUserdata(L, &mt->tags, META_TAGLIST);
+		return 1;
+	}
 	else if(fastcmp(field,"args"))
 	{
 		LUA_PushUserdata(L, mt->args, META_THINGARGS);
@@ -966,6 +969,8 @@ static int mapthing_set(lua_State *L)
 	}
 	else if (fastcmp(field,"tag"))
 		Tag_FSet(&mt->tags, (INT16)luaL_checkinteger(L, 3));
+	else if (fastcmp(field,"taglist"))
+		return LUA_ErrSetDirectly(L, "mapthing_t", "taglist");
 	else if(fastcmp(field,"mobj"))
 		mt->mobj = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 	else
@@ -1003,25 +1008,15 @@ static int lib_iterateMapthings(lua_State *L)
 
 static int lib_getMapthing(lua_State *L)
 {
-	int field;
 	INLEVEL
-	lua_settop(L, 2);
-	lua_remove(L, 1); // dummy userdata table is unused.
-	if (lua_isnumber(L, 1))
+	if (lua_isnumber(L, 2))
 	{
-		size_t i = lua_tointeger(L, 1);
+		size_t i = lua_tointeger(L, 2);
 		if (i >= nummapthings)
 			return 0;
 		LUA_PushUserdata(L, &mapthings[i], META_MAPTHING);
 		return 1;
 	}
-	field = luaL_checkoption(L, 1, NULL, array_opt);
-	switch(field)
-	{
-	case 0: // iterate
-		lua_pushcfunction(L, lib_iterateMapthings);
-		return 1;
-	}
 	return 0;
 }
 
@@ -1068,14 +1063,13 @@ int LUA_MobjLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L,1);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getMapthing);
-			lua_setfield(L, -2, "__index");
+	LUA_PushTaggableObjectArray(L, "mapthings",
+			lib_iterateMapthings,
+			lib_getMapthing,
+			lib_nummapthings,
+			tags_mapthings,
+			&nummapthings, &mapthings,
+			sizeof (mapthing_t), META_MAPTHING);
 
-			lua_pushcfunction(L, lib_nummapthings);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "mapthings");
 	return 0;
 }
diff --git a/src/lua_script.c b/src/lua_script.c
index eb4737f7655d018ced5cec7bd83625768b9b8caf..bc88928f3f2e0e786d12c9e941e87dc875d3ce48 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -53,6 +53,7 @@ static lua_CFunction liblist[] = {
 	LUA_SkinLib, // skin_t, skins[]
 	LUA_ThinkerLib, // thinker_t
 	LUA_MapLib, // line_t, side_t, sector_t, subsector_t
+	LUA_TagLib, // tags
 	LUA_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
@@ -739,25 +740,37 @@ void LUA_PushLightUserdata (lua_State *L, void *data, const char *meta)
 // Pushes it to the stack and stores it in the registry.
 void LUA_PushUserdata(lua_State *L, void *data, const char *meta)
 {
+	if (LUA_RawPushUserdata(L, data) == LPUSHED_NEW)
+	{
+		luaL_getmetatable(L, meta);
+		lua_setmetatable(L, -2);
+	}
+}
+
+// Same as LUA_PushUserdata but don't set a metatable yet.
+lpushed_t LUA_RawPushUserdata(lua_State *L, void *data)
+{
+	lpushed_t status = LPUSHED_NIL;
+
 	void **userdata;
 
 	if (!data) { // push a NULL
 		lua_pushnil(L);
-		return;
+		return status;
 	}
 
 	lua_getfield(L, LUA_REGISTRYINDEX, LREG_VALID);
 	I_Assert(lua_istable(L, -1));
+
 	lua_pushlightuserdata(L, data);
 	lua_rawget(L, -2);
+
 	if (lua_isnil(L, -1)) { // no userdata? deary me, we'll have to make one.
 		lua_pop(L, 1); // pop the nil
 
 		// create the userdata
 		userdata = lua_newuserdata(L, sizeof(void *));
 		*userdata = data;
-		luaL_getmetatable(L, meta);
-		lua_setmetatable(L, -2);
 
 		// Set it in the registry so we can find it again
 		lua_pushlightuserdata(L, data); // k (store the userdata via the data's pointer)
@@ -765,8 +778,15 @@ void LUA_PushUserdata(lua_State *L, void *data, const char *meta)
 		lua_rawset(L, -4);
 
 		// stack is left with the userdata on top, as if getting it had originally succeeded.
+
+		status = LPUSHED_NEW;
 	}
+	else
+		status = LPUSHED_EXISTING;
+
 	lua_remove(L, -2); // remove LREG_VALID
+
+	return status;
 }
 
 // When userdata is freed, use this function to remove it from Lua.
@@ -826,6 +846,7 @@ void LUA_InvalidateLevel(void)
 	{
 		LUA_InvalidateUserdata(&sectors[i]);
 		LUA_InvalidateUserdata(&sectors[i].lines);
+		LUA_InvalidateUserdata(&sectors[i].tags);
 		if (sectors[i].ffloors)
 		{
 			for (rover = sectors[i].ffloors; rover; rover = rover->next)
@@ -835,6 +856,7 @@ void LUA_InvalidateLevel(void)
 	for (i = 0; i < numlines; i++)
 	{
 		LUA_InvalidateUserdata(&lines[i]);
+		LUA_InvalidateUserdata(&lines[i].tags);
 		LUA_InvalidateUserdata(lines[i].sidenum);
 	}
 	for (i = 0; i < numsides; i++)
@@ -866,7 +888,10 @@ void LUA_InvalidateMapthings(void)
 		return;
 
 	for (i = 0; i < nummapthings; i++)
+	{
 		LUA_InvalidateUserdata(&mapthings[i]);
+		LUA_InvalidateUserdata(&mapthings[i].tags);
+	}
 }
 
 void LUA_InvalidatePlayer(player_t *player)
@@ -1681,3 +1706,36 @@ int Lua_optoption(lua_State *L, int narg,
 			return i;
 	return -1;
 }
+
+void LUA_PushTaggableObjectArray
+(		lua_State *L,
+		const char *field,
+		lua_CFunction iterator,
+		lua_CFunction indexer,
+		lua_CFunction counter,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char *meta)
+{
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_createtable(L, 0, 2);
+				lua_pushcfunction(L, iterator);
+				lua_setfield(L, -2, "iterate");
+
+				LUA_InsertTaggroupIterator(L, garray,
+						max_elements, element_array, sizeof_element, meta);
+
+				lua_createtable(L, 0, 1);
+					lua_pushcfunction(L, indexer);
+					lua_setfield(L, -2, "__index");
+				lua_setmetatable(L, -2);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, counter);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, field);
+}
diff --git a/src/lua_script.h b/src/lua_script.h
index 79ba0bb38a5e1aeb6af476fb4c5b01b39aeb63f2..77fbb7c1d125f682db270b7d4f051d6673290d43 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -10,10 +10,14 @@
 /// \file  lua_script.h
 /// \brief Lua scripting basics
 
+#ifndef LUA_SCRIPT_H
+#define LUA_SCRIPT_H
+
 #include "m_fixed.h"
 #include "doomtype.h"
 #include "d_player.h"
 #include "g_state.h"
+#include "taglist.h"
 
 #include "blua/lua.h"
 #include "blua/lualib.h"
@@ -46,12 +50,6 @@ void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults);
 void LUA_DumpFile(const char *filename);
 #endif
 fixed_t LUA_EvalMath(const char *word);
-void LUA_PushLightUserdata(lua_State *L, void *data, const char *meta);
-void LUA_PushUserdata(lua_State *L, void *data, const char *meta);
-void LUA_InvalidateUserdata(void *data);
-void LUA_InvalidateLevel(void);
-void LUA_InvalidateMapthings(void);
-void LUA_InvalidatePlayer(player_t *player);
 void LUA_Step(void);
 void LUA_Archive(void);
 void LUA_UnArchive(void);
@@ -63,11 +61,49 @@ int Lua_optoption(lua_State *L, int narg,
 	const char *def, const char *const lst[]);
 void LUAh_NetArchiveHook(lua_CFunction archFunc);
 
+void LUA_PushTaggableObjectArray
+(		lua_State *L,
+		const char *field,
+		lua_CFunction iterator,
+		lua_CFunction indexer,
+		lua_CFunction counter,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char *meta);
+
+void LUA_InsertTaggroupIterator
+(		lua_State *L,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char * meta);
+
+typedef enum {
+	LPUSHED_NIL,
+	LPUSHED_NEW,
+	LPUSHED_EXISTING,
+} lpushed_t;
+
+void LUA_PushLightUserdata(lua_State *L, void *data, const char *meta);
+void LUA_PushUserdata(lua_State *L, void *data, const char *meta);
+lpushed_t LUA_RawPushUserdata(lua_State *L, void *data);
+
+void LUA_InvalidateUserdata(void *data);
+
+void LUA_InvalidateLevel(void);
+void LUA_InvalidateMapthings(void);
+void LUA_InvalidatePlayer(player_t *player);
+
 // Console wrapper
 void COM_Lua_f(void);
 
 #define LUA_ErrInvalid(L, type) luaL_error(L, "accessed " type " doesn't exist anymore, please check 'valid' before using " type ".");
 
+#define LUA_ErrSetDirectly(L, type, field) luaL_error(L, type " field " LUA_QL(field) " cannot be set directly.")
+
 // Deprecation warnings
 // Shows once upon use. Then doesn't show again.
 #define LUA_Deprecated(L,this_func,use_instead)\
@@ -98,3 +134,5 @@ void COM_Lua_f(void);
 
 #define INLEVEL if (! ISINLEVEL)\
 return luaL_error(L, "This can only be used in a level!");
+
+#endif/*LUA_SCRIPT_H*/
diff --git a/src/lua_taglib.c b/src/lua_taglib.c
new file mode 100644
index 0000000000000000000000000000000000000000..c9f320fe8f230524b0b42b10b04022b8d79a5d10
--- /dev/null
+++ b/src/lua_taglib.c
@@ -0,0 +1,451 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020 by James R.
+// Copyright (C) 2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  lua_taglib.c
+/// \brief tag list iterator for Lua scripting
+
+#include "doomdef.h"
+#include "taglist.h"
+#include "r_state.h"
+
+#include "lua_script.h"
+#include "lua_libs.h"
+
+#ifdef MUTABLE_TAGS
+#include "z_zone.h"
+#endif
+
+static int tag_iterator(lua_State *L)
+{
+	INT32 tag = lua_isnil(L, 2) ? -1 : lua_tonumber(L, 2);
+	do
+	{
+		if (++tag >= MAXTAGS)
+			return 0;
+	}
+	while (! in_bit_array(tags_available, tag)) ;
+	lua_pushnumber(L, tag);
+	return 1;
+}
+
+enum {
+#define UPVALUE lua_upvalueindex
+	up_garray         = UPVALUE(1),
+	up_max_elements   = UPVALUE(2),
+	up_element_array  = UPVALUE(3),
+	up_sizeof_element = UPVALUE(4),
+	up_meta           = UPVALUE(5),
+#undef UPVALUE
+};
+
+static INT32 next_element(lua_State *L, const mtag_t tag, const size_t p)
+{
+	taggroup_t ** garray = lua_touserdata(L, up_garray);
+	const size_t * max_elements = lua_touserdata(L, up_max_elements);
+	return Taggroup_Iterate(garray, *max_elements, tag, p);
+}
+
+static void push_element(lua_State *L, void *element)
+{
+	if (LUA_RawPushUserdata(L, element) == LPUSHED_NEW)
+	{
+		lua_pushvalue(L, up_meta);
+		lua_setmetatable(L, -2);
+	}
+}
+
+static void push_next_element(lua_State *L, const INT32 element)
+{
+	char * element_array = *(char **)lua_touserdata(L, up_element_array);
+	const size_t sizeof_element = lua_tonumber(L, up_sizeof_element);
+	push_element(L, &element_array[element * sizeof_element]);
+}
+
+struct element_iterator_state {
+	mtag_t tag;
+	size_t p;
+};
+
+static int element_iterator(lua_State *L)
+{
+	struct element_iterator_state * state = lua_touserdata(L, 1);
+	if (lua_isnoneornil(L, 3))
+		state->p = 0;
+	lua_pushnumber(L, ++state->p);
+	lua_gettable(L, 1);
+	return 1;
+}
+
+static int lib_iterateTags(lua_State *L)
+{
+	if (lua_gettop(L) < 2)
+	{
+		lua_pushcfunction(L, tag_iterator);
+		return 1;
+	}
+	else
+		return tag_iterator(L);
+}
+
+static int lib_numTags(lua_State *L)
+{
+	lua_pushnumber(L, num_tags);
+	return 1;
+}
+
+static int lib_getTaggroup(lua_State *L)
+{
+	struct element_iterator_state *state;
+
+	mtag_t tag;
+
+	if (lua_gettop(L) > 1)
+		return luaL_error(L, "too many arguments");
+
+	if (lua_isnoneornil(L, 1))
+	{
+		tag = MTAG_GLOBAL;
+	}
+	else
+	{
+		tag = lua_tonumber(L, 1);
+		luaL_argcheck(L, tag >= -1, 1, "tag out of range");
+	}
+
+	state = lua_newuserdata(L, sizeof *state);
+	state->tag = tag;
+	state->p = 0;
+
+	lua_pushvalue(L, lua_upvalueindex(1));
+	lua_setmetatable(L, -2);
+
+	return 1;
+}
+
+static int lib_getTaggroupElement(lua_State *L)
+{
+	const size_t p = luaL_checknumber(L, 2) - 1;
+	const mtag_t tag = *(mtag_t *)lua_touserdata(L, 1);
+	const INT32 element = next_element(L, tag, p);
+
+	if (element == -1)
+		return 0;
+	else
+	{
+		push_next_element(L, element);
+		return 1;
+	}
+}
+
+static int lib_numTaggroupElements(lua_State *L)
+{
+	const mtag_t tag = *(mtag_t *)lua_touserdata(L, 1);
+	if (tag == MTAG_GLOBAL)
+		lua_pushnumber(L, *(size_t *)lua_touserdata(L, up_max_elements));
+	else
+	{
+		const taggroup_t ** garray = lua_touserdata(L, up_garray);
+		lua_pushnumber(L, Taggroup_Count(garray[tag]));
+	}
+	return 1;
+}
+
+#ifdef MUTABLE_TAGS
+static int meta_ref[2];
+#endif
+
+static int has_valid_field(lua_State *L)
+{
+	int equal;
+	lua_rawgeti(L, LUA_ENVIRONINDEX, 1);
+	equal = lua_rawequal(L, 2, -1);
+	lua_pop(L, 1);
+	return equal;
+}
+
+static taglist_t * valid_taglist(lua_State *L, int idx, boolean getting)
+{
+	taglist_t *list = *(taglist_t **)lua_touserdata(L, idx);
+
+	if (list == NULL)
+	{
+		if (getting && has_valid_field(L))
+			lua_pushboolean(L, 0);
+		else
+			LUA_ErrInvalid(L, "taglist");/* doesn't actually return */
+		return NULL;
+	}
+	else
+		return list;
+}
+
+static taglist_t * check_taglist(lua_State *L, int idx)
+{
+	if (lua_isuserdata(L, idx) && lua_getmetatable(L, idx))
+	{
+		lua_getref(L, meta_ref[0]);
+		lua_getref(L, meta_ref[1]);
+
+		if (lua_rawequal(L, -3, -2) || lua_rawequal(L, -3, -1))
+		{
+			lua_pop(L, 3);
+			return valid_taglist(L, idx, false);
+		}
+	}
+
+	return luaL_argerror(L, idx, "must be a tag list"), NULL;
+}
+
+static int taglist_get(lua_State *L)
+{
+	const taglist_t *list = valid_taglist(L, 1, true);
+
+	if (list == NULL)/* valid check */
+		return 1;
+
+	if (lua_isnumber(L, 2))
+	{
+		const size_t i = lua_tonumber(L, 2);
+
+		if (list && i <= list->count)
+		{
+			lua_pushnumber(L, list->tags[i - 1]);
+			return 1;
+		}
+		else
+			return 0;
+	}
+	else if (has_valid_field(L))
+	{
+		lua_pushboolean(L, 1);
+		return 1;
+	}
+	else
+	{
+		lua_getmetatable(L, 1);
+		lua_replace(L, 1);
+		lua_rawget(L, 1);
+		return 1;
+	}
+}
+
+static int taglist_len(lua_State *L)
+{
+	const taglist_t *list = valid_taglist(L, 1, false);
+	lua_pushnumber(L, list->count);
+	return 1;
+}
+
+static int taglist_equal(lua_State *L)
+{
+	const taglist_t *lhs = check_taglist(L, 1);
+	const taglist_t *rhs = check_taglist(L, 2);
+	lua_pushboolean(L, Tag_Compare(lhs, rhs));
+	return 1;
+}
+
+static int taglist_iterator(lua_State *L)
+{
+	const taglist_t *list = valid_taglist(L, 1, false);
+	const size_t i = 1 + lua_tonumber(L, lua_upvalueindex(1));
+	if (i <= list->count)
+	{
+		lua_pushnumber(L, list->tags[i - 1]);
+		/* watch me exploit an upvalue as a control because
+			I want to use the control as the value */
+		lua_pushnumber(L, i);
+		lua_replace(L, lua_upvalueindex(1));
+		return 1;
+	}
+	else
+		return 0;
+}
+
+static int taglist_iterate(lua_State *L)
+{
+	check_taglist(L, 1);
+	lua_pushnumber(L, 0);
+	lua_pushcclosure(L, taglist_iterator, 1);
+	lua_pushvalue(L, 1);
+	return 2;
+}
+
+static int taglist_find(lua_State *L)
+{
+	const taglist_t *list = check_taglist(L, 1);
+	const mtag_t tag = luaL_checknumber(L, 2);
+	lua_pushboolean(L, Tag_Find(list, tag));
+	return 1;
+}
+
+static int taglist_shares(lua_State *L)
+{
+	const taglist_t *lhs = check_taglist(L, 1);
+	const taglist_t *rhs = check_taglist(L, 2);
+	lua_pushboolean(L, Tag_Share(lhs, rhs));
+	return 1;
+}
+
+/* only sector tags are mutable... */
+
+#ifdef MUTABLE_TAGS
+static size_t sector_of_taglist(taglist_t *list)
+{
+	return (sector_t *)((char *)list - offsetof (sector_t, tags)) - sectors;
+}
+
+static int this_taglist(lua_State *L)
+{
+	lua_settop(L, 1);
+	return 1;
+}
+
+static int taglist_add(lua_State *L)
+{
+	taglist_t *list = *(taglist_t **)luaL_checkudata(L, 1, META_SECTORTAGLIST);
+	const mtag_t tag = luaL_checknumber(L, 2);
+
+	if (! Tag_Find(list, tag))
+	{
+		Taggroup_Add(tags_sectors, tag, sector_of_taglist(list));
+		Tag_Add(list, tag);
+	}
+
+	return this_taglist(L);
+}
+
+static int taglist_remove(lua_State *L)
+{
+	taglist_t *list = *(taglist_t **)luaL_checkudata(L, 1, META_SECTORTAGLIST);
+	const mtag_t tag = luaL_checknumber(L, 2);
+
+	size_t i;
+
+	for (i = 0; i < list->count; ++i)
+	{
+		if (list->tags[i] == tag)
+		{
+			if (list->count > 1)
+			{
+				memmove(&list->tags[i], &list->tags[i + 1],
+						(list->count - 1 - i) * sizeof (mtag_t));
+				list->tags = Z_Realloc(list->tags,
+						(--list->count) * sizeof (mtag_t), PU_LEVEL, NULL);
+				Taggroup_Remove(tags_sectors, tag, sector_of_taglist(list));
+			}
+			else/* reset to default tag */
+				Tag_SectorFSet(sector_of_taglist(list), 0);
+			break;
+		}
+	}
+
+	return this_taglist(L);
+}
+#endif/*MUTABLE_TAGS*/
+
+void LUA_InsertTaggroupIterator
+(		lua_State *L,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char * meta)
+{
+	lua_createtable(L, 0, 3);
+		lua_pushlightuserdata(L, garray);
+		lua_pushlightuserdata(L, max_elements);
+
+		lua_pushvalue(L, -2);
+		lua_pushvalue(L, -2);
+		lua_pushlightuserdata(L, element_array);
+		lua_pushnumber(L, sizeof_element);
+		luaL_getmetatable(L, meta);
+		lua_pushcclosure(L, lib_getTaggroupElement, 5);
+		lua_setfield(L, -4, "__index");
+
+		lua_pushcclosure(L, lib_numTaggroupElements, 2);
+		lua_setfield(L, -2, "__len");
+
+		lua_pushcfunction(L, element_iterator);
+		lua_setfield(L, -2, "__call");
+	lua_pushcclosure(L, lib_getTaggroup, 1);
+	lua_setfield(L, -2, "tagged");
+}
+
+static luaL_Reg taglist_lib[] = {
+	{"iterate", taglist_iterate},
+	{"find", taglist_find},
+	{"shares", taglist_shares},
+#ifdef MUTABLE_TAGS
+	{"add", taglist_add},
+	{"remove", taglist_remove},
+#endif
+	{0}
+};
+
+static void open_taglist(lua_State *L)
+{
+	luaL_register(L, "taglist", taglist_lib);
+
+	lua_getfield(L, -1, "find");
+	lua_setfield(L, -2, "has");
+}
+
+#define new_literal(L, s) \
+	(lua_pushliteral(L, s), luaL_ref(L, -2))
+
+#ifdef MUTABLE_TAGS
+static int
+#else
+static void
+#endif
+set_taglist_metatable(lua_State *L, const char *meta)
+{
+	luaL_newmetatable(L, meta);
+		lua_pushcfunction(L, taglist_get);
+		lua_createtable(L, 0, 1);
+			new_literal(L, "valid");
+		lua_setfenv(L, -2);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, taglist_len);
+		lua_setfield(L, -2, "__len");
+
+		lua_pushcfunction(L, taglist_equal);
+		lua_setfield(L, -2, "__eq");
+#ifdef MUTABLE_TAGS
+	return luaL_ref(L, LUA_REGISTRYINDEX);
+#endif
+}
+
+int LUA_TagLib(lua_State *L)
+{
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_createtable(L, 0, 1);
+				lua_pushcfunction(L, lib_iterateTags);
+				lua_setfield(L, -2, "iterate");
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_numTags);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "tags");
+
+	open_taglist(L);
+
+#ifdef MUTABLE_TAGS
+	meta_ref[0] = set_taglist_metatable(L, META_TAGLIST);
+	meta_ref[1] = set_taglist_metatable(L, META_SECTORTAGLIST);
+#else
+	set_taglist_metatable(L, META_TAGLIST);
+#endif
+
+	return 0;
+}
diff --git a/src/taglist.c b/src/taglist.c
index b11216b6cf7b3e7c709b73750c2d46e8e9155c40..a759f4d02bfc0658ea68a6b6becd20dffaad7700 100644
--- a/src/taglist.c
+++ b/src/taglist.c
@@ -15,6 +15,11 @@
 #include "z_zone.h"
 #include "r_data.h"
 
+// Bit array of whether a tag exists for sectors/lines/things.
+bitarray_t tags_available[BIT_ARRAY_SIZE (MAXTAGS)];
+
+size_t num_tags;
+
 // Taggroups are used to list elements of the same tag, for iteration.
 // Since elements can now have multiple tags, it means an element may appear
 // in several taggroups at the same time. These are built on level load.
@@ -105,6 +110,39 @@ size_t Taggroup_Find (const taggroup_t *group, const size_t id)
 	return -1;
 }
 
+/// group->count, but also checks for NULL
+size_t Taggroup_Count (const taggroup_t *group)
+{
+	return group ? group->count : 0;
+}
+
+/// Iterate thru elements in a global taggroup.
+INT32 Taggroup_Iterate
+(		taggroup_t *garray[],
+		const size_t max_elements,
+		const mtag_t tag,
+		const size_t p)
+{
+	const taggroup_t *group;
+
+	if (tag == MTAG_GLOBAL)
+	{
+		if (p < max_elements)
+			return p;
+		return -1;
+	}
+
+	group = garray[(UINT16)tag];
+
+	if (group)
+	{
+		if (p < group->count)
+			return group->elements[p];
+		return -1;
+	}
+	return -1;
+}
+
 /// Add an element to a global taggroup.
 void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 {
@@ -120,6 +158,12 @@ void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 	if (Taggroup_Find(group, id) != (size_t)-1)
 		return;
 
+	if (! in_bit_array(tags_available, tag))
+	{
+		num_tags++;
+		set_bit_array(tags_available, tag);
+	}
+
 	// Create group if empty.
 	if (!group)
 	{
@@ -133,25 +177,34 @@ void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 		for (i = 0; i < group->count; i++)
 			if (group->elements[i] > id)
 				break;
+	}
 
-		group->elements = Z_Realloc(group->elements, (group->count + 1) * sizeof(size_t), PU_LEVEL, NULL);
+	group->elements = Z_Realloc(group->elements, (group->count + 1) * sizeof(size_t), PU_LEVEL, NULL);
 
-		// Offset existing elements to make room for the new one.
-		if (i < group->count)
-			memmove(&group->elements[i + 1], &group->elements[i], group->count - i);
-	}
+	// Offset existing elements to make room for the new one.
+	if (i < group->count)
+		memmove(&group->elements[i + 1], &group->elements[i], group->count - i);
 
 	group->count++;
-	group->elements = Z_Realloc(group->elements, group->count * sizeof(size_t), PU_LEVEL, NULL);
 	group->elements[i] = id;
 }
 
+static size_t total_elements_with_tag (const mtag_t tag)
+{
+	return
+		(
+				Taggroup_Count(tags_sectors[tag]) +
+				Taggroup_Count(tags_lines[tag]) +
+				Taggroup_Count(tags_mapthings[tag])
+		);
+}
+
 /// Remove an element from a global taggroup.
 void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 {
 	taggroup_t *group;
 	size_t rempos;
-	size_t newcount;
+	size_t oldcount;
 
 	if (tag == MTAG_GLOBAL)
 		return;
@@ -161,8 +214,14 @@ void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 	if ((rempos = Taggroup_Find(group, id)) == (size_t)-1)
 		return;
 
+	if (group->count == 1 && total_elements_with_tag(tag) == 1)
+	{
+		num_tags--;
+		unset_bit_array(tags_available, tag);
+	}
+
 	// Strip away taggroup if no elements left.
-	if (!(newcount = --group->count))
+	if (!(oldcount = group->count--))
 	{
 		Z_Free(group->elements);
 		Z_Free(group);
@@ -170,19 +229,18 @@ void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 	}
 	else
 	{
-		size_t *newelements = Z_Malloc(newcount * sizeof(size_t), PU_LEVEL, NULL);
+		size_t *newelements = Z_Malloc(group->count * sizeof(size_t), PU_LEVEL, NULL);
 		size_t i;
 
 		// Copy the previous entries save for the one to remove.
 		for (i = 0; i < rempos; i++)
 			newelements[i] = group->elements[i];
 
-		for (i = rempos + 1; i < group->count; i++)
+		for (i = rempos + 1; i < oldcount; i++)
 			newelements[i - 1] = group->elements[i];
 
 		Z_Free(group->elements);
 		group->elements = newelements;
-		group->count = newcount;
 	}
 }
 
@@ -209,6 +267,9 @@ void Taglist_InitGlobalTables(void)
 {
 	size_t i, j;
 
+	memset(tags_available, 0, sizeof tags_available);
+	num_tags = 0;
+
 	for (i = 0; i < MAXTAGS; i++)
 	{
 		tags_sectors[i] = NULL;
@@ -236,56 +297,17 @@ void Taglist_InitGlobalTables(void)
 
 INT32 Tag_Iterate_Sectors (const mtag_t tag, const size_t p)
 {
-	if (tag == MTAG_GLOBAL)
-	{
-		if (p < numsectors)
-			return p;
-		return -1;
-	}
-
-	if (tags_sectors[(UINT16)tag])
-	{
-		if (p < tags_sectors[(UINT16)tag]->count)
-			return tags_sectors[(UINT16)tag]->elements[p];
-		return -1;
-	}
-	return -1;
+	return Taggroup_Iterate(tags_sectors, numsectors, tag, p);
 }
 
 INT32 Tag_Iterate_Lines (const mtag_t tag, const size_t p)
 {
-	if (tag == MTAG_GLOBAL)
-	{
-		if (p < numlines)
-			return p;
-		return -1;
-	}
-
-	if (tags_lines[(UINT16)tag])
-	{
-		if (p < tags_lines[(UINT16)tag]->count)
-			return tags_lines[(UINT16)tag]->elements[p];
-		return -1;
-	}
-	return -1;
+	return Taggroup_Iterate(tags_lines, numlines, tag, p);
 }
 
 INT32 Tag_Iterate_Things (const mtag_t tag, const size_t p)
 {
-	if (tag == MTAG_GLOBAL)
-	{
-		if (p < nummapthings)
-			return p;
-		return -1;
-	}
-
-	if (tags_mapthings[(UINT16)tag])
-	{
-		if (p < tags_mapthings[(UINT16)tag]->count)
-			return tags_mapthings[(UINT16)tag]->elements[p];
-		return -1;
-	}
-	return -1;
+	return Taggroup_Iterate(tags_mapthings, nummapthings, tag, p);
 }
 
 INT32 Tag_FindLineSpecial(const INT16 special, const mtag_t tag)
diff --git a/src/taglist.h b/src/taglist.h
index 0e6d9f8422bbc150dbbabe3c6048d6e66c448f05..a0529ab6b6afae4fe4252e61fd5848e2bd875ef6 100644
--- a/src/taglist.h
+++ b/src/taglist.h
@@ -43,6 +43,10 @@ typedef struct
 	size_t count;
 } taggroup_t;
 
+extern bitarray_t tags_available[];
+
+extern size_t num_tags;
+
 extern taggroup_t* tags_sectors[];
 extern taggroup_t* tags_lines[];
 extern taggroup_t* tags_mapthings[];
@@ -50,6 +54,13 @@ extern taggroup_t* tags_mapthings[];
 void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id);
 void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id);
 size_t Taggroup_Find (const taggroup_t *group, const size_t id);
+size_t Taggroup_Count (const taggroup_t *group);
+
+INT32 Taggroup_Iterate
+(		taggroup_t *garray[],
+		const size_t max_elements,
+		const mtag_t tag,
+		const size_t p);
 
 void Taglist_InitGlobalTables(void);