diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 962f8f87a..6446ff109 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -263,6 +263,7 @@ set(SRB2_LUA_SOURCES
 	lua_mathlib.c
 	lua_mobjlib.c
 	lua_playerlib.c
+	lua_polyobjlib.c
 	lua_script.c
 	lua_skinlib.c
 	lua_thinkerlib.c
diff --git a/src/blua/Makefile.cfg b/src/blua/Makefile.cfg
index 12ea064b4..eae95ba3a 100644
--- a/src/blua/Makefile.cfg
+++ b/src/blua/Makefile.cfg
@@ -47,5 +47,6 @@ OBJS:=$(OBJS) \
 	$(OBJDIR)/lua_skinlib.o \
 	$(OBJDIR)/lua_thinkerlib.o \
 	$(OBJDIR)/lua_maplib.o \
+	$(OBJDIR)/lua_polyobjlib.o \
 	$(OBJDIR)/lua_blockmaplib.o \
 	$(OBJDIR)/lua_hudlib.o
diff --git a/src/dehacked.c b/src/dehacked.c
index c9a50e6d1..2ede2fd16 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -9908,6 +9908,25 @@ struct {
 	{"FF_COLORMAPONLY",FF_COLORMAPONLY},       ///< Only copy the colormap, not the lightlevel
 	{"FF_GOOWATER",FF_GOOWATER},               ///< Used with ::FF_SWIMMABLE. Makes thick bouncey goop.
 
+	// PolyObject flags
+	{"POF_CLIPLINES",POF_CLIPLINES},               ///< Test against lines for collision
+	{"POF_CLIPPLANES",POF_CLIPPLANES},             ///< Test against tops and bottoms for collision
+	{"POF_SOLID",POF_SOLID},                       ///< Clips things.
+	{"POF_TESTHEIGHT",POF_TESTHEIGHT},             ///< Test line collision with heights
+	{"POF_RENDERSIDES",POF_RENDERSIDES},           ///< Renders the sides.
+	{"POF_RENDERTOP",POF_RENDERTOP},               ///< Renders the top.
+	{"POF_RENDERBOTTOM",POF_RENDERBOTTOM},         ///< Renders the bottom.
+	{"POF_RENDERPLANES",POF_RENDERPLANES},         ///< Renders top and bottom.
+	{"POF_RENDERALL",POF_RENDERALL},               ///< Renders everything.
+	{"POF_INVERT",POF_INVERT},                     ///< Inverts collision (like a cage).
+	{"POF_INVERTPLANES",POF_INVERTPLANES},         ///< Render inside planes.
+	{"POF_INVERTPLANESONLY",POF_INVERTPLANESONLY}, ///< Only render inside planes.
+	{"POF_PUSHABLESTOP",POF_PUSHABLESTOP},         ///< Pushables will stop movement.
+	{"POF_LDEXEC",POF_LDEXEC},                     ///< This PO triggers a linedef executor.
+	{"POF_ONESIDE",POF_ONESIDE},                   ///< Only use the first side of the linedef.
+	{"POF_NOSPECIALS",POF_NOSPECIALS},             ///< Don't apply sector specials.
+	{"POF_SPLAT",POF_SPLAT},                       ///< Use splat flat renderer (treat cyan pixels as invisible).
+
 #ifdef HAVE_LUA_SEGS
 	// Node flags
 	{"NF_SUBSECTOR",NF_SUBSECTOR}, // Indicate a leaf.
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index e753c5af3..132ebc1a8 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -173,8 +173,13 @@ static const struct {
 	{META_SEG,          "seg_t"},
 	{META_NODE,         "node_t"},
 #endif
+	{META_SLOPE,        "slope_t"},
+	{META_VECTOR2,      "vector2_t"},
+	{META_VECTOR3,      "vector3_t"},
 	{META_MAPHEADER,    "mapheader_t"},
 
+	{META_POLYOBJ,      "polyobj_t"},
+
 	{META_CVAR,         "consvar_t"},
 
 	{META_SECTORLINES,  "sector_t.lines"},
diff --git a/src/lua_blockmaplib.c b/src/lua_blockmaplib.c
index 5aae73284..1949d56bb 100644
--- a/src/lua_blockmaplib.c
+++ b/src/lua_blockmaplib.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2016 by Iestyn "Monster Iestyn" Jealous.
+// Copyright (C) 2016-2020 by Iestyn "Monster Iestyn" Jealous.
 // Copyright (C) 2016-2020 by Sonic Team Junior.
 //
 // This program is free software distributed under the
@@ -13,6 +13,7 @@
 #include "doomdef.h"
 #include "p_local.h"
 #include "r_main.h" // validcount
+#include "p_polyobj.h"
 #include "lua_script.h"
 #include "lua_libs.h"
 //#include "lua_hud.h" // hud_running errors
@@ -20,6 +21,7 @@
 static const char *const search_opt[] = {
 	"objects",
 	"lines",
+	"polyobjs",
 	NULL};
 
 // a quickly-made function pointer typedef used by lib_searchBlockmap...
@@ -167,6 +169,55 @@ static UINT8 lib_searchBlockmap_Lines(lua_State *L, INT32 x, INT32 y, mobj_t *th
 	return 0; // Everything was checked.
 }
 
+// Helper function for "polyobjs" search
+static UINT8 lib_searchBlockmap_PolyObjs(lua_State *L, INT32 x, INT32 y, mobj_t *thing)
+{
+	INT32 offset;
+	polymaplink_t *plink; // haleyjd 02/22/06
+
+	if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
+		return 0;
+
+	offset = y*bmapwidth + x;
+
+	// haleyjd 02/22/06: consider polyobject lines
+	plink = polyblocklinks[offset];
+
+	while (plink)
+	{
+		polyobj_t *po = plink->po;
+
+		if (po->validcount != validcount) // if polyobj hasn't been checked
+		{
+			po->validcount = validcount;
+
+			lua_pushvalue(L, 1);
+			LUA_PushUserdata(L, thing, META_MOBJ);
+			LUA_PushUserdata(L, po, META_POLYOBJ);
+			if (lua_pcall(gL, 2, 1, 0)) {
+				if (!blockfuncerror || cv_debug & DBG_LUA)
+					CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
+				lua_pop(gL, 1);
+				blockfuncerror = true;
+				return 0; // *shrugs*
+			}
+			if (!lua_isnil(gL, -1))
+			{ // if nil, continue
+				if (lua_toboolean(gL, -1))
+					return 2; // stop whole search
+				else
+					return 1; // stop block search
+			}
+			lua_pop(gL, 1);
+			if (P_MobjWasRemoved(thing))
+				return 2;
+		}
+		plink = (polymaplink_t *)(plink->link.next);
+	}
+
+	return 0; // Everything was checked.
+}
+
 // The searchBlockmap function
 // arguments: searchBlockmap(searchtype, function, mobj, [x1, x2, y1, y2])
 // return value:
@@ -195,6 +246,9 @@ static int lib_searchBlockmap(lua_State *L)
 		case 1: // "lines"
 			searchFunc = lib_searchBlockmap_Lines;
 			break;
+		case 2: // "polyobjs"
+			searchFunc = lib_searchBlockmap_PolyObjs;
+			break;
 	}
 
 	// the mobj we are searching around, the "calling" mobj we could say
diff --git a/src/lua_libs.h b/src/lua_libs.h
index f987c79fd..03bd99cd2 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -50,6 +50,8 @@ extern lua_State *gL;
 #define META_VECTOR3 "VECTOR3_T"
 #define META_MAPHEADER "MAPHEADER_T*"
 
+#define META_POLYOBJ "POLYOBJ_T*"
+
 #define META_CVAR "CONSVAR_T*"
 
 #define META_SECTORLINES "SECTOR_T*LINES"
@@ -58,6 +60,8 @@ extern lua_State *gL;
 #define META_LINESTRINGARGS "LINE_T*STRINGARGS"
 #define META_THINGARGS "MAPTHING_T*ARGS"
 #define META_THINGSTRINGARGS "MAPTHING_T*STRINGARGS"
+#define META_POLYOBJVERTICES "POLYOBJ_T*VERTICES"
+#define META_POLYOBJLINES "POLYOBJ_T*LINES"
 #ifdef HAVE_LUA_SEGS
 #define META_NODEBBOX "NODE_T*BBOX"
 #define META_NODECHILDREN "NODE_T*CHILDREN"
@@ -88,5 +92,6 @@ 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_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 12a0be27d..d055322f6 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -16,6 +16,7 @@
 #include "p_setup.h"
 #include "z_zone.h"
 #include "p_slopes.h"
+#include "p_polyobj.h"
 #include "r_main.h"
 
 #include "lua_script.h"
@@ -68,6 +69,7 @@ enum subsector_e {
 	subsector_sector,
 	subsector_numlines,
 	subsector_firstline,
+	subsector_polyList
 };
 
 static const char *const subsector_opt[] = {
@@ -75,6 +77,7 @@ static const char *const subsector_opt[] = {
 	"sector",
 	"numlines",
 	"firstline",
+	"polyList",
 	NULL};
 
 enum line_e {
@@ -98,6 +101,7 @@ enum line_e {
 	line_backsector,
 	line_firsttag,
 	line_nexttag,
+	line_polyobj,
 	line_text,
 	line_callcount
 };
@@ -123,6 +127,7 @@ static const char *const line_opt[] = {
 	"backsector",
 	"firsttag",
 	"nexttag",
+	"polyobj",
 	"text",
 	"callcount",
 	NULL};
@@ -223,6 +228,7 @@ enum seg_e {
 	seg_linedef,
 	seg_frontsector,
 	seg_backsector,
+	seg_polyseg
 };
 
 static const char *const seg_opt[] = {
@@ -236,6 +242,7 @@ static const char *const seg_opt[] = {
 	"linedef",
 	"frontsector",
 	"backsector",
+	"polyseg",
 	NULL};
 
 enum node_e {
@@ -325,9 +332,9 @@ static const char *const vector_opt[] = {
 static const char *const array_opt[] ={"iterate",NULL};
 static const char *const valid_opt[] ={"valid",NULL};
 
-///////////////////////////////////
-// sector list iterate functions //
-///////////////////////////////////
+/////////////////////////////////////////////
+// sector/subsector list iterate functions //
+/////////////////////////////////////////////
 
 // iterates through a sector's thinglist!
 static int lib_iterateSectorThinglist(lua_State *L)
@@ -399,6 +406,41 @@ static int lib_iterateSectorFFloors(lua_State *L)
 	return 0;
 }
 
+// iterates through a subsector's polyList! (for polyobj_t)
+static int lib_iterateSubSectorPolylist(lua_State *L)
+{
+	polyobj_t *state = NULL;
+	polyobj_t *po = NULL;
+
+	INLEVEL
+
+	if (lua_gettop(L) < 2)
+		return luaL_error(L, "Don't call subsector.polyList() directly, use it as 'for polyobj in subsector.polyList do <block> end'.");
+
+	if (!lua_isnil(L, 1))
+		state = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	else
+		return 0; // no polylist to iterate through sorry!
+
+	lua_settop(L, 2);
+	lua_remove(L, 1); // remove state now.
+
+	if (!lua_isnil(L, 1))
+	{
+		po = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+		po = (polyobj_t *)(po->link.next);
+	}
+	else
+		po = state; // state is used as the "start" of the polylist
+
+	if (po)
+	{
+		LUA_PushUserdata(L, po, META_POLYOBJ);
+		return 1;
+	}
+	return 0;
+}
+
 static int sector_iterate(lua_State *L)
 {
 	lua_pushvalue(L, lua_upvalueindex(1)); // iterator function, or the "generator"
@@ -687,6 +729,11 @@ static int subsector_get(lua_State *L)
 	case subsector_firstline:
 		lua_pushinteger(L, subsector->firstline);
 		return 1;
+	case subsector_polyList: // polyList
+		lua_pushcfunction(L, lib_iterateSubSectorPolylist);
+		LUA_PushUserdata(L, subsector->polyList, META_POLYOBJ);
+		lua_pushcclosure(L, sector_iterate, 2); // push lib_iterateSubSectorPolylist and subsector->polyList as upvalues for the function
+		return 1;
 	}
 	return 0;
 }
@@ -830,6 +877,9 @@ static int line_get(lua_State *L)
 	case line_nexttag:
 		lua_pushinteger(L, line->nexttag);
 		return 1;
+	case line_polyobj:
+		LUA_PushUserdata(L, line->polyobj, META_POLYOBJ);
+		return 1;
 	case line_text:
 		lua_pushstring(L, line->text);
 		return 1;
@@ -1092,6 +1142,9 @@ static int seg_get(lua_State *L)
 	case seg_backsector:
 		LUA_PushUserdata(L, seg->backsector, META_SECTOR);
 		return 1;
+	case seg_polyseg:
+		LUA_PushUserdata(L, seg->polyseg, META_POLYOBJ);
+		return 1;
 	}
 	return 0;
 }
diff --git a/src/lua_polyobjlib.c b/src/lua_polyobjlib.c
new file mode 100644
index 000000000..c4dfa8ae4
--- /dev/null
+++ b/src/lua_polyobjlib.c
@@ -0,0 +1,486 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020 by Iestyn "Monster Iestyn" Jealous.
+// 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_polyobjlib.c
+/// \brief polyobject library for Lua scripting
+
+#include "doomdef.h"
+#include "fastcmp.h"
+#include "p_local.h"
+#include "p_polyobj.h"
+#include "lua_script.h"
+#include "lua_libs.h"
+#include "lua_hud.h" // hud_running errors
+
+#define NOHUD if (hud_running)\
+return luaL_error(L, "HUD rendering code should not call this function!");
+
+enum polyobj_e {
+	// properties
+	polyobj_valid = 0,
+	polyobj_id,
+	polyobj_parent,
+	polyobj_vertices,
+	polyobj_lines,
+	polyobj_sector,
+	polyobj_angle,
+	polyobj_damage,
+	polyobj_thrust,
+	polyobj_flags,
+	polyobj_translucency,
+	polyobj_triggertag,
+	// special functions - utility
+	polyobj_pointInside,
+	polyobj_mobjTouching,
+	polyobj_mobjInside,
+	// special functions - manipulation
+	polyobj_moveXY,
+	polyobj_rotate
+};
+static const char *const polyobj_opt[] = {
+	// properties
+	"valid",
+	"id",
+	"parent",
+	"vertices",
+	"lines",
+	"sector",
+	"angle",
+	"damage",
+	"thrust",
+	"flags",
+	"translucency",
+	"triggertag",
+	// special functions - utility
+	"pointInside",
+	"mobjTouching",
+	"mobjInside",
+	// special functions - manipulation
+	"moveXY",
+	"rotate",
+	NULL};
+
+static const char *const valid_opt[] ={"valid",NULL};
+
+////////////////////////
+// polyobj.vertices[] //
+////////////////////////
+
+// polyobj.vertices, i -> polyobj.vertices[i]
+// polyobj.vertices.valid, for validity checking
+//
+// see sectorlines_get in lua_maplib.c
+//
+static int polyobjvertices_get(lua_State *L)
+{
+	vertex_t ***polyverts = *((vertex_t ****)luaL_checkudata(L, 1, META_POLYOBJVERTICES));
+	size_t i;
+	size_t numofverts = 0;
+	lua_settop(L, 2);
+	if (!lua_isnumber(L, 2))
+	{
+		int field = luaL_checkoption(L, 2, NULL, valid_opt);
+		if (!polyverts || !(*polyverts))
+		{
+			if (field == 0) {
+				lua_pushboolean(L, 0);
+				return 1;
+			}
+			return luaL_error(L, "accessed polyobj_t.vertices doesn't exist anymore.");
+		} else if (field == 0) {
+			lua_pushboolean(L, 1);
+			return 1;
+		}
+	}
+
+	numofverts = (size_t)(*(size_t *)(((size_t)polyverts) - (offsetof(polyobj_t, vertices) - offsetof(polyobj_t, numVertices))));
+
+	if (!numofverts)
+		return luaL_error(L, "no vertices found!");
+
+	i = (size_t)lua_tointeger(L, 2);
+	if (i >= numofverts)
+		return 0;
+	LUA_PushUserdata(L, (*polyverts)[i], META_VERTEX);
+	return 1;
+}
+
+// #(polyobj.vertices) -> polyobj.numVertices
+static int polyobjvertices_num(lua_State *L)
+{
+	vertex_t ***polyverts = *((vertex_t ****)luaL_checkudata(L, 1, META_POLYOBJVERTICES));
+	size_t numofverts = 0;
+
+	if (!polyverts || !(*polyverts))
+		return luaL_error(L, "accessed polyobj_t.vertices doesn't exist anymore.");
+
+	numofverts = (size_t)(*(size_t *)(((size_t)polyverts) - (offsetof(polyobj_t, vertices) - offsetof(polyobj_t, numVertices))));
+	lua_pushinteger(L, numofverts);
+	return 1;
+}
+
+/////////////////////
+// polyobj.lines[] //
+/////////////////////
+
+// polyobj.lines, i -> polyobj.lines[i]
+// polyobj.lines.valid, for validity checking
+//
+// see sectorlines_get in lua_maplib.c
+//
+static int polyobjlines_get(lua_State *L)
+{
+	line_t ***polylines = *((line_t ****)luaL_checkudata(L, 1, META_POLYOBJLINES));
+	size_t i;
+	size_t numoflines = 0;
+	lua_settop(L, 2);
+	if (!lua_isnumber(L, 2))
+	{
+		int field = luaL_checkoption(L, 2, NULL, valid_opt);
+		if (!polylines || !(*polylines))
+		{
+			if (field == 0) {
+				lua_pushboolean(L, 0);
+				return 1;
+			}
+			return luaL_error(L, "accessed polyobj_t.lines doesn't exist anymore.");
+		} else if (field == 0) {
+			lua_pushboolean(L, 1);
+			return 1;
+		}
+	}
+
+	numoflines = (size_t)(*(size_t *)(((size_t)polylines) - (offsetof(polyobj_t, lines) - offsetof(polyobj_t, numLines))));
+
+	if (!numoflines)
+		return luaL_error(L, "no lines found!");
+
+	i = (size_t)lua_tointeger(L, 2);
+	if (i >= numoflines)
+		return 0;
+	LUA_PushUserdata(L, (*polylines)[i], META_LINE);
+	return 1;
+}
+
+// #(polyobj.lines) -> polyobj.numLines
+static int polyobjlines_num(lua_State *L)
+{
+	line_t ***polylines = *((line_t ****)luaL_checkudata(L, 1, META_POLYOBJLINES));
+	size_t numoflines = 0;
+
+	if (!polylines || !(*polylines))
+		return luaL_error(L, "accessed polyobj_t.lines doesn't exist anymore.");
+
+	numoflines = (size_t)(*(size_t *)(((size_t)polylines) - (offsetof(polyobj_t, lines) - offsetof(polyobj_t, numLines))));
+	lua_pushinteger(L, numoflines);
+	return 1;
+}
+
+/////////////////////////////////
+// polyobj_t function wrappers //
+/////////////////////////////////
+
+// special functions - utility
+static int lib_polyobj_PointInside(lua_State *L)
+{
+	polyobj_t *po = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	INLEVEL
+	if (!po)
+		return LUA_ErrInvalid(L, "polyobj_t");
+	lua_pushboolean(L, P_PointInsidePolyobj(po, x, y));
+	return 1;
+}
+
+static int lib_polyobj_MobjTouching(lua_State *L)
+{
+	polyobj_t *po = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	INLEVEL
+	if (!po)
+		return LUA_ErrInvalid(L, "polyobj_t");
+	if (!mo)
+		return LUA_ErrInvalid(L, "mobj_t");
+	lua_pushboolean(L, P_MobjTouchingPolyobj(po, mo));
+	return 1;
+}
+
+static int lib_polyobj_MobjInside(lua_State *L)
+{
+	polyobj_t *po = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	INLEVEL
+	if (!po)
+		return LUA_ErrInvalid(L, "polyobj_t");
+	if (!mo)
+		return LUA_ErrInvalid(L, "mobj_t");
+	lua_pushboolean(L, P_MobjInsidePolyobj(po, mo));
+	return 1;
+}
+
+// special functions - manipulation
+static int lib_polyobj_moveXY(lua_State *L)
+{
+	polyobj_t *po = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	boolean checkmobjs = lua_opttrueboolean(L, 4);
+	NOHUD
+	INLEVEL
+	if (!po)
+		return LUA_ErrInvalid(L, "polyobj_t");
+	lua_pushboolean(L, Polyobj_moveXY(po, x, y, checkmobjs));
+	return 1;
+}
+
+static int lib_polyobj_rotate(lua_State *L)
+{
+	polyobj_t *po = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	angle_t delta = luaL_checkangle(L, 2);
+	UINT8 turnthings = (UINT8)luaL_optinteger(L, 3, 0); // don't turn anything by default? (could change this if not desired)
+	boolean checkmobjs = lua_opttrueboolean(L, 4);
+	NOHUD
+	INLEVEL
+	if (!po)
+		return LUA_ErrInvalid(L, "polyobj_t");
+	lua_pushboolean(L, Polyobj_rotate(po, delta, turnthings, checkmobjs));
+	return 1;
+}
+
+///////////////
+// polyobj_t //
+///////////////
+
+static int polyobj_get(lua_State *L)
+{
+	polyobj_t *polyobj = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	enum polyobj_e field = luaL_checkoption(L, 2, NULL, polyobj_opt);
+
+	if (!polyobj) {
+		if (field == polyobj_valid) {
+			lua_pushboolean(L, false);
+			return 1;
+		}
+		return LUA_ErrInvalid(L, "polyobj_t");
+	}
+
+	switch (field)
+	{
+	// properties
+	case polyobj_valid:
+		lua_pushboolean(L, true);
+		break;
+	case polyobj_id:
+		lua_pushinteger(L, polyobj->id);
+		break;
+	case polyobj_parent:
+		lua_pushinteger(L, polyobj->parent);
+		break;
+	case polyobj_vertices: // vertices
+		LUA_PushUserdata(L, &polyobj->vertices, META_POLYOBJVERTICES); // push the address of the "vertices" member in the struct, to allow our hacks to work
+		break;
+	case polyobj_lines: // lines
+		LUA_PushUserdata(L, &polyobj->lines, META_POLYOBJLINES); // push the address of the "lines" member in the struct, to allow our hacks to work
+		break;
+	case polyobj_sector: // shortcut that exists only in Lua!
+		LUA_PushUserdata(L, polyobj->lines[0]->backsector, META_SECTOR);
+		break;
+	case polyobj_angle:
+		lua_pushangle(L, polyobj->angle);
+		break;
+	case polyobj_damage:
+		lua_pushinteger(L, polyobj->damage);
+		break;
+	case polyobj_thrust:
+		lua_pushfixed(L, polyobj->thrust);
+		break;
+	case polyobj_flags:
+		lua_pushinteger(L, polyobj->flags);
+		break;
+	case polyobj_translucency:
+		lua_pushinteger(L, polyobj->translucency);
+		break;
+	case polyobj_triggertag:
+		lua_pushinteger(L, polyobj->triggertag);
+		break;
+	// special functions - utility
+	case polyobj_pointInside:
+		lua_pushcfunction(L, lib_polyobj_PointInside);
+		break;
+	case polyobj_mobjTouching:
+		lua_pushcfunction(L, lib_polyobj_MobjTouching);
+		break;
+	case polyobj_mobjInside:
+		lua_pushcfunction(L, lib_polyobj_MobjInside);
+		break;
+	// special functions - manipulation
+	case polyobj_moveXY:
+		lua_pushcfunction(L, lib_polyobj_moveXY);
+		break;
+	case polyobj_rotate:
+		lua_pushcfunction(L, lib_polyobj_rotate);
+		break;
+	}
+	return 1;
+}
+
+static int polyobj_set(lua_State *L)
+{
+	polyobj_t *polyobj = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	enum polyobj_e field = luaL_checkoption(L, 2, NULL, polyobj_opt);
+
+	if (!polyobj)
+		return LUA_ErrInvalid(L, "polyobj_t");
+
+	if (hud_running)
+		return luaL_error(L, "Do not alter polyobj_t in HUD rendering code!");
+
+	switch (field)
+	{
+	default:
+		return luaL_error(L, LUA_QL("polyobj_t") " field " LUA_QS " cannot be modified.", polyobj_opt[field]);
+	case polyobj_angle:
+		return luaL_error(L, LUA_QL("polyobj_t") " field " LUA_QS " should not be set directly. Use the function " LUA_QL("polyobj:rotate(angle)") " instead.", polyobj_opt[field]);
+	case polyobj_parent:
+		polyobj->parent = luaL_checkinteger(L, 3);
+		break;
+	case polyobj_flags:
+		polyobj->flags = luaL_checkinteger(L, 3);
+		break;
+	case polyobj_translucency:
+		polyobj->translucency = luaL_checkinteger(L, 3);
+		break;
+	}
+
+	return 0;
+}
+
+static int polyobj_num(lua_State *L)
+{
+	polyobj_t *polyobj = *((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ));
+	if (!polyobj)
+		return luaL_error(L, "accessed polyobj_t doesn't exist anymore.");
+	lua_pushinteger(L, polyobj-PolyObjects);
+	return 1;
+}
+
+///////////////////
+// PolyObjects[] //
+///////////////////
+
+static int lib_iteratePolyObjects(lua_State *L)
+{
+	INT32 i = -1;
+	if (lua_gettop(L) < 2)
+	{
+		//return luaL_error(L, "Don't call PolyObjects.iterate() directly, use it as 'for polyobj in PolyObjects.iterate do <block> end'.");
+		lua_pushcfunction(L, lib_iteratePolyObjects);
+		return 1;
+	}
+	lua_settop(L, 2);
+	lua_remove(L, 1); // state is unused.
+	if (!lua_isnil(L, 1))
+		i = (INT32)(*((polyobj_t **)luaL_checkudata(L, 1, META_POLYOBJ)) - PolyObjects);
+	for (i++; i < numPolyObjects; i++)
+	{
+		LUA_PushUserdata(L, &PolyObjects[i], META_POLYOBJ);
+		return 1;
+	}
+	return 0;
+}
+
+static int lib_PolyObject_getfornum(lua_State *L)
+{
+	INT32 id = (INT32)luaL_checkinteger(L, 1);
+
+	if (!numPolyObjects)
+		return 0; // if there's no PolyObjects then bail out here
+
+	LUA_PushUserdata(L, Polyobj_GetForNum(id), META_POLYOBJ);
+	return 1;
+}
+
+static int lib_getPolyObject(lua_State *L)
+{
+	const char *field;
+	INT32 i;
+
+	// find PolyObject by number
+	if (lua_type(L, 2) == LUA_TNUMBER)
+	{
+		i = luaL_checkinteger(L, 2);
+		if (i < 0 || i >= numPolyObjects)
+			return luaL_error(L, "PolyObjects[] index %d out of range (0 - %d)", i, numPolyObjects-1);
+		LUA_PushUserdata(L, &PolyObjects[i], META_POLYOBJ);
+		return 1;
+	}
+
+	field = luaL_checkstring(L, 2);
+	// special function iterate
+	if (fastcmp(field,"iterate"))
+	{
+		lua_pushcfunction(L, lib_iteratePolyObjects);
+		return 1;
+	}
+	// find PolyObject by ID
+	else if (fastcmp(field,"GetForNum")) // name could probably be better
+	{
+		lua_pushcfunction(L, lib_PolyObject_getfornum);
+		return 1;
+	}
+	return 0;
+}
+
+static int lib_numPolyObjects(lua_State *L)
+{
+	lua_pushinteger(L, numPolyObjects);
+	return 1;
+}
+
+int LUA_PolyObjLib(lua_State *L)
+{
+	luaL_newmetatable(L, META_POLYOBJVERTICES);
+		lua_pushcfunction(L, polyobjvertices_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, polyobjvertices_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_POLYOBJLINES);
+		lua_pushcfunction(L, polyobjlines_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, polyobjlines_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_POLYOBJ);
+		lua_pushcfunction(L, polyobj_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, polyobj_set);
+		lua_setfield(L, -2, "__newindex");
+
+		lua_pushcfunction(L, polyobj_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L,1);
+
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_getPolyObject);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_numPolyObjects);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "PolyObjects");
+	return 0;
+}
diff --git a/src/lua_script.c b/src/lua_script.c
index 0260f018a..ae7f479f6 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -24,6 +24,7 @@
 #include "p_saveg.h"
 #include "p_local.h"
 #include "p_slopes.h" // for P_SlopeById
+#include "p_polyobj.h" // polyobj_t, PolyObjects
 #ifdef LUA_ALLOW_BYTECODE
 #include "d_netfil.h" // for LUA_DumpFile
 #endif
@@ -50,6 +51,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_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
 	NULL
@@ -774,6 +776,12 @@ void LUA_InvalidateLevel(void)
 		LUA_InvalidateUserdata(&sides[i]);
 	for (i = 0; i < numvertexes; i++)
 		LUA_InvalidateUserdata(&vertexes[i]);
+	for (i = 0; i < (size_t)numPolyObjects; i++)
+	{
+		LUA_InvalidateUserdata(&PolyObjects[i]);
+		LUA_InvalidateUserdata(&PolyObjects[i].vertices);
+		LUA_InvalidateUserdata(&PolyObjects[i].lines);
+	}
 #ifdef HAVE_LUA_SEGS
 	for (i = 0; i < numsegs; i++)
 		LUA_InvalidateUserdata(&segs[i]);
@@ -832,6 +840,7 @@ enum
 	ARCH_NODE,
 #endif
 	ARCH_FFLOOR,
+	ARCH_POLYOBJ,
 	ARCH_SLOPE,
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
@@ -858,6 +867,7 @@ static const struct {
 	{META_NODE,     ARCH_NODE},
 #endif
 	{META_FFLOOR,	ARCH_FFLOOR},
+	{META_POLYOBJ,  ARCH_POLYOBJ},
 	{META_SLOPE,    ARCH_SLOPE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
@@ -1126,6 +1136,17 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
+		case ARCH_POLYOBJ:
+		{
+			polyobj_t *polyobj = *((polyobj_t **)lua_touserdata(gL, myindex));
+			if (!polyobj)
+				WRITEUINT8(save_p, ARCH_NULL);
+			else {
+				WRITEUINT8(save_p, ARCH_POLYOBJ);
+				WRITEUINT16(save_p, polyobj-PolyObjects);
+			}
+			break;
+		}
 		case ARCH_SLOPE:
 		{
 			pslope_t *slope = *((pslope_t **)lua_touserdata(gL, myindex));
@@ -1381,6 +1402,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 			LUA_PushUserdata(gL, rover, META_FFLOOR);
 		break;
 	}
+	case ARCH_POLYOBJ:
+		LUA_PushUserdata(gL, &PolyObjects[READUINT16(save_p)], META_POLYOBJ);
+		break;
 	case ARCH_SLOPE:
 		LUA_PushUserdata(gL, P_SlopeById(READUINT16(save_p)), META_SLOPE);
 		break;
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index b0a794ddf..63d062c22 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -976,7 +976,7 @@ static INT32 Polyobj_clipThings(polyobj_t *po, line_t *line)
 
 
 // Moves a polyobject on the x-y plane.
-static boolean Polyobj_moveXY(polyobj_t *po, fixed_t x, fixed_t y, boolean checkmobjs)
+boolean Polyobj_moveXY(polyobj_t *po, fixed_t x, fixed_t y, boolean checkmobjs)
 {
 	size_t i;
 	vertex_t vec;
@@ -1162,7 +1162,7 @@ static void Polyobj_rotateThings(polyobj_t *po, vector2_t origin, angle_t delta,
 }
 
 // Rotates a polyobject around its start point.
-static boolean Polyobj_rotate(polyobj_t *po, angle_t delta, UINT8 turnthings, boolean checkmobjs)
+boolean Polyobj_rotate(polyobj_t *po, angle_t delta, UINT8 turnthings, boolean checkmobjs)
 {
 	size_t i;
 	angle_t angle;
diff --git a/src/p_polyobj.h b/src/p_polyobj.h
index f24caca4e..8c2946965 100644
--- a/src/p_polyobj.h
+++ b/src/p_polyobj.h
@@ -336,6 +336,8 @@ typedef struct polyfadedata_s
 // Functions
 //
 
+boolean Polyobj_moveXY(polyobj_t *po, fixed_t x, fixed_t y, boolean checkmobjs);
+boolean Polyobj_rotate(polyobj_t *po, angle_t delta, UINT8 turnthings, boolean checkmobjs);
 polyobj_t *Polyobj_GetForNum(INT32 id);
 void Polyobj_InitLevel(void);
 void Polyobj_MoveOnLoad(polyobj_t *po, angle_t angle, fixed_t x, fixed_t y);
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 755fa68e6..5b0ff7425 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -401,6 +401,7 @@
     <ClCompile Include="..\lua_mathlib.c" />
     <ClCompile Include="..\lua_mobjlib.c" />
     <ClCompile Include="..\lua_playerlib.c" />
+    <ClCompile Include="..\lua_polyobjlib.c" />
     <ClCompile Include="..\lua_script.c" />
     <ClCompile Include="..\lua_skinlib.c" />
     <ClCompile Include="..\lua_thinkerlib.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index 3bbcd9cb5..505384313 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -723,6 +723,9 @@
     <ClCompile Include="..\lua_playerlib.c">
       <Filter>LUA</Filter>
     </ClCompile>
+    <ClCompile Include="..\lua_polyobjlib.c">
+      <Filter>LUA</Filter>
+    </ClCompile>
     <ClCompile Include="..\lua_script.c">
       <Filter>LUA</Filter>
     </ClCompile>