diff --git a/src/lua_libs.h b/src/lua_libs.h
index 6c9524dea9d56aacb3f0dc6f08e037727ca47bc7..8cca200861d890fa5fa18a190b15b77d670c40a5 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -39,12 +39,17 @@ extern lua_State *gL;
 #define META_SECTOR "SECTOR_T*"
 #define META_FFLOOR "FFLOOR_T*"
 #define META_SEG "SEG_T*"
+#define META_NODE "NODE_T*"
 #define META_MAPHEADER "MAPHEADER_T*"
 
 #define META_CVAR "CONSVAR_T*"
 
 #define META_SECTORLINES "SECTOR_T*LINES"
 #define META_SIDENUM "LINE_T*SIDENUM"
+#define META_NODEBBOX "NODE_T*BBOX"
+#define META_NODECHILDREN "NODE_T*CHILDREN"
+
+#define META_BBOX "BOUNDING_BOX"
 
 #define META_HUDINFO "HUDINFO_T*"
 #define META_PATCH "PATCH_T*"
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 28561f34efba627d13eecafffee3c239dc57f122..d9fdc127412731feb5cf489a32d42b4fd5d73460 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -211,6 +211,26 @@ static const char *const seg_opt[] = {
 	"backsector",
 	NULL};
 
+enum node_e {
+	node_valid = 0,
+	node_x,
+	node_y,
+	node_dx,
+	node_dy,
+	node_bbox,
+	node_children,
+};
+
+static const char *const node_opt[] = {
+	"valid",
+	"x",
+	"y",
+	"dx",
+	"dy",
+	"bbox",
+	"children",
+	NULL};
+
 static const char *const array_opt[] ={"iterate",NULL};
 static const char *const valid_opt[] ={"valid",NULL};
 
@@ -901,6 +921,145 @@ static int seg_num(lua_State *L)
 	return 1;
 }
 
+static int node_get(lua_State *L)
+{
+	node_t *node = *((node_t **)luaL_checkudata(L, 1, META_NODE));
+	enum node_e field = luaL_checkoption(L, 2, node_opt[0], node_opt);
+
+	if (!node)
+	{
+		if (field == node_valid) {
+			lua_pushboolean(L, 0);
+			return 1;
+		}
+		return luaL_error(L, "accessed node_t doesn't exist anymore.");
+	}
+
+	switch(field)
+	{
+	case node_valid: // valid
+		lua_pushboolean(L, 1);
+		return 1;
+	case node_x:
+		lua_pushfixed(L, node->x);
+		return 1;
+	case node_y:
+		lua_pushfixed(L, node->y);
+		return 1;
+	case node_dx:
+		lua_pushfixed(L, node->x);
+		return 1;
+	case node_dy:
+		lua_pushfixed(L, node->x);
+		return 1;
+	case node_bbox:
+		LUA_PushUserdata(L, node->bbox, META_NODEBBOX);
+		return 1;
+	case node_children:
+		LUA_PushUserdata(L, node->children, META_NODECHILDREN);
+		return 1;
+	}
+	return 0;
+}
+
+static int node_num(lua_State *L)
+{
+	node_t *node = *((node_t **)luaL_checkudata(L, 1, META_NODE));
+	lua_pushinteger(L, node-nodes);
+	return 1;
+}
+
+// node.bbox[i][j]: i = 0 or 1, j = 0 1 2 or 3
+// NOTE: 2D arrays are NOT double pointers,
+//       the second bbox will be directly after the first in memory (hence the way the bbox is pushed here)
+// this function handles the [i] part, bbox_get handles the [j] part
+static int nodebbox_get(lua_State *L)
+{
+	fixed_t *bbox = *((fixed_t **)luaL_checkudata(L, 1, META_NODEBBOX));
+	int i;
+	lua_settop(L, 2);
+	if (!lua_isnumber(L, 2))
+	{
+		int field = luaL_checkoption(L, 2, NULL, valid_opt);
+		if (!bbox)
+		{
+			if (field == 0) {
+				lua_pushboolean(L, 0);
+				return 1;
+			}
+			return luaL_error(L, "accessed node_t doesn't exist anymore.");
+		} else if (field == 0) {
+			lua_pushboolean(L, 1);
+			return 1;
+		}
+	}
+
+	i = lua_tointeger(L, 2);
+	if (i < 0 || i > 1)
+		return 0;
+	LUA_PushUserdata(L, bbox + i*4*sizeof(fixed_t), META_BBOX);
+	return 1;
+}
+
+// node.children[i]: i = 0 or 1
+static int nodechildren_get(lua_State *L)
+{
+	UINT16 *children = *((UINT16 **)luaL_checkudata(L, 1, META_NODECHILDREN));
+	int i;
+	lua_settop(L, 2);
+	if (!lua_isnumber(L, 2))
+	{
+		int field = luaL_checkoption(L, 2, NULL, valid_opt);
+		if (!children)
+		{
+			if (field == 0) {
+				lua_pushboolean(L, 0);
+				return 1;
+			}
+			return luaL_error(L, "accessed node_t doesn't exist anymore.");
+		} else if (field == 0) {
+			lua_pushboolean(L, 1);
+			return 1;
+		}
+	}
+
+	i = lua_tointeger(L, 2);
+	if (i < 0 || i > 1)
+		return 0;
+	lua_pushinteger(L, children[i]);
+	return 1;
+}
+
+// bounding box (aka fixed_t array with four elements)
+// NOTE: may be useful for polyobjects or other things later
+static int bbox_get(lua_State *L)
+{
+	fixed_t *bbox = *((fixed_t **)luaL_checkudata(L, 1, META_BBOX));
+	int i;
+	lua_settop(L, 2);
+	if (!lua_isnumber(L, 2))
+	{
+		int field = luaL_checkoption(L, 2, NULL, valid_opt);
+		if (!bbox)
+		{
+			if (field == 0) {
+				lua_pushboolean(L, 0);
+				return 1;
+			}
+			return luaL_error(L, "accessed node_t doesn't exist anymore.");
+		} else if (field == 0) {
+			lua_pushboolean(L, 1);
+			return 1;
+		}
+	}
+
+	i = lua_tointeger(L, 2);
+	if (i < 0 || i > 3)
+		return 0;
+	lua_pushinteger(L, bbox[i]);
+	return 1;
+}
+
 static int lib_iterateSectors(lua_State *L)
 {
 	size_t i = 0;
@@ -1177,6 +1336,52 @@ static int lib_numsegs(lua_State *L)
 	return 1;
 }
 
+static int lib_iterateNodes(lua_State *L)
+{
+	size_t i = 0;
+	if (lua_gettop(L) < 2)
+		return luaL_error(L, "Don't call nodes.iterate() directly, use it as 'for node in nodes.iterate do <block> end'.");
+	lua_settop(L, 2);
+	lua_remove(L, 1); // state is unused.
+	if (!lua_isnil(L, 1))
+		i = (size_t)(*((node_t **)luaL_checkudata(L, 1, META_NODE)) - nodes)+1;
+	if (i < numsegs)
+	{
+		LUA_PushUserdata(L, &nodes[i], META_NODE);
+		return 1;
+	}
+	return 0;
+}
+
+static int lib_getNode(lua_State *L)
+{
+	int field;
+	lua_settop(L, 2);
+	lua_remove(L, 1); // dummy userdata table is unused.
+	if (lua_isnumber(L, 1))
+	{
+		size_t i = lua_tointeger(L, 1);
+		if (i >= numnodes)
+			return 0;
+		LUA_PushUserdata(L, &nodes[i], META_NODE);
+		return 1;
+	}
+	field = luaL_checkoption(L, 1, NULL, array_opt);
+	switch(field)
+	{
+	case 0: // iterate
+		lua_pushcfunction(L, lib_iterateNodes);
+		return 1;
+	}
+	return 0;
+}
+
+static int lib_numnodes(lua_State *L)
+{
+	lua_pushinteger(L, numnodes);
+	return 1;
+}
+
 static int ffloor_get(lua_State *L)
 {
 	ffloor_t *ffloor = *((ffloor_t **)luaL_checkudata(L, 1, META_FFLOOR));
@@ -1504,6 +1709,29 @@ int LUA_MapLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
+	luaL_newmetatable(L, META_NODE);
+		lua_pushcfunction(L, node_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, node_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_NODEBBOX);
+		lua_pushcfunction(L, nodebbox_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_NODECHILDREN);
+		lua_pushcfunction(L, nodechildren_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_BBOX);
+		lua_pushcfunction(L, bbox_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 1);
+
 	luaL_newmetatable(L, META_MAPHEADER);
 		lua_pushcfunction(L, mapheaderinfo_get);
 		lua_setfield(L, -2, "__index");
@@ -1572,6 +1800,16 @@ int LUA_MapLib(lua_State *L)
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, "segs");
 
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_getNode);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_numnodes);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "nodes");
+
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
 			lua_pushcfunction(L, lib_getMapheaderinfo);
diff --git a/src/lua_script.c b/src/lua_script.c
index 621b7dfe48327e5d47c9cfe2d2896312b27c9072..6274baa73a4cde211c665545e7493430b2bcfbf3 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -456,6 +456,7 @@ enum
 	ARCH_SUBSECTOR,
 	ARCH_SECTOR,
 	ARCH_SEG,
+	ARCH_NODE,
 	ARCH_MAPHEADER,
 
 	ARCH_TEND=0xFF,
@@ -476,6 +477,7 @@ static const struct {
 	{META_SUBSECTOR,ARCH_SUBSECTOR},
 	{META_SECTOR,   ARCH_SECTOR},
 	{META_SEG,      ARCH_SEG},
+	{META_NODE,     ARCH_NODE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{NULL,          ARCH_NULL}
 };
@@ -677,6 +679,17 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
+		case ARCH_NODE:
+		{
+			node_t *node = *((node_t **)lua_touserdata(gL, myindex));
+			if (!node)
+				WRITEUINT8(save_p, ARCH_NULL);
+			else {
+				WRITEUINT8(save_p, ARCH_NODE);
+				WRITEUINT16(save_p, node - nodes);
+			}
+			break;
+		}
 		case ARCH_MAPHEADER:
 		{
 			mapheader_t *header = *((mapheader_t **)lua_touserdata(gL, myindex));
@@ -858,6 +871,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_SEG:
 		LUA_PushUserdata(gL, &segs[READUINT16(save_p)], META_SEG);
 		break;
+	case ARCH_NODE:
+		LUA_PushUserdata(gL, &nodes[READUINT16(save_p)], META_NODE);
+		break;
 	case ARCH_MAPHEADER:
 		LUA_PushUserdata(gL, &sectors[READUINT16(save_p)], META_MAPHEADER);
 		break;