diff --git a/src/deh_soc.c b/src/deh_soc.c
index 343beb3012676b93256af35c0bd57ff6ba7e9076..9d571610b6f6847da8179fd06d70d392d3fee72a 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -874,6 +874,7 @@ static void readspriteframe(MYFILE *f, spriteinfo_t *sprinfo, UINT8 frame)
 	char *tmp;
 	INT32 value;
 	char *lastline;
+	boolean available = false;
 
 	do
 	{
@@ -925,9 +926,15 @@ static void readspriteframe(MYFILE *f, spriteinfo_t *sprinfo, UINT8 frame)
 			value = atoi(word2); // used for numerical settings
 
 			if (fastcmp(word, "XPIVOT"))
-				sprinfo->pivot[frame].x = value;
+			{
+				sprinfo->frames[frame].pivot.x = value;
+				available = true;
+			}
 			else if (fastcmp(word, "YPIVOT"))
-				sprinfo->pivot[frame].y = value;
+			{
+				sprinfo->frames[frame].pivot.y = value;
+				available = true;
+			}
 			// TODO: 2.3: Delete
 			else if (fastcmp(word, "ROTAXIS"))
 				deh_warning("SpriteInfo: ROTAXIS is deprecated and will be removed.");
@@ -938,6 +945,10 @@ static void readspriteframe(MYFILE *f, spriteinfo_t *sprinfo, UINT8 frame)
 			}
 		}
 	} while (!myfeof(f)); // finish when the line is empty
+
+	if (available)
+		sprinfo->frames[frame].pivot.available = true;
+
 	Z_Free(s);
 }
 
@@ -955,7 +966,6 @@ void readspriteinfo(MYFILE *f, INT32 num, boolean sprite2)
 
 	// allocate a spriteinfo
 	spriteinfo_t *info = Z_Calloc(sizeof(spriteinfo_t), PU_STATIC, NULL);
-	info->available = true;
 
 	do
 	{
@@ -1074,6 +1084,12 @@ void readspriteinfo(MYFILE *f, INT32 num, boolean sprite2)
 
 				// read sprite frame and store it in the spriteinfo_t struct
 				readspriteframe(f, info, frame);
+				set_bit_array(info->available, frame);
+
+				// TODO: 2.3: Delete
+				info->frames[SPRINFO_DEFAULT_FRAME].pivot.available = true;
+				set_bit_array(info->available, SPRINFO_DEFAULT_FRAME);
+
 				if (sprite2)
 				{
 					INT32 i;
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index ecd1ee55e648019fb883917ca361cc45ba8847b2..9e1528445157dadcf8f56444caeb8065e731b845 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -171,6 +171,8 @@ static const struct {
 	{META_SKINCOLOR,    "skincolor_t"},
 	{META_COLORRAMP,    "skincolor_t.ramp"},
 	{META_SPRITEINFO,   "spriteinfo_t"},
+	{META_SPRITEINFOFRAMELIST,"spriteinfoframe_t[]"},
+	{META_SPRITEINFOFRAME,"spriteinfoframe_t"},
 	{META_PIVOTLIST,    "spriteframepivot_t[]"},
 	{META_FRAMEPIVOT,   "spriteframepivot_t"},
 
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index a65ee23ebc913a62711e9960990c5935662b8dfd..84f787d1e333f7b73792e1ba4de957d6dc683ea8 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1,7 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Kart Krew.
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2024 by Sonic Team Junior.
+// Copyright (C) 2012-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -236,6 +237,38 @@ static int lib_spr2namelen(lua_State *L)
 // SPRITE INFO //
 /////////////////
 
+struct SpriteInfoFrame {
+	spriteinfo_t *sprinfo;
+	UINT16 frame;
+};
+
+static UINT16 GetSpriteInfoFrameIndex(lua_State *L, int idx)
+{
+	if (lua_type(L, idx) == LUA_TSTRING)
+	{
+		const char *field = luaL_checkstring(L, idx);
+
+		if (fastcmp("default", field))
+		{
+			return SPRINFO_DEFAULT_FRAME;
+		}
+		else
+		{
+			UINT8 frame = R_Char2Frame(field[0]);
+			if (frame == 255)
+				return luaL_error(L, "invalid frame %s", field);
+			return (UINT16)frame;
+		}
+	}
+	else
+	{
+		int frameID = luaL_checknumber(L, idx);
+		if (frameID < 0 || frameID >= MAXFRAMENUM)
+			return luaL_error(L, "frame %d out of range (0 - %d)", frameID, MAXFRAMENUM - 1);
+		return (UINT16)frameID;
+	}
+}
+
 // spriteinfo[]
 static int lib_getSpriteInfo(lua_State *L)
 {
@@ -263,95 +296,161 @@ static int lib_getSpriteInfo(lua_State *L)
 #define FIELDERROR(f, e) luaL_error(L, "bad value for " LUA_QL(f) " in table passed to spriteinfo[] (%s)", e);
 #define TYPEERROR(f, t1, t2) FIELDERROR(f, va("%s expected, got %s", lua_typename(L, t1), lua_typename(L, t2)))
 
-static int PopPivotSubTable(spriteframepivot_t *pivot, lua_State *L, int stk, int idx)
+static int PopPivotSubTable(spriteinfoframe_t *frame, lua_State *L, int stk);
+
+static int PopFrameSubTable(spriteinfoframe_t *frame, lua_State *L, int stk)
 {
 	int okcool = 0;
+
 	switch (lua_type(L, stk))
 	{
-		case LUA_TTABLE:
-			lua_pushnil(L);
-			while (lua_next(L, stk))
+	case LUA_TTABLE:
+		lua_pushnil(L);
+		while (lua_next(L, stk))
+		{
+			const char *key = luaL_checkstring(L, stk+1);
+			if (fastcmp(key, "pivot"))
 			{
-				const char *key = NULL;
-				lua_Integer ikey = -1;
-				lua_Integer value = 0;
-				// x or y?
-				switch (lua_type(L, stk+1))
-				{
-					case LUA_TSTRING:
-						key = lua_tostring(L, stk+1);
-						break;
-					case LUA_TNUMBER:
-						ikey = lua_tointeger(L, stk+1);
-						break;
-					default:
-						FIELDERROR("pivot key", va("string or number expected, got %s", luaL_typename(L, stk+1)))
-				}
-				// then get value
-				switch (lua_type(L, stk+2))
-				{
-					case LUA_TNUMBER:
-						value = lua_tonumber(L, stk+2);
-						break;
-					case LUA_TBOOLEAN:
-						value = (UINT8)lua_toboolean(L, stk+2);
-						break;
-					default:
-						TYPEERROR("pivot value", LUA_TNUMBER, lua_type(L, stk+2))
-				}
-				// finally set omg!!!!!!!!!!!!!!!!!!
-				if (ikey == 1 || (key && fastcmp(key, "x")))
-					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))
-					FIELDERROR("pivot key", va("invalid option %s", key));
-				okcool = 1;
-				lua_pop(L, 1);
+				if (PopPivotSubTable(frame, L, stk+2))
+					okcool = 1;
 			}
-			break;
-		default:
-			TYPEERROR("sprite pivot", LUA_TTABLE, lua_type(L, stk))
+			else
+			{
+				FIELDERROR("sprite info frame key", va("invalid option %s", key));
+			}
+
+			okcool = 1;
+
+			lua_pop(L, 1);
+		}
+		break;
+	default:
+		TYPEERROR("sprite info frame", LUA_TTABLE, lua_type(L, stk))
 	}
+
 	return okcool;
 }
 
-static int PopPivotTable(spriteinfo_t *info, lua_State *L, int stk)
+static int PopFrameTable(spriteinfo_t *info, lua_State *L, int stk)
 {
-	// Just in case?
-	if (!lua_istable(L, stk))
-		TYPEERROR("pivot table", LUA_TTABLE, lua_type(L, stk));
-
 	lua_pushnil(L);
-	// stk = 0 has the pivot table
-	// stk = 1 has the frame key
-	// stk = 2 has the frame table
-	// stk = 3 has either a string or a number as key
-	// stk = 4 has the value for the key mentioned above
+
 	while (lua_next(L, stk))
 	{
 		int idx = 0;
-		const char *framestr = NULL;
 		switch (lua_type(L, stk+1))
 		{
+		case LUA_TSTRING:
+		case LUA_TNUMBER:
+			idx = GetSpriteInfoFrameIndex(L, stk+1);
+			break;
+		default:
+			TYPEERROR("sprite info frame", LUA_TNUMBER, lua_type(L, stk+1));
+		}
+
+		// the values in frames[] are also tables
+		if (PopFrameSubTable(&info->frames[idx], L, stk+2))
+			set_bit_array(info->available, idx);
+
+		lua_pop(L, 1);
+	}
+
+	return 0;
+}
+
+static int PopPivotSubTable(spriteinfoframe_t *frame, lua_State *L, int stk)
+{
+	int okcool = 0;
+
+	switch (lua_type(L, stk))
+	{
+	case LUA_TTABLE:
+		lua_pushnil(L);
+		while (lua_next(L, stk))
+		{
+			const char *key = NULL;
+			lua_Integer ikey = -1;
+			lua_Integer value = 0;
+
+			// x or y?
+			switch (lua_type(L, stk+1))
+			{
 			case LUA_TSTRING:
-				framestr = lua_tostring(L, stk+1);
-				idx = R_Char2Frame(framestr[0]);
+				key = lua_tostring(L, stk+1);
 				break;
 			case LUA_TNUMBER:
-				idx = lua_tonumber(L, stk+1);
+				ikey = lua_tointeger(L, stk+1);
 				break;
 			default:
-				TYPEERROR("pivot frame", LUA_TNUMBER, lua_type(L, stk+1));
+				FIELDERROR("pivot key", va("string or number expected, got %s", luaL_typename(L, stk+1)))
+			}
+
+			// then get value
+			switch (lua_type(L, stk+2))
+			{
+			case LUA_TNUMBER:
+				value = lua_tonumber(L, stk+2);
+				break;
+			case LUA_TBOOLEAN:
+				value = (UINT8)lua_toboolean(L, stk+2);
+				break;
+			default:
+				TYPEERROR("pivot value", LUA_TNUMBER, lua_type(L, stk+2))
+			}
+
+			// Set it
+			if (ikey == 1 || (key && fastcmp(key, "x")))
+				frame->pivot.x = (INT32)value;
+			else if (ikey == 2 || (key && fastcmp(key, "y")))
+				frame->pivot.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))
+				FIELDERROR("pivot key", va("invalid option %s", key));
+
+			okcool = 1;
+
+			lua_pop(L, 1);
+		}
+		break;
+	default:
+		TYPEERROR("sprite pivot", LUA_TTABLE, lua_type(L, stk))
+	}
+
+	if (okcool)
+		frame->pivot.available = true;
+
+	return okcool;
+}
+
+static int PopPivotTable(spriteinfo_t *info, lua_State *L, int stk)
+{
+	lua_pushnil(L);
+
+	while (lua_next(L, stk))
+	{
+		int idx = 0;
+		switch (lua_type(L, stk+1))
+		{
+		case LUA_TSTRING:
+		case LUA_TNUMBER:
+			idx = GetSpriteInfoFrameIndex(L, stk+1);
+			break;
+		default:
+			TYPEERROR("pivot frame", LUA_TNUMBER, lua_type(L, stk+1));
 		}
-		if ((idx < 0) || (idx >= MAXFRAMENUM))
-			return luaL_error(L, "pivot frame %d out of range (0 - %d)", idx, MAXFRAMENUM - 1);
+
 		// the values in pivot[] are also tables
-		if (PopPivotSubTable(info->pivot, L, stk+2, idx))
-			info->available = true;
+		if (PopPivotSubTable(&info->frames[idx], L, stk+2))
+		{
+			set_bit_array(info->available, idx);
+
+			// TODO: 2.3: Delete
+			info->frames[SPRINFO_DEFAULT_FRAME].pivot.available = true;
+			set_bit_array(info->available, SPRINFO_DEFAULT_FRAME);
+		}
+
 		lua_pop(L, 1);
 	}
 
@@ -397,6 +496,14 @@ static int lib_setSpriteInfo(lua_State *L)
 			else
 				FIELDERROR("pivot", va("%s expected, got %s", lua_typename(L, LUA_TTABLE), luaL_typename(L, -1)))
 		}
+		else if (str && fastcmp(str, "frames"))
+		{
+			// frames[] is a table
+			if (lua_istable(L, 3))
+				return PopFrameTable(info, L, 3);
+			else
+				FIELDERROR("frames", va("%s expected, got %s", lua_typename(L, LUA_TTABLE), luaL_typename(L, -1)))
+		}
 
 		lua_pop(L, 1);
 	}
@@ -422,17 +529,26 @@ static int spriteinfo_get(lua_State *L)
 	I_Assert(sprinfo != NULL);
 
 	// push spriteframepivot_t userdata
+	// TODO: 2.3: delete
 	if (fastcmp(field, "pivot"))
 	{
-		// bypass LUA_PushUserdata
 		void **userdata = lua_newuserdata(L, sizeof(void *));
-		*userdata = &sprinfo->pivot;
+		*userdata = sprinfo;
 		luaL_getmetatable(L, META_PIVOTLIST);
 		lua_setmetatable(L, -2);
 
 		// stack is left with the userdata on top, as if getting it had originally succeeded.
 		return 1;
 	}
+	else if (fastcmp(field, "frames"))
+	{
+		void **userdata = lua_newuserdata(L, sizeof(void *));
+		*userdata = sprinfo;
+		luaL_getmetatable(L, META_SPRITEINFOFRAMELIST);
+		lua_setmetatable(L, -2);
+
+		return 1;
+	}
 	else
 		return luaL_error(L, LUA_QL("spriteinfo_t") " has no field named " LUA_QS, field);
 
@@ -465,9 +581,28 @@ static int spriteinfo_set(lua_State *L)
 		// pivot[] is userdata
 		else if (lua_isuserdata(L, 1))
 		{
-			spriteframepivot_t *pivot = *((spriteframepivot_t **)luaL_checkudata(L, 1, META_PIVOTLIST));
-			memcpy(&sprinfo->pivot, pivot, sizeof(spriteframepivot_t));
-			sprinfo->available = true; // Just in case?
+			spriteinfo_t *copyinfo = *((spriteinfo_t **)luaL_checkudata(L, 1, META_PIVOTLIST));
+			for (UINT16 i = 0; i <= MAXFRAMENUM; i++)
+			{
+				if (in_bit_array(copyinfo->available, i))
+					memcpy(&sprinfo->frames[i].pivot, &copyinfo->frames[i].pivot, sizeof(spriteframepivot_t));
+			}
+		}
+	}
+	else if (fastcmp(field, "frames"))
+	{
+		// frames[] is a table
+		if (lua_istable(L, 1))
+			return PopFrameTable(sprinfo, L, 1);
+		// frames[] is userdata
+		else if (lua_isuserdata(L, 1))
+		{
+			spriteinfo_t *copyinfo = *((spriteinfo_t **)luaL_checkudata(L, 1, META_SPRITEINFOFRAMELIST));
+			for (UINT16 i = 0; i <= MAXFRAMENUM; i++)
+			{
+				if (in_bit_array(copyinfo->available, i))
+					memcpy(&sprinfo->frames[i], &copyinfo->frames[i], sizeof(spriteinfoframe_t));
+			}
 		}
 	}
 	else
@@ -487,21 +622,138 @@ static int spriteinfo_num(lua_State *L)
 	return 1;
 }
 
-// framepivot_t
-static int pivotlist_get(lua_State *L)
+// spriteinfoframe_t
+static int framelist_get(lua_State *L)
+{
+	struct SpriteInfoFrame *container;
+	spriteinfo_t *sprinfo = *((spriteinfo_t **)luaL_checkudata(L, 1, META_SPRITEINFOFRAMELIST));
+	UINT16 frame = GetSpriteInfoFrameIndex(L, 2);
+
+	container = lua_newuserdata(L, sizeof *container);
+	container->sprinfo = sprinfo;
+	container->frame = frame;
+	luaL_getmetatable(L, META_SPRITEINFOFRAME);
+	lua_setmetatable(L, -2);
+
+	// stack is left with the userdata on top, as if getting it had originally succeeded.
+	return 1;
+}
+
+static int framelist_set(lua_State *L)
+{
+	spriteinfo_t *sprinfo = *((spriteinfo_t **)luaL_checkudata(L, 1, META_SPRITEINFOFRAMELIST));
+	UINT16 frame;
+	int okcool = 0;
+
+	if (!lua_lumploading)
+		return luaL_error(L, "Do not alter spriteinfoframe_t from within a hook or coroutine!");
+	if (hud_running)
+		return luaL_error(L, "Do not alter spriteinfoframe_t in HUD rendering code!");
+	if (hook_cmd_running)
+		return luaL_error(L, "Do not alter spriteinfoframe_t in CMD building code!");
+
+	frame = GetSpriteInfoFrameIndex(L, 2);
+
+	// frames[] is a table
+	if (lua_istable(L, 3))
+		okcool = PopFrameSubTable(&sprinfo->frames[frame], L, 3);
+	// frames[] is userdata
+	else if (lua_isuserdata(L, 3))
+	{
+		struct SpriteInfoFrame *container = luaL_checkudata(L, 3, META_SPRITEINFOFRAME);
+		memcpy(&sprinfo->frames[frame],
+			&container->sprinfo->frames[container->frame],
+			sizeof(spriteinfoframe_t));
+		okcool = 1;
+	}
+
+	if (okcool)
+		set_bit_array(sprinfo->available, frame);
+
+	return 0;
+}
+
+static int framelist_num(lua_State *L)
+{
+	lua_pushinteger(L, MAXFRAMENUM);
+	return 1;
+}
+
+// spriteinfoframe_t
+static int sprinfoframe_get(lua_State *L)
 {
-	void **userdata;
-	spriteframepivot_t *framepivot = *((spriteframepivot_t **)luaL_checkudata(L, 1, META_PIVOTLIST));
+	struct SpriteInfoFrame *container = luaL_checkudata(L, 1, META_SPRITEINFOFRAME);
 	const char *field = luaL_checkstring(L, 2);
-	UINT8 frame;
 
-	frame = R_Char2Frame(field[0]);
-	if (frame == 255)
-		luaL_error(L, "invalid frame %s", field);
+	if (fastcmp("pivot", field))
+	{
+		struct SpriteInfoFrame *other_container = lua_newuserdata(L, sizeof *container);
+		memcpy(other_container, container, sizeof *container);
+		luaL_getmetatable(L, META_FRAMEPIVOT);
+		lua_setmetatable(L, -2);
+	}
+	else
+		return luaL_error(L, "Field %s does not exist in spriteinfoframe_t", field);
+
+	return 1;
+}
+
+static int sprinfoframe_set(lua_State *L)
+{
+	struct SpriteInfoFrame *container = luaL_checkudata(L, 1, META_SPRITEINFOFRAME);
+	spriteinfoframe_t *frame = &container->sprinfo->frames[container->frame];
+	UINT8 *available = container->sprinfo->available;
+	const char *field = luaL_checkstring(L, 2);
+	boolean is_available = false;
+
+	if (!lua_lumploading)
+		return luaL_error(L, "Do not alter spriteinfoframe_t from within a hook or coroutine!");
+	if (hud_running)
+		return luaL_error(L, "Do not alter spriteinfoframe_t in HUD rendering code!");
+	if (hook_cmd_running)
+		return luaL_error(L, "Do not alter spriteinfoframe_t in CMD building code!");
+
+	I_Assert(frame != NULL);
+
+	if (fastcmp("pivot", field))
+	{
+		// pivot[] is a table
+		if (lua_istable(L, 3))
+		{
+			if (PopPivotSubTable(frame, L, 3))
+				is_available = true;
+		}
+		// pivot[] is userdata
+		else if (lua_isuserdata(L, 3))
+		{
+			struct SpriteInfoFrame *other_container = luaL_checkudata(L, 3, META_FRAMEPIVOT);
+			memcpy(&frame->pivot,
+				&other_container->sprinfo->frames[other_container->frame].pivot,
+				sizeof(spriteframepivot_t));
+			is_available = true;
+		}
+	}
+	else
+		return luaL_error(L, "Field %s does not exist in spriteframepivot_t", field);
+
+	if (is_available)
+	{
+		set_bit_array(available, container->frame);
+	}
+
+	return 0;
+}
 
-	// bypass LUA_PushUserdata
-	userdata = lua_newuserdata(L, sizeof(void *));
-	*userdata = &framepivot[frame];
+// framepivot_t
+static int pivotlist_get(lua_State *L)
+{
+	struct SpriteInfoFrame *container;
+	spriteinfo_t *sprinfo = *((spriteinfo_t **)luaL_checkudata(L, 1, META_PIVOTLIST));
+	UINT16 frame = GetSpriteInfoFrameIndex(L, 2);
+
+	container = lua_newuserdata(L, sizeof *container);
+	container->sprinfo = sprinfo;
+	container->frame = frame;
 	luaL_getmetatable(L, META_FRAMEPIVOT);
 	lua_setmetatable(L, -2);
 
@@ -511,11 +763,9 @@ static int pivotlist_get(lua_State *L)
 
 static int pivotlist_set(lua_State *L)
 {
-	// Because I already know it's a spriteframepivot_t anyway
-	spriteframepivot_t *pivotlist = *((spriteframepivot_t **)lua_touserdata(L, 1));
-	//spriteframepivot_t *framepivot = *((spriteframepivot_t **)luaL_checkudata(L, 1, META_FRAMEPIVOT));
-	const char *field = luaL_checkstring(L, 2);
-	UINT8 frame;
+	spriteinfo_t *sprinfo = *((spriteinfo_t **)luaL_checkudata(L, 1, META_PIVOTLIST));
+	UINT16 frame;
+	int okcool = 0;
 
 	if (!lua_lumploading)
 		return luaL_error(L, "Do not alter spriteframepivot_t from within a hook or coroutine!");
@@ -524,20 +774,24 @@ static int pivotlist_set(lua_State *L)
 	if (hook_cmd_running)
 		return luaL_error(L, "Do not alter spriteframepivot_t in CMD building code!");
 
-	frame = R_Char2Frame(field[0]);
-	if (frame == 255)
-		luaL_error(L, "invalid frame %s", field);
+	frame = GetSpriteInfoFrameIndex(L, 2);
 
 	// pivot[] is a table
 	if (lua_istable(L, 3))
-		return PopPivotSubTable(pivotlist, L, 3, frame);
+		okcool = PopPivotSubTable(&sprinfo->frames[frame], L, 3);
 	// pivot[] is userdata
 	else if (lua_isuserdata(L, 3))
 	{
-		spriteframepivot_t *copypivot = *((spriteframepivot_t **)luaL_checkudata(L, 3, META_FRAMEPIVOT));
-		memcpy(&pivotlist[frame], copypivot, sizeof(spriteframepivot_t));
+		struct SpriteInfoFrame *container = luaL_checkudata(L, 3, META_FRAMEPIVOT);
+		memcpy(&sprinfo->frames[frame].pivot,
+			&container->sprinfo->frames[container->frame].pivot,
+			sizeof(spriteframepivot_t));
+		okcool = 1;
 	}
 
+	if (okcool)
+		set_bit_array(sprinfo->available, frame);
+
 	return 0;
 }
 
@@ -549,7 +803,8 @@ static int pivotlist_num(lua_State *L)
 
 static int framepivot_get(lua_State *L)
 {
-	spriteframepivot_t *framepivot = *((spriteframepivot_t **)luaL_checkudata(L, 1, META_FRAMEPIVOT));
+	struct SpriteInfoFrame *container = luaL_checkudata(L, 1, META_FRAMEPIVOT);
+	spriteframepivot_t *framepivot = &container->sprinfo->frames[container->frame].pivot;
 	const char *field = luaL_checkstring(L, 2);
 
 	I_Assert(framepivot != NULL);
@@ -572,7 +827,9 @@ static int framepivot_get(lua_State *L)
 
 static int framepivot_set(lua_State *L)
 {
-	spriteframepivot_t *framepivot = *((spriteframepivot_t **)luaL_checkudata(L, 1, META_FRAMEPIVOT));
+	struct SpriteInfoFrame *container = luaL_checkudata(L, 1, META_FRAMEPIVOT);
+	spriteframepivot_t *framepivot = &container->sprinfo->frames[container->frame].pivot;
+	UINT8 *available = container->sprinfo->available;
 	const char *field = luaL_checkstring(L, 2);
 
 	if (!lua_lumploading)
@@ -585,9 +842,15 @@ static int framepivot_set(lua_State *L)
 	I_Assert(framepivot != NULL);
 
 	if (fastcmp("x", field))
+	{
 		framepivot->x = luaL_checkinteger(L, 3);
+		set_bit_array(available, container->frame);
+	}
 	else if (fastcmp("y", field))
+	{
 		framepivot->y = luaL_checkinteger(L, 3);
+		set_bit_array(available, container->frame);
+	}
 	// TODO: 2.3: delete
 	else if (fastcmp("rotaxis", field))
 		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
@@ -1909,6 +2172,8 @@ int LUA_InfoLib(lua_State *L)
 	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_SPRITEINFOFRAMELIST, framelist_get, framelist_set, framelist_num);
+	LUA_RegisterUserdataMetatable(L, META_SPRITEINFOFRAME, sprinfoframe_get, sprinfoframe_set, NULL);
 	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);
diff --git a/src/lua_libs.h b/src/lua_libs.h
index e1585f488a8a6205b1ee967dcfeffac125680fee..b6e61456a8bb5efd2ef7579ad862f2eb0eaf83df 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -29,6 +29,8 @@ extern boolean ignoregameinputs;
 #define META_SKINCOLOR "SKINCOLOR_T*"
 #define META_COLORRAMP "SKINCOLOR_T*RAMP"
 #define META_SPRITEINFO "SPRITEINFO_T*"
+#define META_SPRITEINFOFRAMELIST "SPRITEINFOFRAME_T[]"
+#define META_SPRITEINFOFRAME "SPRITEINFOFRAME_T*"
 #define META_PIVOTLIST "SPRITEFRAMEPIVOT_T[]"
 #define META_FRAMEPIVOT "SPRITEFRAMEPIVOT_T*"
 
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index 989665d74179854fcfac552814132671c3d00ed2..8df10848243d6e8eb676a6f85c0880fa51ccd25b 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -63,6 +63,16 @@ patch_t *Patch_GetRotated(patch_t *patch, INT32 angle, boolean flip)
 	return rotsprite->patches[angle];
 }
 
+static spriteframepivot_t *GetSpriteInfoRotationPivot(spriteinfo_t *info, UINT16 frame)
+{
+	if (R_IsSpriteInfoAvailable(info, frame) && info->frames[frame].pivot.available)
+	{
+		return &info->frames[frame].pivot;
+	}
+
+	return NULL;
+}
+
 patch_t *Patch_GetRotatedSprite(
 	spriteframe_t *sprite,
 	size_t frame, size_t spriteangle,
@@ -90,16 +100,21 @@ patch_t *Patch_GetRotatedSprite(
 		patch_t *patch;
 		INT32 xpivot = 0, ypivot = 0;
 		lumpnum_t lump = sprite->lumppat[spriteangle];
+		spriteframepivot_t *pivot;
 
 		if (lump == LUMPERROR)
 			return NULL;
 
 		patch = W_CachePatchNum(lump, PU_SPRITE);
 
-		if (sprinfo->available)
+		pivot = GetSpriteInfoRotationPivot(sprinfo, frame);
+		if (pivot == NULL)
+			pivot = GetSpriteInfoRotationPivot(sprinfo, SPRINFO_DEFAULT_FRAME);
+
+		if (pivot != NULL)
 		{
-			xpivot = sprinfo->pivot[frame].x;
-			ypivot = sprinfo->pivot[frame].y;
+			xpivot = pivot->x;
+			ypivot = pivot->y;
 		}
 		else
 		{
diff --git a/src/r_picformats.c b/src/r_picformats.c
index a45c143b01698de2abbb48f534f543a3a68d845e..94452e20e8a2e660cf228142e4afe945cb40ac77 100644
--- a/src/r_picformats.c
+++ b/src/r_picformats.c
@@ -2,8 +2,9 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 2005-2009 by Andrey "entryway" Budko.
-// Copyright (C) 2018-2024 by Lactozilla.
-// Copyright (C) 2019-2024 by Sonic Team Junior.
+// Copyright (C) 2024 by Kart Krew.
+// Copyright (C) 2018-2025 by Lactozilla.
+// Copyright (C) 2019-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1479,36 +1480,184 @@ boolean Picture_PNGDimensions(UINT8 *png, INT32 *width, INT32 *height, INT16 *to
 #endif
 #endif
 
-//
-// R_ParseSpriteInfoFrame
-//
-// Parse a SPRTINFO frame.
-//
-static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
+struct ParseSpriteInfoState {
+	boolean spr2;
+	spriteinfo_t *info;
+	spritenum_t sprnum;
+	playersprite_t spr2num;
+	boolean any;
+	INT32 skinnumbers[MAXSKINS];
+	INT32 foundskins;
+};
+
+#define PARSER_FRAME (false)
+#define PARSER_DEFAULT (true)
+
+static void R_ParseSpriteInfoSkin(struct ParseSpriteInfoState *parser)
 {
 	char *sprinfoToken;
 	size_t sprinfoTokenLength;
-	char *frameChar = NULL;
-	UINT8 frameFrame = 0xFF;
-	INT16 frameXPivot = 0;
-	INT16 frameYPivot = 0;
 
-	// Sprite identifier
+	INT32 skinnum;
+	char *skinName = NULL;
+
+	// Skin name
 	sprinfoToken = M_GetToken(NULL);
 	if (sprinfoToken == NULL)
 	{
-		I_Error("Error parsing SPRTINFO lump: Unexpected end of file where sprite frame should be");
+		I_Error("Error parsing SPRTINFO lump: Unexpected end of file where skin frame should be");
 	}
-	sprinfoTokenLength = strlen(sprinfoToken);
-	if (sprinfoTokenLength != 1)
+
+	if (strcmp(sprinfoToken, "*")==0) // All skins
 	{
-		I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",sprinfoToken);
+		parser->foundskins = -1;
 	}
 	else
-		frameChar = sprinfoToken;
+	{
+		// copy skin name yada yada
+		sprinfoTokenLength = strlen(sprinfoToken);
+		skinName = (char *)Z_Malloc((sprinfoTokenLength+1)*sizeof(char),PU_STATIC,NULL);
+		M_Memcpy(skinName,sprinfoToken,sprinfoTokenLength*sizeof(char));
+		skinName[sprinfoTokenLength] = '\0';
+		strlwr(skinName);
+
+		skinnum = R_SkinAvailable(skinName);
+		if (skinnum == -1)
+			I_Error("Error parsing SPRTINFO lump: Unknown skin \"%s\"", skinName);
+
+		parser->skinnumbers[parser->foundskins] = skinnum;
+		parser->foundskins++;
+	}
 
-	frameFrame = R_Char2Frame(frameChar[0]);
 	Z_Free(sprinfoToken);
+}
+
+static void copy_to_skin (struct ParseSpriteInfoState *parser, INT32 skinnum)
+{
+	skin_t *skin = skins[skinnum];
+	spriteinfo_t *sprinfo = skin->sprinfo;
+
+	if (parser->any)
+	{
+		playersprite_t spr2num;
+
+		for (spr2num = 0; spr2num < NUMPLAYERSPRITES; ++spr2num)
+		{
+			M_Memcpy(&sprinfo[spr2num], parser->info, sizeof(spriteinfo_t));
+		}
+	}
+	else
+	{
+		M_Memcpy(&sprinfo[parser->spr2num], parser->info, sizeof(spriteinfo_t));
+	}
+}
+
+struct ParsedSpriteInfoFrame {
+	INT32 pivotX;
+	INT32 pivotY;
+};
+
+static boolean define_spriteinfo_frame(struct ParsedSpriteInfoFrame *frame, spriteinfoframe_t *dest)
+{
+	boolean defined = false;
+
+	if (frame->pivotX != INT32_MAX)
+	{
+		dest->pivot.x = frame->pivotX;
+		dest->pivot.available = true;
+		defined = true;
+	}
+	if (frame->pivotY != INT32_MAX)
+	{
+		dest->pivot.y = frame->pivotY;
+		dest->pivot.available = true;
+		defined = true;
+	}
+
+	return defined;
+}
+
+static void R_ParseSpriteInfoFrame(struct ParseSpriteInfoState *parser, boolean all)
+{
+	char *sprinfoToken;
+	UINT16 frameID = 0;
+	UINT16 frameEndID = UINT16_MAX;
+
+	boolean usingDeprecatedPivot = false;
+
+	struct ParsedSpriteInfoFrame frame = {
+		.pivotX = INT32_MAX,
+		.pivotY = INT32_MAX
+	};
+
+	if (all)
+	{
+		frameID = SPRINFO_DEFAULT_FRAME;
+	}
+	else
+	{
+		// Sprite identifier
+		char *frameToken = NULL;
+		char *startRange = NULL;
+		char *endRange = NULL;
+		sprinfoToken = M_GetToken(NULL);
+		if (sprinfoToken == NULL)
+		{
+			I_Error("Error parsing SPRTINFO lump: Unexpected end of file where sprite frame should be");
+		}
+
+		// Parse range
+		frameToken = Z_StrDup(sprinfoToken);
+		startRange = frameToken;
+		endRange = strstr(frameToken, "..");
+		if (endRange != NULL)
+		{
+			*endRange = '\0';
+			endRange += 2;
+			if (strstr(endRange, "."))
+				I_Error("Error parsing SPRTINFO lump: Invalid range \"%s\"",sprinfoToken);
+		}
+
+		int parseStartFrameID = -1;
+		if (!M_StringToNumber(startRange, &parseStartFrameID))
+		{
+			if (strlen(startRange) != 1)
+				I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",startRange);
+			parseStartFrameID = R_Char2Frame(startRange[0]);
+			if (parseStartFrameID == 255)
+				I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",startRange);
+		}
+		if (parseStartFrameID < 0 || parseStartFrameID >= MAXFRAMENUM)
+			I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",startRange);
+		frameID = (UINT16)parseStartFrameID;
+
+		// Parse range ID
+		if (endRange != NULL)
+		{
+			int parseEndFrameID = -1;
+			if (!M_StringToNumber(endRange, &parseEndFrameID))
+			{
+				if (strlen(endRange) != 1)
+					I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",endRange);
+				parseEndFrameID = R_Char2Frame(endRange[0]);
+				if (parseEndFrameID == 255)
+					I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",endRange);
+			}
+			if (parseEndFrameID < 0 || parseEndFrameID >= MAXFRAMENUM)
+				I_Error("Error parsing SPRTINFO lump: Invalid frame \"%s\"",endRange);
+			frameEndID = (UINT16)parseEndFrameID;
+		}
+
+		Z_Free(frameToken);
+
+		// Validate the range
+		if (frameEndID != UINT16_MAX && frameID >= frameEndID)
+		{
+			I_Error("Error parsing SPRTINFO lump: Invalid range \"%s\"",sprinfoToken);
+		}
+
+		Z_Free(sprinfoToken);
+	}
 
 	// Left Curly Brace
 	sprinfoToken = M_GetToken(NULL);
@@ -1526,18 +1675,35 @@ static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
 			}
 			while (strcmp(sprinfoToken,"}")!=0)
 			{
-				if (stricmp(sprinfoToken, "XPIVOT")==0)
+				if (stricmp(sprinfoToken, "ROTATIONXPIVOT")==0)
+				{
+					Z_Free(sprinfoToken);
+					sprinfoToken = M_GetToken(NULL);
+					frame.pivotX = atoi(sprinfoToken);
+				}
+				else if (stricmp(sprinfoToken, "ROTATIONYPIVOT")==0)
+				{
+					Z_Free(sprinfoToken);
+					sprinfoToken = M_GetToken(NULL);
+					frame.pivotY = atoi(sprinfoToken);
+				}
+				// TODO: 2.3: Delete
+				else if (stricmp(sprinfoToken, "XPIVOT")==0)
 				{
 					Z_Free(sprinfoToken);
 					sprinfoToken = M_GetToken(NULL);
-					frameXPivot = atoi(sprinfoToken);
+					frame.pivotX = atoi(sprinfoToken);
+					usingDeprecatedPivot = true;
 				}
+				// TODO: 2.3: Delete
 				else if (stricmp(sprinfoToken, "YPIVOT")==0)
 				{
 					Z_Free(sprinfoToken);
 					sprinfoToken = M_GetToken(NULL);
-					frameYPivot = atoi(sprinfoToken);
+					frame.pivotY = atoi(sprinfoToken);
+					usingDeprecatedPivot = true;
 				}
+				// TODO: 2.3: Delete
 				else if (stricmp(sprinfoToken, "ROTAXIS")==0)
 				{
 					Z_Free(sprinfoToken);
@@ -1555,9 +1721,70 @@ static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
 		Z_Free(sprinfoToken);
 	}
 
-	// set fields
-	info->pivot[frameFrame].x = frameXPivot;
-	info->pivot[frameFrame].y = frameYPivot;
+	// Apply to the specified range of frames
+	if (frameEndID != UINT16_MAX)
+	{
+		for (UINT16 frameIter = frameID; frameIter <= frameEndID; frameIter++)
+		{
+			if (define_spriteinfo_frame(&frame, &parser->info->frames[frameIter]))
+			{
+				set_bit_array(parser->info->available, frameIter);
+			}
+		}
+	}
+	else
+	{
+		if (define_spriteinfo_frame(&frame, &parser->info->frames[frameID]))
+		{
+			set_bit_array(parser->info->available, frameID);
+		}
+	}
+
+	// TODO: 2.3: Delete
+	if (usingDeprecatedPivot)
+	{
+		parser->info->frames[SPRINFO_DEFAULT_FRAME].pivot.available = true;
+		set_bit_array(parser->info->available, SPRINFO_DEFAULT_FRAME);
+	}
+
+	if (parser->spr2)
+	{
+		INT32 i;
+
+		if (!parser->foundskins)
+			I_Error("Error parsing SPRTINFO lump: No skins specified in this sprite2 definition");
+
+		if (parser->foundskins < 0)
+		{
+			for (i = 0; i < numskins; i++)
+			{
+				copy_to_skin(parser, i);
+			}
+		}
+		else
+		{
+			for (i = 0; i < parser->foundskins; i++)
+			{
+				copy_to_skin(parser, parser->skinnumbers[i]);
+			}
+		}
+	}
+	else
+	{
+		if (parser->any)
+		{
+			spritenum_t sprnum;
+
+			for (sprnum = 0; sprnum < NUMSPRITES; ++sprnum)
+			{
+				M_Memcpy(&spriteinfo[sprnum], parser->info, sizeof(spriteinfo_t));
+			}
+		}
+		else
+		{
+			M_Memcpy(&spriteinfo[parser->sprnum], parser->info, sizeof(spriteinfo_t));
+		}
+	}
 }
 
 //
@@ -1567,15 +1794,19 @@ static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
 //
 static void R_ParseSpriteInfo(boolean spr2)
 {
-	spriteinfo_t *info;
 	char *sprinfoToken;
 	size_t sprinfoTokenLength;
 	char newSpriteName[MAXSPRITENAME + 1]; // no longer dynamically allocated
-	spritenum_t sprnum = NUMSPRITES;
-	playersprite_t spr2num = NUMPLAYERSPRITES;
+
+	struct ParseSpriteInfoState parser = {
+		.spr2 = spr2,
+		.sprnum = NUMSPRITES,
+		.spr2num = NUMPLAYERSPRITES,
+		.any = false,
+		.foundskins = 0,
+	};
+
 	INT32 i;
-	UINT8 *skinnumbers = NULL;
-	INT32 foundskins = 0;
 
 	// Sprite name
 	sprinfoToken = M_GetToken(NULL);
@@ -1583,17 +1814,28 @@ static void R_ParseSpriteInfo(boolean spr2)
 	{
 		I_Error("Error parsing SPRTINFO lump: Unexpected end of file where sprite name should be");
 	}
-	sprinfoTokenLength = strlen(sprinfoToken);
-	if (sprinfoTokenLength > MAXSPRITENAME)
-		I_Error("Error parsing SPRTINFO lump: Sprite name \"%s\" is longer than %d characters", sprinfoToken, MAXSPRITENAME);
-	strcpy(newSpriteName, sprinfoToken);
-	strupr(newSpriteName); // Just do this now so we don't have to worry about it
+
+	if (!strcmp(sprinfoToken, "*")) // All sprites
+	{
+		parser.any = true;
+	}
+	else
+	{
+		sprinfoTokenLength = strlen(sprinfoToken);
+		if (sprinfoTokenLength > MAXSPRITENAME)
+			I_Error("Error parsing SPRTINFO lump: Sprite name \"%s\" is longer than %d characters", sprinfoToken, MAXSPRITENAME);
+		strcpy(newSpriteName, sprinfoToken);
+		strupr(newSpriteName); // Just do this now so we don't have to worry about it
+	}
+
 	Z_Free(sprinfoToken);
 
-	if (!spr2)
+	if (parser.any)
+		;
+	else if (!spr2)
 	{
-		sprnum = R_GetSpriteNumByName(newSpriteName);
-		if (sprnum == NUMSPRITES)
+		parser.sprnum = R_GetSpriteNumByName(newSpriteName);
+		if (parser.sprnum == NUMSPRITES)
 			I_Error("Error parsing SPRTINFO lump: Unknown sprite name \"%s\"", newSpriteName);
 	}
 	else
@@ -1604,15 +1846,14 @@ static void R_ParseSpriteInfo(boolean spr2)
 				I_Error("Error parsing SPRTINFO lump: Unknown sprite2 name \"%s\"", newSpriteName);
 			if (!memcmp(newSpriteName,spr2names[i],4))
 			{
-				spr2num = i;
+				parser.spr2num = i;
 				break;
 			}
 		}
 	}
 
 	// allocate a spriteinfo
-	info = Z_Calloc(sizeof(spriteinfo_t), PU_STATIC, NULL);
-	info->available = true;
+	parser.info = Z_Calloc(sizeof(spriteinfo_t), PU_STATIC, NULL);
 
 	// Left Curly Brace
 	sprinfoToken = M_GetToken(NULL);
@@ -1632,54 +1873,22 @@ static void R_ParseSpriteInfo(boolean spr2)
 		{
 			if (stricmp(sprinfoToken, "SKIN")==0)
 			{
-				INT32 skinnum;
-				char *skinName = NULL;
 				if (!spr2)
 					I_Error("Error parsing SPRTINFO lump: \"SKIN\" token found outside of a sprite2 definition");
 
 				Z_Free(sprinfoToken);
 
-				// Skin name
-				sprinfoToken = M_GetToken(NULL);
-				if (sprinfoToken == NULL)
-				{
-					I_Error("Error parsing SPRTINFO lump: Unexpected end of file where skin frame should be");
-				}
-
-				// copy skin name yada yada
-				sprinfoTokenLength = strlen(sprinfoToken);
-				skinName = (char *)Z_Malloc((sprinfoTokenLength+1)*sizeof(char),PU_STATIC,NULL);
-				M_Memcpy(skinName,sprinfoToken,sprinfoTokenLength*sizeof(char));
-				skinName[sprinfoTokenLength] = '\0';
-				strlwr(skinName);
-				Z_Free(sprinfoToken);
-
-				skinnum = R_SkinAvailable(skinName);
-				if (skinnum == -1)
-					I_Error("Error parsing SPRTINFO lump: Unknown skin \"%s\"", skinName);
-
-				if (skinnumbers == NULL)
-					skinnumbers = Z_Malloc(sizeof(UINT8) * numskins, PU_STATIC, NULL);
-				skinnumbers[foundskins] = (UINT8)skinnum;
-				foundskins++;
+				R_ParseSpriteInfoSkin(&parser);
 			}
 			else if (stricmp(sprinfoToken, "FRAME")==0)
 			{
-				R_ParseSpriteInfoFrame(info);
 				Z_Free(sprinfoToken);
-				if (spr2)
-				{
-					if (!foundskins)
-						I_Error("Error parsing SPRTINFO lump: No skins specified in this sprite2 definition");
-					for (i = 0; i < foundskins; i++)
-					{
-						skin_t *skin = skins[skinnumbers[i]];
-						spriteinfo_t *sprinfo = skin->sprinfo;
-						M_Memcpy(&sprinfo[spr2num], info, sizeof(spriteinfo_t));
-					}
-				}
-				else
-					M_Memcpy(&spriteinfo[sprnum], info, sizeof(spriteinfo_t));
+				R_ParseSpriteInfoFrame(&parser, PARSER_FRAME);
+			}
+			else if (stricmp(sprinfoToken, "DEFAULT")==0)
+			{
+				Z_Free(sprinfoToken);
+				R_ParseSpriteInfoFrame(&parser, PARSER_DEFAULT);
 			}
 			else
 			{
@@ -1699,9 +1908,7 @@ static void R_ParseSpriteInfo(boolean spr2)
 	}
 
 	Z_Free(sprinfoToken);
-	Z_Free(info);
-	if (skinnumbers)
-		Z_Free(skinnumbers);
+	Z_Free(parser.info);
 }
 
 //
@@ -1767,3 +1974,8 @@ void R_LoadSpriteInfoLumps(UINT16 wadnum, UINT16 numlumps)
 			R_ParseSPRTINFOLump(wadnum, i);
 	}
 }
+
+boolean R_IsSpriteInfoAvailable(spriteinfo_t *info, UINT16 frame)
+{
+	return info && frame <= SPRINFO_DEFAULT_FRAME && in_bit_array(info->available, frame);
+}
diff --git a/src/r_picformats.h b/src/r_picformats.h
index 123dda976c204787505fb1a90bad859fe39b836b..9c6d788b22d8ed68422168f2d8f988374a0ae65b 100644
--- a/src/r_picformats.h
+++ b/src/r_picformats.h
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
-// Copyright (C) 2018-2024 by Lactozilla.
-// Copyright (C) 2019-2024 by Sonic Team Junior.
+// Copyright (C) 2018-2025 by Lactozilla.
+// Copyright (C) 2019-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -96,12 +96,20 @@ typedef enum
 typedef struct
 {
 	INT32 x, y;
+	boolean available;
 } spriteframepivot_t;
 
 typedef struct
 {
-	spriteframepivot_t pivot[MAXFRAMENUM];
-	boolean available;
+	spriteframepivot_t pivot;
+} spriteinfoframe_t;
+
+#define SPRINFO_DEFAULT_FRAME (MAXFRAMENUM)
+
+typedef struct
+{
+	UINT8 available[BIT_ARRAY_SIZE(MAXFRAMENUM + 1)]; // 1 extra for default_frame
+	spriteinfoframe_t frames[MAXFRAMENUM + 1];
 } spriteinfo_t;
 
 // PNG support
@@ -126,4 +134,6 @@ extern spriteinfo_t spriteinfo[NUMSPRITES];
 void R_LoadSpriteInfoLumps(UINT16 wadnum, UINT16 numlumps);
 void R_ParseSPRTINFOLump(UINT16 wadNum, UINT16 lumpNum);
 
+boolean R_IsSpriteInfoAvailable(spriteinfo_t *info, UINT16 frame);
+
 #endif // __R_PICFORMATS__