diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e8c9c31820c3ba1fcf46691cff5f4476df98220c..bc4a6114ecf1187a9cd191cc21d77eec40c6e5f0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -120,6 +120,7 @@ set(SRB2_CORE_RENDER_SOURCES
 	r_main.c
 	r_plane.c
 	r_segs.c
+	r_skins.c
 	r_sky.c
 	r_splats.c
 	r_things.c
@@ -134,6 +135,7 @@ set(SRB2_CORE_RENDER_SOURCES
 	r_main.h
 	r_plane.h
 	r_segs.h
+	r_skins.h
 	r_sky.h
 	r_splats.h
 	r_state.h
diff --git a/src/Makefile b/src/Makefile
index 9f368217cc14a453138af6ac1f354434c888f4ef..701f2cfdac19dd68c92e8801ec49579a7832d897 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -468,6 +468,7 @@ OBJS:=$(i_main_o) \
 		$(OBJDIR)/r_main.o   \
 		$(OBJDIR)/r_plane.o  \
 		$(OBJDIR)/r_segs.o   \
+		$(OBJDIR)/r_skins.o  \
 		$(OBJDIR)/r_sky.o    \
 		$(OBJDIR)/r_splats.o \
 		$(OBJDIR)/r_things.o \
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index c25929929a2e46fe840253dcc965b48f973d4a4e..a55e702875ade5d54066f5cb987193854dc34dc1 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -22,7 +22,7 @@
 #include "g_input.h"
 #include "m_menu.h"
 #include "r_local.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "p_local.h"
 #include "p_setup.h"
 #include "s_sound.h"
diff --git a/src/dehacked.c b/src/dehacked.c
index dea0289b9d720604d03658665f7cd6742b45b2b0..e6466f792cbfa13a85363e720ecd5c078240b195 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -31,6 +31,7 @@
 #include "r_data.h"
 #include "r_draw.h"
 #include "r_patch.h"
+#include "r_things.h" // R_Char2Frame
 #include "r_sky.h"
 #include "fastcmp.h"
 #include "lua_script.h"
diff --git a/src/f_wipe.c b/src/f_wipe.c
index 8d12262efb2d6377395ac06c7c6d37d2f6ed48a5..0fd6c1431c099877710e3e39ec74282cf7fb4b0e 100644
--- a/src/f_wipe.c
+++ b/src/f_wipe.c
@@ -16,6 +16,7 @@
 #include "i_video.h"
 #include "v_video.h"
 
+#include "r_state.h" // fadecolormap
 #include "r_draw.h" // transtable
 #include "p_pspr.h" // tr_transxxx
 #include "p_local.h"
diff --git a/src/g_game.c b/src/g_game.c
index 08192bfb8b82247a0f3c391fb97a143a32f5eb2b..1bf76f33fc8e06d1270d730dd9e8a69c791c78dd 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -38,7 +38,7 @@
 #include "byteptr.h"
 #include "i_joy.h"
 #include "r_local.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "y_inter.h"
 #include "v_video.h"
 #include "dehacked.h" // get_number (for ghost thok)
diff --git a/src/hardware/hw3sound.c b/src/hardware/hw3sound.c
index f7c6e1da025e1db33471bd8775bc6a8ff4c826a3..7858640c018ac1c15609e7b0387c182efaf868cb 100644
--- a/src/hardware/hw3sound.c
+++ b/src/hardware/hw3sound.c
@@ -28,7 +28,7 @@
 #include "../tables.h"
 #include "../sounds.h"
 #include "../r_main.h"
-#include "../r_things.h"
+#include "../r_skins.h"
 #include "../m_random.h"
 #include "../p_local.h"
 #include "hw3dsdrv.h"
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 03f14244614e73c2d9e35085f637424c41ed2d84..d001408be805b952c18cf94482fc52ee73393850 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -21,7 +21,7 @@
 #include "z_zone.h"
 #include "r_main.h"
 #include "r_draw.h"
-#include "r_things.h"
+#include "r_things.h" // R_Frame2Char etc
 #include "m_random.h"
 #include "s_sound.h"
 #include "g_game.h"
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index efed9adb7b7e61b2a230ceef184991e5de0d7051..92080e8eda16674593f9ea0f745c5370ce9b2e15 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -15,7 +15,7 @@
 #include "doomstat.h"
 #include "p_mobj.h"
 #include "g_game.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "b_bot.h"
 #include "z_zone.h"
 
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index a7bd8da940adb8d574e641b7651f68fb214d99de..9e46344cf5472780cbebda2d73eba51b31406ae3 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -13,7 +13,7 @@
 #include "doomdef.h"
 #ifdef HAVE_BLUA
 #include "fastcmp.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "p_local.h"
 #include "g_game.h"
 #include "p_setup.h"
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 3ade06042efc9f2183c0c3107d9e795f67713a50..f26aed2f0fd82a8bb39d2a56cf36f1c58dc6df01 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -13,7 +13,7 @@
 #include "doomdef.h"
 #ifdef HAVE_BLUA
 #include "fastcmp.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "sounds.h"
 
 #include "lua_script.h"
diff --git a/src/m_cond.c b/src/m_cond.c
index 89058a488ae1833167caea247b60bc1391e62c15..0abc7adf88c7ace2907b69ea4c8277a500549b99 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -18,7 +18,7 @@
 #include "v_video.h" // video flags
 
 #include "g_game.h" // record info
-#include "r_things.h" // numskins
+#include "r_skins.h" // numskins
 #include "r_draw.h" // R_GetColorByName
 
 // Map triggers for linedef executors
diff --git a/src/m_menu.h b/src/m_menu.h
index 18b681ff034c5e4d949c0d0ae16fbe4b8ad58f64..e7270380d503a84c1beb8cfe6e3f4fd69fc5bdb1 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -15,9 +15,10 @@
 #ifndef __X_MENU__
 #define __X_MENU__
 
+#include "doomstat.h" // for NUMGAMETYPES
 #include "d_event.h"
 #include "command.h"
-#include "r_things.h" // for SKINNAMESIZE
+#include "r_skins.h" // for SKINNAMESIZE
 #include "f_finale.h" // for ttmode_enum
 
 //
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 5d067ebc3c24f0af29b4bda036ea14c103a99b18..14e5c4d3e5050a00e21ff6e7453ca76824a99e8f 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -21,7 +21,7 @@
 #include "s_sound.h"
 #include "m_random.h"
 #include "m_misc.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "i_video.h"
 #include "z_zone.h"
 #include "lua_hook.h"
diff --git a/src/p_mobj.c b/src/p_mobj.c
index faee245d3b9e2f63e39eeedda617d20134c7ddd5..9194cf9f7e2dfbbf2b14e8e32f7f66348cfb2bb9 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -19,7 +19,7 @@
 #include "p_local.h"
 #include "p_setup.h"
 #include "r_main.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "r_sky.h"
 #include "r_splats.h"
 #include "s_sound.h"
diff --git a/src/p_saveg.c b/src/p_saveg.c
index fc8d9d4ee82b9a9b21ccc5316ea6d1521c5a1415..e6c7d07fdf58da4d264b578fcc03ce056cb1b5a0 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -22,7 +22,7 @@
 #include "p_setup.h"
 #include "p_saveg.h"
 #include "r_data.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "r_state.h"
 #include "w_wad.h"
 #include "y_inter.h"
diff --git a/src/p_setup.c b/src/p_setup.c
index 7b4c6773b07a0bef06905839fbfc62317744881a..857bb1d43a70c21e163c8092a4feeca06c49ce62 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -27,7 +27,7 @@
 #include "i_system.h"
 
 #include "r_data.h"
-#include "r_things.h"
+#include "r_things.h" // for R_AddSpriteDefs
 #include "r_patch.h"
 #include "r_sky.h"
 #include "r_draw.h"
diff --git a/src/p_spec.c b/src/p_spec.c
index d9bbab246ea4b8327666e0695f14776aa2ba86b8..aecdb3b98f75583a2bb7492442085645f4517720 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -36,7 +36,7 @@
 #include "m_cond.h" //unlock triggers
 #include "lua_hook.h" // LUAh_LinedefExecute
 #include "f_finale.h" // control text prompt
-#include "r_things.h" // skins
+#include "r_skins.h" // skins
 
 #ifdef HW3SOUND
 #include "hardware/hw3sound.h"
diff --git a/src/p_user.c b/src/p_user.c
index 9167d5345049324b83d41f813e0d619b1443bc99..c34d37264a9a0b4a7655d32740b6d7bb8a7ea2b5 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -22,7 +22,7 @@
 #include "p_local.h"
 #include "r_main.h"
 #include "s_sound.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "d_think.h"
 #include "r_sky.h"
 #include "p_setup.h"
diff --git a/src/r_skins.c b/src/r_skins.c
new file mode 100644
index 0000000000000000000000000000000000000000..2e9548bd7cd0831469b3169f6618d726996ab4c6
--- /dev/null
+++ b/src/r_skins.c
@@ -0,0 +1,825 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  r_skins.c
+/// \brief Loading skins
+
+#include "doomdef.h"
+#include "console.h"
+#include "g_game.h"
+#include "r_local.h"
+#include "st_stuff.h"
+#include "w_wad.h"
+#include "z_zone.h"
+#include "m_misc.h"
+#include "info.h" // spr2names
+#include "i_video.h" // rendermode
+#include "i_system.h"
+#include "r_things.h"
+#include "r_skins.h"
+#include "p_local.h"
+#include "dehacked.h" // get_number (for thok)
+#include "m_cond.h"
+#ifdef HWRENDER
+#include "hardware/hw_md2.h"
+#endif
+
+#ifdef PC_DOS
+#include <stdio.h> // for snprintf
+int	snprintf(char *str, size_t n, const char *fmt, ...);
+//int	vsnprintf(char *str, size_t n, const char *fmt, va_list ap);
+#endif
+
+INT32 numskins = 0;
+skin_t skins[MAXSKINS];
+
+// FIXTHIS: don't work because it must be inistilised before the config load
+//#define SKINVALUES
+#ifdef SKINVALUES
+CV_PossibleValue_t skin_cons_t[MAXSKINS+1];
+#endif
+
+//
+// P_GetSkinSprite2
+// For non-super players, tries each sprite2's immediate predecessor until it finds one with a number of frames or ends up at standing.
+// For super players, does the same as above - but tries the super equivalent for each sprite2 before the non-super version.
+//
+
+UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player)
+{
+	UINT8 super = 0, i = 0;
+
+	if (!skin)
+		return 0;
+
+	if ((playersprite_t)(spr2 & ~FF_SPR2SUPER) >= free_spr2)
+		return 0;
+
+	while (!skin->sprites[spr2].numframes
+		&& spr2 != SPR2_STND
+		&& ++i < 32) // recursion limiter
+	{
+		if (spr2 & FF_SPR2SUPER)
+		{
+			super = FF_SPR2SUPER;
+			spr2 &= ~FF_SPR2SUPER;
+			continue;
+		}
+
+		switch(spr2)
+		{
+		// Normal special cases.
+		case SPR2_JUMP:
+			spr2 = ((player
+					? player->charflags
+					: skin->flags)
+					& SF_NOJUMPSPIN) ? SPR2_SPNG : SPR2_ROLL;
+			break;
+		case SPR2_TIRE:
+			spr2 = ((player
+					? player->charability
+					: skin->ability)
+					== CA_SWIM) ? SPR2_SWIM : SPR2_FLY;
+			break;
+		// Use the handy list, that's what it's there for!
+		default:
+			spr2 = spr2defaults[spr2];
+			break;
+		}
+
+		spr2 |= super;
+	}
+
+	if (i >= 32) // probably an infinite loop...
+		return 0;
+
+	return spr2;
+}
+
+static void Sk_SetDefaultValue(skin_t *skin)
+{
+	INT32 i;
+	//
+	// set default skin values
+	//
+	memset(skin, 0, sizeof (skin_t));
+	snprintf(skin->name,
+		sizeof skin->name, "skin %u", (UINT32)(skin-skins));
+	skin->name[sizeof skin->name - 1] = '\0';
+	skin->wadnum = INT16_MAX;
+
+	skin->flags = 0;
+
+	strcpy(skin->realname, "Someone");
+	strcpy(skin->hudname, "???");
+
+	skin->starttranscolor = 96;
+	skin->prefcolor = SKINCOLOR_GREEN;
+	skin->supercolor = SKINCOLOR_SUPERGOLD1;
+	skin->prefoppositecolor = 0; // use tables
+
+	skin->normalspeed = 36<<FRACBITS;
+	skin->runspeed = 28<<FRACBITS;
+	skin->thrustfactor = 5;
+	skin->accelstart = 96;
+	skin->acceleration = 40;
+
+	skin->ability = CA_NONE;
+	skin->ability2 = CA2_SPINDASH;
+	skin->jumpfactor = FRACUNIT;
+	skin->actionspd = 30<<FRACBITS;
+	skin->mindash = 15<<FRACBITS;
+	skin->maxdash = 70<<FRACBITS;
+
+	skin->radius = mobjinfo[MT_PLAYER].radius;
+	skin->height = mobjinfo[MT_PLAYER].height;
+	skin->spinheight = FixedMul(skin->height, 2*FRACUNIT/3);
+
+	skin->shieldscale = FRACUNIT;
+	skin->camerascale = FRACUNIT;
+
+	skin->thokitem = -1;
+	skin->spinitem = -1;
+	skin->revitem = -1;
+	skin->followitem = 0;
+
+	skin->highresscale = FRACUNIT;
+	skin->contspeed = 17;
+	skin->contangle = 0;
+
+	skin->availability = 0;
+
+	for (i = 0; i < sfx_skinsoundslot0; i++)
+		if (S_sfx[i].skinsound != -1)
+			skin->soundsid[S_sfx[i].skinsound] = i;
+}
+
+//
+// Initialize the basic skins
+//
+void R_InitSkins(void)
+{
+#ifdef SKINVALUES
+	INT32 i;
+
+	for (i = 0; i <= MAXSKINS; i++)
+	{
+		skin_cons_t[i].value = 0;
+		skin_cons_t[i].strvalue = NULL;
+	}
+#endif
+
+	// no default skin!
+	numskins = 0;
+}
+
+UINT32 R_GetSkinAvailabilities(void)
+{
+	INT32 s;
+	UINT32 response = 0;
+
+	for (s = 0; s < MAXSKINS; s++)
+	{
+		if (skins[s].availability && unlockables[skins[s].availability - 1].unlocked)
+			response |= (1 << s);
+	}
+	return response;
+}
+
+// returns true if available in circumstances, otherwise nope
+// warning don't use with an invalid skinnum other than -1 which always returns true
+boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
+{
+	return ((skinnum == -1) // Simplifies things elsewhere, since there's already plenty of checks for less-than-0...
+		|| (!skins[skinnum].availability)
+		|| (((netgame || multiplayer) && playernum != -1) ? (players[playernum].availabilities & (1 << skinnum)) : (unlockables[skins[skinnum].availability - 1].unlocked))
+		|| (modeattacking) // If you have someone else's run you might as well take a look
+		|| (Playing() && (R_SkinAvailable(mapheaderinfo[gamemap-1]->forcecharacter) == skinnum)) // Force 1.
+		|| (netgame && (cv_forceskin.value == skinnum)) // Force 2.
+		|| (metalrecording && skinnum == 5) // Force 3.
+		);
+}
+
+// returns true if the skin name is found (loaded from pwad)
+// warning return -1 if not found
+INT32 R_SkinAvailable(const char *name)
+{
+	INT32 i;
+
+	for (i = 0; i < numskins; i++)
+	{
+		// search in the skin list
+		if (stricmp(skins[i].name,name)==0)
+			return i;
+	}
+	return -1;
+}
+
+// network code calls this when a 'skin change' is received
+void SetPlayerSkin(INT32 playernum, const char *skinname)
+{
+	INT32 i = R_SkinAvailable(skinname);
+	player_t *player = &players[playernum];
+
+	if ((i != -1) && R_SkinUsable(playernum, i))
+	{
+		SetPlayerSkinByNum(playernum, i);
+		return;
+	}
+
+	if (P_IsLocalPlayer(player))
+		CONS_Alert(CONS_WARNING, M_GetText("Skin '%s' not found.\n"), skinname);
+	else if(server || IsPlayerAdmin(consoleplayer))
+		CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) skin '%s' not found\n"), playernum, player_names[playernum], skinname);
+
+	SetPlayerSkinByNum(playernum, 0);
+}
+
+// Same as SetPlayerSkin, but uses the skin #.
+// network code calls this when a 'skin change' is received
+void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
+{
+	player_t *player = &players[playernum];
+	skin_t *skin = &skins[skinnum];
+	UINT8 newcolor = 0;
+
+	if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum)) // Make sure it exists!
+	{
+		player->skin = skinnum;
+
+		player->camerascale = skin->camerascale;
+		player->shieldscale = skin->shieldscale;
+
+		player->charability = (UINT8)skin->ability;
+		player->charability2 = (UINT8)skin->ability2;
+
+		player->charflags = (UINT32)skin->flags;
+
+		player->thokitem = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
+		player->spinitem = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
+		player->revitem = skin->revitem < 0 ? (mobjtype_t)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
+		player->followitem = skin->followitem;
+
+		if (((player->powers[pw_shield] & SH_NOSTACK) == SH_PINK) && (player->revitem == MT_LHRT || player->spinitem == MT_LHRT || player->thokitem == MT_LHRT)) // Healers can't keep their buff.
+			player->powers[pw_shield] &= SH_STACK;
+
+		player->actionspd = skin->actionspd;
+		player->mindash = skin->mindash;
+		player->maxdash = skin->maxdash;
+
+		player->normalspeed = skin->normalspeed;
+		player->runspeed = skin->runspeed;
+		player->thrustfactor = skin->thrustfactor;
+		player->accelstart = skin->accelstart;
+		player->acceleration = skin->acceleration;
+
+		player->jumpfactor = skin->jumpfactor;
+
+		player->height = skin->height;
+		player->spinheight = skin->spinheight;
+
+		if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
+		{
+			if (playernum == consoleplayer)
+				CV_StealthSetValue(&cv_playercolor, skin->prefcolor);
+			else if (playernum == secondarydisplayplayer)
+				CV_StealthSetValue(&cv_playercolor2, skin->prefcolor);
+			player->skincolor = newcolor = skin->prefcolor;
+		}
+
+		if (player->followmobj)
+		{
+			P_RemoveMobj(player->followmobj);
+			P_SetTarget(&player->followmobj, NULL);
+		}
+
+		if (player->mo)
+		{
+			fixed_t radius = FixedMul(skin->radius, player->mo->scale);
+			if ((player->powers[pw_carry] == CR_NIGHTSMODE) && (skin->sprites[SPR2_NFLY].numframes == 0)) // If you don't have a sprite for flying horizontally, use the default NiGHTS skin.
+			{
+				skin = &skins[DEFAULTNIGHTSSKIN];
+				player->followitem = skin->followitem;
+				if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
+					newcolor = skin->prefcolor; // will be updated in thinker to flashing
+			}
+			player->mo->skin = skin;
+			if (newcolor)
+				player->mo->color = newcolor;
+			P_SetScale(player->mo, player->mo->scale);
+			player->mo->radius = radius;
+
+			P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames
+		}
+		return;
+	}
+
+	if (P_IsLocalPlayer(player))
+		CONS_Alert(CONS_WARNING, M_GetText("Requested skin %d not found\n"), skinnum);
+	else if(server || IsPlayerAdmin(consoleplayer))
+		CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum);
+	SetPlayerSkinByNum(playernum, 0); // not found put the sonic skin
+}
+
+//
+// Add skins from a pwad, each skin preceded by 'S_SKIN' marker
+//
+
+// Does the same is in w_wad, but check only for
+// the first 6 characters (this is so we can have S_SKIN1, S_SKIN2..
+// for wad editors that don't like multiple resources of the same name)
+//
+static UINT16 W_CheckForSkinMarkerInPwad(UINT16 wadid, UINT16 startlump)
+{
+	UINT16 i;
+	const char *S_SKIN = "S_SKIN";
+	lumpinfo_t *lump_p;
+
+	// scan forward, start at <startlump>
+	if (startlump < wadfiles[wadid]->numlumps)
+	{
+		lump_p = wadfiles[wadid]->lumpinfo + startlump;
+		for (i = startlump; i < wadfiles[wadid]->numlumps; i++, lump_p++)
+			if (memcmp(lump_p->name,S_SKIN,6)==0)
+				return i;
+	}
+	return INT16_MAX; // not found
+}
+
+#define HUDNAMEWRITE(value) STRBUFCPY(skin->hudname, value)
+
+// turn _ into spaces and . into katana dot
+#define SYMBOLCONVERT(name) for (value = name; *value; value++)\
+					{\
+						if (*value == '_') *value = ' ';\
+						else if (*value == '.') *value = '\x1E';\
+					}
+
+//
+// Patch skins from a pwad, each skin preceded by 'P_SKIN' marker
+//
+
+// Does the same is in w_wad, but check only for
+// the first 6 characters (this is so we can have P_SKIN1, P_SKIN2..
+// for wad editors that don't like multiple resources of the same name)
+//
+static UINT16 W_CheckForPatchSkinMarkerInPwad(UINT16 wadid, UINT16 startlump)
+{
+	UINT16 i;
+	const char *P_SKIN = "P_SKIN";
+	lumpinfo_t *lump_p;
+
+	// scan forward, start at <startlump>
+	if (startlump < wadfiles[wadid]->numlumps)
+	{
+		lump_p = wadfiles[wadid]->lumpinfo + startlump;
+		for (i = startlump; i < wadfiles[wadid]->numlumps; i++, lump_p++)
+			if (memcmp(lump_p->name,P_SKIN,6)==0)
+				return i;
+	}
+	return INT16_MAX; // not found
+}
+
+static void R_LoadSkinSprites(UINT16 wadnum, UINT16 *lump, UINT16 *lastlump, skin_t *skin)
+{
+	UINT16 newlastlump;
+	UINT8 sprite2;
+
+	*lump += 1; // start after S_SKIN
+	*lastlump = W_CheckNumForNamePwad("S_END",wadnum,*lump); // stop at S_END
+
+	// old wadding practices die hard -- stop at S_SKIN (or P_SKIN) or S_START if they come before S_END.
+	newlastlump = W_CheckForSkinMarkerInPwad(wadnum,*lump);
+	if (newlastlump < *lastlump) *lastlump = newlastlump;
+	newlastlump = W_CheckForPatchSkinMarkerInPwad(wadnum,*lump);
+	if (newlastlump < *lastlump) *lastlump = newlastlump;
+	newlastlump = W_CheckNumForNamePwad("S_START",wadnum,*lump);
+	if (newlastlump < *lastlump) *lastlump = newlastlump;
+
+	// ...and let's handle super, too
+	newlastlump = W_CheckNumForNamePwad("S_SUPER",wadnum,*lump);
+	if (newlastlump < *lastlump)
+	{
+		newlastlump++;
+		// load all sprite sets we are aware of... for super!
+		for (sprite2 = 0; sprite2 < free_spr2; sprite2++)
+			R_AddSingleSpriteDef(spr2names[sprite2], &skin->sprites[FF_SPR2SUPER|sprite2], wadnum, newlastlump, *lastlump);
+
+		newlastlump--;
+		*lastlump = newlastlump; // okay, make the normal sprite set loading end there
+	}
+
+	// load all sprite sets we are aware of... for normal stuff.
+	for (sprite2 = 0; sprite2 < free_spr2; sprite2++)
+		R_AddSingleSpriteDef(spr2names[sprite2], &skin->sprites[sprite2], wadnum, *lump, *lastlump);
+
+	if (skin->sprites[0].numframes == 0)
+		I_Error("R_LoadSkinSprites: no frames found for sprite SPR2_%s\n", spr2names[0]);
+}
+
+// returns whether found appropriate property
+static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
+{
+	// custom translation table
+	if (!stricmp(stoken, "startcolor"))
+		skin->starttranscolor = atoi(value);
+
+#define FULLPROCESS(field) else if (!stricmp(stoken, #field)) skin->field = get_number(value);
+	// character type identification
+	FULLPROCESS(flags)
+	FULLPROCESS(ability)
+	FULLPROCESS(ability2)
+
+	FULLPROCESS(thokitem)
+	FULLPROCESS(spinitem)
+	FULLPROCESS(revitem)
+	FULLPROCESS(followitem)
+#undef FULLPROCESS
+
+#define GETFRACBITS(field) else if (!stricmp(stoken, #field)) skin->field = atoi(value)<<FRACBITS;
+	GETFRACBITS(normalspeed)
+	GETFRACBITS(runspeed)
+
+	GETFRACBITS(mindash)
+	GETFRACBITS(maxdash)
+	GETFRACBITS(actionspd)
+
+	GETFRACBITS(radius)
+	GETFRACBITS(height)
+	GETFRACBITS(spinheight)
+#undef GETFRACBITS
+
+#define GETINT(field) else if (!stricmp(stoken, #field)) skin->field = atoi(value);
+	GETINT(thrustfactor)
+	GETINT(accelstart)
+	GETINT(acceleration)
+	GETINT(contspeed)
+	GETINT(contangle)
+#undef GETINT
+
+#define GETSKINCOLOR(field) else if (!stricmp(stoken, #field)) skin->field = R_GetColorByName(value);
+	GETSKINCOLOR(prefcolor)
+	GETSKINCOLOR(prefoppositecolor)
+#undef GETSKINCOLOR
+	else if (!stricmp(stoken, "supercolor"))
+		skin->supercolor = R_GetSuperColorByName(value);
+
+#define GETFLOAT(field) else if (!stricmp(stoken, #field)) skin->field = FLOAT_TO_FIXED(atof(value));
+	GETFLOAT(jumpfactor)
+	GETFLOAT(highresscale)
+	GETFLOAT(shieldscale)
+	GETFLOAT(camerascale)
+#undef GETFLOAT
+
+#define GETFLAG(field) else if (!stricmp(stoken, #field)) { \
+	strupr(value); \
+	if (atoi(value) || value[0] == 'T' || value[0] == 'Y') \
+		skin->flags |= (SF_##field); \
+	else \
+		skin->flags &= ~(SF_##field); \
+}
+	// parameters for individual character flags
+	// these are uppercase so they can be concatenated with SF_
+	// 1, true, yes are all valid values
+	GETFLAG(SUPER)
+	GETFLAG(NOSUPERSPIN)
+	GETFLAG(NOSPINDASHDUST)
+	GETFLAG(HIRES)
+	GETFLAG(NOSKID)
+	GETFLAG(NOSPEEDADJUST)
+	GETFLAG(RUNONWATER)
+	GETFLAG(NOJUMPSPIN)
+	GETFLAG(NOJUMPDAMAGE)
+	GETFLAG(STOMPDAMAGE)
+	GETFLAG(MARIODAMAGE)
+	GETFLAG(MACHINE)
+	GETFLAG(DASHMODE)
+	GETFLAG(FASTEDGE)
+	GETFLAG(MULTIABILITY)
+	GETFLAG(NONIGHTSROTATION)
+#undef GETFLAG
+
+	else // let's check if it's a sound, otherwise error out
+	{
+		boolean found = false;
+		sfxenum_t i;
+		size_t stokenadjust;
+
+		// Remove the prefix. (We need to affect an adjusting variable so that we can print error messages if it's not actually a sound.)
+		if ((stoken[0] == 'D' || stoken[0] == 'd') && (stoken[1] == 'S' || stoken[1] == 's')) // DS*
+			stokenadjust = 2;
+		else // sfx_*
+			stokenadjust = 4;
+
+		// Remove the prefix. (We can affect this directly since we're not going to use it again.)
+		if ((value[0] == 'D' || value[0] == 'd') && (value[1] == 'S' || value[1] == 's')) // DS*
+			value += 2;
+		else // sfx_*
+			value += 4;
+
+		// copy name of sounds that are remapped
+		// for this skin
+		for (i = 0; i < sfx_skinsoundslot0; i++)
+		{
+			if (!S_sfx[i].name)
+				continue;
+			if (S_sfx[i].skinsound != -1
+				&& !stricmp(S_sfx[i].name,
+					stoken + stokenadjust))
+			{
+				skin->soundsid[S_sfx[i].skinsound] =
+					S_AddSoundFx(value, S_sfx[i].singularity, S_sfx[i].pitch, true);
+				found = true;
+			}
+		}
+		return found;
+	}
+	return true;
+}
+
+//
+// Find skin sprites, sounds & optional status bar face, & add them
+//
+void R_AddSkins(UINT16 wadnum)
+{
+	UINT16 lump, lastlump = 0;
+	char *buf;
+	char *buf2;
+	char *stoken;
+	char *value;
+	size_t size;
+	skin_t *skin;
+	boolean hudname, realname;
+
+	//
+	// search for all skin markers in pwad
+	//
+
+	while ((lump = W_CheckForSkinMarkerInPwad(wadnum, lastlump)) != INT16_MAX)
+	{
+		// advance by default
+		lastlump = lump + 1;
+
+		if (numskins >= MAXSKINS)
+		{
+			CONS_Debug(DBG_RENDER, "ignored skin (%d skins maximum)\n", MAXSKINS);
+			continue; // so we know how many skins couldn't be added
+		}
+		buf = W_CacheLumpNumPwad(wadnum, lump, PU_CACHE);
+		size = W_LumpLengthPwad(wadnum, lump);
+
+		// for strtok
+		buf2 = malloc(size+1);
+		if (!buf2)
+			I_Error("R_AddSkins: No more free memory\n");
+		M_Memcpy(buf2,buf,size);
+		buf2[size] = '\0';
+
+		// set defaults
+		skin = &skins[numskins];
+		Sk_SetDefaultValue(skin);
+		skin->wadnum = wadnum;
+		hudname = realname = false;
+		// parse
+		stoken = strtok (buf2, "\r\n= ");
+		while (stoken)
+		{
+			if ((stoken[0] == '/' && stoken[1] == '/')
+				|| (stoken[0] == '#'))// skip comments
+			{
+				stoken = strtok(NULL, "\r\n"); // skip end of line
+				goto next_token;              // find the real next token
+			}
+
+			value = strtok(NULL, "\r\n= ");
+
+			if (!value)
+				I_Error("R_AddSkins: syntax error in S_SKIN lump# %d(%s) in WAD %s\n", lump, W_CheckNameForNumPwad(wadnum,lump), wadfiles[wadnum]->filename);
+
+			// Some of these can't go in R_ProcessPatchableFields because they have side effects for future lines.
+			// Others can't go in there because we don't want them to be patchable.
+			if (!stricmp(stoken, "name"))
+			{
+				INT32 skinnum = R_SkinAvailable(value);
+				strlwr(value);
+				if (skinnum == -1)
+					STRBUFCPY(skin->name, value);
+				// the skin name must uniquely identify a single skin
+				// if the name is already used I make the name 'namex'
+				// using the default skin name's number set above
+				else
+				{
+					const size_t stringspace =
+						strlen(value) + sizeof (numskins) + 1;
+					char *value2 = Z_Malloc(stringspace, PU_STATIC, NULL);
+					snprintf(value2, stringspace,
+						"%s%d", value, numskins);
+					value2[stringspace - 1] = '\0';
+					if (R_SkinAvailable(value2) == -1)
+						// I'm lazy so if NEW name is already used I leave the 'skin x'
+						// default skin name set in Sk_SetDefaultValue
+						STRBUFCPY(skin->name, value2);
+					Z_Free(value2);
+				}
+
+				// copy to hudname and fullname as a default.
+				if (!realname)
+				{
+					STRBUFCPY(skin->realname, skin->name);
+					for (value = skin->realname; *value; value++)
+					{
+						if (*value == '_') *value = ' '; // turn _ into spaces.
+						else if (*value == '.') *value = '\x1E'; // turn . into katana dot.
+					}
+				}
+				if (!hudname)
+				{
+					HUDNAMEWRITE(skin->name);
+					strupr(skin->hudname);
+					SYMBOLCONVERT(skin->hudname)
+				}
+			}
+			else if (!stricmp(stoken, "realname"))
+			{ // Display name (eg. "Knuckles")
+				realname = true;
+				STRBUFCPY(skin->realname, value);
+				SYMBOLCONVERT(skin->realname)
+				if (!hudname)
+					HUDNAMEWRITE(skin->realname);
+			}
+			else if (!stricmp(stoken, "hudname"))
+			{ // Life icon name (eg. "K.T.E")
+				hudname = true;
+				HUDNAMEWRITE(value);
+				SYMBOLCONVERT(skin->hudname)
+				if (!realname)
+					STRBUFCPY(skin->realname, skin->hudname);
+			}
+			else if (!stricmp(stoken, "availability"))
+			{
+				skin->availability = atoi(value);
+				if (skin->availability >= MAXUNLOCKABLES)
+					skin->availability = 0;
+			}
+			else if (!R_ProcessPatchableFields(skin, stoken, value))
+				CONS_Debug(DBG_SETUP, "R_AddSkins: Unknown keyword '%s' in S_SKIN lump #%d (WAD %s)\n", stoken, lump, wadfiles[wadnum]->filename);
+
+next_token:
+			stoken = strtok(NULL, "\r\n= ");
+		}
+		free(buf2);
+
+		// Add sprites
+		R_LoadSkinSprites(wadnum, &lump, &lastlump, skin);
+		//ST_LoadFaceGraphics(numskins); -- nah let's do this elsewhere
+
+		R_FlushTranslationColormapCache();
+
+		if (!skin->availability) // Safe to print...
+			CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name);
+#ifdef SKINVALUES
+		skin_cons_t[numskins].value = numskins;
+		skin_cons_t[numskins].strvalue = skin->name;
+#endif
+
+#ifdef HWRENDER
+		if (rendermode == render_opengl)
+			HWR_AddPlayerModel(numskins);
+#endif
+
+		numskins++;
+	}
+	return;
+}
+
+//
+// Patch skin sprites
+//
+void R_PatchSkins(UINT16 wadnum)
+{
+	UINT16 lump, lastlump = 0;
+	char *buf;
+	char *buf2;
+	char *stoken;
+	char *value;
+	size_t size;
+	skin_t *skin;
+	boolean noskincomplain, realname, hudname;
+
+	//
+	// search for all skin patch markers in pwad
+	//
+
+	while ((lump = W_CheckForPatchSkinMarkerInPwad(wadnum, lastlump)) != INT16_MAX)
+	{
+		INT32 skinnum = 0;
+
+		// advance by default
+		lastlump = lump + 1;
+
+		buf = W_CacheLumpNumPwad(wadnum, lump, PU_CACHE);
+		size = W_LumpLengthPwad(wadnum, lump);
+
+		// for strtok
+		buf2 = malloc(size+1);
+		if (!buf2)
+			I_Error("R_PatchSkins: No more free memory\n");
+		M_Memcpy(buf2,buf,size);
+		buf2[size] = '\0';
+
+		skin = NULL;
+		noskincomplain = realname = hudname = false;
+
+		/*
+		Parse. Has more phases than the parser in R_AddSkins because it needs to have the patching name first (no default skin name is acceptible for patching, unlike skin creation)
+		*/
+
+		stoken = strtok(buf2, "\r\n= ");
+		while (stoken)
+		{
+			if ((stoken[0] == '/' && stoken[1] == '/')
+				|| (stoken[0] == '#'))// skip comments
+			{
+				stoken = strtok(NULL, "\r\n"); // skip end of line
+				goto next_token;              // find the real next token
+			}
+
+			value = strtok(NULL, "\r\n= ");
+
+			if (!value)
+				I_Error("R_PatchSkins: syntax error in P_SKIN lump# %d(%s) in WAD %s\n", lump, W_CheckNameForNumPwad(wadnum,lump), wadfiles[wadnum]->filename);
+
+			if (!skin) // Get the name!
+			{
+				if (!stricmp(stoken, "name"))
+				{
+					strlwr(value);
+					skinnum = R_SkinAvailable(value);
+					if (skinnum != -1)
+						skin = &skins[skinnum];
+					else
+					{
+						CONS_Debug(DBG_SETUP, "R_PatchSkins: unknown skin name in P_SKIN lump# %d(%s) in WAD %s\n", lump, W_CheckNameForNumPwad(wadnum,lump), wadfiles[wadnum]->filename);
+						noskincomplain = true;
+					}
+				}
+			}
+			else // Get the properties!
+			{
+				// Some of these can't go in R_ProcessPatchableFields because they have side effects for future lines.
+				if (!stricmp(stoken, "realname"))
+				{ // Display name (eg. "Knuckles")
+					realname = true;
+					STRBUFCPY(skin->realname, value);
+					SYMBOLCONVERT(skin->realname)
+					if (!hudname)
+						HUDNAMEWRITE(skin->realname);
+				}
+				else if (!stricmp(stoken, "hudname"))
+				{ // Life icon name (eg. "K.T.E")
+					hudname = true;
+					HUDNAMEWRITE(value);
+					SYMBOLCONVERT(skin->hudname)
+					if (!realname)
+						STRBUFCPY(skin->realname, skin->hudname);
+				}
+				else if (!R_ProcessPatchableFields(skin, stoken, value))
+					CONS_Debug(DBG_SETUP, "R_PatchSkins: Unknown keyword '%s' in P_SKIN lump #%d (WAD %s)\n", stoken, lump, wadfiles[wadnum]->filename);
+			}
+
+			if (!skin)
+				break;
+
+next_token:
+			stoken = strtok(NULL, "\r\n= ");
+		}
+		free(buf2);
+
+		if (!skin) // Didn't include a name parameter? What a waste.
+		{
+			if (!noskincomplain)
+				CONS_Debug(DBG_SETUP, "R_PatchSkins: no skin name given in P_SKIN lump #%d (WAD %s)\n", lump, wadfiles[wadnum]->filename);
+			continue;
+		}
+
+		// Patch sprites
+		R_LoadSkinSprites(wadnum, &lump, &lastlump, skin);
+		//ST_LoadFaceGraphics(skinnum); -- nah let's do this elsewhere
+
+		R_FlushTranslationColormapCache();
+
+		if (!skin->availability) // Safe to print...
+			CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name);
+	}
+	return;
+}
+
+#undef HUDNAMEWRITE
+#undef SYMBOLCONVERT
diff --git a/src/r_skins.h b/src/r_skins.h
new file mode 100644
index 0000000000000000000000000000000000000000..96697b4220038c1bdbe491cbd4943b2b4127113b
--- /dev/null
+++ b/src/r_skins.h
@@ -0,0 +1,103 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  r_skins.h
+/// \brief Skins stuff
+
+#ifndef __R_SKINS__
+#define __R_SKINS__
+
+#include "info.h"
+#include "sounds.h"
+#include "d_player.h" // skinflags
+#include "r_patch.h" // spriteinfo_t
+#include "r_defs.h" // spritedef_t
+
+/// Defaults
+#define SKINNAMESIZE 16
+// should be all lowercase!! S_SKIN processing does a strlwr
+#define DEFAULTSKIN "sonic"
+#define DEFAULTSKIN2 "tails" // secondary player
+#define DEFAULTNIGHTSSKIN 0
+
+/// The skin_t struct
+typedef struct
+{
+	char name[SKINNAMESIZE+1]; // INT16 descriptive name of the skin
+	UINT16 wadnum;
+	skinflags_t flags;
+
+	char realname[SKINNAMESIZE+1]; // Display name for level completion.
+	char hudname[SKINNAMESIZE+1]; // HUD name to display (officially exactly 5 characters long)
+
+	UINT8 ability; // ability definition
+	UINT8 ability2; // secondary ability definition
+	INT32 thokitem;
+	INT32 spinitem;
+	INT32 revitem;
+	INT32 followitem;
+	fixed_t actionspd;
+	fixed_t mindash;
+	fixed_t maxdash;
+
+	fixed_t normalspeed; // Normal ground
+	fixed_t runspeed; // Speed that you break into your run animation
+
+	UINT8 thrustfactor; // Thrust = thrustfactor * acceleration
+	UINT8 accelstart; // Acceleration if speed = 0
+	UINT8 acceleration; // Acceleration
+
+	fixed_t jumpfactor; // multiple of standard jump height
+
+	fixed_t radius; // Bounding box changes.
+	fixed_t height;
+	fixed_t spinheight;
+
+	fixed_t shieldscale; // no change to bounding box, but helps set the shield's sprite size
+	fixed_t camerascale;
+
+	// Definable color translation table
+	UINT8 starttranscolor;
+	UINT8 prefcolor;
+	UINT8 supercolor;
+	UINT8 prefoppositecolor; // if 0 use tables instead
+
+	fixed_t highresscale; // scale of highres, default is 0.5
+	UINT8 contspeed; // continue screen animation speed
+	UINT8 contangle; // initial angle on continue screen
+
+	// specific sounds per skin
+	sfxenum_t soundsid[NUMSKINSOUNDS]; // sound # in S_sfx table
+
+	// contains super versions too
+	spritedef_t sprites[NUMPLAYERSPRITES*2];
+	spriteinfo_t sprinfo[NUMPLAYERSPRITES*2];
+
+	UINT8 availability; // lock?
+} skin_t;
+
+/// Externs
+extern INT32 numskins;
+extern skin_t skins[MAXSKINS];
+
+/// Function prototypes
+void R_InitSkins(void);
+
+void SetPlayerSkin(INT32 playernum,const char *skinname);
+void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002
+boolean R_SkinUsable(INT32 playernum, INT32 skinnum);
+UINT32 R_GetSkinAvailabilities(void);
+INT32 R_SkinAvailable(const char *name);
+void R_PatchSkins(UINT16 wadnum);
+void R_AddSkins(UINT16 wadnum);
+
+UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player);
+
+#endif //__R_SKINS__
diff --git a/src/r_things.c b/src/r_things.c
index 953825d0f3aaf3d2e38f8245d4b5ffafe3dd1ceb..2fc44faf89ae3c34eb021a01efc45a64a45cdaa6 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -30,11 +30,8 @@
 #include "p_tick.h"
 #include "p_local.h"
 #include "p_slopes.h"
-#include "dehacked.h" // get_number (for thok)
 #include "d_netfil.h" // blargh. for nameonly().
 #include "m_cheat.h" // objectplace
-#include "m_cond.h"
-#include "fastcmp.h"
 #ifdef HWRENDER
 #include "hardware/hw_md2.h"
 #include "hardware/hw_glob.h"
@@ -42,14 +39,6 @@
 #include "hardware/hw_drv.h"
 #endif
 
-#ifdef PC_DOS
-#include <stdio.h> // for snprintf
-int	snprintf(char *str, size_t n, const char *fmt, ...);
-//int	vsnprintf(char *str, size_t n, const char *fmt, va_list ap);
-#endif
-
-static void R_InitSkins(void);
-
 #define MINZ (FRACUNIT*4)
 #define BASEYCENTER (BASEVIDHEIGHT/2)
 
@@ -233,7 +222,7 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 //
 // Returns true if the sprite was succesfully added
 //
-static boolean R_AddSingleSpriteDef(const char *sprname, spritedef_t *spritedef, UINT16 wadnum, UINT16 startlump, UINT16 endlump)
+boolean R_AddSingleSpriteDef(const char *sprname, spritedef_t *spritedef, UINT16 wadnum, UINT16 startlump, UINT16 endlump)
 {
 	UINT16 l;
 	UINT8 frame;
@@ -245,6 +234,8 @@ static boolean R_AddSingleSpriteDef(const char *sprname, spritedef_t *spritedef,
 	memset(sprtemp,0xFF, sizeof (sprtemp));
 	maxframe = (size_t)-1;
 
+	spritename = sprname;
+
 	// are we 'patching' a sprite already loaded ?
 	// if so, it might patch only certain frames, not all
 	if (spritedef->numframes) // (then spriteframes is not null)
@@ -476,11 +467,10 @@ void R_AddSpriteDefs(UINT16 wadnum)
 	//
 	for (i = 0; i < numsprites; i++)
 	{
-		spritename = sprnames[i];
-		if (spritename[4] && wadnum >= (UINT16)spritename[4])
+		if (sprnames[i][4] && wadnum >= (UINT16)sprnames[i][4])
 			continue;
 
-		if (R_AddSingleSpriteDef(spritename, &sprites[i], wadnum, start, end))
+		if (R_AddSingleSpriteDef(sprnames[i], &sprites[i], wadnum, start, end))
 		{
 #ifdef HWRENDER
 			if (rendermode == render_opengl)
@@ -489,7 +479,7 @@ void R_AddSpriteDefs(UINT16 wadnum)
 			// if a new sprite was added (not just replaced)
 			addsprites++;
 #ifndef ZDEBUG
-			CONS_Debug(DBG_SETUP, "sprite %s set in pwad %d\n", spritename, wadnum);
+			CONS_Debug(DBG_SETUP, "sprite %s set in pwad %d\n", sprnames[i], wadnum);
 #endif
 		}
 	}
@@ -2964,795 +2954,3 @@ void R_DrawMasked(maskcount_t* masks, UINT8 nummasks)
 
 	free(heads);
 }
-
-// ==========================================================================
-//
-//                              SKINS CODE
-//
-// ==========================================================================
-
-INT32 numskins = 0;
-skin_t skins[MAXSKINS];
-// FIXTHIS: don't work because it must be inistilised before the config load
-//#define SKINVALUES
-#ifdef SKINVALUES
-CV_PossibleValue_t skin_cons_t[MAXSKINS+1];
-#endif
-
-//
-// P_GetSkinSprite2
-// For non-super players, tries each sprite2's immediate predecessor until it finds one with a number of frames or ends up at standing.
-// For super players, does the same as above - but tries the super equivalent for each sprite2 before the non-super version.
-//
-
-UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player)
-{
-	UINT8 super = 0, i = 0;
-
-	if (!skin)
-		return 0;
-
-	if ((playersprite_t)(spr2 & ~FF_SPR2SUPER) >= free_spr2)
-		return 0;
-
-	while (!skin->sprites[spr2].numframes
-		&& spr2 != SPR2_STND
-		&& ++i < 32) // recursion limiter
-	{
-		if (spr2 & FF_SPR2SUPER)
-		{
-			super = FF_SPR2SUPER;
-			spr2 &= ~FF_SPR2SUPER;
-			continue;
-		}
-
-		switch(spr2)
-		{
-		// Normal special cases.
-		case SPR2_JUMP:
-			spr2 = ((player
-					? player->charflags
-					: skin->flags)
-					& SF_NOJUMPSPIN) ? SPR2_SPNG : SPR2_ROLL;
-			break;
-		case SPR2_TIRE:
-			spr2 = ((player
-					? player->charability
-					: skin->ability)
-					== CA_SWIM) ? SPR2_SWIM : SPR2_FLY;
-			break;
-		// Use the handy list, that's what it's there for!
-		default:
-			spr2 = spr2defaults[spr2];
-			break;
-		}
-
-		spr2 |= super;
-	}
-
-	if (i >= 32) // probably an infinite loop...
-		return 0;
-
-	return spr2;
-}
-
-static void Sk_SetDefaultValue(skin_t *skin)
-{
-	INT32 i;
-	//
-	// set default skin values
-	//
-	memset(skin, 0, sizeof (skin_t));
-	snprintf(skin->name,
-		sizeof skin->name, "skin %u", (UINT32)(skin-skins));
-	skin->name[sizeof skin->name - 1] = '\0';
-	skin->wadnum = INT16_MAX;
-
-	skin->flags = 0;
-
-	strcpy(skin->realname, "Someone");
-	strcpy(skin->hudname, "???");
-
-	skin->starttranscolor = 96;
-	skin->prefcolor = SKINCOLOR_GREEN;
-	skin->supercolor = SKINCOLOR_SUPERGOLD1;
-	skin->prefoppositecolor = 0; // use tables
-
-	skin->normalspeed = 36<<FRACBITS;
-	skin->runspeed = 28<<FRACBITS;
-	skin->thrustfactor = 5;
-	skin->accelstart = 96;
-	skin->acceleration = 40;
-
-	skin->ability = CA_NONE;
-	skin->ability2 = CA2_SPINDASH;
-	skin->jumpfactor = FRACUNIT;
-	skin->actionspd = 30<<FRACBITS;
-	skin->mindash = 15<<FRACBITS;
-	skin->maxdash = 70<<FRACBITS;
-
-	skin->radius = mobjinfo[MT_PLAYER].radius;
-	skin->height = mobjinfo[MT_PLAYER].height;
-	skin->spinheight = FixedMul(skin->height, 2*FRACUNIT/3);
-
-	skin->shieldscale = FRACUNIT;
-	skin->camerascale = FRACUNIT;
-
-	skin->thokitem = -1;
-	skin->spinitem = -1;
-	skin->revitem = -1;
-	skin->followitem = 0;
-
-	skin->highresscale = FRACUNIT;
-	skin->contspeed = 17;
-	skin->contangle = 0;
-
-	skin->availability = 0;
-
-	for (i = 0; i < sfx_skinsoundslot0; i++)
-		if (S_sfx[i].skinsound != -1)
-			skin->soundsid[S_sfx[i].skinsound] = i;
-}
-
-//
-// Initialize the basic skins
-//
-void R_InitSkins(void)
-{
-#ifdef SKINVALUES
-	INT32 i;
-
-	for (i = 0; i <= MAXSKINS; i++)
-	{
-		skin_cons_t[i].value = 0;
-		skin_cons_t[i].strvalue = NULL;
-	}
-#endif
-
-	// no default skin!
-	numskins = 0;
-}
-
-UINT32 R_GetSkinAvailabilities(void)
-{
-	INT32 s;
-	UINT32 response = 0;
-
-	for (s = 0; s < MAXSKINS; s++)
-	{
-		if (skins[s].availability && unlockables[skins[s].availability - 1].unlocked)
-			response |= (1 << s);
-	}
-	return response;
-}
-
-// returns true if available in circumstances, otherwise nope
-// warning don't use with an invalid skinnum other than -1 which always returns true
-boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
-{
-	return ((skinnum == -1) // Simplifies things elsewhere, since there's already plenty of checks for less-than-0...
-		|| (!skins[skinnum].availability)
-		|| (((netgame || multiplayer) && playernum != -1) ? (players[playernum].availabilities & (1 << skinnum)) : (unlockables[skins[skinnum].availability - 1].unlocked))
-		|| (modeattacking) // If you have someone else's run you might as well take a look
-		|| (Playing() && (R_SkinAvailable(mapheaderinfo[gamemap-1]->forcecharacter) == skinnum)) // Force 1.
-		|| (netgame && (cv_forceskin.value == skinnum)) // Force 2.
-		|| (metalrecording && skinnum == 5) // Force 3.
-		);
-}
-
-// returns true if the skin name is found (loaded from pwad)
-// warning return -1 if not found
-INT32 R_SkinAvailable(const char *name)
-{
-	INT32 i;
-
-	for (i = 0; i < numskins; i++)
-	{
-		// search in the skin list
-		if (stricmp(skins[i].name,name)==0)
-			return i;
-	}
-	return -1;
-}
-
-// network code calls this when a 'skin change' is received
-void SetPlayerSkin(INT32 playernum, const char *skinname)
-{
-	INT32 i = R_SkinAvailable(skinname);
-	player_t *player = &players[playernum];
-
-	if ((i != -1) && R_SkinUsable(playernum, i))
-	{
-		SetPlayerSkinByNum(playernum, i);
-		return;
-	}
-
-	if (P_IsLocalPlayer(player))
-		CONS_Alert(CONS_WARNING, M_GetText("Skin '%s' not found.\n"), skinname);
-	else if(server || IsPlayerAdmin(consoleplayer))
-		CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) skin '%s' not found\n"), playernum, player_names[playernum], skinname);
-
-	SetPlayerSkinByNum(playernum, 0);
-}
-
-// Same as SetPlayerSkin, but uses the skin #.
-// network code calls this when a 'skin change' is received
-void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
-{
-	player_t *player = &players[playernum];
-	skin_t *skin = &skins[skinnum];
-	UINT8 newcolor = 0;
-
-	if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum)) // Make sure it exists!
-	{
-		player->skin = skinnum;
-
-		player->camerascale = skin->camerascale;
-		player->shieldscale = skin->shieldscale;
-
-		player->charability = (UINT8)skin->ability;
-		player->charability2 = (UINT8)skin->ability2;
-
-		player->charflags = (UINT32)skin->flags;
-
-		player->thokitem = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
-		player->spinitem = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
-		player->revitem = skin->revitem < 0 ? (mobjtype_t)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
-		player->followitem = skin->followitem;
-
-		if (((player->powers[pw_shield] & SH_NOSTACK) == SH_PINK) && (player->revitem == MT_LHRT || player->spinitem == MT_LHRT || player->thokitem == MT_LHRT)) // Healers can't keep their buff.
-			player->powers[pw_shield] &= SH_STACK;
-
-		player->actionspd = skin->actionspd;
-		player->mindash = skin->mindash;
-		player->maxdash = skin->maxdash;
-
-		player->normalspeed = skin->normalspeed;
-		player->runspeed = skin->runspeed;
-		player->thrustfactor = skin->thrustfactor;
-		player->accelstart = skin->accelstart;
-		player->acceleration = skin->acceleration;
-
-		player->jumpfactor = skin->jumpfactor;
-
-		player->height = skin->height;
-		player->spinheight = skin->spinheight;
-
-		if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
-		{
-			if (playernum == consoleplayer)
-				CV_StealthSetValue(&cv_playercolor, skin->prefcolor);
-			else if (playernum == secondarydisplayplayer)
-				CV_StealthSetValue(&cv_playercolor2, skin->prefcolor);
-			player->skincolor = newcolor = skin->prefcolor;
-		}
-
-		if (player->followmobj)
-		{
-			P_RemoveMobj(player->followmobj);
-			P_SetTarget(&player->followmobj, NULL);
-		}
-
-		if (player->mo)
-		{
-			fixed_t radius = FixedMul(skin->radius, player->mo->scale);
-			if ((player->powers[pw_carry] == CR_NIGHTSMODE) && (skin->sprites[SPR2_NFLY].numframes == 0)) // If you don't have a sprite for flying horizontally, use the default NiGHTS skin.
-			{
-				skin = &skins[DEFAULTNIGHTSSKIN];
-				player->followitem = skin->followitem;
-				if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
-					newcolor = skin->prefcolor; // will be updated in thinker to flashing
-			}
-			player->mo->skin = skin;
-			if (newcolor)
-				player->mo->color = newcolor;
-			P_SetScale(player->mo, player->mo->scale);
-			player->mo->radius = radius;
-
-			P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames
-		}
-		return;
-	}
-
-	if (P_IsLocalPlayer(player))
-		CONS_Alert(CONS_WARNING, M_GetText("Requested skin %d not found\n"), skinnum);
-	else if(server || IsPlayerAdmin(consoleplayer))
-		CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum);
-	SetPlayerSkinByNum(playernum, 0); // not found put the sonic skin
-}
-
-//
-// Add skins from a pwad, each skin preceded by 'S_SKIN' marker
-//
-
-// Does the same is in w_wad, but check only for
-// the first 6 characters (this is so we can have S_SKIN1, S_SKIN2..
-// for wad editors that don't like multiple resources of the same name)
-//
-static UINT16 W_CheckForSkinMarkerInPwad(UINT16 wadid, UINT16 startlump)
-{
-	UINT16 i;
-	const char *S_SKIN = "S_SKIN";
-	lumpinfo_t *lump_p;
-
-	// scan forward, start at <startlump>
-	if (startlump < wadfiles[wadid]->numlumps)
-	{
-		lump_p = wadfiles[wadid]->lumpinfo + startlump;
-		for (i = startlump; i < wadfiles[wadid]->numlumps; i++, lump_p++)
-			if (memcmp(lump_p->name,S_SKIN,6)==0)
-				return i;
-	}
-	return INT16_MAX; // not found
-}
-
-#define HUDNAMEWRITE(value) STRBUFCPY(skin->hudname, value)
-
-// turn _ into spaces and . into katana dot
-#define SYMBOLCONVERT(name) for (value = name; *value; value++)\
-					{\
-						if (*value == '_') *value = ' ';\
-						else if (*value == '.') *value = '\x1E';\
-					}
-
-//
-// Patch skins from a pwad, each skin preceded by 'P_SKIN' marker
-//
-
-// Does the same is in w_wad, but check only for
-// the first 6 characters (this is so we can have P_SKIN1, P_SKIN2..
-// for wad editors that don't like multiple resources of the same name)
-//
-static UINT16 W_CheckForPatchSkinMarkerInPwad(UINT16 wadid, UINT16 startlump)
-{
-	UINT16 i;
-	const char *P_SKIN = "P_SKIN";
-	lumpinfo_t *lump_p;
-
-	// scan forward, start at <startlump>
-	if (startlump < wadfiles[wadid]->numlumps)
-	{
-		lump_p = wadfiles[wadid]->lumpinfo + startlump;
-		for (i = startlump; i < wadfiles[wadid]->numlumps; i++, lump_p++)
-			if (memcmp(lump_p->name,P_SKIN,6)==0)
-				return i;
-	}
-	return INT16_MAX; // not found
-}
-
-static void R_LoadSkinSprites(UINT16 wadnum, UINT16 *lump, UINT16 *lastlump, skin_t *skin)
-{
-	UINT16 newlastlump;
-	UINT8 sprite2;
-
-	*lump += 1; // start after S_SKIN
-	*lastlump = W_CheckNumForNamePwad("S_END",wadnum,*lump); // stop at S_END
-
-	// old wadding practices die hard -- stop at S_SKIN (or P_SKIN) or S_START if they come before S_END.
-	newlastlump = W_CheckForSkinMarkerInPwad(wadnum,*lump);
-	if (newlastlump < *lastlump) *lastlump = newlastlump;
-	newlastlump = W_CheckForPatchSkinMarkerInPwad(wadnum,*lump);
-	if (newlastlump < *lastlump) *lastlump = newlastlump;
-	newlastlump = W_CheckNumForNamePwad("S_START",wadnum,*lump);
-	if (newlastlump < *lastlump) *lastlump = newlastlump;
-
-	// ...and let's handle super, too
-	newlastlump = W_CheckNumForNamePwad("S_SUPER",wadnum,*lump);
-	if (newlastlump < *lastlump)
-	{
-		newlastlump++;
-		// load all sprite sets we are aware of... for super!
-		for (sprite2 = 0; sprite2 < free_spr2; sprite2++)
-			R_AddSingleSpriteDef((spritename = spr2names[sprite2]), &skin->sprites[FF_SPR2SUPER|sprite2], wadnum, newlastlump, *lastlump);
-
-		newlastlump--;
-		*lastlump = newlastlump; // okay, make the normal sprite set loading end there
-	}
-
-	// load all sprite sets we are aware of... for normal stuff.
-	for (sprite2 = 0; sprite2 < free_spr2; sprite2++)
-		R_AddSingleSpriteDef((spritename = spr2names[sprite2]), &skin->sprites[sprite2], wadnum, *lump, *lastlump);
-
-	if (skin->sprites[0].numframes == 0)
-		I_Error("R_LoadSkinSprites: no frames found for sprite SPR2_%s\n", spr2names[0]);
-}
-
-// returns whether found appropriate property
-static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
-{
-	// custom translation table
-	if (!stricmp(stoken, "startcolor"))
-		skin->starttranscolor = atoi(value);
-
-#define FULLPROCESS(field) else if (!stricmp(stoken, #field)) skin->field = get_number(value);
-	// character type identification
-	FULLPROCESS(flags)
-	FULLPROCESS(ability)
-	FULLPROCESS(ability2)
-
-	FULLPROCESS(thokitem)
-	FULLPROCESS(spinitem)
-	FULLPROCESS(revitem)
-	FULLPROCESS(followitem)
-#undef FULLPROCESS
-
-#define GETFRACBITS(field) else if (!stricmp(stoken, #field)) skin->field = atoi(value)<<FRACBITS;
-	GETFRACBITS(normalspeed)
-	GETFRACBITS(runspeed)
-
-	GETFRACBITS(mindash)
-	GETFRACBITS(maxdash)
-	GETFRACBITS(actionspd)
-
-	GETFRACBITS(radius)
-	GETFRACBITS(height)
-	GETFRACBITS(spinheight)
-#undef GETFRACBITS
-
-#define GETINT(field) else if (!stricmp(stoken, #field)) skin->field = atoi(value);
-	GETINT(thrustfactor)
-	GETINT(accelstart)
-	GETINT(acceleration)
-	GETINT(contspeed)
-	GETINT(contangle)
-#undef GETINT
-
-#define GETSKINCOLOR(field) else if (!stricmp(stoken, #field)) skin->field = R_GetColorByName(value);
-	GETSKINCOLOR(prefcolor)
-	GETSKINCOLOR(prefoppositecolor)
-#undef GETSKINCOLOR
-	else if (!stricmp(stoken, "supercolor"))
-		skin->supercolor = R_GetSuperColorByName(value);
-
-#define GETFLOAT(field) else if (!stricmp(stoken, #field)) skin->field = FLOAT_TO_FIXED(atof(value));
-	GETFLOAT(jumpfactor)
-	GETFLOAT(highresscale)
-	GETFLOAT(shieldscale)
-	GETFLOAT(camerascale)
-#undef GETFLOAT
-
-#define GETFLAG(field) else if (!stricmp(stoken, #field)) { \
-	strupr(value); \
-	if (atoi(value) || value[0] == 'T' || value[0] == 'Y') \
-		skin->flags |= (SF_##field); \
-	else \
-		skin->flags &= ~(SF_##field); \
-}
-	// parameters for individual character flags
-	// these are uppercase so they can be concatenated with SF_
-	// 1, true, yes are all valid values
-	GETFLAG(SUPER)
-	GETFLAG(NOSUPERSPIN)
-	GETFLAG(NOSPINDASHDUST)
-	GETFLAG(HIRES)
-	GETFLAG(NOSKID)
-	GETFLAG(NOSPEEDADJUST)
-	GETFLAG(RUNONWATER)
-	GETFLAG(NOJUMPSPIN)
-	GETFLAG(NOJUMPDAMAGE)
-	GETFLAG(STOMPDAMAGE)
-	GETFLAG(MARIODAMAGE)
-	GETFLAG(MACHINE)
-	GETFLAG(DASHMODE)
-	GETFLAG(FASTEDGE)
-	GETFLAG(MULTIABILITY)
-	GETFLAG(NONIGHTSROTATION)
-#undef GETFLAG
-
-	else // let's check if it's a sound, otherwise error out
-	{
-		boolean found = false;
-		sfxenum_t i;
-		size_t stokenadjust;
-
-		// Remove the prefix. (We need to affect an adjusting variable so that we can print error messages if it's not actually a sound.)
-		if ((stoken[0] == 'D' || stoken[0] == 'd') && (stoken[1] == 'S' || stoken[1] == 's')) // DS*
-			stokenadjust = 2;
-		else // sfx_*
-			stokenadjust = 4;
-
-		// Remove the prefix. (We can affect this directly since we're not going to use it again.)
-		if ((value[0] == 'D' || value[0] == 'd') && (value[1] == 'S' || value[1] == 's')) // DS*
-			value += 2;
-		else // sfx_*
-			value += 4;
-
-		// copy name of sounds that are remapped
-		// for this skin
-		for (i = 0; i < sfx_skinsoundslot0; i++)
-		{
-			if (!S_sfx[i].name)
-				continue;
-			if (S_sfx[i].skinsound != -1
-				&& !stricmp(S_sfx[i].name,
-					stoken + stokenadjust))
-			{
-				skin->soundsid[S_sfx[i].skinsound] =
-					S_AddSoundFx(value, S_sfx[i].singularity, S_sfx[i].pitch, true);
-				found = true;
-			}
-		}
-		return found;
-	}
-	return true;
-}
-
-//
-// Find skin sprites, sounds & optional status bar face, & add them
-//
-void R_AddSkins(UINT16 wadnum)
-{
-	UINT16 lump, lastlump = 0;
-	char *buf;
-	char *buf2;
-	char *stoken;
-	char *value;
-	size_t size;
-	skin_t *skin;
-	boolean hudname, realname;
-
-	//
-	// search for all skin markers in pwad
-	//
-
-	while ((lump = W_CheckForSkinMarkerInPwad(wadnum, lastlump)) != INT16_MAX)
-	{
-		// advance by default
-		lastlump = lump + 1;
-
-		if (numskins >= MAXSKINS)
-		{
-			CONS_Debug(DBG_RENDER, "ignored skin (%d skins maximum)\n", MAXSKINS);
-			continue; // so we know how many skins couldn't be added
-		}
-		buf = W_CacheLumpNumPwad(wadnum, lump, PU_CACHE);
-		size = W_LumpLengthPwad(wadnum, lump);
-
-		// for strtok
-		buf2 = malloc(size+1);
-		if (!buf2)
-			I_Error("R_AddSkins: No more free memory\n");
-		M_Memcpy(buf2,buf,size);
-		buf2[size] = '\0';
-
-		// set defaults
-		skin = &skins[numskins];
-		Sk_SetDefaultValue(skin);
-		skin->wadnum = wadnum;
-		hudname = realname = false;
-		// parse
-		stoken = strtok (buf2, "\r\n= ");
-		while (stoken)
-		{
-			if ((stoken[0] == '/' && stoken[1] == '/')
-				|| (stoken[0] == '#'))// skip comments
-			{
-				stoken = strtok(NULL, "\r\n"); // skip end of line
-				goto next_token;              // find the real next token
-			}
-
-			value = strtok(NULL, "\r\n= ");
-
-			if (!value)
-				I_Error("R_AddSkins: syntax error in S_SKIN lump# %d(%s) in WAD %s\n", lump, W_CheckNameForNumPwad(wadnum,lump), wadfiles[wadnum]->filename);
-
-			// Some of these can't go in R_ProcessPatchableFields because they have side effects for future lines.
-			// Others can't go in there because we don't want them to be patchable.
-			if (!stricmp(stoken, "name"))
-			{
-				INT32 skinnum = R_SkinAvailable(value);
-				strlwr(value);
-				if (skinnum == -1)
-					STRBUFCPY(skin->name, value);
-				// the skin name must uniquely identify a single skin
-				// if the name is already used I make the name 'namex'
-				// using the default skin name's number set above
-				else
-				{
-					const size_t stringspace =
-						strlen(value) + sizeof (numskins) + 1;
-					char *value2 = Z_Malloc(stringspace, PU_STATIC, NULL);
-					snprintf(value2, stringspace,
-						"%s%d", value, numskins);
-					value2[stringspace - 1] = '\0';
-					if (R_SkinAvailable(value2) == -1)
-						// I'm lazy so if NEW name is already used I leave the 'skin x'
-						// default skin name set in Sk_SetDefaultValue
-						STRBUFCPY(skin->name, value2);
-					Z_Free(value2);
-				}
-
-				// copy to hudname and fullname as a default.
-				if (!realname)
-				{
-					STRBUFCPY(skin->realname, skin->name);
-					for (value = skin->realname; *value; value++)
-					{
-						if (*value == '_') *value = ' '; // turn _ into spaces.
-						else if (*value == '.') *value = '\x1E'; // turn . into katana dot.
-					}
-				}
-				if (!hudname)
-				{
-					HUDNAMEWRITE(skin->name);
-					strupr(skin->hudname);
-					SYMBOLCONVERT(skin->hudname)
-				}
-			}
-			else if (!stricmp(stoken, "realname"))
-			{ // Display name (eg. "Knuckles")
-				realname = true;
-				STRBUFCPY(skin->realname, value);
-				SYMBOLCONVERT(skin->realname)
-				if (!hudname)
-					HUDNAMEWRITE(skin->realname);
-			}
-			else if (!stricmp(stoken, "hudname"))
-			{ // Life icon name (eg. "K.T.E")
-				hudname = true;
-				HUDNAMEWRITE(value);
-				SYMBOLCONVERT(skin->hudname)
-				if (!realname)
-					STRBUFCPY(skin->realname, skin->hudname);
-			}
-			else if (!stricmp(stoken, "availability"))
-			{
-				skin->availability = atoi(value);
-				if (skin->availability >= MAXUNLOCKABLES)
-					skin->availability = 0;
-			}
-			else if (!R_ProcessPatchableFields(skin, stoken, value))
-				CONS_Debug(DBG_SETUP, "R_AddSkins: Unknown keyword '%s' in S_SKIN lump #%d (WAD %s)\n", stoken, lump, wadfiles[wadnum]->filename);
-
-next_token:
-			stoken = strtok(NULL, "\r\n= ");
-		}
-		free(buf2);
-
-		// Add sprites
-		R_LoadSkinSprites(wadnum, &lump, &lastlump, skin);
-		//ST_LoadFaceGraphics(numskins); -- nah let's do this elsewhere
-
-		R_FlushTranslationColormapCache();
-
-		if (!skin->availability) // Safe to print...
-			CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name);
-#ifdef SKINVALUES
-		skin_cons_t[numskins].value = numskins;
-		skin_cons_t[numskins].strvalue = skin->name;
-#endif
-
-#ifdef HWRENDER
-		if (rendermode == render_opengl)
-			HWR_AddPlayerModel(numskins);
-#endif
-
-		numskins++;
-	}
-	return;
-}
-
-//
-// Patch skin sprites
-//
-void R_PatchSkins(UINT16 wadnum)
-{
-	UINT16 lump, lastlump = 0;
-	char *buf;
-	char *buf2;
-	char *stoken;
-	char *value;
-	size_t size;
-	skin_t *skin;
-	boolean noskincomplain, realname, hudname;
-
-	//
-	// search for all skin patch markers in pwad
-	//
-
-	while ((lump = W_CheckForPatchSkinMarkerInPwad(wadnum, lastlump)) != INT16_MAX)
-	{
-		INT32 skinnum = 0;
-
-		// advance by default
-		lastlump = lump + 1;
-
-		buf = W_CacheLumpNumPwad(wadnum, lump, PU_CACHE);
-		size = W_LumpLengthPwad(wadnum, lump);
-
-		// for strtok
-		buf2 = malloc(size+1);
-		if (!buf2)
-			I_Error("R_PatchSkins: No more free memory\n");
-		M_Memcpy(buf2,buf,size);
-		buf2[size] = '\0';
-
-		skin = NULL;
-		noskincomplain = realname = hudname = false;
-
-		/*
-		Parse. Has more phases than the parser in R_AddSkins because it needs to have the patching name first (no default skin name is acceptible for patching, unlike skin creation)
-		*/
-
-		stoken = strtok(buf2, "\r\n= ");
-		while (stoken)
-		{
-			if ((stoken[0] == '/' && stoken[1] == '/')
-				|| (stoken[0] == '#'))// skip comments
-			{
-				stoken = strtok(NULL, "\r\n"); // skip end of line
-				goto next_token;              // find the real next token
-			}
-
-			value = strtok(NULL, "\r\n= ");
-
-			if (!value)
-				I_Error("R_PatchSkins: syntax error in P_SKIN lump# %d(%s) in WAD %s\n", lump, W_CheckNameForNumPwad(wadnum,lump), wadfiles[wadnum]->filename);
-
-			if (!skin) // Get the name!
-			{
-				if (!stricmp(stoken, "name"))
-				{
-					strlwr(value);
-					skinnum = R_SkinAvailable(value);
-					if (skinnum != -1)
-						skin = &skins[skinnum];
-					else
-					{
-						CONS_Debug(DBG_SETUP, "R_PatchSkins: unknown skin name in P_SKIN lump# %d(%s) in WAD %s\n", lump, W_CheckNameForNumPwad(wadnum,lump), wadfiles[wadnum]->filename);
-						noskincomplain = true;
-					}
-				}
-			}
-			else // Get the properties!
-			{
-				// Some of these can't go in R_ProcessPatchableFields because they have side effects for future lines.
-				if (!stricmp(stoken, "realname"))
-				{ // Display name (eg. "Knuckles")
-					realname = true;
-					STRBUFCPY(skin->realname, value);
-					SYMBOLCONVERT(skin->realname)
-					if (!hudname)
-						HUDNAMEWRITE(skin->realname);
-				}
-				else if (!stricmp(stoken, "hudname"))
-				{ // Life icon name (eg. "K.T.E")
-					hudname = true;
-					HUDNAMEWRITE(value);
-					SYMBOLCONVERT(skin->hudname)
-					if (!realname)
-						STRBUFCPY(skin->realname, skin->hudname);
-				}
-				else if (!R_ProcessPatchableFields(skin, stoken, value))
-					CONS_Debug(DBG_SETUP, "R_PatchSkins: Unknown keyword '%s' in P_SKIN lump #%d (WAD %s)\n", stoken, lump, wadfiles[wadnum]->filename);
-			}
-
-			if (!skin)
-				break;
-
-next_token:
-			stoken = strtok(NULL, "\r\n= ");
-		}
-		free(buf2);
-
-		if (!skin) // Didn't include a name parameter? What a waste.
-		{
-			if (!noskincomplain)
-				CONS_Debug(DBG_SETUP, "R_PatchSkins: no skin name given in P_SKIN lump #%d (WAD %s)\n", lump, wadfiles[wadnum]->filename);
-			continue;
-		}
-
-		// Patch sprites
-		R_LoadSkinSprites(wadnum, &lump, &lastlump, skin);
-		//ST_LoadFaceGraphics(skinnum); -- nah let's do this elsewhere
-
-		R_FlushTranslationColormapCache();
-
-		if (!skin->availability) // Safe to print...
-			CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name);
-	}
-	return;
-}
-
-#undef HUDNAMEWRITE
-#undef SYMBOLCONVERT
diff --git a/src/r_things.h b/src/r_things.h
index bd6271b60eaa2333662ddd0582f3677b7466145a..05d6fb27b7728e7f3a1103af3956468120a8ae9c 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -14,26 +14,27 @@
 #ifndef __R_THINGS__
 #define __R_THINGS__
 
-#include "sounds.h"
 #include "r_plane.h"
 #include "r_patch.h"
 #include "r_portal.h"
 #include "r_defs.h"
+#include "r_skins.h"
 
-// number of sprite lumps for spritewidth,offset,topoffset lookup tables
-// Fab: this is a hack : should allocate the lookup tables per sprite
-#define MAXVISSPRITES 2048 // added 2-2-98 was 128
-
-#define VISSPRITECHUNKBITS 6	// 2^6 = 64 sprites per chunk
-#define VISSPRITESPERCHUNK (1 << VISSPRITECHUNKBITS)
-#define VISSPRITEINDEXMASK (VISSPRITESPERCHUNK - 1)
+// --------------
+// SPRITE LOADING
+// --------------
 
 #define FEETADJUST (4<<FRACBITS) // R_AddSingleSpriteDef
 
-// Constant arrays used for psprite clipping
-//  and initializing clipping.
-extern INT16 negonearray[MAXVIDWIDTH];
-extern INT16 screenheightarray[MAXVIDWIDTH];
+boolean R_AddSingleSpriteDef(const char *sprname, spritedef_t *spritedef, UINT16 wadnum, UINT16 startlump, UINT16 endlump);
+
+//faB: find sprites in wadfile, replace existing, add new ones
+//     (only sprites from namelist are added or replaced)
+void R_AddSpriteDefs(UINT16 wadnum);
+
+// ---------------------
+// MASKED COLUMN DRAWING
+// ---------------------
 
 // vars for R_DrawMaskedColumn
 extern INT16 *mfloorclip;
@@ -47,9 +48,14 @@ extern fixed_t windowbottom;
 void R_DrawMaskedColumn(column_t *column);
 void R_DrawFlippedMaskedColumn(column_t *column, INT32 texheight);
 
-//faB: find sprites in wadfile, replace existing, add new ones
-//     (only sprites from namelist are added or replaced)
-void R_AddSpriteDefs(UINT16 wadnum);
+// ----------------
+// SPRITE RENDERING
+// ----------------
+
+// Constant arrays used for psprite clipping
+//  and initializing clipping.
+extern INT16 negonearray[MAXVIDWIDTH];
+extern INT16 screenheightarray[MAXVIDWIDTH];
 
 fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope);
 
@@ -68,6 +74,9 @@ boolean R_ThingVisibleWithinDist (mobj_t *thing,
 boolean R_PrecipThingVisible (precipmobj_t *precipthing,
 		fixed_t precip_draw_dist);
 
+// --------------
+// MASKED DRAWING
+// --------------
 /** Used to count the amount of masked elements
  * per portal to later group them in separate
  * drawnode lists.
@@ -82,73 +91,18 @@ typedef struct
 
 void R_DrawMasked(maskcount_t* masks, UINT8 nummasks);
 
-// -----------
-// SKINS STUFF
-// -----------
-#define SKINNAMESIZE 16
-// should be all lowercase!! S_SKIN processing does a strlwr
-#define DEFAULTSKIN "sonic"
-#define DEFAULTSKIN2 "tails" // secondary player
-#define DEFAULTNIGHTSSKIN 0
+// ----------
+// VISSPRITES
+// ----------
+
+// number of sprite lumps for spritewidth,offset,topoffset lookup tables
+// Fab: this is a hack : should allocate the lookup tables per sprite
+#define MAXVISSPRITES 2048 // added 2-2-98 was 128
+
+#define VISSPRITECHUNKBITS 6	// 2^6 = 64 sprites per chunk
+#define VISSPRITESPERCHUNK (1 << VISSPRITECHUNKBITS)
+#define VISSPRITEINDEXMASK (VISSPRITESPERCHUNK - 1)
 
-typedef struct
-{
-	char name[SKINNAMESIZE+1]; // INT16 descriptive name of the skin
-	UINT16 wadnum;
-	skinflags_t flags;
-
-	char realname[SKINNAMESIZE+1]; // Display name for level completion.
-	char hudname[SKINNAMESIZE+1]; // HUD name to display (officially exactly 5 characters long)
-
-	UINT8 ability; // ability definition
-	UINT8 ability2; // secondary ability definition
-	INT32 thokitem;
-	INT32 spinitem;
-	INT32 revitem;
-	INT32 followitem;
-	fixed_t actionspd;
-	fixed_t mindash;
-	fixed_t maxdash;
-
-	fixed_t normalspeed; // Normal ground
-	fixed_t runspeed; // Speed that you break into your run animation
-
-	UINT8 thrustfactor; // Thrust = thrustfactor * acceleration
-	UINT8 accelstart; // Acceleration if speed = 0
-	UINT8 acceleration; // Acceleration
-
-	fixed_t jumpfactor; // multiple of standard jump height
-
-	fixed_t radius; // Bounding box changes.
-	fixed_t height;
-	fixed_t spinheight;
-
-	fixed_t shieldscale; // no change to bounding box, but helps set the shield's sprite size
-	fixed_t camerascale;
-
-	// Definable color translation table
-	UINT8 starttranscolor;
-	UINT8 prefcolor;
-	UINT8 supercolor;
-	UINT8 prefoppositecolor; // if 0 use tables instead
-
-	fixed_t highresscale; // scale of highres, default is 0.5
-	UINT8 contspeed; // continue screen animation speed
-	UINT8 contangle; // initial angle on continue screen
-
-	// specific sounds per skin
-	sfxenum_t soundsid[NUMSKINSOUNDS]; // sound # in S_sfx table
-
-	// contains super versions too
-	spritedef_t sprites[NUMPLAYERSPRITES*2];
-	spriteinfo_t sprinfo[NUMPLAYERSPRITES*2];
-
-	UINT8 availability; // lock?
-} skin_t;
-
-// -----------
-// NOT SKINS STUFF !
-// -----------
 typedef enum
 {
 	// actual cuts
@@ -227,6 +181,12 @@ typedef struct vissprite_s
 	INT32 dispoffset; // copy of info->dispoffset, affects ordering but not drawing
 } vissprite_t;
 
+extern UINT32 visspritecount;
+
+// ----------
+// DRAW NODES
+// ----------
+
 // A drawnode is something that points to a 3D floor, 3D side, or masked
 // middle texture. This is used for sorting with sprites.
 typedef struct drawnode_s
@@ -241,23 +201,11 @@ typedef struct drawnode_s
 	struct drawnode_s *prev;
 } drawnode_t;
 
-extern INT32 numskins;
-extern skin_t skins[MAXSKINS];
-extern UINT32 visspritecount;
-
-void SetPlayerSkin(INT32 playernum,const char *skinname);
-void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002
-boolean R_SkinUsable(INT32 playernum, INT32 skinnum);
-UINT32 R_GetSkinAvailabilities(void);
-INT32 R_SkinAvailable(const char *name);
-void R_PatchSkins(UINT16 wadnum);
-void R_AddSkins(UINT16 wadnum);
-
-UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player);
-
 void R_InitDrawNodes(void);
 
-char *GetPlayerFacePic(INT32 skinnum);
+// -----------------------
+// SPRITE FRAME CHARACTERS
+// -----------------------
 
 // Functions to go from sprite character ID to frame number
 // for 2.1 compatibility this still uses the old 'A' + frame code
diff --git a/src/s_sound.c b/src/s_sound.c
index 8193fdb9b948a7fe0b7aa9fedb53e9b77f0cd516..8e9461d785b9eecef326d4238494230791425211 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -27,7 +27,7 @@ extern INT32 msg_id;
 #include "g_game.h"
 #include "m_argv.h"
 #include "r_main.h" // R_PointToAngle2() used to calc stereo sep.
-#include "r_things.h" // for skins
+#include "r_skins.h" // for skins
 #include "i_system.h"
 #include "i_sound.h"
 #include "s_sound.h"
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index b334f6313614b5890c9b971b07068e7c404b6cec..7e260f4c01ed5a83960bd8f65bdc1f50fbad5ee6 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -284,6 +284,7 @@
     <ClInclude Include="..\r_patch.h" />
     <ClInclude Include="..\r_portal.h" />
     <ClInclude Include="..\r_segs.h" />
+    <ClInclude Include="..\r_skins.h" />
     <ClInclude Include="..\r_sky.h" />
     <ClInclude Include="..\r_splats.h" />
     <ClInclude Include="..\r_state.h" />
@@ -446,6 +447,7 @@
 	<ClCompile Include="..\r_patch.c" />
     <ClCompile Include="..\r_portal.c" />
     <ClCompile Include="..\r_segs.c" />
+    <ClCompile Include="..\r_skins.c" />
     <ClCompile Include="..\r_sky.c" />
     <ClCompile Include="..\r_splats.c" />
     <ClCompile Include="..\r_things.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index 3f61e87098fd08e732f7c521435d93fb63ca77f9..21820551a4ad1bfcfe85765d7975fa482c646e55 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -417,6 +417,9 @@
     <ClInclude Include="..\r_segs.h">
       <Filter>R_Rend</Filter>
     </ClInclude>
+    <ClInclude Include="..\r_skins.h">
+      <Filter>R_Rend</Filter>
+    </ClInclude>
     <ClInclude Include="..\r_sky.h">
       <Filter>R_Rend</Filter>
     </ClInclude>
@@ -849,6 +852,9 @@
     <ClCompile Include="..\r_segs.c">
       <Filter>R_Rend</Filter>
     </ClCompile>
+    <ClCompile Include="..\r_skins.c">
+      <Filter>R_Rend</Filter>
+    </ClCompile>
     <ClCompile Include="..\r_sky.c">
       <Filter>R_Rend</Filter>
     </ClCompile>
diff --git a/src/sounds.c b/src/sounds.c
index a9d720d5c44800ef5dddf0eb208ac3db3a918388..ca943c2d06f0fd9d86e0a3616a86c84b857f65e3 100644
--- a/src/sounds.c
+++ b/src/sounds.c
@@ -15,7 +15,7 @@
 #include "i_sound.h"
 #include "sounds.h"
 #include "r_defs.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "z_zone.h"
 #include "w_wad.h"
 #include "lua_script.h"
diff --git a/src/y_inter.c b/src/y_inter.c
index 36cb64d0625de2b1d3862cf85bcd0fcf6cbbe486..6f23ef1b1f6fa1f983f064ef1abc11154702f274 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -19,7 +19,7 @@
 #include "i_video.h"
 #include "p_tick.h"
 #include "r_defs.h"
-#include "r_things.h"
+#include "r_skins.h"
 #include "s_sound.h"
 #include "st_stuff.h"
 #include "v_video.h"