diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index af64bb0bcca7724d125df5224bece1333fd5a246..f61513b3147401bd8ff3bb013c2baa991729e4b8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -122,6 +122,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	lua_blockmaplib.c
 	lua_hudlib.c
 	lua_hudlib_drawlist.c
+	lua_followerlib.c
 	lua_profile.cpp
 	k_kart.c
 	k_respawn.c
diff --git a/src/deh_tables.c b/src/deh_tables.c
index df818c09456301efb412c9a7aebc7fbc2cd73a8c..b8109b385154888b5d91a1a2733749dc4d8380b5 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -24,6 +24,7 @@
 #include "g_state.h" // gamestate_t (for lua)
 #include "r_data.h" // patchalphastyle_t
 #include "k_boss.h" // spottype_t (for lua)
+#include "k_follower.h" // followermode_t (for lua)
 
 #include "deh_tables.h"
 
@@ -5150,6 +5151,10 @@ struct int_const_s const INT_CONST[] = {
 	{"PRECIPFX_THUNDER",PRECIPFX_THUNDER},
 	{"PRECIPFX_LIGHTNING",PRECIPFX_LIGHTNING},
 	{"PRECIPFX_WATERPARTICLES",PRECIPFX_WATERPARTICLES},
+	
+	// followermode_t
+	{"FOLLOWERMODE_FLOAT",FOLLOWERMODE_FLOAT},
+	{"FOLLOWERMODE_GROUND",FOLLOWERMODE_GROUND},
 
 	{NULL,0}
 };
diff --git a/src/lua_followerlib.c b/src/lua_followerlib.c
new file mode 100644
index 0000000000000000000000000000000000000000..f6855b2aa4c7fd71341b2ee43741376952cd9fb6
--- /dev/null
+++ b/src/lua_followerlib.c
@@ -0,0 +1,298 @@
+// DR. ROBOTNIK'S RING RACERS
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Kart Krew.
+// Copyright (C) 2020 by Sonic Team Junior.
+// Copyright (C) 2016 by John "JTE" Muniz.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  lua_followerlib.c
+/// \brief player follower structure library for Lua scripting
+
+#include "doomdef.h"
+#include "fastcmp.h"
+#include "k_follower.h"
+#include "r_skins.h"
+#include "sounds.h"
+
+#include "lua_script.h"
+#include "lua_libs.h"
+
+enum follower {
+	follower_valid = 0,
+	follower_name,
+	follower_icon,
+	follower_category,
+	follower_defaultcolor,
+	follower_mode,
+	follower_scale,
+	follower_bubblescale,
+	follower_atangle,
+	follower_dist,
+	follower_height,
+	follower_zoffs,
+	follower_horzlag,
+	follower_vertlag,
+	follower_anglelag,
+	follower_bobamp,
+	follower_bobspeed,
+	// states
+	follower_idlestate,
+	follower_followstate,
+	follower_hurtstate,
+	follower_winstate,
+	follower_losestate,
+	follower_hitconfirmstate,
+	follower_hitconfirmtime,
+	follower_ringstate,
+	follower_ringtime,
+	//
+	follower_hornsound,
+};
+static const char *const follower_opt[] = {
+	"valid",
+	"name",
+	"icon",
+	"category",
+	"defaultcolor",
+	"mode",
+	"scale",
+	"bubblescale",
+	"atangle",
+	"dist",
+	"height",
+	"zoffs",
+	"horzlag",
+	"vertlag",
+	"anglelag",
+	"bobamp",
+	"bobspeed",
+	// states
+	"idlestate",
+	"followstate",
+	"hurtstate",
+	"winstate",
+	"losestate",
+	"hitconfirmstate",
+	"hitconfirmtime",
+	"ringstate",
+	"ringtime",
+	//
+	"hornsound",
+	NULL
+};
+
+#define UNIMPLEMENTED luaL_error(L, LUA_QL("follower_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", follower_opt[field])
+
+static int follower_get(lua_State *L)
+{
+	follower_t *follower = *((follower_t **)luaL_checkudata(L, 1, META_FOLLOWER));
+	enum follower field = luaL_checkoption(L, 2, NULL, follower_opt);
+
+	// followers are always valid, only added, never removed
+	I_Assert(follower != NULL);
+
+	switch (field)
+	{
+	case follower_valid:
+		lua_pushboolean(L, follower != NULL);
+		break;
+	case follower_name:
+		lua_pushstring(L, follower->name);
+		break;
+	case follower_icon:
+		lua_pushstring(L, follower->icon);
+		break;
+	case follower_category:
+		// This would require me to expose followercategory_t as well
+		// Not doing that for now, so this has no use.
+		return UNIMPLEMENTED;
+	case follower_defaultcolor:
+		lua_pushinteger(L, follower->defaultcolor);
+		break;
+	case follower_mode:
+		lua_pushinteger(L, follower->mode);
+		break;
+	case follower_scale:
+		lua_pushfixed(L, follower->scale);
+		break;
+	case follower_bubblescale:
+		lua_pushfixed(L, follower->bubblescale);
+		break;
+	case follower_atangle:
+		lua_pushangle(L, follower->atangle);
+		break;
+	case follower_dist:
+		lua_pushfixed(L, follower->dist);
+		break;
+	case follower_height:
+		lua_pushfixed(L, follower->height);
+		break;
+	case follower_zoffs:
+		lua_pushfixed(L, follower->zoffs);
+		break;
+	case follower_horzlag:
+		lua_pushfixed(L, follower->horzlag);
+		break;
+	case follower_vertlag:
+		lua_pushfixed(L, follower->vertlag);
+		break;
+	case follower_anglelag:
+		lua_pushfixed(L, follower->anglelag);
+		break;
+	case follower_bobamp:
+		lua_pushfixed(L, follower->bobamp);
+		break;
+	case follower_bobspeed:
+		lua_pushinteger(L, follower->bobspeed);
+		break;
+	case follower_idlestate:
+		lua_pushinteger(L, follower->idlestate);
+		break;
+	case follower_followstate:
+		lua_pushinteger(L, follower->followstate);
+		break;
+	case follower_hurtstate:
+		lua_pushinteger(L, follower->hurtstate);
+		break;
+	case follower_winstate:
+		lua_pushinteger(L, follower->winstate);
+		break;
+	case follower_losestate:
+		lua_pushinteger(L, follower->losestate);
+		break;
+	case follower_hitconfirmstate:
+		lua_pushinteger(L, follower->hitconfirmstate);
+		break;
+	case follower_hitconfirmtime:
+		lua_pushinteger(L, follower->hitconfirmtime);
+		break;
+	case follower_ringstate:
+		lua_pushinteger(L, follower->ringstate);
+		break;
+	case follower_ringtime:
+		lua_pushinteger(L, follower->ringtime);
+		break;
+	case follower_hornsound:
+		lua_pushinteger(L, follower->hornsound);
+		break;
+	}
+	return 1;
+}
+
+static int follower_set(lua_State *L)
+{
+	return luaL_error(L, LUA_QL("follower_t") " struct cannot be edited by Lua.");
+}
+
+static int follower_num(lua_State *L)
+{
+	follower_t *follower = *((follower_t **)luaL_checkudata(L, 1, META_FOLLOWER));
+
+	// skins are always valid, only added, never removed
+	I_Assert(follower != NULL);
+
+	lua_pushinteger(L, follower-followers);
+	return 1;
+}
+
+static int lib_iterateFollowers(lua_State *L)
+{
+	INT32 i;
+
+	if (lua_gettop(L) < 2)
+	{
+		//return luaL_error(L, "Don't call skins.iterate() directly, use it as 'for skin in skins.iterate do <block> end'.");
+		lua_pushcfunction(L, lib_iterateFollowers);
+		return 1;
+	}
+
+	lua_settop(L, 2);
+	lua_remove(L, 1); // state is unused.
+
+	if (!lua_isnil(L, 1))
+		i = (INT32)(*((follower_t **)luaL_checkudata(L, 1, META_FOLLOWER)) - followers) + 1;
+	else
+		i = 0;
+
+	// skins are always valid, only added, never removed
+	if (i < numfollowers)
+	{
+		LUA_PushUserdata(L, &followers[i], META_FOLLOWER);
+		return 1;
+	}
+
+	return 0;
+}
+
+static int lib_getFollower(lua_State *L)
+{
+	const char *field;
+	INT32 i;
+
+	// find skin by number
+	if (lua_type(L, 2) == LUA_TNUMBER)
+	{
+		i = luaL_checkinteger(L, 2);
+		// It's kind of funny how the follower limit is 1023 while skins have 255
+		if (i < 0 || i >= MAXFOLLOWERS)
+			return luaL_error(L, "followers[] index %d out of range (0 - %d)", i, MAXFOLLOWERS-1);
+		if (i >= numfollowers)
+			return 0;
+		LUA_PushUserdata(L, &followers[i], META_FOLLOWER);
+		return 1;
+	}
+
+	field = luaL_checkstring(L, 2);
+
+	// special function iterate
+	if (fastcmp(field,"iterate"))
+	{
+		lua_pushcfunction(L, lib_iterateFollowers);
+		return 1;
+	}
+
+	// find skin by name
+	i = K_FollowerAvailable(field);
+	if (i != -1)
+	{
+		LUA_PushUserdata(L, &followers[i], META_FOLLOWER);
+		return 1;
+	}
+
+	return 0;
+}
+
+static int lib_numFollowers(lua_State *L)
+{
+	lua_pushinteger(L, numfollowers);
+	return 1;
+}
+
+int LUA_FollowerLib(lua_State *L)
+{
+	luaL_newmetatable(L, META_FOLLOWER);
+		lua_pushcfunction(L, follower_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, follower_set);
+		lua_setfield(L, -2, "__newindex");
+
+		lua_pushcfunction(L, follower_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L,1);
+
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_getFollower);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_numFollowers);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "followers");
+
+	return 0;
+}
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 09df94514eba567196187c1cd651732831c616d5..290be410104741582c634a5fc70a606a49fff79a 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -102,6 +102,8 @@ extern lua_State *gL;
 
 #define META_ACTIVATOR "ACTIVATOR_T*"
 
+#define META_FOLLOWER "FOLLOWER_T*"
+
 boolean luaL_checkboolean(lua_State *L, int narg);
 
 int LUA_EnumLib(lua_State *L);
@@ -120,6 +122,7 @@ int LUA_TagLib(lua_State *L);
 int LUA_PolyObjLib(lua_State *L);
 int LUA_BlockmapLib(lua_State *L);
 int LUA_HudLib(lua_State *L);
+int LUA_FollowerLib(lua_State *L);
 
 #ifdef __cplusplus
 } // extern "C"
diff --git a/src/lua_script.c b/src/lua_script.c
index 4f743efa3e6ba54d0b654b28ba3040ffc2582ae8..3a4f64f4a2dce4118ced98fc17fdc45c476546a7 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -60,6 +60,7 @@ static lua_CFunction liblist[] = {
 	LUA_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
+	LUA_FollowerLib, // follower_t, followers[]
 	NULL
 };