From b975eedd9b1030815d7e6395d93b9edc7922cf6c Mon Sep 17 00:00:00 2001
From: James R <justsomejames2@gmail.com>
Date: Mon, 26 Apr 2021 14:00:34 -0700
Subject: [PATCH 01/23] Fix two bugs with Lua MIN, MAX Plus cvars

1) Any cvar without MIN, MAX would be disallowed; now
check that at least one of those values is actually
present.
2) Handle value index properly when shifting array.
---
 src/lua_consolelib.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index e839d4e15..51e83d792 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -400,23 +400,23 @@ static int lib_cvRegisterVar(lua_State *L)
 						{
 							memmove(&cvpv[2], &cvpv[0],
 									i * sizeof *cvpv);
+							i += 2;
 						}
 						cvpv[n].strvalue = MINMAX[n];
 						minmax_unset &= ~(1 << n);
 					}
 					else
 					{
-						n = i;
+						n = i++;
 						cvpv[n].strvalue = Z_StrDup(strval);
 					}
 
 					cvpv[n].value = (INT32)lua_tonumber(L, 6);
 
-					i++;
 					lua_pop(L, 1);
 				}
 
-				if (minmax_unset)
+				if (minmax_unset && minmax_unset != 3)
 					FIELDERROR("PossibleValue", "custom PossibleValue table requires requires both MIN and MAX keys if one is present");
 
 				cvpv[i].value = 0;

From 39869b4492d133f044b970826e5ee74782d3854d Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Wed, 31 May 2023 22:21:31 -0500
Subject: [PATCH 02/23] Expose P_TouchSpecialThing

---
 src/lua_baselib.c | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 25fa38769..8409a8377 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -2195,6 +2195,19 @@ static int lib_pDoMatchSuper(lua_State *L)
 	return 0;
 }
 
+static int lib_pTouchSpecialThing(lua_State *L)
+{
+	mobj_t *special = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	mobj_t *toucher = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	boolean heightcheck = lua_optboolean(L, 3);
+	NOHUD
+	INLEVEL
+	if (!special || !toucher)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_TouchSpecialThing(special, toucher, heightcheck);
+	return 0;
+}
+
 // P_SPEC
 ////////////
 
@@ -4133,6 +4146,7 @@ static luaL_Reg lib[] = {
 	{"P_FloorzAtPos",lib_pFloorzAtPos},
 	{"P_CeilingzAtPos",lib_pCeilingzAtPos},
 	{"P_DoSpring",lib_pDoSpring},
+	{"P_TouchSpecialThing",lib_pTouchSpecialThing},
 	{"P_TryCameraMove", lib_pTryCameraMove},
 	{"P_TeleportCameraMove", lib_pTeleportCameraMove},
 

From 16e44e1b6e4b61cbe2c7516ceb6dd13d03e138bb Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Wed, 31 May 2023 22:28:53 -0500
Subject: [PATCH 03/23] Expose FollowMobj-related functions

---
 src/lua_baselib.c | 45 ++++++++++++++++++++++++++++++++++++++++++
 src/p_local.h     |  4 ++++
 src/p_user.c      | 50 +++++++++++++++++++++++++----------------------
 3 files changed, 76 insertions(+), 23 deletions(-)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 25fa38769..7f64b7ddf 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1714,6 +1714,48 @@ static int lib_pSwitchShield(lua_State *L)
 	return 0;
 }
 
+static int lib_pDoTailsOverlay(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *tails = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	if (!tails)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_DoTailsOverlay(player, tails);
+	return 0;
+}
+
+static int lib_pDoMetalJetFume(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *fume = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	if (!fume)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_DoMetalJetFume(player, fume);
+	return 0;
+}
+
+static int lib_pDoFollowMobj(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *followmobj = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	if (!followmobj)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_DoFollowMobj(player, followmobj);
+	return 0;
+}
+
 static int lib_pPlayerCanEnterSpinGaps(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -4115,6 +4157,9 @@ static luaL_Reg lib[] = {
 	{"P_SpawnSpinMobj",lib_pSpawnSpinMobj},
 	{"P_Telekinesis",lib_pTelekinesis},
 	{"P_SwitchShield",lib_pSwitchShield},
+	{"P_DoTailsOverlay",lib_pDoTailsOverlay},
+	{"P_DoMetalJetFume",lib_pDoMetalJetFume},
+	{"P_DoFollowMobj",lib_pDoFollowMobj},
 	{"P_PlayerCanEnterSpinGaps",lib_pPlayerCanEnterSpinGaps},
 	{"P_PlayerShouldUseSpinHeight",lib_pPlayerShouldUseSpinHeight},
 
diff --git a/src/p_local.h b/src/p_local.h
index cc060e4ee..776e9e122 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -213,6 +213,10 @@ void P_SpawnThokMobj(player_t *player);
 void P_SpawnSpinMobj(player_t *player, mobjtype_t type);
 void P_Telekinesis(player_t *player, fixed_t thrust, fixed_t range);
 
+void P_DoTailsOverlay(player_t *player, mobj_t *tails);
+void P_DoMetalJetFume(player_t *player, mobj_t *fume);
+void P_DoFollowMobj(player_t *player, mobj_t *followmobj);
+
 void P_PlayLivesJingle(player_t *player);
 #define P_PlayRinglossSound(s)	S_StartSound(s, (mariomode) ? sfx_mario8 : sfx_altow1 + P_RandomKey(4));
 #define P_PlayDeathSound(s)		S_StartSound(s, sfx_altdi1 + P_RandomKey(4));
diff --git a/src/p_user.c b/src/p_user.c
index 0f3282da5..253161c04 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -11103,7 +11103,7 @@ static void P_MinecartThink(player_t *player)
 }
 
 // Handle Tails' fluff
-static void P_DoTailsOverlay(player_t *player, mobj_t *tails)
+void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 {
 	// init...
 	boolean smilesonground = P_IsObjectOnGround(player->mo);
@@ -11308,7 +11308,7 @@ static void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 }
 
 // Metal Sonic's jet fume
-static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
+void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 {
 	static const UINT8 FUME_SKINCOLORS[] =
 	{
@@ -11439,6 +11439,30 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 		P_SpawnGhostMobj(fume);
 }
 
+// Handle Followmobj behavior
+void P_DoFollowMobj(player_t *player, mobj_t *followmobj)
+{
+	if (LUAh_FollowMobj(player, followmobj) || P_MobjWasRemoved(followmobj))
+		{;}
+	else
+	{
+		switch (followmobj->type)
+		{
+			case MT_TAILSOVERLAY: // c:
+				P_DoTailsOverlay(player, followmobj);
+				break;
+			case MT_METALJETFUME:
+				P_DoMetalJetFume(player, followmobj);
+				break;
+			default:
+				var1 = 1;
+				var2 = 0;
+				A_CapeChase(followmobj);
+				break;
+		}
+	}
+}
+
 //
 // P_PlayerThink
 //
@@ -12894,27 +12918,7 @@ void P_PlayerAfterThink(player_t *player)
 		}
 
 		if (player->followmobj)
-		{
-			if (LUA_HookFollowMobj(player, player->followmobj) || P_MobjWasRemoved(player->followmobj))
-				{;}
-			else
-			{
-				switch (player->followmobj->type)
-				{
-					case MT_TAILSOVERLAY: // c:
-						P_DoTailsOverlay(player, player->followmobj);
-						break;
-					case MT_METALJETFUME:
-						P_DoMetalJetFume(player, player->followmobj);
-						break;
-					default:
-						var1 = 1;
-						var2 = 0;
-						A_CapeChase(player->followmobj);
-						break;
-				}
-			}
-		}
+			P_DoFollowMobj(player, player->followmobj);
 	}
 
 	P_DoPlayerHeadSigns(player); // Spawn Tag/CTF signs over player's head

From 84438ff8376391fee0cfb160fb637a23102963e2 Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Wed, 31 May 2023 22:39:08 -0500
Subject: [PATCH 04/23] Expose P_GivePlayerSpheres to Lua

---
 src/lua_baselib.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 25fa38769..0ebb143ca 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1422,6 +1422,18 @@ static int lib_pGivePlayerRings(lua_State *L)
 	return 0;
 }
 
+static int lib_pGivePlayerSpheres(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	INT32 num_spheres = (INT32)luaL_checkinteger(L, 2);
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	P_GivePlayerSpheres(player, num_spheres);
+	return 0;
+}
+
 static int lib_pGivePlayerLives(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -4091,6 +4103,7 @@ static luaL_Reg lib[] = {
 	{"P_SpawnShieldOrb",lib_pSpawnShieldOrb},
 	{"P_SpawnGhostMobj",lib_pSpawnGhostMobj},
 	{"P_GivePlayerRings",lib_pGivePlayerRings},
+	{"P_GivePlayerSpheres",lib_pGivePlayerSpheres},
 	{"P_GivePlayerLives",lib_pGivePlayerLives},
 	{"P_GiveCoopLives",lib_pGiveCoopLives},
 	{"P_ResetScore",lib_pResetScore},

From 34477133cc646e42441cde2679bb6ac580292e52 Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Wed, 31 May 2023 22:49:30 -0500
Subject: [PATCH 05/23] Expose P_DoSpinDashDust to Lua

---
 src/lua_baselib.c | 12 ++++++++++++
 src/p_local.h     |  1 +
 src/p_user.c      |  2 +-
 3 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 25fa38769..090c1c371 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1664,6 +1664,17 @@ static int lib_pDoJump(lua_State *L)
 	return 0;
 }
 
+static int lib_pDoSpinDashDust(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	P_DoSpinDashDust(player);
+	return 0;
+}
+
 static int lib_pSpawnThokMobj(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -4111,6 +4122,7 @@ static luaL_Reg lib[] = {
 	{"P_HomingAttack",lib_pHomingAttack},
 	{"P_SuperReady",lib_pSuperReady},
 	{"P_DoJump",lib_pDoJump},
+	{"P_DoSpinDashDust",lib_pDoSpinDashDust},
 	{"P_SpawnThokMobj",lib_pSpawnThokMobj},
 	{"P_SpawnSpinMobj",lib_pSpawnSpinMobj},
 	{"P_Telekinesis",lib_pTelekinesis},
diff --git a/src/p_local.h b/src/p_local.h
index cc060e4ee..c07b7029b 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -203,6 +203,7 @@ void P_Earthquake(mobj_t *inflictor, mobj_t *source, fixed_t radius);
 boolean P_HomingAttack(mobj_t *source, mobj_t *enemy); /// \todo doesn't belong in p_user
 boolean P_SuperReady(player_t *player);
 void P_DoJump(player_t *player, boolean soundandstate);
+void P_DoSpinDashDust(player_t *player);
 #define P_AnalogMove(player) (P_ControlStyle(player) == CS_LMAOGALOG)
 boolean P_TransferToNextMare(player_t *player);
 UINT8 P_FindLowestMare(void);
diff --git a/src/p_user.c b/src/p_user.c
index 0f3282da5..22c2bd3a9 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -4537,7 +4537,7 @@ void P_DoJump(player_t *player, boolean soundandstate)
 	}
 }
 
-static void P_DoSpinDashDust(player_t *player)
+void P_DoSpinDashDust(player_t *player)
 {
 	UINT32 i;
 	mobj_t *particle;

From 7a7cee7ca575246d2b01632977dd886484b589f3 Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Wed, 31 May 2023 23:03:22 -0500
Subject: [PATCH 06/23] Expose P_CheckSkyHit to Lua

---
 src/lua_baselib.c | 15 +++++++++++++++
 src/p_local.h     |  1 +
 src/p_mobj.c      | 21 +++++++++++----------
 3 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 25fa38769..1a189fabd 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1030,6 +1030,20 @@ static int lib_pRailThinker(lua_State *L)
 	return 1;
 }
 
+static int lib_pCheckSkyHit(lua_State *L)
+{
+	mobj_t *mobj = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	line_t *line = *((line_t **)luaL_checkudata(L, 2, META_LINE));
+	//HUDSAFE
+	INLEVEL
+	if (!mobj)
+		return LUA_ErrInvalid(L, "mobj_t");
+	if (!line)
+		return LUA_ErrInvalid(L, "line_t");
+	lua_pushboolean(L, P_CheckSkyHit(mobj, line));
+	return 1;
+}
+
 static int lib_pXYMovement(lua_State *L)
 {
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -4059,6 +4073,7 @@ static luaL_Reg lib[] = {
 	{"P_CreateFloorSpriteSlope",lib_pCreateFloorSpriteSlope},
 	{"P_RemoveFloorSpriteSlope",lib_pRemoveFloorSpriteSlope},
 	{"P_RailThinker",lib_pRailThinker},
+	{"P_CheckSkyHit",lib_pCheckSkyHit},
 	{"P_XYMovement",lib_pXYMovement},
 	{"P_RingXYMovement",lib_pRingXYMovement},
 	{"P_SceneryXYMovement",lib_pSceneryXYMovement},
diff --git a/src/p_local.h b/src/p_local.h
index cc060e4ee..91634e803 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -297,6 +297,7 @@ void P_RunOverlays(void);
 void P_HandleMinecartSegments(mobj_t *mobj);
 void P_MobjThinker(mobj_t *mobj);
 boolean P_RailThinker(mobj_t *mobj);
+boolean P_CheckSkyHit(mobj_t *mo, line_t *line);
 void P_PushableThinker(mobj_t *mobj);
 void P_SceneryThinker(mobj_t *mobj);
 
diff --git a/src/p_mobj.c b/src/p_mobj.c
index eeaf54776..fad4f7743 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -1753,14 +1753,15 @@ bustupdone:
 //
 // P_CheckSkyHit
 //
-static boolean P_CheckSkyHit(mobj_t *mo)
+boolean P_CheckSkyHit(mobj_t *mo, line_t *line)
 {
-	if (ceilingline && ceilingline->backsector
-		&& ceilingline->backsector->ceilingpic == skyflatnum
-		&& ceilingline->frontsector
-		&& ceilingline->frontsector->ceilingpic == skyflatnum
-		&& (mo->z >= ceilingline->frontsector->ceilingheight
-		|| mo->z >= ceilingline->backsector->ceilingheight))
+	if (line && (line->special == 41 ||
+		(line->backsector
+		&& line->backsector->ceilingpic == skyflatnum
+		&& line->frontsector
+		&& line->frontsector->ceilingpic == skyflatnum
+		&& (mo->z >= line->frontsector->ceilingheight
+		|| mo->z >= line->backsector->ceilingheight))))
 			return true;
 	return false;
 }
@@ -1867,7 +1868,7 @@ void P_XYMovement(mobj_t *mo)
 					mo->fuse += ((5 - mo->threshold) * TICRATE);
 
 				// Check for hit against sky here
-				if (P_CheckSkyHit(mo))
+				if (P_CheckSkyHit(mo, ceilingline))
 				{
 					// Hack to prevent missiles exploding
 					// against the sky.
@@ -1887,7 +1888,7 @@ void P_XYMovement(mobj_t *mo)
 			mo->flags &= ~MF_STICKY; //Don't check again!
 
 			// Check for hit against sky here
-			if (P_CheckSkyHit(mo))
+			if (P_CheckSkyHit(mo, ceilingline))
 			{
 				// Hack to prevent missiles exploding
 				// against the sky.
@@ -1946,7 +1947,7 @@ void P_XYMovement(mobj_t *mo)
 		else if (mo->flags & MF_MISSILE)
 		{
 			// explode a missile
-			if (P_CheckSkyHit(mo))
+			if (P_CheckSkyHit(mo, ceilingline))
 			{
 				// Hack to prevent missiles exploding
 				// against the sky.

From 826a70323ccabcdce551b2b0e8e786dcd525f1d7 Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Fri, 9 Jun 2023 13:04:01 -0500
Subject: [PATCH 07/23] LUA_HookFollowMobj instead of LUAh_FollowMobj

---
 src/p_user.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/p_user.c b/src/p_user.c
index 253161c04..2200185ac 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -11442,7 +11442,7 @@ void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 // Handle Followmobj behavior
 void P_DoFollowMobj(player_t *player, mobj_t *followmobj)
 {
-	if (LUAh_FollowMobj(player, followmobj) || P_MobjWasRemoved(followmobj))
+	if (LUA_HookFollowMobj(player, followmobj) || P_MobjWasRemoved(followmobj))
 		{;}
 	else
 	{

From 2647b108406258aac6fcf8bf522cb2be8b68305c Mon Sep 17 00:00:00 2001
From: SMS Alfredo <65426124+SMS-Alfredo@users.noreply.github.com>
Date: Sat, 1 Jul 2023 22:24:11 -0500
Subject: [PATCH 08/23] Require valid toucher.player

---
 src/lua_baselib.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 8409a8377..1f8baedc2 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -2204,6 +2204,8 @@ static int lib_pTouchSpecialThing(lua_State *L)
 	INLEVEL
 	if (!special || !toucher)
 		return LUA_ErrInvalid(L, "mobj_t");
+	if (!toucher->player)
+		return luaL_error(L, "P_TouchSpecialThing requires a valid toucher.player.");
 	P_TouchSpecialThing(special, toucher, heightcheck);
 	return 0;
 }

From a8fe500d0e22beb37e7c734d74cfa2632199bbdf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gustaf=20Alh=C3=A4ll?= <gustaf@hanicef.me>
Date: Tue, 19 Sep 2023 19:05:43 +0200
Subject: [PATCH 09/23] Add auto-kick for inactive players

---
 src/netcode/d_clisrv.c | 35 +++++++++++++++++++++++++++++++++++
 src/netcode/d_clisrv.h |  4 ++--
 src/netcode/d_net.h    |  1 +
 src/netcode/d_netcmd.c |  1 +
 src/netcode/protocol.h |  1 +
 5 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index f06192f2c..efa251ff7 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -113,6 +113,7 @@ consvar_t cv_blamecfail = CVAR_INIT ("blamecfail", "Off", CV_SAVE|CV_NETVAR, CV_
 static CV_PossibleValue_t playbackspeed_cons_t[] = {{1, "MIN"}, {10, "MAX"}, {0, NULL}};
 consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_cons_t, NULL);
 
+consvar_t cv_idletime = CVAR_INIT ("idletime", "0", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
 
 void ResetNode(INT32 node)
@@ -492,6 +493,10 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false);
 			kickreason = KR_BAN;
 			break;
+		case KICK_MSG_IDLE:
+			HU_AddChatText(va("\x82*%s has left the game (Inactive for too long)", player_names[pnum]), false);
+			kickreason = KR_TIMEOUT;
+			break;
 	}
 
 	if (pnum == consoleplayer)
@@ -507,6 +512,8 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			M_StartMessage(M_GetText("Server closed connection\n(synch failure)\nPress ESC\n"), NULL, MM_NOTHING);
 		else if (msg == KICK_MSG_PING_HIGH)
 			M_StartMessage(M_GetText("Server closed connection\n(Broke ping limit)\nPress ESC\n"), NULL, MM_NOTHING);
+		else if (msg == KICK_MSG_IDLE)
+			M_StartMessage(M_GetText("Server closed connection\n(Inactive for too long)\nPress ESC\n"), NULL, MM_NOTHING);
 		else if (msg == KICK_MSG_BANNED)
 			M_StartMessage(M_GetText("You have been banned by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
 		else if (msg == KICK_MSG_CUSTOM_KICK)
@@ -1267,6 +1274,30 @@ static void UpdatePingTable(void)
 	}
 }
 
+static void IdleUpdate(void)
+{
+	INT32 i;
+	if (!server || !netgame)
+		return;
+
+	for (i = 1; i < MAXPLAYERS; i++)
+	{
+		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && i != serverplayer)
+		{
+			if (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons)
+				netnodes[i].lastinput = gametime;
+
+			if (gametime - netnodes[i].lastinput > cv_idletime.value * TICRATE)
+			{
+				netnodes[i].lastinput = gametime;
+				SendKick(i, KICK_MSG_IDLE | KICK_MSG_KEEP_BODY);
+			}
+		}
+		else
+			netnodes[i].lastinput = gametime;
+	}
+}
+
 // Handle timeouts to prevent definitive freezes from happenning
 static void HandleNodeTimeouts(void)
 {
@@ -1299,6 +1330,8 @@ void NetKeepAlive(void)
 
 	GetPackets();
 
+	IdleUpdate();
+
 #ifdef MASTERSERVER
 	MasterClient_Ticker();
 #endif
@@ -1419,6 +1452,8 @@ void NetUpdate(void)
 
 	GetPackets(); // get packet from client or from server
 
+	IdleUpdate();
+
 	// The client sends the command after receiving from the server
 	// The server sends it before because this is better in single player
 
diff --git a/src/netcode/d_clisrv.h b/src/netcode/d_clisrv.h
index d87ead9ec..db9a780cd 100644
--- a/src/netcode/d_clisrv.h
+++ b/src/netcode/d_clisrv.h
@@ -48,7 +48,7 @@ typedef enum
 	KR_TIMEOUT       = 4, //Connection Timeout
 	KR_BAN           = 5, //Banned by server
 	KR_LEAVE         = 6, //Quit the game
-
+	KR_IDLE          = 7, //Remained still for too long
 } kickreason_t;
 
 /* the max number of name changes in some time period */
@@ -73,7 +73,7 @@ extern UINT32 realpingtable[MAXPLAYERS];
 extern UINT32 playerpingtable[MAXPLAYERS];
 extern tic_t servermaxping;
 
-extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_dedicatedidletime;
+extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_idletime, cv_dedicatedidletime;
 
 // Used in d_net, the only dependence
 void D_ClientServerInit(void);
diff --git a/src/netcode/d_net.h b/src/netcode/d_net.h
index 549f2b93c..e8e3b5895 100644
--- a/src/netcode/d_net.h
+++ b/src/netcode/d_net.h
@@ -43,6 +43,7 @@ typedef struct netnode_s
 {
 	boolean ingame; // set false as nodes leave game
 	tic_t freezetimeout; // Until when can this node freeze the server before getting a timeout?
+	tic_t lastinput; // the last tic the player has made any input
 
 	SINT8 player;
 	SINT8 player2; // say the numplayer for this node if any (splitscreen)
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index 5afa95561..0cceac112 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -601,6 +601,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_showjoinaddress);
 	CV_RegisterVar(&cv_blamecfail);
 	CV_RegisterVar(&cv_dedicatedidletime);
+	CV_RegisterVar(&cv_idletime);
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
 	CV_RegisterVar(&cv_nettimeout);
diff --git a/src/netcode/protocol.h b/src/netcode/protocol.h
index a992e3b69..c084d920c 100644
--- a/src/netcode/protocol.h
+++ b/src/netcode/protocol.h
@@ -331,6 +331,7 @@ typedef struct
 #define KICK_MSG_PING_HIGH   6
 #define KICK_MSG_CUSTOM_KICK 7
 #define KICK_MSG_CUSTOM_BAN  8
+#define KICK_MSG_IDLE        9
 #define KICK_MSG_KEEP_BODY   0x80
 
 #endif

From 436de2180831aec6b14de01e1aa61ba894c5c1ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gustaf=20Alh=C3=A4ll?= <gustaf@hanicef.me>
Date: Sat, 7 Oct 2023 00:07:56 +0200
Subject: [PATCH 10/23] Refactor code after review comments

---
 src/d_player.h         |  1 +
 src/netcode/d_clisrv.c | 10 +++++-----
 src/netcode/d_net.h    |  1 -
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/d_player.h b/src/d_player.h
index 7ad5b9f81..ca700d4e1 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -607,6 +607,7 @@ typedef struct player_s
 
 	tic_t jointime; // Timer when player joins game to change skin/color
 	tic_t quittime; // Time elapsed since user disconnected, zero if connected
+	tic_t lastinputtime; // the last tic the player has made any input
 #ifdef HWRENDER
 	fixed_t fovadd; // adjust FOV for hw rendering
 #endif
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index efa251ff7..4571930f8 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -1282,19 +1282,19 @@ static void IdleUpdate(void)
 
 	for (i = 1; i < MAXPLAYERS; i++)
 	{
-		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && i != serverplayer)
+		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && !IsPlayerAdmin(i))
 		{
 			if (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons)
-				netnodes[i].lastinput = gametime;
+				players[i].lastinputtime = gametime;
 
-			if (gametime - netnodes[i].lastinput > cv_idletime.value * TICRATE)
+			if (gametime - players[i].lastinputtime > (tic_t)cv_idletime.value * TICRATE * 60)
 			{
-				netnodes[i].lastinput = gametime;
+				players[i].lastinputtime = gametime;
 				SendKick(i, KICK_MSG_IDLE | KICK_MSG_KEEP_BODY);
 			}
 		}
 		else
-			netnodes[i].lastinput = gametime;
+			players[i].lastinputtime = gametime;
 	}
 }
 
diff --git a/src/netcode/d_net.h b/src/netcode/d_net.h
index e8e3b5895..549f2b93c 100644
--- a/src/netcode/d_net.h
+++ b/src/netcode/d_net.h
@@ -43,7 +43,6 @@ typedef struct netnode_s
 {
 	boolean ingame; // set false as nodes leave game
 	tic_t freezetimeout; // Until when can this node freeze the server before getting a timeout?
-	tic_t lastinput; // the last tic the player has made any input
 
 	SINT8 player;
 	SINT8 player2; // say the numplayer for this node if any (splitscreen)

From 80e386b16f66079fcef80bcfb116135b8524efe0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gustaf=20Alh=C3=A4ll?= <gustaf@hanicef.me>
Date: Sat, 14 Oct 2023 21:50:59 +0200
Subject: [PATCH 11/23] Expose lastinputtime to Lua

---
 src/lua_baselib.c      |  1 +
 src/lua_playerlib.c    |  8 ++++++++
 src/netcode/d_clisrv.c | 13 ++++++++-----
 3 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 8cb6d185d..d1fa8f368 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -3572,6 +3572,7 @@ static int lib_gAddPlayer(lua_State *L)
 
 	newplayer->jointime = 0;
 	newplayer->quittime = 0;
+	newplayer->lastinputtime = 0;
 
 	// Read the skin argument (defaults to Sonic)
 	if (!lua_isnoneornil(L, 1))
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 827e5a405..d0858e044 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -223,6 +223,7 @@ enum player_e
 	player_blocked,
 	player_jointime,
 	player_quittime,
+	player_lastinputtime,
 	player_ping,
 #ifdef HWRENDER
 	player_fovadd,
@@ -371,6 +372,7 @@ static const char *const player_opt[] = {
 	"blocked",
 	"jointime",
 	"quittime",
+	"lastinputtime",
 	"ping",
 #ifdef HWRENDER
 	"fovadd",
@@ -826,6 +828,9 @@ static int player_get(lua_State *L)
 	case player_quittime:
 		lua_pushinteger(L, plr->quittime);
 		break;
+	case player_lastinputtime:
+		lua_pushinteger(L, plr->lastinputtime);
+		break;
 	case player_ping:
 		lua_pushinteger(L, playerpingtable[plr - players]);
 		break;
@@ -1349,6 +1354,9 @@ static int player_set(lua_State *L)
 	case player_quittime:
 		plr->quittime = (tic_t)luaL_checkinteger(L, 3);
 		break;
+	case player_lastinputtime:
+		plr->lastinputtime = (tic_t)luaL_checkinteger(L, 3);
+		break;
 #ifdef HWRENDER
 	case player_fovadd:
 		plr->fovadd = luaL_checkfixed(L, 3);
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index 4571930f8..7804b068f 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -227,6 +227,7 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 
 	newplayer->jointime = 0;
 	newplayer->quittime = 0;
+	newplayer->lastinputtime = 0;
 
 	READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME);
 
@@ -1282,19 +1283,21 @@ static void IdleUpdate(void)
 
 	for (i = 1; i < MAXPLAYERS; i++)
 	{
-		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && !IsPlayerAdmin(i))
+		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && !players[i].bot && !IsPlayerAdmin(i) && i != serverplayer)
 		{
 			if (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons)
-				players[i].lastinputtime = gametime;
+				players[i].lastinputtime = 0;
+			else
+				players[i].lastinputtime++;
 
-			if (gametime - players[i].lastinputtime > (tic_t)cv_idletime.value * TICRATE * 60)
+			if (players[i].lastinputtime > (tic_t)cv_idletime.value * TICRATE * 60)
 			{
-				players[i].lastinputtime = gametime;
+				players[i].lastinputtime = 0;
 				SendKick(i, KICK_MSG_IDLE | KICK_MSG_KEEP_BODY);
 			}
 		}
 		else
-			players[i].lastinputtime = gametime;
+			players[i].lastinputtime = 0;
 	}
 }
 

From 23268bf3e53abfe0667465631a4027d1f56cd49d Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Fri, 27 Oct 2023 16:29:47 +0200
Subject: [PATCH 12/23] Move input-related Lua variables into the library

---
 src/lua_inputlib.c | 60 ++++++++++++++++++++++++++++++++++++----------
 src/lua_script.c   |  2 ++
 2 files changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 1f75ee6fe..0602b3db6 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -145,6 +145,30 @@ static luaL_Reg lib[] = {
 	{NULL, NULL}
 };
 
+///////////////
+// VARIABLES //
+///////////////
+
+static int lib_get(lua_State *L)
+{
+	const char *field = luaL_checkstring(L, 2);
+
+	if (fastcmp(field, "mouse"))
+	{
+		LUA_PushUserdata(L, &mouse, META_MOUSE);
+		return 1;
+	}
+	else if (fastcmp(field, "mouse2"))
+	{
+		LUA_PushUserdata(L, &mouse2, META_MOUSE);
+		return 1;
+	}
+	else
+	{
+		return 0;
+	}
+}
+
 ///////////////////
 // gamekeydown[] //
 ///////////////////
@@ -239,19 +263,6 @@ static int mouse_num(lua_State *L)
 
 int LUA_InputLib(lua_State *L)
 {
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getGameKeyDown);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setGameKeyDown);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_lenGameKeyDown);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "gamekeydown");
-
 	luaL_newmetatable(L, META_KEYEVENT);
 		lua_pushcfunction(L, keyevent_get);
 		lua_setfield(L, -2, "__index");
@@ -265,6 +276,29 @@ int LUA_InputLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
+	// Register the library, then add __index and __newindex
+	// metamethods to it to allow global variables
 	luaL_register(L, "input", lib);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_get);
+			lua_setfield(L, -2, "__index");
+		lua_setmetatable(L, -2);
+
+		lua_newuserdata(L, 0);
+			lua_createtable(L, 0, 2);
+				lua_pushcfunction(L, lib_getGameKeyDown);
+				lua_setfield(L, -2, "__index");
+
+				lua_pushcfunction(L, lib_setGameKeyDown);
+				lua_setfield(L, -2, "__newindex");
+
+				lua_pushcfunction(L, lib_lenGameKeyDown);
+				lua_setfield(L, -2, "__len");
+			lua_setmetatable(L, -2);
+		lua_pushvalue(L, -1); // TODO: 2.3: Delete (gamekeydown moved to input library)
+		lua_setglobal(L, "gamekeydown"); // Delete too
+		lua_setfield(L, -2, "gamekeydown");
+	lua_pop(L, 1);
+
 	return 0;
 }
diff --git a/src/lua_script.c b/src/lua_script.c
index 392935b4f..9e106999d 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -415,9 +415,11 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word, "stagefailed")) {
 		lua_pushboolean(L, stagefailed);
 		return 1;
+	// TODO: 2.3: Deprecated (moved to the input library)
 	} else if (fastcmp(word, "mouse")) {
 		LUA_PushUserdata(L, &mouse, META_MOUSE);
 		return 1;
+	// TODO: 2.3: Deprecated (moved to the input library)
 	} else if (fastcmp(word, "mouse2")) {
 		LUA_PushUserdata(L, &mouse2, META_MOUSE);
 		return 1;

From 4fa4d6e0766b2e427357f36a178cb797c4177ac9 Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Fri, 27 Oct 2023 17:34:29 +0200
Subject: [PATCH 13/23] Implement input.ignoregameinputs

---
 src/g_game.c       |  3 ++-
 src/g_input.c      | 11 ++++++++---
 src/lua_inputlib.c | 25 +++++++++++++++++++++++++
 src/lua_libs.h     |  1 +
 4 files changed, 36 insertions(+), 4 deletions(-)

diff --git a/src/g_game.c b/src/g_game.c
index fae311694..962c2fd14 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -51,6 +51,7 @@
 #include "r_fps.h" // frame interpolation/uncapped
 
 #include "lua_hud.h"
+#include "lua_libs.h"
 
 gameaction_t gameaction;
 gamestate_t gamestate = GS_NULL;
@@ -1170,7 +1171,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	// why build a ticcmd if we're paused?
 	// Or, for that matter, if we're being reborn.
 	// ...OR if we're blindfolded. No looking into the floor.
-	if (paused || P_AutoPause() || (gamestate == GS_LEVEL && (player->playerstate == PST_REBORN || ((gametyperules & GTR_TAG)
+	if (ignoregameinputs || paused || P_AutoPause() || (gamestate == GS_LEVEL && (player->playerstate == PST_REBORN || ((gametyperules & GTR_TAG)
 	&& (leveltime < hidetime * TICRATE) && (player->pflags & PF_TAGIT)))))
 	{//@TODO splitscreen player
 		cmd->angleturn = ticcmd_oldangleturn[forplayer];
diff --git a/src/g_input.c b/src/g_input.c
index fa30c1984..97c0505a3 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -18,6 +18,8 @@
 #include "hu_stuff.h" // need HUFONT start & end
 #include "netcode/d_net.h"
 #include "console.h"
+#include "lua_script.h"
+#include "lua_libs.h"
 
 #define MAXMOUSESENSITIVITY 100 // sensitivity steps
 
@@ -116,7 +118,10 @@ void G_MapEventsToControls(event_t *ev)
 	{
 		case ev_keydown:
 			if (ev->key < NUMINPUTS)
-				gamekeydown[ev->key] = 1;
+			{
+				if (!ignoregameinputs)
+					gamekeydown[ev->key] = 1;
+			}
 #ifdef PARANOIA
 			else
 			{
@@ -144,7 +149,7 @@ void G_MapEventsToControls(event_t *ev)
 
 		case ev_joystick: // buttons are virtual keys
 			i = ev->key;
-			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
+			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on || ignoregameinputs)
 				break;
 			if (ev->x != INT32_MAX) joyxmove[i] = ev->x;
 			if (ev->y != INT32_MAX) joyymove[i] = ev->y;
@@ -152,7 +157,7 @@ void G_MapEventsToControls(event_t *ev)
 
 		case ev_joystick2: // buttons are virtual keys
 			i = ev->key;
-			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
+			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on || ignoregameinputs)
 				break;
 			if (ev->x != INT32_MAX) joy2xmove[i] = ev->x;
 			if (ev->y != INT32_MAX) joy2ymove[i] = ev->y;
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 0602b3db6..eeb61fed7 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -20,6 +20,7 @@
 #include "lua_libs.h"
 
 boolean mousegrabbedbylua = true;
+boolean ignoregameinputs = false;
 
 ///////////////
 // FUNCTIONS //
@@ -163,12 +164,33 @@ static int lib_get(lua_State *L)
 		LUA_PushUserdata(L, &mouse2, META_MOUSE);
 		return 1;
 	}
+	else if (fastcmp(field, "ignoregameinputs"))
+	{
+		lua_pushboolean(L, ignoregameinputs);
+		return 1;
+	}
 	else
 	{
 		return 0;
 	}
 }
 
+static int lib_set(lua_State *L)
+{
+	const char *field = luaL_checkstring(L, 2);
+
+	if (fastcmp(field, "ignoregameinputs"))
+	{
+		ignoregameinputs = luaL_checkboolean(L, 3);
+	}
+	else
+	{
+		lua_rawset(L, 1);
+	}
+
+	return 0;
+}
+
 ///////////////////
 // gamekeydown[] //
 ///////////////////
@@ -282,6 +304,9 @@ int LUA_InputLib(lua_State *L)
 		lua_createtable(L, 0, 2);
 			lua_pushcfunction(L, lib_get);
 			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_set);
+			lua_setfield(L, -2, "__newindex");
 		lua_setmetatable(L, -2);
 
 		lua_newuserdata(L, 0);
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 7f8d21f38..2e3c70652 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -13,6 +13,7 @@
 extern lua_State *gL;
 
 extern boolean mousegrabbedbylua;
+extern boolean ignoregameinputs;
 
 #define MUTABLE_TAGS
 

From bc9a378521f2d63dc46b75579b9cce63f3b7e0e5 Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Fri, 27 Oct 2023 20:17:27 +0200
Subject: [PATCH 14/23] Refactor userdata metatable registration

---
 src/lua_consolelib.c |   5 +-
 src/lua_hudlib.c     |  40 ++---------
 src/lua_infolib.c    | 107 +++-------------------------
 src/lua_inputlib.c   |  14 +---
 src/lua_maplib.c     | 163 ++++++-------------------------------------
 src/lua_mobjlib.c    |  39 ++---------
 src/lua_playerlib.c  |  33 +--------
 src/lua_polyobjlib.c |  29 +-------
 src/lua_script.c     |  29 ++++++++
 src/lua_script.h     |   8 +++
 src/lua_skinlib.c    |  35 ++--------
 11 files changed, 89 insertions(+), 413 deletions(-)

diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index 0ab8ad9c3..b34105004 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -626,10 +626,7 @@ static int cvar_get(lua_State *L)
 int LUA_ConsoleLib(lua_State *L)
 {
 	// Metatable for consvar_t
-	luaL_newmetatable(L, META_CVAR);
-		lua_pushcfunction(L, cvar_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_CVAR, cvar_get, NULL, NULL);
 
 	cvar_fields_ref = Lua_CreateFieldTable(L, cvar_opt);
 
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 6eec91273..ed0ec86f8 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -1404,16 +1404,13 @@ int LUA_HudLib(lua_State *L)
 	luaL_register(L, NULL, lib_draw);
 	lib_draw_ref = luaL_ref(L, LUA_REGISTRYINDEX);
 
-	luaL_newmetatable(L, META_HUDINFO);
-		lua_pushcfunction(L, hudinfo_get);
-		lua_setfield(L, -2, "__index");
+	LUA_RegisterUserdataMetatable(L, META_HUDINFO, hudinfo_get, hudinfo_set, hudinfo_num);
+	LUA_RegisterUserdataMetatable(L, META_COLORMAP, colormap_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_PATCH, patch_get, patch_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_CAMERA, camera_get, camera_set, NULL);
 
-		lua_pushcfunction(L, hudinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, hudinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
+	patch_fields_ref = Lua_CreateFieldTable(L, patch_opt);
+	camera_fields_ref = Lua_CreateFieldTable(L, camera_opt);
 
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
@@ -1425,31 +1422,6 @@ int LUA_HudLib(lua_State *L)
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, "hudinfo");
 
-	luaL_newmetatable(L, META_COLORMAP);
-		lua_pushcfunction(L, colormap_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_PATCH);
-		lua_pushcfunction(L, patch_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, patch_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
-
-	patch_fields_ref = Lua_CreateFieldTable(L, patch_opt);
-
-	luaL_newmetatable(L, META_CAMERA);
-		lua_pushcfunction(L, camera_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, camera_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
-
-	camera_fields_ref = Lua_CreateFieldTable(L, camera_opt);
-
 	luaL_register(L, "hud", lib_hud);
 	return 0;
 }
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 3764acf6a..6b15cc33f 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1914,96 +1914,18 @@ int LUA_InfoLib(lua_State *L)
 	lua_newtable(L);
 	lua_setfield(L, LUA_REGISTRYINDEX, LREG_ACTIONS);
 
-	luaL_newmetatable(L, META_STATE);
-		lua_pushcfunction(L, state_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, state_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, state_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_MOBJINFO);
-		lua_pushcfunction(L, mobjinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mobjinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, mobjinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	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_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);
+	LUA_RegisterUserdataMetatable(L, META_SPRITEINFO, spriteinfo_get, spriteinfo_set, spriteinfo_num);
+	LUA_RegisterUserdataMetatable(L, META_PIVOTLIST, pivotlist_get, pivotlist_set, pivotlist_num);
+	LUA_RegisterUserdataMetatable(L, META_FRAMEPIVOT, framepivot_get, framepivot_set, framepivot_num);
+	LUA_RegisterUserdataMetatable(L, META_LUABANKS, lib_getluabanks, lib_setluabanks, lib_luabankslen);
 
 	mobjinfo_fields_ref = Lua_CreateFieldTable(L, mobjinfo_opt);
 
-	luaL_newmetatable(L, META_SKINCOLOR);
-		lua_pushcfunction(L, skincolor_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, skincolor_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, skincolor_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_COLORRAMP);
-		lua_pushcfunction(L, colorramp_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, colorramp_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, colorramp_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_SFXINFO);
-		lua_pushcfunction(L, sfxinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, sfxinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, sfxinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SPRITEINFO);
-		lua_pushcfunction(L, spriteinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, spriteinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, spriteinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_PIVOTLIST);
-		lua_pushcfunction(L, pivotlist_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, pivotlist_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, pivotlist_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_FRAMEPIVOT);
-		lua_pushcfunction(L, framepivot_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, framepivot_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, framepivot_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
 			lua_pushcfunction(L, lib_getSprname);
@@ -2104,16 +2026,5 @@ int LUA_InfoLib(lua_State *L)
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, "spriteinfo");
 
-	luaL_newmetatable(L, META_LUABANKS);
-		lua_pushcfunction(L, lib_getluabanks);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, lib_setluabanks);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, lib_luabankslen);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	return 0;
 }
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index eeb61fed7..34b44e3af 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -285,18 +285,8 @@ static int mouse_num(lua_State *L)
 
 int LUA_InputLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_KEYEVENT);
-		lua_pushcfunction(L, keyevent_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_MOUSE);
-		lua_pushcfunction(L, mouse_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mouse_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	LUA_RegisterUserdataMetatable(L, META_KEYEVENT, keyevent_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_MOUSE, mouse_get, NULL, mouse_num);
 
 	// Register the library, then add __index and __newindex
 	// metamethods to it to allow global variables
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index e34397993..998615850 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -2843,120 +2843,37 @@ static int mapheaderinfo_get(lua_State *L)
 
 int LUA_MapLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_SECTORLINES);
-		lua_pushcfunction(L, sectorlines_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, sectorlines_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SECTOR);
-		lua_pushcfunction(L, sector_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, sector_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, sector_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	LUA_RegisterUserdataMetatable(L, META_SECTORLINES, sectorlines_get, NULL, sectorlines_num);
+	LUA_RegisterUserdataMetatable(L, META_SECTOR, sector_get, sector_set, sector_num);
+	LUA_RegisterUserdataMetatable(L, META_SUBSECTOR, subsector_get, NULL, subsector_num);
+	LUA_RegisterUserdataMetatable(L, META_LINE, line_get, NULL, line_num);
+	LUA_RegisterUserdataMetatable(L, META_LINEARGS, lineargs_get, NULL, lineargs_len);
+	LUA_RegisterUserdataMetatable(L, META_LINESTRINGARGS, linestringargs_get, NULL, linestringargs_len);
+	LUA_RegisterUserdataMetatable(L, META_SIDENUM, sidenum_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_SIDE, side_get, side_set, side_num);
+	LUA_RegisterUserdataMetatable(L, META_VERTEX, vertex_get, NULL, vertex_num);
+	LUA_RegisterUserdataMetatable(L, META_FFLOOR, ffloor_get, ffloor_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_BBOX, bbox_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_SLOPE, slope_get, slope_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_VECTOR2, vector2_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_VECTOR3, vector3_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_MAPHEADER, mapheaderinfo_get, NULL, NULL);
 
 	sector_fields_ref = Lua_CreateFieldTable(L, sector_opt);
-
-	luaL_newmetatable(L, META_SUBSECTOR);
-		lua_pushcfunction(L, subsector_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, subsector_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	subsector_fields_ref = Lua_CreateFieldTable(L, subsector_opt);
-
-	luaL_newmetatable(L, META_LINE);
-		lua_pushcfunction(L, line_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, line_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	line_fields_ref = Lua_CreateFieldTable(L, line_opt);
-
-	luaL_newmetatable(L, META_LINEARGS);
-		lua_pushcfunction(L, lineargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, lineargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_LINESTRINGARGS);
-		lua_pushcfunction(L, linestringargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, linestringargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SIDENUM);
-		lua_pushcfunction(L, sidenum_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SIDE);
-		lua_pushcfunction(L, side_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, side_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, side_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	side_fields_ref = Lua_CreateFieldTable(L, side_opt);
-
-	luaL_newmetatable(L, META_VERTEX);
-		lua_pushcfunction(L, vertex_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, vertex_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	vertex_fields_ref = Lua_CreateFieldTable(L, vertex_opt);
-
-	luaL_newmetatable(L, META_FFLOOR);
-		lua_pushcfunction(L, ffloor_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, ffloor_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L, 1);
-
 	ffloor_fields_ref = Lua_CreateFieldTable(L, ffloor_opt);
+	slope_fields_ref = Lua_CreateFieldTable(L, slope_opt);
+	mapheaderinfo_fields_ref = Lua_CreateFieldTable(L, mapheaderinfo_opt);
 
 #ifdef HAVE_LUA_SEGS
-	luaL_newmetatable(L, META_SEG);
-		lua_pushcfunction(L, seg_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, seg_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	LUA_RegisterUserdataMetatable(L, META_SEG, seg_get, NULL, seg_num);
+	LUA_RegisterUserdataMetatable(L, META_NODE, node_get, NULL, node_num);
+	LUA_RegisterUserdataMetatable(L, META_NODECHILDREN, nodechildren_get, NULL, NULL);
 
 	seg_fields_ref = Lua_CreateFieldTable(L, seg_opt);
-
-	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);
-
 	node_fields_ref = Lua_CreateFieldTable(L, node_opt);
 
 	luaL_newmetatable(L, META_NODEBBOX);
@@ -2965,48 +2882,8 @@ int LUA_MapLib(lua_State *L)
 		lua_pushcfunction(L, nodebbox_call);
 		lua_setfield(L, -2, "__call");
 	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_NODECHILDREN);
-		lua_pushcfunction(L, nodechildren_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
 #endif
 
-	luaL_newmetatable(L, META_BBOX);
-		lua_pushcfunction(L, bbox_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SLOPE);
-		lua_pushcfunction(L, slope_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, slope_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L, 1);
-
-	slope_fields_ref = Lua_CreateFieldTable(L, slope_opt);
-
-	luaL_newmetatable(L, META_VECTOR2);
-		lua_pushcfunction(L, vector2_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_VECTOR3);
-		lua_pushcfunction(L, vector3_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");
-
-		//lua_pushcfunction(L, mapheaderinfo_num);
-		//lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	mapheaderinfo_fields_ref = Lua_CreateFieldTable(L, mapheaderinfo_opt);
-
 	LUA_PushTaggableObjectArray(L, "sectors",
 			lib_iterateSectors,
 			lib_getSector,
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index fddf958be..19f30b70e 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -1163,43 +1163,12 @@ static int lib_nummapthings(lua_State *L)
 
 int LUA_MobjLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_MOBJ);
-		lua_pushcfunction(L, mobj_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mobj_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_MOBJ, mobj_get, mobj_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_THINGARGS, thingargs_get, NULL, thingargs_len);
+	LUA_RegisterUserdataMetatable(L, META_THINGSTRINGARGS, thingstringargs_get, NULL, thingstringargs_len);
+	LUA_RegisterUserdataMetatable(L, META_MAPTHING, mapthing_get, mapthing_set, mapthing_num);
 
 	mobj_fields_ref = Lua_CreateFieldTable(L, mobj_opt);
-
-	luaL_newmetatable(L, META_THINGARGS);
-		lua_pushcfunction(L, thingargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, thingargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_THINGSTRINGARGS);
-		lua_pushcfunction(L, thingstringargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, thingstringargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_MAPTHING);
-		lua_pushcfunction(L, mapthing_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mapthing_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, mapthing_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
 	mapthing_fields_ref = Lua_CreateFieldTable(L, mapthing_opt);
 
 	LUA_PushTaggableObjectArray(L, "mapthings",
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 827e5a405..975025c44 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -1523,38 +1523,11 @@ static int ticcmd_set(lua_State *L)
 
 int LUA_PlayerLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_PLAYER);
-		lua_pushcfunction(L, player_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, player_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, player_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_PLAYER, player_get, player_set, player_num);
+	LUA_RegisterUserdataMetatable(L, META_POWERS, power_get, power_set, power_len);
+	LUA_RegisterUserdataMetatable(L, META_TICCMD, ticcmd_get, ticcmd_set, NULL);
 
 	player_fields_ref = Lua_CreateFieldTable(L, player_opt);
-
-	luaL_newmetatable(L, META_POWERS);
-		lua_pushcfunction(L, power_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, power_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, power_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_TICCMD);
-		lua_pushcfunction(L, ticcmd_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, ticcmd_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
-
 	ticcmd_fields_ref = Lua_CreateFieldTable(L, ticcmd_opt);
 
 	lua_newuserdata(L, 0);
diff --git a/src/lua_polyobjlib.c b/src/lua_polyobjlib.c
index c3d9d9d1a..d011ccfaa 100644
--- a/src/lua_polyobjlib.c
+++ b/src/lua_polyobjlib.c
@@ -447,32 +447,9 @@ static int lib_numPolyObjects(lua_State *L)
 
 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_RegisterUserdataMetatable(L, META_POLYOBJVERTICES, polyobjvertices_get, NULL, polyobjvertices_num);
+	LUA_RegisterUserdataMetatable(L, META_POLYOBJLINES, polyobjlines_get, NULL, polyobjlines_num);
+	LUA_RegisterUserdataMetatable(L, META_POLYOBJ, polyobj_get, polyobj_set, polyobj_num);
 
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
diff --git a/src/lua_script.c b/src/lua_script.c
index 9e106999d..562114583 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -1832,3 +1832,32 @@ void LUA_PushTaggableObjectArray
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, field);
 }
+
+void LUA_RegisterUserdataMetatable(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+)
+{
+	luaL_newmetatable(L, name);
+
+	if (get)
+	{
+		lua_pushcfunction(L, get);
+		lua_setfield(L, -2, "__index");
+	}
+	if (set)
+	{
+		lua_pushcfunction(L, set);
+		lua_setfield(L, -2, "__newindex");
+	}
+	if (len)
+	{
+		lua_pushcfunction(L, len);
+		lua_setfield(L, -2, "__len");
+	}
+
+	lua_pop(L, 1);
+}
diff --git a/src/lua_script.h b/src/lua_script.h
index d0b06a719..8a4eb9f13 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -73,6 +73,14 @@ void LUA_PushTaggableObjectArray
 		size_t sizeof_element,
 		const char *meta);
 
+void LUA_RegisterUserdataMetatable(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+);
+
 void LUA_InsertTaggroupIterator
 (		lua_State *L,
 		taggroup_t *garray[],
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 041c5d598..6c41758b1 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -373,40 +373,13 @@ static int sprite_get(lua_State *L)
 
 int LUA_SkinLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_SKIN);
-		lua_pushcfunction(L, skin_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, skin_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, skin_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_SKIN, skin_get, skin_set, skin_num);
+	LUA_RegisterUserdataMetatable(L, META_SOUNDSID, soundsid_get, NULL, soundsid_num);
+	LUA_RegisterUserdataMetatable(L, META_SKINSPRITES, lib_getSkinSprite, NULL, lib_numSkinsSprites);
+	LUA_RegisterUserdataMetatable(L, META_SKINSPRITESLIST, sprite_get, NULL, NULL);
 
 	skin_fields_ref = Lua_CreateFieldTable(L, skin_opt);
 
-	luaL_newmetatable(L, META_SOUNDSID);
-		lua_pushcfunction(L, soundsid_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, soundsid_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_SKINSPRITES);
-		lua_pushcfunction(L, lib_getSkinSprite);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, lib_numSkinsSprites);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_SKINSPRITESLIST);
-		lua_pushcfunction(L, sprite_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L,1);
-
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
 			lua_pushcfunction(L, lib_getSkin);

From 7a7d4f832a3d1ad18949bc02bda04dea7296398f Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Fri, 27 Oct 2023 21:34:53 +0200
Subject: [PATCH 15/23] Remove stupid whitespace

---
 src/lua_maplib.c | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 998615850..a0a7120a2 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -35,7 +35,7 @@ enum sector_e {
 	sector_floorpic,
 	sector_floorxoffset,
 	sector_flooryoffset,
-	sector_floorangle,	
+	sector_floorangle,
 	sector_ceilingpic,
 	sector_ceilingxoffset,
 	sector_ceilingyoffset,
@@ -43,7 +43,7 @@ enum sector_e {
 	sector_lightlevel,
 	sector_floorlightlevel,
 	sector_floorlightabsolute,
-	sector_floorlightsec,	
+	sector_floorlightsec,
 	sector_ceilinglightlevel,
 	sector_ceilinglightabsolute,
 	sector_ceilinglightsec,
@@ -77,14 +77,14 @@ static const char *const sector_opt[] = {
 	"ceilingpic",
 	"ceilingxoffset",
 	"ceilingyoffset",
-	"ceilingangle",	
+	"ceilingangle",
 	"lightlevel",
 	"floorlightlevel",
 	"floorlightabsolute",
 	"floorlightsec",
 	"ceilinglightlevel",
 	"ceilinglightabsolute",
-	"ceilinglightsec",	
+	"ceilinglightsec",
 	"special",
 	"tag",
 	"taglist",
@@ -663,11 +663,11 @@ static int sector_get(lua_State *L)
 		lua_pushfixed(L, sector->flooryoffset);
 		return 1;
 	}
-	case sector_floorangle: 
+	case sector_floorangle:
 	{
 		lua_pushangle(L, sector->floorangle);
 		return 1;
-	}	
+	}
 	case sector_ceilingpic: // ceilingpic
 	{
 		levelflat_t *levelflat = &levelflats[sector->ceilingpic];
@@ -691,7 +691,7 @@ static int sector_get(lua_State *L)
 	{
 		lua_pushangle(L, sector->ceilingangle);
 		return 1;
-	}	
+	}
 	case sector_lightlevel:
 		lua_pushinteger(L, sector->lightlevel);
 		return 1;
@@ -703,7 +703,7 @@ static int sector_get(lua_State *L)
 		return 1;
 	case sector_floorlightsec:
 		lua_pushinteger(L, sector->floorlightsec);
-		return 1;		
+		return 1;
 	case sector_ceilinglightlevel:
 		lua_pushinteger(L, sector->ceilinglightlevel);
 		return 1;
@@ -712,7 +712,7 @@ static int sector_get(lua_State *L)
 		return 1;
 	case sector_ceilinglightsec:
 		lua_pushinteger(L, sector->ceilinglightsec);
-		return 1;		
+		return 1;
 	case sector_special:
 		lua_pushinteger(L, sector->special);
 		return 1;
@@ -842,7 +842,7 @@ static int sector_set(lua_State *L)
 		break;
 	case sector_floorangle:
 		sector->floorangle = luaL_checkangle(L, 3);
-		break;				
+		break;
 	case sector_ceilingpic:
 		sector->ceilingpic = P_AddLevelFlatRuntime(luaL_checkstring(L, 3));
 		break;
@@ -866,7 +866,7 @@ static int sector_set(lua_State *L)
 		break;
 	case sector_floorlightsec:
 		sector->floorlightsec = (INT32)luaL_checkinteger(L, 3);
-		break;		
+		break;
 	case sector_ceilinglightlevel:
 		sector->ceilinglightlevel = (INT16)luaL_checkinteger(L, 3);
 		break;
@@ -875,7 +875,7 @@ static int sector_set(lua_State *L)
 		break;
 	case sector_ceilinglightsec:
 		sector->ceilinglightsec = (INT32)luaL_checkinteger(L, 3);
-		break;		
+		break;
 	case sector_special:
 		sector->special = (INT16)luaL_checkinteger(L, 3);
 		break;

From d1dbdee016631a62dd9883bc01eb777054d7942c Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Sat, 28 Oct 2023 00:24:04 +0200
Subject: [PATCH 16/23] Refactor global userdata registration

---
 src/lua_hudlib.c     |  10 +---
 src/lua_infolib.c    | 109 ++++---------------------------------------
 src/lua_inputlib.c   |  25 ++--------
 src/lua_maplib.c     | 101 ++++++++++-----------------------------
 src/lua_playerlib.c  |  10 +---
 src/lua_polyobjlib.c |  10 +---
 src/lua_script.c     |  72 ++++++++++++++++++++++++++--
 src/lua_script.h     |  26 +++++++++++
 src/lua_skinlib.c    |  10 +---
 src/lua_taglib.c     |  17 +++----
 10 files changed, 142 insertions(+), 248 deletions(-)

diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index ed0ec86f8..666a11abc 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -1412,15 +1412,7 @@ int LUA_HudLib(lua_State *L)
 	patch_fields_ref = Lua_CreateFieldTable(L, patch_opt);
 	camera_fields_ref = Lua_CreateFieldTable(L, camera_opt);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getHudInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_hudinfolen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "hudinfo");
+	LUA_RegisterGlobalUserdata(L, "hudinfo", lib_getHudInfo, NULL, lib_hudinfolen);
 
 	luaL_register(L, "hud", lib_hud);
 	return 0;
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 6b15cc33f..020eba152 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1926,105 +1926,16 @@ int LUA_InfoLib(lua_State *L)
 
 	mobjinfo_fields_ref = Lua_CreateFieldTable(L, mobjinfo_opt);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSprname);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_sprnamelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "sprnames");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSpr2name);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_spr2namelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "spr2names");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSpr2default);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSpr2default);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_spr2namelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "spr2defaults");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getState);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setState);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_statelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "states");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getMobjInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setMobjInfo);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_mobjinfolen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "mobjinfo");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSkinColor);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSkinColor);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_skincolorslen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "skincolors");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSfxInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSfxInfo);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_sfxlen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_pushvalue(L, -1);
-	lua_setglobal(L, "S_sfx");
-	lua_setglobal(L, "sfxinfo");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSpriteInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSpriteInfo);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_spriteinfolen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "spriteinfo");
+	LUA_RegisterGlobalUserdata(L, "sprnames", lib_getSprname, NULL, lib_sprnamelen);
+	LUA_RegisterGlobalUserdata(L, "spr2names", lib_getSpr2name, NULL, lib_spr2namelen);
+	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, "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);
+	// TODO: 2.3: Delete this alias
+	LUA_RegisterGlobalUserdata(L, "S_sfx", lib_getSfxInfo, lib_setSfxInfo, lib_sfxlen);
 
 	return 0;
 }
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 34b44e3af..ef3a9011f 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -291,28 +291,11 @@ int LUA_InputLib(lua_State *L)
 	// Register the library, then add __index and __newindex
 	// metamethods to it to allow global variables
 	luaL_register(L, "input", lib);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_get);
-			lua_setfield(L, -2, "__index");
+		LUA_CreateAndSetMetatable(L, lib_get, lib_set, NULL, false);
 
-			lua_pushcfunction(L, lib_set);
-			lua_setfield(L, -2, "__newindex");
-		lua_setmetatable(L, -2);
-
-		lua_newuserdata(L, 0);
-			lua_createtable(L, 0, 2);
-				lua_pushcfunction(L, lib_getGameKeyDown);
-				lua_setfield(L, -2, "__index");
-
-				lua_pushcfunction(L, lib_setGameKeyDown);
-				lua_setfield(L, -2, "__newindex");
-
-				lua_pushcfunction(L, lib_lenGameKeyDown);
-				lua_setfield(L, -2, "__len");
-			lua_setmetatable(L, -2);
-		lua_pushvalue(L, -1); // TODO: 2.3: Delete (gamekeydown moved to input library)
-		lua_setglobal(L, "gamekeydown"); // Delete too
-		lua_setfield(L, -2, "gamekeydown");
+		LUA_CreateAndSetUserdataField(L, -1, "gamekeydown", lib_getGameKeyDown, lib_setGameKeyDown, lib_lenGameKeyDown, false);
+		// TODO: 2.3: Delete this alias (moved to input library)
+		LUA_RegisterGlobalUserdata(L, "gamekeydown", lib_getGameKeyDown, lib_setGameKeyDown, lib_lenGameKeyDown);
 	lua_pop(L, 1);
 
 	return 0;
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index a0a7120a2..2e1aab3eb 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -2868,6 +2868,27 @@ int LUA_MapLib(lua_State *L)
 	slope_fields_ref = Lua_CreateFieldTable(L, slope_opt);
 	mapheaderinfo_fields_ref = Lua_CreateFieldTable(L, mapheaderinfo_opt);
 
+	LUA_RegisterGlobalUserdata(L, "subsectors", lib_getSubsector, NULL, lib_numsubsectors);
+	LUA_RegisterGlobalUserdata(L, "sides", lib_getSide, NULL, lib_numsides);
+	LUA_RegisterGlobalUserdata(L, "vertexes", lib_getVertex, NULL, lib_numvertexes);
+	LUA_RegisterGlobalUserdata(L, "mapheaderinfo", lib_getMapheaderinfo, NULL, lib_nummapheaders);
+
+	LUA_PushTaggableObjectArray(L, "sectors",
+			lib_iterateSectors,
+			lib_getSector,
+			lib_numsectors,
+			tags_sectors,
+			&numsectors, &sectors,
+			sizeof (sector_t), META_SECTOR);
+
+	LUA_PushTaggableObjectArray(L, "lines",
+			lib_iterateLines,
+			lib_getLine,
+			lib_numlines,
+			tags_lines,
+			&numlines, &lines,
+			sizeof (line_t), META_LINE);
+
 #ifdef HAVE_LUA_SEGS
 	LUA_RegisterUserdataMetatable(L, META_SEG, seg_get, NULL, seg_num);
 	LUA_RegisterUserdataMetatable(L, META_NODE, node_get, NULL, node_num);
@@ -2882,84 +2903,10 @@ int LUA_MapLib(lua_State *L)
 		lua_pushcfunction(L, nodebbox_call);
 		lua_setfield(L, -2, "__call");
 	lua_pop(L, 1);
+
+	LUA_RegisterGlobalUserdata(L, "segs", lib_getSeg, NULL, lib_numsegs);
+	LUA_RegisterGlobalUserdata(L, "nodes", lib_getNode, NULL, lib_numnodes);
 #endif
 
-	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);
-			lua_pushcfunction(L, lib_getSubsector);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsubsectors);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "subsectors");
-
-	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);
-			lua_pushcfunction(L, lib_getSide);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsides);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "sides");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getVertex);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numvertexes);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "vertexes");
-
-#ifdef HAVE_LUA_SEGS
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSeg);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsegs);
-			lua_setfield(L, -2, "__len");
-		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");
-#endif
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getMapheaderinfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_nummapheaders);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "mapheaderinfo");
 	return 0;
 }
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 975025c44..ac829d0c4 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -1530,14 +1530,6 @@ int LUA_PlayerLib(lua_State *L)
 	player_fields_ref = Lua_CreateFieldTable(L, player_opt);
 	ticcmd_fields_ref = Lua_CreateFieldTable(L, ticcmd_opt);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getPlayer);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_lenPlayer);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "players");
+	LUA_RegisterGlobalUserdata(L, "players", lib_getPlayer, NULL, lib_lenPlayer);
 	return 0;
 }
diff --git a/src/lua_polyobjlib.c b/src/lua_polyobjlib.c
index d011ccfaa..6c016f8b2 100644
--- a/src/lua_polyobjlib.c
+++ b/src/lua_polyobjlib.c
@@ -451,14 +451,6 @@ int LUA_PolyObjLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_POLYOBJLINES, polyobjlines_get, NULL, polyobjlines_num);
 	LUA_RegisterUserdataMetatable(L, META_POLYOBJ, polyobj_get, polyobj_set, polyobj_num);
 
-	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");
+	LUA_RegisterGlobalUserdata(L, "polyobjects", lib_getPolyObject, NULL, lib_numPolyObjects);
 	return 0;
 }
diff --git a/src/lua_script.c b/src/lua_script.c
index 562114583..9aea96ac7 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -1833,16 +1833,13 @@ void LUA_PushTaggableObjectArray
 	lua_setglobal(L, field);
 }
 
-void LUA_RegisterUserdataMetatable(
+static void SetBasicMetamethods(
 	lua_State *L,
-	const char *name,
 	lua_CFunction get,
 	lua_CFunction set,
 	lua_CFunction len
 )
 {
-	luaL_newmetatable(L, name);
-
 	if (get)
 	{
 		lua_pushcfunction(L, get);
@@ -1858,6 +1855,73 @@ void LUA_RegisterUserdataMetatable(
 		lua_pushcfunction(L, len);
 		lua_setfield(L, -2, "__len");
 	}
+}
 
+void LUA_RegisterUserdataMetatable(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+)
+{
+	luaL_newmetatable(L, name);
+	SetBasicMetamethods(L, get, set, len);
 	lua_pop(L, 1);
 }
+
+// If keep is true, leaves the metatable on the stack.
+// Otherwise, the stack size remains unchanged.
+void LUA_CreateAndSetMetatable(
+	lua_State *L,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+)
+{
+	lua_newtable(L);
+	SetBasicMetamethods(L, get, set, len);
+
+	lua_pushvalue(L, -1);
+	lua_setmetatable(L, -3);
+
+	if (!keep)
+		lua_pop(L, 1);
+}
+
+// If keep is true, leaves the userdata and metatable on the stack.
+// Otherwise, the stack size remains unchanged.
+void LUA_CreateAndSetUserdataField(
+	lua_State *L,
+	int index,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+)
+{
+	if (index < 0 && index > LUA_REGISTRYINDEX)
+		index -= 3;
+
+	lua_newuserdata(L, 0);
+	LUA_CreateAndSetMetatable(L, get, set, len, true);
+
+	lua_pushvalue(L, -2);
+	lua_setfield(L, index, name);
+
+	if (!keep)
+		lua_pop(L, 2);
+}
+
+void LUA_RegisterGlobalUserdata(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+)
+{
+	LUA_CreateAndSetUserdataField(L, LUA_GLOBALSINDEX, name, get, set, len, false);
+}
diff --git a/src/lua_script.h b/src/lua_script.h
index 8a4eb9f13..dd9a2568e 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -81,6 +81,32 @@ void LUA_RegisterUserdataMetatable(
 	lua_CFunction len
 );
 
+void LUA_CreateAndSetMetatable(
+	lua_State *L,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+);
+
+void LUA_CreateAndSetUserdataField(
+	lua_State *L,
+	int index,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+);
+
+void LUA_RegisterGlobalUserdata(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+);
+
 void LUA_InsertTaggroupIterator
 (		lua_State *L,
 		taggroup_t *garray[],
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 6c41758b1..3debd3746 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -380,15 +380,7 @@ int LUA_SkinLib(lua_State *L)
 
 	skin_fields_ref = Lua_CreateFieldTable(L, skin_opt);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSkin);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numSkins);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "skins");
+	LUA_RegisterGlobalUserdata(L, "skins", lib_getSkin, NULL, lib_numSkins);
 
 	return 0;
 }
diff --git a/src/lua_taglib.c b/src/lua_taglib.c
index 2ba60df99..5ed457534 100644
--- a/src/lua_taglib.c
+++ b/src/lua_taglib.c
@@ -426,17 +426,12 @@ set_taglist_metatable(lua_State *L, const char *meta)
 
 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");
+	LUA_CreateAndSetUserdataField(L, LUA_GLOBALSINDEX, "tags", NULL, NULL, lib_numTags, true);
+		lua_createtable(L, 0, 1);
+			lua_pushcfunction(L, lib_iterateTags);
+			lua_setfield(L, -2, "iterate");
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 2);
 
 	open_taglist(L);
 

From c3f79a112f3ec5cff160f9040196d284ffd4f9d8 Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Sat, 28 Oct 2023 11:06:30 +0200
Subject: [PATCH 17/23] Add LUA_SetCFunctionField

---
 src/deh_lua.c        |  3 +--
 src/lua_baselib.c    |  3 +--
 src/lua_maplib.c     |  6 ++----
 src/lua_script.c     | 33 +++++++++++++--------------------
 src/lua_script.h     |  2 ++
 src/lua_taglib.c     | 12 ++++--------
 src/lua_thinkerlib.c |  3 +--
 7 files changed, 24 insertions(+), 38 deletions(-)

diff --git a/src/deh_lua.c b/src/deh_lua.c
index 0b789547b..7188d25e3 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -771,8 +771,7 @@ int LUA_SOCLib(lua_State *L)
 	lua_register(L,"getActionName",lib_getActionName);
 
 	luaL_newmetatable(L, META_ACTION);
-		lua_pushcfunction(L, action_call);
-		lua_setfield(L, -2, "__call");
+		LUA_SetCFunctionField(L, "__call", action_call);
 	lua_pop(L, 1);
 
 	return 0;
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 1821550b8..f7ed763a9 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -4300,8 +4300,7 @@ int LUA_BaseLib(lua_State *L)
 	// Set metatable for string
 	lua_pushliteral(L, "");  // dummy string
 	lua_getmetatable(L, -1);  // get string metatable
-	lua_pushcfunction(L,lib_concat); // push concatination function
-	lua_setfield(L,-2,"__add"); // ... store it as mathematical addition
+	LUA_SetCFunctionField(L, "__add", lib_concat);
 	lua_pop(L, 2); // pop metatable and dummy string
 
 	lua_newtable(L);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 2e1aab3eb..efa2edf20 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -2898,10 +2898,8 @@ int LUA_MapLib(lua_State *L)
 	node_fields_ref = Lua_CreateFieldTable(L, node_opt);
 
 	luaL_newmetatable(L, META_NODEBBOX);
-		//lua_pushcfunction(L, nodebbox_get);
-		//lua_setfield(L, -2, "__index");
-		lua_pushcfunction(L, nodebbox_call);
-		lua_setfield(L, -2, "__call");
+		//LUA_SetCFunctionField(L, "__index", nodebbox_get);
+		LUA_SetCFunctionField(L, "__call", nodebbox_call);
 	lua_pop(L, 1);
 
 	LUA_RegisterGlobalUserdata(L, "segs", lib_getSeg, NULL, lib_numsegs);
diff --git a/src/lua_script.c b/src/lua_script.c
index 9aea96ac7..5cacc3f01 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -578,8 +578,7 @@ static void LUA_ClearState(void)
 
 	// lock the global namespace
 	lua_getmetatable(L, LUA_GLOBALSINDEX);
-		lua_pushcfunction(L, setglobals);
-		lua_setfield(L, -2, "__newindex");
+		LUA_SetCFunctionField(L, "__newindex", setglobals);
 		lua_newtable(L);
 		lua_setfield(L, -2, "__metatable");
 	lua_pop(L, 1);
@@ -1815,20 +1814,17 @@ void LUA_PushTaggableObjectArray
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
 			lua_createtable(L, 0, 2);
-				lua_pushcfunction(L, iterator);
-				lua_setfield(L, -2, "iterate");
+				LUA_SetCFunctionField(L, "iterate", iterator);
 
 				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_SetCFunctionField(L, "__index", indexer);
 				lua_setmetatable(L, -2);
 			lua_setfield(L, -2, "__index");
 
-			lua_pushcfunction(L, counter);
-			lua_setfield(L, -2, "__len");
+			LUA_SetCFunctionField(L, "__len", counter);
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, field);
 }
@@ -1841,20 +1837,17 @@ static void SetBasicMetamethods(
 )
 {
 	if (get)
-	{
-		lua_pushcfunction(L, get);
-		lua_setfield(L, -2, "__index");
-	}
+		LUA_SetCFunctionField(L, "__index", get);
 	if (set)
-	{
-		lua_pushcfunction(L, set);
-		lua_setfield(L, -2, "__newindex");
-	}
+		LUA_SetCFunctionField(L, "__newindex", set);
 	if (len)
-	{
-		lua_pushcfunction(L, len);
-		lua_setfield(L, -2, "__len");
-	}
+		LUA_SetCFunctionField(L, "__len", len);
+}
+
+void LUA_SetCFunctionField(lua_State *L, const char *name, lua_CFunction value)
+{
+	lua_pushcfunction(L, value);
+	lua_setfield(L, -2, name);
 }
 
 void LUA_RegisterUserdataMetatable(
diff --git a/src/lua_script.h b/src/lua_script.h
index dd9a2568e..53d848f8e 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -73,6 +73,8 @@ void LUA_PushTaggableObjectArray
 		size_t sizeof_element,
 		const char *meta);
 
+void LUA_SetCFunctionField(lua_State *L, const char *name, lua_CFunction value);
+
 void LUA_RegisterUserdataMetatable(
 	lua_State *L,
 	const char *name,
diff --git a/src/lua_taglib.c b/src/lua_taglib.c
index 5ed457534..9e73a050c 100644
--- a/src/lua_taglib.c
+++ b/src/lua_taglib.c
@@ -372,8 +372,7 @@ void LUA_InsertTaggroupIterator
 		lua_pushcclosure(L, lib_numTaggroupElements, 2);
 		lua_setfield(L, -2, "__len");
 
-		lua_pushcfunction(L, element_iterator);
-		lua_setfield(L, -2, "__call");
+		LUA_SetCFunctionField(L, "__call", element_iterator);
 	lua_pushcclosure(L, lib_getTaggroup, 1);
 	lua_setfield(L, -2, "tagged");
 }
@@ -414,11 +413,9 @@ set_taglist_metatable(lua_State *L, const char *meta)
 		lua_setfenv(L, -2);
 		lua_setfield(L, -2, "__index");
 
-		lua_pushcfunction(L, taglist_len);
-		lua_setfield(L, -2, "__len");
+		LUA_SetCFunctionField(L, "__len", taglist_len);
 
-		lua_pushcfunction(L, taglist_equal);
-		lua_setfield(L, -2, "__eq");
+		LUA_SetCFunctionField(L, "__eq", taglist_equal);
 #ifdef MUTABLE_TAGS
 	return luaL_ref(L, LUA_REGISTRYINDEX);
 #endif
@@ -428,8 +425,7 @@ int LUA_TagLib(lua_State *L)
 {
 	LUA_CreateAndSetUserdataField(L, LUA_GLOBALSINDEX, "tags", NULL, NULL, lib_numTags, true);
 		lua_createtable(L, 0, 1);
-			lua_pushcfunction(L, lib_iterateTags);
-			lua_setfield(L, -2, "iterate");
+			LUA_SetCFunctionField(L, "iterate", lib_iterateTags);
 		lua_setfield(L, -2, "__index");
 	lua_pop(L, 2);
 
diff --git a/src/lua_thinkerlib.c b/src/lua_thinkerlib.c
index cff92f34d..f1be8c789 100644
--- a/src/lua_thinkerlib.c
+++ b/src/lua_thinkerlib.c
@@ -127,8 +127,7 @@ static int lib_startIterate(lua_State *L)
 int LUA_ThinkerLib(lua_State *L)
 {
 	luaL_newmetatable(L, META_ITERATIONSTATE);
-	lua_pushcfunction(L, iterationState_gc);
-	lua_setfield(L, -2, "__gc");
+	LUA_SetCFunctionField(L, "__gc", iterationState_gc);
 	lua_pop(L, 1);
 
 	lua_createtable(L, 0, 1);

From c0146744b7af018a73c29c7df9d57c523841da89 Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Sat, 28 Oct 2023 14:46:10 +0200
Subject: [PATCH 18/23] Normalise 2.3-related TODO comments

---
 src/deh_lua.c       |  6 ++++--
 src/g_game.c        |  2 +-
 src/g_input.c       |  2 +-
 src/lua_maplib.c    | 12 +-----------
 src/lua_playerlib.c |  2 +-
 src/p_mobj.c        |  6 +++---
 src/p_saveg.h       |  2 +-
 7 files changed, 12 insertions(+), 20 deletions(-)

diff --git a/src/deh_lua.c b/src/deh_lua.c
index 7188d25e3..e596e166f 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -297,7 +297,8 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 			CacheAndPushConstant(L, word, (lua_Integer)PF_FULLSTASIS);
 			return 1;
 		}
-		else if (fastcmp(p, "USEDOWN")) // Remove case when 2.3 nears release...
+		// TODO: 2.3: Delete this alias
+		else if (fastcmp(p, "USEDOWN"))
 		{
 			CacheAndPushConstant(L, word, (lua_Integer)PF_SPINDOWN);
 			return 1;
@@ -583,7 +584,8 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 		return 0;
 	}
 
-	if (fastcmp(word, "BT_USE")) // Remove case when 2.3 nears release...
+	// TODO: 2.3: Delete this alias
+	if (fastcmp(word, "BT_USE"))
 	{
 		CacheAndPushConstant(L, word, (lua_Integer)BT_SPIN);
 		return 1;
diff --git a/src/g_game.c b/src/g_game.c
index 962c2fd14..438428b90 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -4347,7 +4347,7 @@ void G_LoadGameSettings(void)
 }
 
 #define GAMEDATA_ID 0x86E4A27C // Change every major version, as usual
-#define COMPAT_GAMEDATA_ID 0xFCAFE211 // Can be removed entirely for 2.3
+#define COMPAT_GAMEDATA_ID 0xFCAFE211 // TODO: 2.3: Delete
 
 // G_LoadGameData
 // Loads the main data file, which stores information such as emblems found, etc.
diff --git a/src/g_input.c b/src/g_input.c
index 97c0505a3..8b5056331 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -1002,7 +1002,7 @@ static void setcontrol(INT32 (*gc)[2])
 	INT32 player = ((void*)gc == (void*)&gamecontrolbis ? 1 : 0);
 	boolean nestedoverride = false;
 
-	// Update me for 2.3
+	// TODO: 2.3: Delete the "use" alias
 	namectrl = (stricmp(COM_Argv(1), "use")) ? COM_Argv(1) : "spin";
 
 	for (numctrl = 0; numctrl < NUM_GAMECONTROLS && stricmp(namectrl, gamecontrolname[numctrl]);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index efa2edf20..588a6b4e6 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -1043,17 +1043,7 @@ static int line_get(lua_State *L)
 		lua_pushinteger(L, line->special);
 		return 1;
 	case line_tag:
-		// HELLO
-		// THIS IS LJ SONIC
-		// HOW IS YOUR DAY?
-		// BY THE WAY WHEN 2.3 OR 3.0 OR 4.0 OR SRB3 OR SRB4 OR WHATEVER IS OUT
-		// YOU SHOULD REMEMBER TO CHANGE THIS SO IT ALWAYS RETURNS A UNSIGNED VALUE
-		// HAVE A NICE DAY
-		//
-		//
-		//
-		//
-		// you are ugly
+		// TODO: 2.3: Always return a unsigned value
 		lua_pushinteger(L, Tag_FGet(&line->tags));
 		return 1;
 	case line_taglist:
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index ac829d0c4..66d9c009b 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -407,7 +407,7 @@ static int player_get(lua_State *L)
 	case player_realmo:
 		LUA_PushUserdata(L, plr->mo, META_MOBJ);
 		break;
-	// Kept for backward-compatibility
+	// TODO: 2.3: Kept for backward-compatibility
 	// Should be fixed to work like "realmo" later
 	case player_mo:
 		if (plr->spectator)
diff --git a/src/p_mobj.c b/src/p_mobj.c
index a81845918..1ec46d528 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -10571,14 +10571,14 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 		case MT_EXPLOSIONRING:
 		case MT_SCATTERRING:
 		case MT_GRENADERING:
-		
+
 		case MT_BOUNCEPICKUP:
 		case MT_RAILPICKUP:
 		case MT_AUTOPICKUP:
 		case MT_EXPLODEPICKUP:
 		case MT_SCATTERPICKUP:
 		case MT_GRENADEPICKUP:
-		
+
 		case MT_REDRING:
 		case MT_THROWNBOUNCE:
 		case MT_THROWNINFINITY:
@@ -13319,7 +13319,7 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 	return true;
 }
 
-// Pre-UDMF backwards compatibility stuff. Remove for 2.3
+// TODO: 2.3: Delete (Pre-UDMF backwards compatibility stuff)
 static void P_SetAmbush(mapthing_t *mthing, mobj_t *mobj)
 {
 	if (mobj->type == MT_NIGHTSBUMPER
diff --git a/src/p_saveg.h b/src/p_saveg.h
index 73fcfd583..545008e7e 100644
--- a/src/p_saveg.h
+++ b/src/p_saveg.h
@@ -18,7 +18,7 @@
 #pragma interface
 #endif
 
-#define NEWSKINSAVES (INT16_MAX) // Purely for backwards compatibility, remove this for 2.3
+#define NEWSKINSAVES (INT16_MAX) // TODO: 2.3: Delete (Purely for backwards compatibility)
 
 // Persistent storage/archiving.
 // These are the load / save game routines.

From 8608ded317c46e435dcfa2fb1936bfd963e1ec18 Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Sat, 28 Oct 2023 15:19:35 +0200
Subject: [PATCH 19/23] Add deprecation TODOs

---
 src/deh_soc.c        |  2 ++
 src/lua_baselib.c    |  2 ++
 src/lua_consolelib.c |  1 +
 src/lua_infolib.c    |  3 +++
 src/lua_maplib.c     |  2 ++
 src/lua_mathlib.c    |  1 +
 src/p_setup.c        |  9 +++++++--
 src/p_spec.c         |  4 +++-
 src/p_user.c         | 13 +++++++------
 9 files changed, 28 insertions(+), 9 deletions(-)

diff --git a/src/deh_soc.c b/src/deh_soc.c
index 59eb0a9bd..6162034de 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -911,6 +911,7 @@ static void readspriteframe(MYFILE *f, spriteinfo_t *sprinfo, UINT8 frame)
 				sprinfo->pivot[frame].x = value;
 			else if (fastcmp(word, "YPIVOT"))
 				sprinfo->pivot[frame].y = value;
+			// TODO: 2.3: Delete
 			else if (fastcmp(word, "ROTAXIS"))
 				deh_warning("SpriteInfo: ROTAXIS is deprecated and will be removed.");
 			else
@@ -1617,6 +1618,7 @@ void readlevelheader(MYFILE *f, INT32 num)
 						sizeof(mapheaderinfo[num-1]->musname), va("Level header %d: music", num));
 				}
 			}
+			// TODO: 2.3: Delete
 			else if (fastcmp(word, "MUSICSLOT"))
 				deh_warning("Level header %d: MusicSlot parameter is deprecated and will be removed.\nUse \"Music\" instead.", num);
 			else if (fastcmp(word, "MUSICTRACK"))
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index f7ed763a9..c5ed4ddb6 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1788,6 +1788,7 @@ static int lib_pMove(lua_State *L)
 	return 2;
 }
 
+// TODO: 2.3: Delete
 static int lib_pTeleportMove(lua_State *L)
 {
 	mobj_t *ptmthing = tmthing;
@@ -2266,6 +2267,7 @@ static int lib_pMobjTouchingSectorSpecial(lua_State *L)
 	return 1;
 }
 
+// TODO: 2.3: Delete
 static int lib_pThingOnSpecial3DFloor(lua_State *L)
 {
 	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index b34105004..c281b23b3 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -194,6 +194,7 @@ static int lib_comAddCommand(lua_State *L)
 	if (lua_gettop(L) >= 3)
 	{ // For the third argument, only take a boolean or a number.
 		lua_settop(L, 3);
+		// TODO: 2.3: Remove boolean option
 		if (lua_type(L, 3) == LUA_TBOOLEAN)
 		{
 			CONS_Alert(CONS_WARNING,
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 020eba152..ed78811ce 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -318,6 +318,7 @@ static int PopPivotSubTable(spriteframepivot_t *pivot, lua_State *L, int stk, in
 					pivot[idx].x = (INT32)value;
 				else if (ikey == 2 || (key && fastcmp(key, "y")))
 					pivot[idx].y = (INT32)value;
+				// TODO: 2.3: Delete
 				else if (ikey == 3 || (key && fastcmp(key, "rotaxis")))
 					LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
 				else if (ikey == -1 && (key != NULL))
@@ -571,6 +572,7 @@ static int framepivot_get(lua_State *L)
 		lua_pushinteger(L, framepivot->x);
 	else if (fastcmp("y", field))
 		lua_pushinteger(L, framepivot->y);
+	// TODO: 2.3: Delete
 	else if (fastcmp("rotaxis", field))
 	{
 		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.");
@@ -600,6 +602,7 @@ static int framepivot_set(lua_State *L)
 		framepivot->x = luaL_checkinteger(L, 3);
 	else if (fastcmp("y", field))
 		framepivot->y = luaL_checkinteger(L, 3);
+	// TODO: 2.3: delete
 	else if (fastcmp("rotaxis", field))
 		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
 	else
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 588a6b4e6..df06e9721 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -1098,6 +1098,7 @@ static int line_get(lua_State *L)
 	case line_polyobj:
 		LUA_PushUserdata(L, line->polyobj, META_POLYOBJ);
 		return 1;
+	// TODO: 2.3: Delete
 	case line_text:
 		{
 			if (udmf)
@@ -1231,6 +1232,7 @@ static int side_get(lua_State *L)
 	case side_repeatcnt:
 		lua_pushinteger(L, side->repeatcnt);
 		return 1;
+	// TODO: 2.3: Delete
 	case side_text:
 		{
 			if (udmf)
diff --git a/src/lua_mathlib.c b/src/lua_mathlib.c
index d0fe6863f..1bc6019de 100644
--- a/src/lua_mathlib.c
+++ b/src/lua_mathlib.c
@@ -125,6 +125,7 @@ static int lib_fixeddiv(lua_State *L)
 	return 1;
 }
 
+// TODO: 2.3: Delete
 static int lib_fixedrem(lua_State *L)
 {
 	LUA_Deprecated(L, "FixedRem(a, b)", "a % b");
diff --git a/src/p_setup.c b/src/p_setup.c
index 0390761b6..7f6fcd36c 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -1679,7 +1679,7 @@ static void ParseTextmapSectorParameter(UINT32 i, const char *param, const char
 			if ((id = strchr(id, ' ')))
 				id++;
 		}
-	}	
+	}
 	else if (fastcmp(param, "xpanningfloor"))
 		sectors[i].floorxoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "ypanningfloor"))
@@ -6132,6 +6132,7 @@ static void P_ConvertBinarySectorTypes(void)
 			case 14: //Non-ramp sector
 				sectors[i].specialflags |= SSF_NOSTEPDOWN;
 				break;
+			// TODO: 2.3: Delete
 			case 15: //Bouncy FOF
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated bouncy FOF sector type detected. Please use linedef type 76 instead.\n"));
 				break;
@@ -6166,12 +6167,14 @@ static void P_ConvertBinarySectorTypes(void)
 				sectors[i].flags |= MSF_TRIGGERLINE_PLANE;
 				sectors[i].triggerer = TO_PLAYER;
 				break;
+			// TODO: 2.3: Delete
 			case 6: //Trigger linedef executor (Emerald check)
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated emerald check sector type detected. Please use linedef types 337-339 instead.\n"));
 				sectors[i].triggertag = tag;
 				sectors[i].flags &= ~MSF_TRIGGERLINE_PLANE;
 				sectors[i].triggerer = TO_PLAYEREMERALDS;
 				break;
+			// TODO: 2.3: Delete
 			case 7: //Trigger linedef executor (NiGHTS mare)
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated NiGHTS mare sector type detected. Please use linedef types 340-342 instead.\n"));
 				sectors[i].triggertag = tag;
@@ -6181,9 +6184,11 @@ static void P_ConvertBinarySectorTypes(void)
 			case 8: //Check for linedef executor on FOFs
 				sectors[i].flags |= MSF_TRIGGERLINE_MOBJ;
 				break;
+			// TODO: 2.3: Delete
 			case 10: //Special stage time/spheres requirements
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated sector type for special stage requirements detected. Please use the SpecialStageTime and SpecialStageSpheres level header options instead.\n"));
 				break;
+			// TODO: 2.3: Delete
 			case 11: //Custom global gravity
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated sector type for global gravity detected. Please use the Gravity level header option instead.\n"));
 				break;
@@ -6822,7 +6827,7 @@ static void P_ConvertBinaryThingTypes(void)
 		default:
 			break;
 		}
-		
+
 		// Clear binary thing height hacks, to prevent interfering with UDMF-only flags
 		mapthings[i].options &= 0xF;
 	}
diff --git a/src/p_spec.c b/src/p_spec.c
index 28ecc60f4..aa4ee37cf 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2661,7 +2661,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					titlemapcameraref = altview;
 				else if (!mo->player->awayviewtics || mo->player->awayviewmobj != altview) {
 					P_SetTarget(&mo->player->awayviewmobj, altview);
-					
+
 					if (mo->player == &players[displayplayer])
 						P_ResetCamera(mo->player, &camera); // reset p1 camera on p1 getting an awayviewmobj
 					else if (splitscreen && mo->player == &players[secondarydisplayplayer])
@@ -4178,6 +4178,7 @@ sector_t *P_MobjTouchingSectorSpecial(mobj_t *mo, INT32 section, INT32 number)
 	return NULL;
 }
 
+// TODO: 2.3: Delete
 // Deprecated in favor of P_MobjTouchingSectorSpecial
 // Kept for Lua backwards compatibility only
 sector_t *P_ThingOnSpecial3DFloor(mobj_t *mo)
@@ -6229,6 +6230,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 			sector->flags |= MSF_TRIGGERSPECIAL_TOUCH;
 		}
 
+		// TODO: 2.3: Delete everything below
 		// Process deprecated binary sector specials
 		if (udmf || !sector->special)
 			continue;
diff --git a/src/p_user.c b/src/p_user.c
index 5730d476d..453a10b5b 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2775,6 +2775,7 @@ static void P_CheckBouncySectors(player_t *player)
 			if (!(rover->fofflags & FOF_EXISTS))
 				continue; // FOFs should not be bouncy if they don't even "exist"
 
+			// TODO: 2.3: Delete
 			// Handle deprecated bouncy FOF sector type
 			if (!udmf && GETSECSPECIAL(rover->master->frontsector->special, 1) == 15)
 			{
@@ -11390,7 +11391,7 @@ static void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 	tails->y = player->mo->y + P_ReturnThrustY(tails, tails->angle, FixedMul(backwards, tails->scale));
 	tails->z = player->mo->z + zoffs;
 	P_SetThingPosition(tails);
-	
+
 	if (player->mo->flags2 & MF2_SHADOW)
 		tails->flags2 |= MF2_SHADOW;
 	else
@@ -11606,7 +11607,7 @@ void P_PlayerThink(player_t *player)
 		P_SetTarget(&player->awayviewmobj, NULL); // remove awayviewmobj asap if invalid
 		player->awayviewtics = 1; // reset to one, the below code will immediately set it to zero
 	}
-	
+
 	if (player->awayviewtics && player->awayviewtics != -1)
 	{
 		player->awayviewtics--;
@@ -12278,11 +12279,11 @@ void P_PlayerThink(player_t *player)
 		if (!(player->stronganim))
 			player->stronganim = player->panim;
 		else if (player->panim != player->stronganim)
-			player->powers[pw_strong] = STR_NONE; 
-	}	
+			player->powers[pw_strong] = STR_NONE;
+	}
 	else if (player->stronganim)
 		player->stronganim = 0;
-			
+
 	//pw_super acts as a timer now
 	if (player->powers[pw_super]
 	&& (player->mo->state < &states[S_PLAY_SUPER_TRANS1]
@@ -12389,7 +12390,7 @@ void P_PlayerThink(player_t *player)
 			if (player->jumpfactor < FixedMul(skins[player->skin].jumpfactor, 5*FRACUNIT/4)) // Boost jump height.
 				player->jumpfactor += FRACUNIT/300;
 
-			if ((player->charflags & SF_MACHINE) && (!(player->powers[pw_strong] == STR_METAL))) 
+			if ((player->charflags & SF_MACHINE) && (!(player->powers[pw_strong] == STR_METAL)))
 					player->powers[pw_strong] = STR_METAL;
 		}
 

From 4e446d22d602f8a35dc269834dbddddeaddccaee Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Tue, 31 Oct 2023 04:04:01 -0300
Subject: [PATCH 20/23] Fix pickedchar not being set when it should have been

---
 src/d_main.c | 5 +++--
 src/g_demo.c | 8 ++++++--
 src/m_menu.c | 4 ++++
 3 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index 80907a013..274e4ceb3 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1614,6 +1614,9 @@ void D_SRB2Main(void)
 	if (D_CheckNetGame())
 		autostart = true;
 
+	if (!dedicated)
+		pickedchar = R_SkinAvailable(cv_defaultskin.string);
+
 	// check for a driver that wants intermission stats
 	// start the apropriate game based on parms
 	if (M_CheckParm("-metal"))
@@ -1627,8 +1630,6 @@ void D_SRB2Main(void)
 		autostart = true;
 	}
 
-	pickedchar = R_SkinAvailable(cv_defaultskin.string);
-
 	// user settings come before "+" parameters.
 	if (dedicated)
 		COM_ImmedExecute(va("exec \"%s"PATHSEP"adedserv.cfg\"\n", srb2home));
diff --git a/src/g_demo.c b/src/g_demo.c
index cb168dfd9..632947291 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -1492,8 +1492,12 @@ void G_BeginRecording(void)
 	demo_p += 16;
 
 	// Skin
-	for (i = 0; i < 16 && cv_skin.string[i]; i++)
-		name[i] = cv_skin.string[i];
+	const char *skinname = cv_skin.string;
+	if (pickedchar >= 0 && pickedchar < numskins)
+		skinname = skins[pickedchar].name;
+
+	for (i = 0; i < 16 && skinname[i]; i++)
+		name[i] = skinname[i];
 	for (; i < 16; i++)
 		name[i] = '\0';
 	M_Memcpy(demo_p,name,16);
diff --git a/src/m_menu.c b/src/m_menu.c
index 629f53d24..e2866f32b 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -10372,6 +10372,8 @@ static void M_ChooseNightsAttack(INT32 choice)
 	sprintf(gpath,"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value));
 	snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, skins[cv_chooseskin.value-1].name);
 
+	pickedchar = cv_chooseskin.value-1;
+
 	if (!cv_autorecord.value)
 		remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
 	else
@@ -10401,6 +10403,8 @@ static void M_ChooseTimeAttack(INT32 choice)
 	sprintf(gpath,"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value));
 	snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, skins[cv_chooseskin.value-1].name);
 
+	pickedchar = cv_chooseskin.value-1;
+
 	if (!cv_autorecord.value)
 		remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
 	else

From e9637322f92160e015b50c7fa240c006baf9223f Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Tue, 31 Oct 2023 16:45:47 -0300
Subject: [PATCH 21/23] Simplify fix pickedchar doesn't need to be set in these
 two places, since G_DeferedInitNew already does that.

---
 src/g_demo.c | 5 +----
 src/m_menu.c | 4 ----
 2 files changed, 1 insertion(+), 8 deletions(-)

diff --git a/src/g_demo.c b/src/g_demo.c
index 632947291..7026c3391 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -1492,10 +1492,7 @@ void G_BeginRecording(void)
 	demo_p += 16;
 
 	// Skin
-	const char *skinname = cv_skin.string;
-	if (pickedchar >= 0 && pickedchar < numskins)
-		skinname = skins[pickedchar].name;
-
+	const char *skinname = skins[players[0].skin].name;
 	for (i = 0; i < 16 && skinname[i]; i++)
 		name[i] = skinname[i];
 	for (; i < 16; i++)
diff --git a/src/m_menu.c b/src/m_menu.c
index e2866f32b..629f53d24 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -10372,8 +10372,6 @@ static void M_ChooseNightsAttack(INT32 choice)
 	sprintf(gpath,"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value));
 	snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, skins[cv_chooseskin.value-1].name);
 
-	pickedchar = cv_chooseskin.value-1;
-
 	if (!cv_autorecord.value)
 		remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
 	else
@@ -10403,8 +10401,6 @@ static void M_ChooseTimeAttack(INT32 choice)
 	sprintf(gpath,"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value));
 	snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, skins[cv_chooseskin.value-1].name);
 
-	pickedchar = cv_chooseskin.value-1;
-
 	if (!cv_autorecord.value)
 		remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
 	else

From 7643ffadbc7887eab545e061674ab359e075a5c6 Mon Sep 17 00:00:00 2001
From: Alam Ed Arias <alam@srb2.org>
Date: Wed, 1 Nov 2023 15:13:40 +0000
Subject: [PATCH 22/23] Update .gitlab-ci.yml file

Limit builds to just "Windows x86" and "Debian stable:amd64", all other builds can be started manually
---
 .gitlab-ci.yml | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c61e181a5..ef2304410 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -159,6 +159,9 @@ stages:
 
 Debian testing GCC:
   stage: build
+
+  when: manual
+
   image: debian:testing-slim
 
   allow_failure: true
@@ -198,6 +201,8 @@ Debian testing GCC:
 Windows x86:
   stage: build
 
+  when: on_success
+
   artifacts:
     paths:
       - "bin/"
@@ -228,6 +233,8 @@ Windows x86:
 Debian stable:amd64:
   stage: build
 
+  when: on_success
+
   artifacts:
     paths:
       - "bin/"
@@ -270,6 +277,8 @@ Debian stable:amd64:
 Debian stable:i386:
   stage: build
 
+  when: manual
+
   artifacts:
     paths:
       - "bin/"
@@ -311,6 +320,8 @@ Debian stable:i386:
 Debian stable:arm64:
   stage: build
 
+  when: manual
+
   artifacts:
     paths:
       - "bin/"
@@ -353,6 +364,8 @@ Debian stable:arm64:
 Windows x64:
   stage: build
 
+  when: manual
+
   artifacts:
     paths:
       - "bin/"
@@ -383,6 +396,8 @@ Windows x64:
 Debian stable Clang:
   stage: build
 
+  when: manual
+
   allow_failure: true
 
   artifacts:
@@ -422,6 +437,8 @@ Debian stable Clang:
 Debian stable musl:
   stage: build
 
+  when: manual
+
   allow_failure: true
 
   artifacts:
@@ -459,6 +476,8 @@ Debian stable musl:
 Debian testing Clang:
   extends: Debian stable Clang
 
+  when: manual
+
   image: debian:testing-slim
 
   artifacts:
@@ -473,6 +492,8 @@ Debian testing Clang:
 Debian testing musl:
   extends: Debian stable musl
 
+  when: manual
+
   image: debian:testing-slim
 
   artifacts:

From 55444cf20460c8d38f843446195e43a8ab0b9b68 Mon Sep 17 00:00:00 2001
From: Krabs <75001008+krabsisa@users.noreply.github.com>
Date: Thu, 9 Nov 2023 10:31:03 -0500
Subject: [PATCH 23/23] Update version num (no, .14 isn't releasing yet)

---
 appveyor.yml         | 2 +-
 src/version.h        | 6 +++---
 src/win32/Srb2win.rc | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/appveyor.yml b/appveyor.yml
index 63d801b73..d0d94b982 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.2.13.{branch}-{build}
+version: 2.2.14.{branch}-{build}
 os: MinGW
 
 environment:
diff --git a/src/version.h b/src/version.h
index 3242cad67..8d8f8978e 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,4 +1,4 @@
-#define SRB2VERSION "2.2.13"/* this must be the first line, for cmake !! */
+#define SRB2VERSION "2.2.14"/* this must be the first line, for cmake !! */
 
 // The Modification ID; must be obtained from a Master Server Admin ( https://mb.srb2.org/members/?key=ms_admin ).
 // DO NOT try to set this otherwise, or your modification will be unplayable through the Master Server.
@@ -9,7 +9,7 @@
 // it's only for detection of the version the player is using so the MS can alert them of an update.
 // Only set it higher, not lower, obviously.
 // Note that we use this to help keep internal testing in check; this is why v2.2.0 is not version "1".
-#define MODVERSION 54
+#define MODVERSION 55
 
 // Define this as a prerelease version suffix (pre#, RC#)
-//#define BETAVERSION "pre1"
+#define BETAVERSION "nightly"
diff --git a/src/win32/Srb2win.rc b/src/win32/Srb2win.rc
index b69900746..9ee9b7d3f 100644
--- a/src/win32/Srb2win.rc
+++ b/src/win32/Srb2win.rc
@@ -77,8 +77,8 @@ END
 #include "../doomdef.h" // Needed for version string
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 2,2,13,0
- PRODUCTVERSION 2,2,13,0
+ FILEVERSION 2,2,14,0
+ PRODUCTVERSION 2,2,14,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L