diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1355c85ee0003fea66f4aad5727f3bac5afb6ec4..53eeb52486850b2a12179fe7e2c8c0c0af06b1a0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -302,6 +302,7 @@ target_compile_definitions(SRB2SDL2 PRIVATE -DCMAKECONFIG)
 #)
 
 add_subdirectory(sdl)
+add_subdirectory(objects)
 
 if(${CMAKE_SYSTEM} MATCHES Windows)
 	add_subdirectory(win32)
diff --git a/src/Makefile b/src/Makefile
index 904aa0bbee5b5be7bae3c653deea5fbcca12f6ef..d777cbbcc5c33ba2d8f62888ef6aaecfa32a1842 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -208,6 +208,7 @@ objdir:=$(makedir)/objs
 sources+=\
 	$(call List,Sourcefile)\
 	$(call List,blua/Sourcefile)\
+	$(call List,objects/Sourcefile)\
 
 depends:=$(basename $(filter %.c %.s,$(sources)))
 objects:=$(basename $(filter %.c %.s %.nas,$(sources)))
diff --git a/src/Sourcefile b/src/Sourcefile
index 9c7960081b8ece49637d048bddba693584aded34..c1e29e11914954777021860e89297c1f63971927 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -118,3 +118,4 @@ k_hud.c
 k_terrain.c
 k_brightmap.c
 k_director.c
+k_follower.c
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index c9a030ab8f1bda7287daad31a2c4f5510e49538d..cd03e1e222521b4d2168fe77ca4d08ef1fa7f27e 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -58,6 +58,7 @@
 #include "k_respawn.h"
 #include "k_grandprix.h"
 #include "k_boss.h"
+#include "k_follower.h"
 #include "doomstat.h"
 #include "deh_tables.h"
 
@@ -1421,7 +1422,7 @@ static void SendNameAndColor(UINT8 n)
 		CV_StealthSet(&cv_followercolor[n], "Match"); // set it to "Match". I don't care about your stupidity!
 
 	// so like, this is sent before we even use anything like cvars or w/e so it's possible that follower is set to a pretty yikes value, so let's fix that before we send garbage that could crash the game:
-	if (cv_follower[n].value > numfollowers-1 || cv_follower[n].value < -1)
+	if (cv_follower[n].value >= numfollowers || cv_follower[n].value < -1)
 		CV_StealthSet(&cv_follower[n], "-1");
 
 	if (!strcmp(cv_playername[n].string, player_names[playernum])
@@ -1447,12 +1448,11 @@ static void SendNameAndColor(UINT8 n)
 
 		player->skincolor = cv_playercolor[n].value;
 
-		if (player->mo && !player->dye)
-			player->mo->color = player->skincolor;
+		K_KartResetPlayerColor(player);
 
 		// Update follower for local games:
 		if (cv_follower[n].value >= -1 && cv_follower[n].value != player->followerskin)
-			SetFollower(playernum, cv_follower[n].value);
+			K_SetFollowerByNum(playernum, cv_follower[n].value);
 
 		player->followercolor = cv_followercolor[n].value;
 
@@ -1632,11 +1632,13 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 		SetPlayerSkinByNum(playernum, skin);
 
 	// set follower colour:
-	// Don't bother doing garbage and kicking if we receive None, this is both silly and a waste of time, this will be handled properly in P_HandleFollower.
+	// Don't bother doing garbage and kicking if we receive None,
+	// this is both silly and a waste of time,
+	// this will be handled properly in K_HandleFollower.
 	p->followercolor = followercolor;
 
 	// set follower
-	SetFollower(playernum, follower);
+	K_SetFollowerByNum(playernum, follower);
 
 #ifdef HAVE_DISCORDRPC
 	if (playernum == consoleplayer)
@@ -4364,7 +4366,11 @@ static void Command_Version_f(void)
 #endif
 
 	// DEVELOP build
-#ifdef DEVELOP
+#if defined(TESTERS)
+	CONS_Printf("\x88" "TESTERS " "\x80");
+#elif defined(HOSTTESTERS)
+	CONS_Printf("\x82" "HOSTTESTERS " "\x80");
+#elif defined(DEVELOP)
 	CONS_Printf("\x87" "DEVELOP " "\x80");
 #endif
 
@@ -5316,7 +5322,7 @@ static void Follower_OnChange(void)
 			return;
 		}
 
-		num = R_FollowerAvailable(str);
+		num = K_FollowerAvailable(str);
 
 		if (num == -1) // that's an error.
 			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);
@@ -5370,7 +5376,7 @@ static void Follower2_OnChange(void)
 			return;
 		}
 
-		num = R_FollowerAvailable(str);
+		num = K_FollowerAvailable(str);
 
 		if (num == -1) // that's an error.
 			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);
@@ -5421,7 +5427,7 @@ static void Follower3_OnChange(void)
 			return;
 		}
 
-		num = R_FollowerAvailable(str);
+		num = K_FollowerAvailable(str);
 
 		if (num == -1) // that's an error.
 			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);
@@ -5472,7 +5478,7 @@ static void Follower4_OnChange(void)
 			return;
 		}
 
-		num = R_FollowerAvailable(str);
+		num = K_FollowerAvailable(str);
 
 		if (num == -1) // that's an error.
 			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);
diff --git a/src/d_player.h b/src/d_player.h
index 24a0c0403e115470cac64c227824e7de2a0d3a8b..c6328c3caf52f8b2699e37f6144907d95a64437a 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -405,7 +405,7 @@ typedef struct player_s
 	UINT8 justbumped;		// Prevent players from endlessly bumping into each other
 	UINT8 tumbleBounces;
 	UINT16 tumbleHeight;	// In *mobjscaled* fracunits, or mfu, not raw fu
-	boolean justDI;			// Directional Influence ended, true until letting go of turn
+	UINT8 justDI;			// Turn-lockout timer to briefly prevent unintended turning after DI, resets when actionable or no input
 	boolean flipDI;			// Bananas flip the DI direction. Was a bug, but it made bananas much more interesting.
 
 	SINT8 drift;			// (-5 to 5) - Drifting Left or Right, plus a bigger counter = sharper turn
@@ -472,6 +472,7 @@ typedef struct player_s
 
 	UINT16 hyudorotimer;	// Duration of the Hyudoro offroad effect itself
 	SINT8 stealingtimer;	// if >0 you are stealing, if <0 you are being stolen from
+	mobj_t *hoverhyudoro;   // First hyudoro hovering next to player
 
 	UINT16 sneakertimer;	// Duration of a Sneaker Boost (from Sneakers or level boosters)
 	UINT8 numsneakers;		// Number of stacked sneaker effects
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 973daadf115e9977133066000c18f63553940a6f..adb44c61ed647668fe3d4d638dc631a94d46ccda 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -49,6 +49,7 @@
 
 // SRB2Kart
 #include "filesrch.h" // refreshdirmenu
+#include "k_follower.h"
 
 // Loops through every constant and operation in word and performs its calculations, returning the final value.
 fixed_t get_number(const char *word)
@@ -3818,16 +3819,18 @@ void readfollower(MYFILE *f)
 	s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
 
 	// Ready the default variables for followers. We will overwrite them as we go! We won't set the name or states RIGHT HERE as this is handled down instead.
+	followers[numfollowers].mode = FOLLOWERMODE_FLOAT;
 	followers[numfollowers].scale = FRACUNIT;
-	followers[numfollowers].bubblescale = 0;	// No bubble by default
-	followers[numfollowers].atangle = 230;
-	followers[numfollowers].dist = 32;		// changed from 16 to 32 to better account for ogl models
-	followers[numfollowers].height = 16;
-	followers[numfollowers].zoffs = 32;
-	followers[numfollowers].horzlag = 2;
-	followers[numfollowers].vertlag = 6;
+	followers[numfollowers].bubblescale = 0; // No bubble by default
+	followers[numfollowers].atangle = FixedAngle(230 * FRACUNIT);
+	followers[numfollowers].dist = 32*FRACUNIT; // changed from 16 to 32 to better account for ogl models
+	followers[numfollowers].height = 16*FRACUNIT;
+	followers[numfollowers].zoffs = 32*FRACUNIT;
+	followers[numfollowers].horzlag = 3*FRACUNIT;
+	followers[numfollowers].vertlag = 6*FRACUNIT;
+	followers[numfollowers].anglelag = 8*FRACUNIT;
 	followers[numfollowers].bobspeed = TICRATE*2;
-	followers[numfollowers].bobamp = 4;
+	followers[numfollowers].bobamp = 4*FRACUNIT;
 	followers[numfollowers].hitconfirmtime = TICRATE;
 	followers[numfollowers].defaultcolor = SKINCOLOR_GREEN;
 
@@ -3863,50 +3866,65 @@ void readfollower(MYFILE *f)
 				strcpy(followers[numfollowers].name, word2);
 				nameset = true;
 			}
+			else if (fastcmp(word, "MODE"))
+			{
+				if (word2)
+					strupr(word2);
+
+				if (fastcmp(word2, "FLOAT") || fastcmp(word2, "DEFAULT"))
+					followers[numfollowers].mode = FOLLOWERMODE_FLOAT;
+				else if (fastcmp(word2, "GROUND"))
+					followers[numfollowers].mode = FOLLOWERMODE_GROUND;
+				else
+					deh_warning("Follower %d: unknown follower mode '%s'", numfollowers, word2);
+			}
 			else if (fastcmp(word, "DEFAULTCOLOR"))
 			{
-				followers[numfollowers].defaultcolor = (UINT16)get_number(word2);
+				followers[numfollowers].defaultcolor = get_number(word2);
 			}
-
 			else if (fastcmp(word, "SCALE"))
 			{
-				followers[numfollowers].scale = get_number(word2);
+				followers[numfollowers].scale = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "BUBBLESCALE"))
 			{
-				followers[numfollowers].bubblescale = get_number(word2);
+				followers[numfollowers].bubblescale = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "ATANGLE"))
 			{
-				followers[numfollowers].atangle = (INT32)atoi(word2);
+				followers[numfollowers].atangle = (angle_t)(get_number(word2) * ANG1);
 			}
 			else if (fastcmp(word, "HORZLAG"))
 			{
-				followers[numfollowers].horzlag = (INT32)atoi(word2);
+				followers[numfollowers].horzlag = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "VERTLAG"))
 			{
-				followers[numfollowers].vertlag = (INT32)atoi(word2);
+				followers[numfollowers].vertlag = (fixed_t)get_number(word2);
+			}
+			else if (fastcmp(word, "ANGLELAG"))
+			{
+				followers[numfollowers].anglelag = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "BOBSPEED"))
 			{
-				followers[numfollowers].bobspeed = (INT32)atoi(word2);
+				followers[numfollowers].bobspeed = (tic_t)get_number(word2);
 			}
 			else if (fastcmp(word, "BOBAMP"))
 			{
-				followers[numfollowers].bobamp = (INT32)atoi(word2);
+				followers[numfollowers].bobamp = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "ZOFFSET") || (fastcmp(word, "ZOFFS")))
 			{
-				followers[numfollowers].zoffs = (INT32)atoi(word2);
+				followers[numfollowers].zoffs = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "DISTANCE") || (fastcmp(word, "DIST")))
 			{
-				followers[numfollowers].dist = (INT32)atoi(word2);
+				followers[numfollowers].dist = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "HEIGHT"))
 			{
-				followers[numfollowers].height = (INT32)atoi(word2);
+				followers[numfollowers].height = (fixed_t)get_number(word2);
 			}
 			else if (fastcmp(word, "IDLESTATE"))
 			{
@@ -3947,16 +3965,19 @@ void readfollower(MYFILE *f)
 			}
 			else if (fastcmp(word, "HITTIME") || (fastcmp(word, "HITCONFIRMTIME")))
 			{
-				followers[numfollowers].hitconfirmtime = (INT32)atoi(word2);
+				followers[numfollowers].hitconfirmtime = (tic_t)get_number(word2);
 			}
 			else
+			{
 				deh_warning("Follower %d: unknown word '%s'", numfollowers, word);
+			}
 		}
 	} while (!myfeof(f)); // finish when the line is empty
 
-	if (!nameset)	// well this is problematic.
+	if (!nameset)
 	{
-		strcpy(followers[numfollowers].name, va("Follower%d", numfollowers));	// this is lazy, so what
+		// well this is problematic.
+		strcpy(followers[numfollowers].name, va("Follower%d", numfollowers)); // this is lazy, so what
 	}
 
 	// set skin name (this is just the follower's name in lowercases):
@@ -3966,7 +3987,7 @@ void readfollower(MYFILE *f)
 
 	// lower testname for skin checks...
 	strlwr(testname);
-	res = R_FollowerAvailable(testname);
+	res = K_FollowerAvailable(testname);
 	if (res > -1)	// yikes, someone else has stolen our name already
 	{
 		INT32 startlen = strlen(testname);
@@ -3989,33 +4010,41 @@ void readfollower(MYFILE *f)
 
 	// fallbacks for variables
 	// Print a warning if the variable is on a weird value and set it back to the minimum available if that's the case.
+
+	if (followers[numfollowers].mode < FOLLOWERMODE_FLOAT || followers[numfollowers].mode >= FOLLOWERMODE__MAX)
+	{
+		followers[numfollowers].mode = FOLLOWERMODE_FLOAT;
+		deh_warning("Follower '%s': Value for 'mode' should be between %d and %d.", dname, FOLLOWERMODE_FLOAT, FOLLOWERMODE__MAX-1);
+	}
+
 #define FALLBACK(field, field2, threshold, set) \
-if (followers[numfollowers].field < threshold) \
+if ((signed)followers[numfollowers].field < threshold) \
 { \
 	followers[numfollowers].field = set; \
-	deh_warning("Follower '%s': Value for '%s' is too low! Minimum should be %d. Value was overwritten to %d.", dname, field2, set, set); \
+	deh_warning("Follower '%s': Value for '%s' is too low! Minimum should be %d. Value was overwritten to %d.", dname, field2, threshold, set); \
 } \
 
 	FALLBACK(dist, "DIST", 0, 0);
 	FALLBACK(height, "HEIGHT", 1, 1);
 	FALLBACK(zoffs, "ZOFFS", 0, 0);
-	FALLBACK(horzlag, "HORZLAG", 1, 1);
-	FALLBACK(vertlag, "VERTLAG", 1, 1);
+	FALLBACK(horzlag, "HORZLAG", FRACUNIT, FRACUNIT);
+	FALLBACK(vertlag, "VERTLAG", FRACUNIT, FRACUNIT);
+	FALLBACK(anglelag, "ANGLELAG", FRACUNIT, FRACUNIT);
 	FALLBACK(bobamp, "BOBAMP", 0, 0);
 	FALLBACK(bobspeed, "BOBSPEED", 0, 0);
 	FALLBACK(hitconfirmtime, "HITCONFIRMTIME", 1, 1);
 	FALLBACK(scale, "SCALE", 1, 1);				// No null/negative scale
 	FALLBACK(bubblescale, "BUBBLESCALE", 0, 0);	// No negative scale
 
+#undef FALLBACK
+
 	// Special case for color I suppose
-	if (followers[numfollowers].defaultcolor > numskincolors-1)
+	if (followers[numfollowers].defaultcolor > (unsigned)(numskincolors-1))
 	{
 		followers[numfollowers].defaultcolor = SKINCOLOR_GREEN;
 		deh_warning("Follower \'%s\': Value for 'color' should be between 1 and %d.\n", dname, numskincolors-1);
 	}
 
-#undef FALLBACK
-
 	// also check if we forgot states. If we did, we will set any missing state to the follower's idlestate.
 	// Print a warning in case we don't have a fallback and set the state to S_INVISIBLE (rather than S_NULL) if unavailable.
 
@@ -4036,7 +4065,7 @@ if (!followers[numfollowers].field) \
 #undef NOSTATE
 
 	CONS_Printf("Added follower '%s'\n", dname);
-	numfollowers++;	// add 1 follower
+	numfollowers++; // add 1 follower
 	Z_Free(s);
 }
 
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 662169cd470a32ad618450c20bc7d49bf29aff6c..b01e3ff4b52a1f2838853e788b92658da2a24a09 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -368,8 +368,8 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_XDEATHSTATE",
 	"S_RAISESTATE",
 
-	// Thok
 	"S_THOK",
+	"S_SHADOW",
 
 	// SRB2kart Frames
 	"S_KART_STILL",
@@ -3745,6 +3745,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_FLAMESHIELDLINE3",
 	"S_FLAMESHIELDFLASH",
 
+	// Caked-Up Booty-Sheet Ghost
+	"S_HYUDORO",
+
 	// The legend
 	"S_SINK",
 	"S_SINK_SHIELD",
@@ -4493,6 +4496,7 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_UNKNOWN",
 
 	"MT_THOK", // Thok! mobj
+	"MT_SHADOW", // Linkdraw Shadow (for invisible objects)
 	"MT_PLAYER",
 	"MT_KART_LEFTOVER",
 	"MT_KART_TIRE",
@@ -5315,6 +5319,9 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_FLAMESHIELDPAPER",
 	"MT_BUBBLESHIELDTRAP",
 
+	"MT_HYUDORO",
+	"MT_HYUDORO_CENTER",
+
 	"MT_SINK", // Kitchen Sink Stuff
 	"MT_SINK_SHIELD",
 	"MT_SINKTRAIL",
diff --git a/src/doomdef.h b/src/doomdef.h
index 0b7745be2444485e6d53c922e4b251faeaada281..f36e780e617206433ddaaae78d41bb5ac5b760df 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -206,7 +206,7 @@ extern char logfilename[1024];
 #define MAXPLAYERNAME 21
 #define MAXSPLITSCREENPLAYERS 4 // Max number of players on a single computer
 
-#define MAXSKINS 128
+#define MAXSKINS UINT8_MAX
 
 #define COLORRAMPSIZE 16
 #define MAXCOLORNAME 32
diff --git a/src/doomstat.h b/src/doomstat.h
index 163bfee96de6669044dd3d73895249ca48242fa9..8c2671c9647129ba7639e7dd1cc8f4f34609a7e7 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -685,7 +685,6 @@ extern boolean comeback;
 extern SINT8 battlewanted[4];
 extern tic_t wantedcalcdelay;
 extern tic_t indirectitemcooldown;
-extern tic_t hyubgone;
 extern tic_t mapreset;
 extern boolean thwompsactive;
 extern UINT8 lastLowestLap;
diff --git a/src/f_finale.c b/src/f_finale.c
index c6596e378c86f12afe7b8eea525d551c5f9df4fd..52f2556f4b5fa25c0712f790cfd77fc19180b671 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -1998,6 +1998,38 @@ void F_TitleScreenDrawer(void)
 			V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_bumper, NULL);
 
 			V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_copyright, NULL);
+
+			// An adapted thing from old menus - most games have version info on the title screen now...
+			{
+				INT32 texty = vid.height - 10*vid.dupy;
+#define addtext(f, str) {\
+	V_DrawThinString(vid.dupx, texty, V_NOSCALESTART|f, str);\
+	texty -= 10*vid.dupy;\
+}
+				if (customversionstring[0] != '\0')
+				{
+					addtext(V_ALLOWLOWERCASE, customversionstring);
+					addtext(0, "Mod version:");
+				}
+				else
+				{
+// Development -- show revision / branch info
+#if defined(TESTERS)
+					addtext(V_ALLOWLOWERCASE|V_SKYMAP, "Tester client");
+					addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", compdate));
+#elif defined(HOSTTESTERS)
+					addtext(V_ALLOWLOWERCASE|V_REDMAP, "Netgame host for testers");
+					addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", compdate));
+#elif defined(DEVELOP)
+					addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, comprevision);
+					addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, compbranch);
+#else // Regular build
+					addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", VERSIONSTRING));
+#endif
+				}
+#undef addtext
+			}
+
 			break;
 		}
 
diff --git a/src/g_demo.c b/src/g_demo.c
index ddf4716844195dce3327c0f3654e1bbff7b2f863..fcc19e224ec008eb6452e774239b83766f01e577 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -51,6 +51,7 @@
 #include "k_respawn.h"
 #include "k_bot.h"
 #include "k_color.h"
+#include "k_follower.h"
 
 static CV_PossibleValue_t recordmultiplayerdemos_cons_t[] = {{0, "Disabled"}, {1, "Manual Save"}, {2, "Auto Save"}, {0, NULL}};
 consvar_t cv_recordmultiplayerdemos = CVAR_INIT ("netdemo_record", "Manual Save", CV_SAVE, recordmultiplayerdemos_cons_t, NULL);
@@ -300,7 +301,7 @@ void G_ReadDemoExtraData(void)
 			// Set our follower
 			M_Memcpy(name, demo_p, 16);
 			demo_p += 16;
-			SetPlayerFollower(p, name);
+			K_SetFollowerByName(p, name);
 
 			// Follower's color
 			M_Memcpy(name, demo_p, 16);
@@ -3057,7 +3058,7 @@ void G_DoPlayDemo(char *defdemoname)
 		// Follower
 		M_Memcpy(follower, demo_p, 16);
 		demo_p += 16;
-		SetPlayerFollower(p, follower);
+		K_SetFollowerByName(p, follower);
 
 		// Follower colour
 		M_Memcpy(color, demo_p, 16);
diff --git a/src/g_game.c b/src/g_game.c
index 13194abf98633ba80f6da7b0ca867413a071fc6d..d7ba5a1236e5102d1f7e8963c92bdbb301cebe70 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -314,7 +314,6 @@ SINT8 pickedvote; // What vote the host rolls
 SINT8 battlewanted[4]; // WANTED players in battle, worth x2 points
 tic_t wantedcalcdelay; // Time before it recalculates WANTED
 tic_t indirectitemcooldown; // Cooldown before any more Shrink, SPB, or any other item that works indirectly is awarded
-tic_t hyubgone; // Cooldown before hyudoro is allowed to be rerolled
 tic_t mapreset; // Map reset delay when enough players have joined an empty game
 boolean thwompsactive; // Thwomps activate on lap 2
 UINT8 lastLowestLap; // Last lowest lap, for activating race lap executors
diff --git a/src/info.c b/src/info.c
index 3cb1f8f77a68910860393c1dc081844cb06b4b43..7545308379f3f036b1725eaebb3e7cf1335c9b33 100644
--- a/src/info.c
+++ b/src/info.c
@@ -29,6 +29,7 @@
 char sprnames[NUMSPRITES + 1][5] =
 {
 	"NULL", // invisible object
+	"NONE", // invisible but still rendered
 	"UNKN",
 
 	"THOK", // Thok! mobj
@@ -570,6 +571,7 @@ char sprnames[NUMSPRITES + 1][5] =
 	"FLMP", // Flame Shield paper sprites
 	"FLML", // Flame Shield speed lines
 	"FLMF", // Flame Shield flash
+	"HYUU", // Hyudoro
 	"SINK", // Kitchen Sink
 	"SITR", // Kitchen Sink Trail
 	"KBLN", // Battle Mode Bumper
@@ -856,6 +858,7 @@ state_t states[NUMSTATES] =
 	{SPR_UNKN, FF_FULLBRIGHT, -1, {A_InfoState}, 6, 0, S_NULL}, // S_RAISESTATE
 
 	{SPR_THOK, FF_TRANS50, 8, {NULL}, 0, 0, S_NULL}, // S_THOK
+	{SPR_NONE, 0, -1, {NULL}, 0, 0, S_NULL}, // S_SHADOW
 
 	// Player
 	{SPR_PLAY, SPR2_STIN,					  1, {NULL}, 0, 0, S_KART_STILL},				// S_KART_STILL
@@ -4300,6 +4303,8 @@ state_t states[NUMSTATES] =
 	{SPR_FLML, FF_FULLBRIGHT|FF_PAPERSPRITE|FF_ANIMATE|14, 7, {NULL}, 6, 1, S_NULL}, // S_FLAMESHIELDLINE3
 	{SPR_FLMF, FF_FULLBRIGHT, 2, {NULL}, 0, 0, S_NULL}, // S_FLAMESHIELDFLASH
 
+	{SPR_HYUU, FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_HYUDORO
+
 	{SPR_SINK, 0,  1, {A_SmokeTrailer}, MT_SINKTRAIL, 0, S_SINK},	// S_SINK
 	{SPR_SINK, 0|FF_TRANS80|FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_SINK_SHIELD}, // S_SINK_SHIELD
 	{SPR_SITR, 0,  1, {NULL}, 0, 0, S_SINKTRAIL2},				// S_SINKTRAIL1
@@ -5154,6 +5159,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_SHADOW
+		-1,             // doomednum
+		S_SHADOW,       // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		8,              // speed
+		32*FRACUNIT,    // radius
+		64*FRACUNIT,    // height
+		-1,             // display offset
+		16,             // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_SCENERY|MF_DONTENCOREMAP, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_PLAYER
 		-1,             // doomednum
 		S_KART_STILL,   // spawnstate
@@ -23881,6 +23913,60 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_HYUDORO
+		-1,             // doomednum
+		S_HYUDORO,      // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		32*FRACUNIT,    // radius
+		24*FRACUNIT,    // height
+		0,              // display offset
+		0,              // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_SPECIAL|MF_NOCLIP|MF_NOGRAVITY|MF_DONTENCOREMAP, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_HYUDORO_CENTER
+		-1,             // doomednum
+		S_INVISIBLE,    // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		64*FRACUNIT,    // radius
+		32*FRACUNIT,    // height
+		0,              // display offset
+		0,              // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_NOGRAVITY|MF_DONTENCOREMAP, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_SINK
 		-1,             // doomednum
 		S_SINK,         // spawnstate
diff --git a/src/info.h b/src/info.h
index 8520b89e392aa59f06d833dac22ff00aaa3c8063..54aa9eaec1425e8f14c4976e999edd1acfc6859f 100644
--- a/src/info.h
+++ b/src/info.h
@@ -575,6 +575,7 @@ extern boolean actionsoverridden[NUMACTIONS];
 typedef enum sprite
 {
 	SPR_NULL, // invisible object
+	SPR_NONE, // invisible but still rendered
 	SPR_UNKN,
 
 	SPR_THOK, // Thok! mobj
@@ -1116,6 +1117,7 @@ typedef enum sprite
 	SPR_FLMP, // Flame Shield paper sprites
 	SPR_FLML, // Flame Shield speed lines
 	SPR_FLMF, // Flame Shield flash
+	SPR_HYUU, // Hyudoro
 	SPR_SINK, // Kitchen Sink
 	SPR_SITR, // Kitchen Sink Trail
 	SPR_KBLN, // Battle Mode Bumper
@@ -1355,8 +1357,8 @@ typedef enum state
 	S_XDEATHSTATE,
 	S_RAISESTATE,
 
-	// Thok
 	S_THOK,
+	S_SHADOW,
 
 	S_KART_STILL,
 	S_KART_STILL_L,
@@ -4731,6 +4733,9 @@ typedef enum state
 	S_FLAMESHIELDLINE3,
 	S_FLAMESHIELDFLASH,
 
+	// Caked-Up Booty-Sheet Ghost
+	S_HYUDORO,
+
 	// The legend
 	S_SINK,
 	S_SINK_SHIELD,
@@ -5516,6 +5521,7 @@ typedef enum mobj_type
 	MT_UNKNOWN,
 
 	MT_THOK, // Thok! mobj
+	MT_SHADOW, // Linkdraw Shadow (for invisible objects)
 	MT_PLAYER,
 	MT_KART_LEFTOVER,
 	MT_KART_TIRE,
@@ -6338,6 +6344,9 @@ typedef enum mobj_type
 	MT_FLAMESHIELDPAPER,
 	MT_BUBBLESHIELDTRAP,
 
+	MT_HYUDORO,
+	MT_HYUDORO_CENTER,
+
 	MT_SINK, // Kitchen Sink Stuff
 	MT_SINK_SHIELD,
 	MT_SINKTRAIL,
diff --git a/src/k_battle.c b/src/k_battle.c
index e64fc5a9a881ac5974a30d0c3c58f7e33832e322..14ecfabf534eb2c28e0dd89eba04c9c77ca325ba 100644
--- a/src/k_battle.c
+++ b/src/k_battle.c
@@ -672,12 +672,11 @@ void K_RunBattleOvertime(void)
 
 	if (battleovertime.radius > 0)
 	{
-		const fixed_t pi = (22 * FRACUNIT) / 7; // loose approximation, this doesn't need to be incredibly precise
 		const INT32 orbs = 32;
 		const angle_t angoff = ANGLE_MAX / orbs;
 		const UINT8 spriteSpacing = 128;
 
-		fixed_t circumference = FixedMul(pi, battleovertime.radius * 2);
+		fixed_t circumference = FixedMul(M_PI_FIXED, battleovertime.radius * 2);
 		fixed_t scale = max(circumference / spriteSpacing / orbs, mapobjectscale);
 
 		fixed_t size = FixedMul(mobjinfo[MT_OVERTIME_PARTICLE].radius, scale);
diff --git a/src/k_botitem.c b/src/k_botitem.c
index 9b3cdbd6a1f0b042ab5ba9e369185d251b121731..865a18fdd5ab41d36f48b32ea78210ac7f8d5ffc 100644
--- a/src/k_botitem.c
+++ b/src/k_botitem.c
@@ -1308,7 +1308,7 @@ void K_BotItemUsage(player_t *player, ticcmd_t *cmd, INT16 turnamt)
 							K_BotItemGenericOrbitShield(player, cmd);
 						}
 						else if (player->position != 1) // Hold onto orbiting items when in 1st :)
-						/* FALL-THRU */
+						/* FALLTHRU */
 					case KITEM_BALLHOG:
 						{
 							K_BotItemOrbinaut(player, cmd);
diff --git a/src/k_botsearch.c b/src/k_botsearch.c
index 07935d79c4fb399caba5fdcc33eb97249f3d6889..b62b558522247695c8a18f9cef4e1722b691b300 100644
--- a/src/k_botsearch.c
+++ b/src/k_botsearch.c
@@ -51,7 +51,7 @@ struct globalsmuggle
 } globalsmuggle;
 
 /*--------------------------------------------------
-	static boolean K_FindEggboxes(mobj_t *thing)
+	static BlockItReturn_t K_FindEggboxes(mobj_t *thing)
 
 		Blockmap search function.
 		Increments the random items and egg boxes counters.
@@ -60,27 +60,27 @@ struct globalsmuggle
 		thing - Object passed in from iteration.
 
 	Return:-
-		true continues searching, false ends the search early.
+		BlockItReturn_t enum, see its definition for more information.
 --------------------------------------------------*/
-static boolean K_FindEggboxes(mobj_t *thing)
+static BlockItReturn_t K_FindEggboxes(mobj_t *thing)
 {
 	fixed_t dist;
 
 	if (thing->type != MT_RANDOMITEM && thing->type != MT_EGGMANITEM)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (!thing->health)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	dist = P_AproxDistance(thing->x - globalsmuggle.eggboxx, thing->y - globalsmuggle.eggboxy);
 
 	if (dist > globalsmuggle.distancetocheck)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (thing->type == MT_RANDOMITEM)
@@ -92,7 +92,7 @@ static boolean K_FindEggboxes(mobj_t *thing)
 		globalsmuggle.eggboxes++;
 	}
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 /*--------------------------------------------------
@@ -347,7 +347,7 @@ static boolean K_PlayerAttackSteer(mobj_t *thing, UINT8 side, UINT8 weight, bool
 }
 
 /*--------------------------------------------------
-	static boolean K_FindObjectsForNudging(mobj_t *thing)
+	static BlockItReturn_t K_FindObjectsForNudging(mobj_t *thing)
 
 		Blockmap search function.
 		Finds objects around the bot to steer towards/away from.
@@ -356,9 +356,9 @@ static boolean K_PlayerAttackSteer(mobj_t *thing, UINT8 side, UINT8 weight, bool
 		thing - Object passed in from iteration.
 
 	Return:-
-		true continues searching, false ends the search early.
+		BlockItReturn_t enum, see its definition for more information.
 --------------------------------------------------*/
-static boolean K_FindObjectsForNudging(mobj_t *thing)
+static BlockItReturn_t K_FindObjectsForNudging(mobj_t *thing)
 {
 	INT16 anglediff;
 	fixed_t fulldist;
@@ -367,29 +367,29 @@ static boolean K_FindObjectsForNudging(mobj_t *thing)
 
 	if (!globalsmuggle.botmo || P_MobjWasRemoved(globalsmuggle.botmo) || !globalsmuggle.botmo->player)
 	{
-		return false;
+		return BMIT_ABORT;
 	}
 
 	if (thing->health <= 0)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (globalsmuggle.botmo == thing)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	fulldist = R_PointToDist2(globalsmuggle.botmo->x, globalsmuggle.botmo->y, thing->x, thing->y) - thing->radius;
 
 	if (fulldist > globalsmuggle.distancetocheck)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (P_CheckSight(globalsmuggle.botmo, thing) == false)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	predictangle = R_PointToAngle2(globalsmuggle.botmo->x, globalsmuggle.botmo->y, globalsmuggle.predict->x, globalsmuggle.predict->y);
@@ -607,7 +607,7 @@ static boolean K_FindObjectsForNudging(mobj_t *thing)
 			break;
 	}
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 /*--------------------------------------------------
@@ -776,7 +776,7 @@ void K_NudgePredictionTowardsObjects(botprediction_t *predict, player_t *player)
 }
 
 /*--------------------------------------------------
-	static boolean K_FindPlayersToBully(mobj_t *thing)
+	static BlockItReturn_t K_FindPlayersToBully(mobj_t *thing)
 
 		Blockmap search function.
 		Finds players around the bot to bump.
@@ -785,9 +785,9 @@ void K_NudgePredictionTowardsObjects(botprediction_t *predict, player_t *player)
 		thing - Object passed in from iteration.
 
 	Return:-
-		true continues searching, false ends the search early.
+		BlockItReturn_t enum, see its definition for more information.
 --------------------------------------------------*/
-static boolean K_FindPlayersToBully(mobj_t *thing)
+static BlockItReturn_t K_FindPlayersToBully(mobj_t *thing)
 {
 	INT16 anglediff;
 	fixed_t fulldist;
@@ -796,34 +796,34 @@ static boolean K_FindPlayersToBully(mobj_t *thing)
 
 	if (!globalsmuggle.botmo || P_MobjWasRemoved(globalsmuggle.botmo) || !globalsmuggle.botmo->player)
 	{
-		return false;
+		return BMIT_ABORT;
 	}
 
 	if (thing->health <= 0)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (!thing->player)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (globalsmuggle.botmo == thing)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	fulldist = R_PointToDist2(globalsmuggle.botmo->x, globalsmuggle.botmo->y, thing->x, thing->y) - thing->radius;
 
 	if (fulldist > globalsmuggle.distancetocheck)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (P_CheckSight(globalsmuggle.botmo, thing) == false)
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	ourangle = globalsmuggle.botmo->angle;
@@ -860,7 +860,7 @@ static boolean K_FindPlayersToBully(mobj_t *thing)
 		globalsmuggle.annoymo = thing;
 	}
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 /*--------------------------------------------------
diff --git a/src/k_collide.c b/src/k_collide.c
index 92c4bd7fd9f880d02db414dba7016b88483840de..306c01dd832961b8c95ff0dd432d636bc45af57e 100644
--- a/src/k_collide.c
+++ b/src/k_collide.c
@@ -323,26 +323,26 @@ static inline boolean PIT_SSMineChecks(mobj_t *thing)
 	return false;
 }
 
-static inline boolean PIT_SSMineSearch(mobj_t *thing)
+static inline BlockItReturn_t PIT_SSMineSearch(mobj_t *thing)
 {
 	if (grenade == NULL || P_MobjWasRemoved(grenade))
-		return false; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot
+		return BMIT_ABORT; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot
 
 	if (grenade->flags2 & MF2_DEBRIS) // don't explode twice
-		return false;
+		return BMIT_ABORT;
 
 	if (thing->type != MT_PLAYER) // Don't explode for anything but an actual player.
-		return true;
+		return BMIT_CONTINUE;
 
 	if (thing == grenade->target && grenade->threshold != 0) // Don't blow up at your owner instantly.
-		return true;
+		return BMIT_CONTINUE;
 
 	if (PIT_SSMineChecks(thing) == true)
-		return true;
+		return BMIT_CONTINUE;
 
 	// Explode!
 	P_SetMobjState(grenade, grenade->info->deathstate);
-	return false;
+	return BMIT_ABORT;
 }
 
 void K_DoMineSearch(mobj_t *actor, fixed_t size)
@@ -364,21 +364,21 @@ void K_DoMineSearch(mobj_t *actor, fixed_t size)
 			P_BlockThingsIterator(bx, by, PIT_SSMineSearch);
 }
 
-static inline boolean PIT_SSMineExplode(mobj_t *thing)
+static inline BlockItReturn_t PIT_SSMineExplode(mobj_t *thing)
 {
 	if (grenade == NULL || P_MobjWasRemoved(grenade))
-		return false; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot
+		return BMIT_ABORT; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot
 
 #if 0
 	if (grenade->flags2 & MF2_DEBRIS) // don't explode twice
-		return false;
+		return BMIT_ABORT;
 #endif
 
 	if (PIT_SSMineChecks(thing) == true)
-		return true;
+		return BMIT_CONTINUE;
 
 	P_DamageMobj(thing, grenade, grenade->target, 1, (explodespin ? DMG_NORMAL : DMG_EXPLODE));
-	return true;
+	return BMIT_CONTINUE;
 }
 
 void K_MineExplodeAttack(mobj_t *actor, fixed_t size, boolean spin)
@@ -667,60 +667,54 @@ boolean K_DropTargetCollide(mobj_t *t1, mobj_t *t2)
 static mobj_t *lightningSource;
 static fixed_t lightningDist;
 
-static inline boolean PIT_LightningShieldAttack(mobj_t *thing)
+static inline BlockItReturn_t PIT_LightningShieldAttack(mobj_t *thing)
 {
 	if (lightningSource == NULL || P_MobjWasRemoved(lightningSource))
 	{
 		// Invalid?
-		return false;
+		return BMIT_ABORT;
 	}
 
 	if (thing == lightningSource)
 	{
 		// Don't explode yourself!!
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (thing->health <= 0)
 	{
 		// Dead
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (!(thing->flags & MF_SHOOTABLE) || (thing->flags & MF_SCENERY))
 	{
 		// Not shootable
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (thing->player && thing->player->spectator)
 	{
 		// Spectator
-		return true;
-	}
-
-	if ((lightningSource->eflags & MFE_VERTICALFLIP)
-		? (thing->z > lightningSource->z + lightningSource->height)
-		: (thing->z + thing->height < lightningSource->z))
-	{
-		// Underneath
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (P_AproxDistance(thing->x - lightningSource->x, thing->y - lightningSource->y) > lightningDist + thing->radius)
 	{
 		// Too far away
-		return true;
+		return BMIT_CONTINUE;
 	}
 
+#if 0
 	if (P_CheckSight(lightningSource, thing) == false)
 	{
 		// Not in sight
-		return true;
+		return BMIT_CONTINUE;
 	}
+#endif
 
 	P_DamageMobj(thing, lightningSource, lightningSource, 1, DMG_NORMAL|DMG_CANTHURTSELF|DMG_WOMBO);
-	return true;
+	return BMIT_CONTINUE;
 }
 
 void K_LightningShieldAttack(mobj_t *actor, fixed_t size)
diff --git a/src/k_follower.c b/src/k_follower.c
new file mode 100644
index 0000000000000000000000000000000000000000..76a2d9dd77a7a228cf9bd20b7aedc06997cc846e
--- /dev/null
+++ b/src/k_follower.c
@@ -0,0 +1,567 @@
+
+#include "k_follower.h"
+
+#include "k_kart.h"
+
+#include "doomtype.h"
+#include "doomdef.h"
+#include "g_game.h"
+#include "g_demo.h"
+#include "r_main.h"
+#include "r_skins.h"
+#include "p_local.h"
+#include "p_mobj.h"
+
+INT32 numfollowers = 0;
+follower_t followers[MAXSKINS];
+
+CV_PossibleValue_t Followercolor_cons_t[MAXSKINCOLORS+3];	// +3 to account for "Match", "Opposite" & NULL
+
+/*--------------------------------------------------
+	INT32 K_FollowerAvailable(const char *name)
+
+		See header file for description.
+--------------------------------------------------*/
+INT32 K_FollowerAvailable(const char *name)
+{
+	INT32 i;
+
+	for (i = 0; i < numfollowers; i++)
+	{
+		if (stricmp(followers[i].skinname, name) == 0)
+			return i;
+	}
+
+	return -1;
+}
+
+/*--------------------------------------------------
+	boolean K_SetFollowerByName(INT32 playernum, const char *skinname)
+
+		See header file for description.
+--------------------------------------------------*/
+boolean K_SetFollowerByName(INT32 playernum, const char *skinname)
+{
+	INT32 i;
+	player_t *player = &players[playernum];
+
+	if (stricmp("None", skinname) == 0)
+	{
+		K_SetFollowerByNum(playernum, -1); // reminder that -1 is nothing
+		return true;
+	}
+
+	for (i = 0; i < numfollowers; i++)
+	{
+		// search in the skin list
+		if (stricmp(followers[i].skinname, skinname) == 0)
+		{
+			K_SetFollowerByNum(playernum, i);
+			return true;
+		}
+	}
+
+	if (P_IsLocalPlayer(player))
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found.\n"), skinname);
+	}
+	else if (server || IsPlayerAdmin(consoleplayer))
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) follower '%s' not found\n"), playernum, player_names[playernum], skinname);
+	}
+
+	K_SetFollowerByNum(playernum, -1); // reminder that -1 is nothing
+	return false;
+}
+
+/*--------------------------------------------------
+	void K_SetFollowerByNum(INT32 playernum, INT32 skinnum)
+
+		See header file for description.
+--------------------------------------------------*/
+void K_SetFollowerByNum(INT32 playernum, INT32 skinnum)
+{
+	player_t *player = &players[playernum];
+	mobj_t *bub;
+	mobj_t *tmp;
+
+	player->followerready = true; // we are ready to perform follower related actions in the player thinker, now.
+
+	if (skinnum >= -1 && skinnum <= numfollowers) // Make sure it exists!
+	{
+		/*
+			We don't spawn the follower here since it'll be easier to handle all of it in the Player thinker itself.
+			However, we will despawn it right here if there's any to make it easy for the player thinker to replace it or delete it.
+		*/
+
+		if (player->follower && skinnum != player->followerskin) // this is also called when we change colour so don't respawn the follower unless we changed skins
+		{
+			// Remove follower's possible hnext list (bubble)
+			bub = player->follower->hnext;
+
+			while (bub && !P_MobjWasRemoved(bub))
+			{
+				tmp = bub->hnext;
+				P_RemoveMobj(bub);
+				bub = tmp;
+			}
+
+			P_RemoveMobj(player->follower);
+			P_SetTarget(&player->follower, NULL);
+		}
+
+		player->followerskin = skinnum;
+
+		// for replays: We have changed our follower mid-game; let the game know so it can do the same in the replay!
+		demo_extradata[playernum] |= DXD_FOLLOWER;
+		return;
+	}
+
+	if (P_IsLocalPlayer(player))
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Follower %d not found\n"), skinnum);
+	}
+	else if (server || IsPlayerAdmin(consoleplayer))
+	{
+		CONS_Alert(CONS_WARNING, "Player %d (%s) follower %d not found\n", playernum, player_names[playernum], skinnum);
+	}
+
+	K_SetFollowerByNum(playernum, -1); // Not found, then set -1 (nothing) as our follower.
+}
+
+
+/*--------------------------------------------------
+	static void K_SetFollowerState(mobj_t *f, statenum_t state)
+
+		Sets a follower object's state.
+		This is done as a separate function to prevent running follower actions.
+
+	Input Arguments:-
+		f - The follower's mobj_t.
+		state - The state to set.
+
+	Return:-
+		None
+--------------------------------------------------*/
+static void K_SetFollowerState(mobj_t *f, statenum_t state)
+{
+	if (f == NULL || P_MobjWasRemoved(f) == true)
+	{
+		// safety net
+		return;
+	}
+
+	// No, do NOT set the follower to S_NULL. Set it to S_INVISIBLE.
+	if (state == S_NULL)
+	{
+		state = S_INVISIBLE;
+		f->threshold = 1; // Threshold = 1 means stop doing anything related to setting states, so that we don't get out of S_INVISIBLE
+	}
+
+	// extravalue2 stores the last "first state" we used.
+	// because states default to idlestates, if we use an animation that uses an "ongoing" state line, don't reset it!
+	// this prevents it from looking very dumb
+	if (state == (statenum_t)f->extravalue2)
+	{
+		return;
+	}
+
+	// we will save the state into extravalue2.
+	f->extravalue2 = state;
+
+	P_SetMobjStateNF(f, state);
+	if (f->state->tics > 0)
+	{
+		f->tics++;
+	}
+}
+
+/*--------------------------------------------------
+	void K_HandleFollower(player_t *player)
+
+		See header file for description.
+--------------------------------------------------*/
+void K_HandleFollower(player_t *player)
+{
+	follower_t fl;
+	angle_t an;
+	fixed_t zoffs;
+	fixed_t ourheight;
+	fixed_t sx, sy, sz, deltaz;
+	fixed_t fh = INT32_MIN, ch = INT32_MAX;
+	UINT16 color;
+
+	fixed_t bubble; // bubble scale (0 if no bubble)
+	mobj_t *bmobj; // temp bubble mobj
+
+	angle_t destAngle;
+	INT32 angleDiff;
+
+	if (player->followerready == false)
+	{
+		// we aren't ready to perform anything follower related yet.
+		return;
+	}
+
+	// How about making sure our follower exists and is added before trying to spawn it n' all?
+	if (player->followerskin >= numfollowers || player->followerskin < -1)
+	{
+		//CONS_Printf("Follower skin invlaid. Setting to -1.\n");
+		player->followerskin = -1;
+		return;
+	}
+
+	// don't do anything if we can't have a follower to begin with.
+	// (It gets removed under those conditions)
+	if (player->spectator)
+	{
+		return;
+	}
+
+	if (player->followerskin < 0)
+	{
+		return;
+	}
+
+	// Before we do anything, let's be sure of where we're supposed to be
+	fl = followers[player->followerskin];
+
+	an = player->mo->angle + fl.atangle;
+	zoffs = fl.zoffs;
+	bubble = fl.bubblescale; // 0 if no bubble to spawn.
+
+	// do you like angle maths? I certainly don't...
+	sx = player->mo->x + player->mo->momx + FixedMul(FixedMul(player->mo->scale, fl.dist), FINECOSINE((an) >> ANGLETOFINESHIFT));
+	sy = player->mo->y + player->mo->momy + FixedMul(FixedMul(player->mo->scale, fl.dist), FINESINE((an) >> ANGLETOFINESHIFT));
+
+	// interp info helps with stretchy fix
+	deltaz = (player->mo->z - player->mo->old_z);
+
+	// for the z coordinate, don't be a doof like Steel and forget that MFE_VERTICALFLIP exists :P
+	sz = player->mo->z + player->mo->momz + FixedMul(player->mo->scale, zoffs * P_MobjFlip(player->mo));
+	ourheight = FixedMul(fl.height, player->mo->scale);
+	if (player->mo->eflags & MFE_VERTICALFLIP)
+	{
+		sz += ourheight;
+	}
+
+	fh = player->mo->floorz;
+	ch = player->mo->ceilingz - ourheight;
+
+	switch (fl.mode)
+	{
+		case FOLLOWERMODE_GROUND:
+		{
+			if (player->mo->eflags & MFE_VERTICALFLIP)
+			{
+				sz = ch;
+			}
+			else
+			{
+				sz = fh;
+			}
+			break;
+		}
+		case FOLLOWERMODE_FLOAT:
+		default:
+		{
+			// finally, add a cool floating effect to the z height.
+			// not stolen from k_kart I swear!!
+			fixed_t sine = FixedMul(fl.bobamp, FINESINE(((FixedMul(4 * M_TAU_FIXED, fl.bobspeed) * leveltime) >> ANGLETOFINESHIFT) & FINEMASK));
+			sz += FixedMul(player->mo->scale, sine) * P_MobjFlip(player->mo);
+			break;
+		}
+	}
+
+	// Set follower colour
+	switch (player->followercolor)
+	{
+		case FOLLOWERCOLOR_MATCH: // "Match"
+			color = player->skincolor;
+			break;
+
+		case FOLLOWERCOLOR_OPPOSITE: // "Opposite"
+			color = skincolors[player->skincolor].invcolor;
+			break;
+
+		default:
+			color = player->followercolor;
+			if (color == 0 || color > MAXSKINCOLORS+2) // Make sure this isn't garbage
+			{
+				color = player->skincolor; // "Match" as fallback.
+			}
+			break;
+	}
+
+	if (player->follower == NULL) // follower doesn't exist / isn't valid
+	{
+		//CONS_Printf("Spawning follower...\n");
+
+		// so let's spawn one!
+		P_SetTarget(&player->follower, P_SpawnMobj(sx, sy, sz, MT_FOLLOWER));
+		K_SetFollowerState(player->follower, fl.idlestate);
+		P_SetTarget(&player->follower->target, player->mo);	// we need that to know when we need to disappear
+		P_InitAngle(player->follower, player->mo->angle);
+
+		// This is safe to only spawn it here, the follower is removed then respawned when switched.
+		if (bubble)
+		{
+			bmobj = P_SpawnMobj(player->follower->x, player->follower->y, player->follower->z, MT_FOLLOWERBUBBLE_FRONT);
+			P_SetTarget(&player->follower->hnext, bmobj);
+			P_SetTarget(&bmobj->target, player->follower); // Used to know if we have to despawn at some point.
+
+			bmobj = P_SpawnMobj(player->follower->x, player->follower->y, player->follower->z, MT_FOLLOWERBUBBLE_BACK);
+			P_SetTarget(&player->follower->hnext->hnext, bmobj); // this seems absolutely stupid, I know, but this will make updating the momentums/flags of these a bit easier.
+			P_SetTarget(&bmobj->target, player->follower); // Ditto
+		}
+
+		player->follower->extravalue1 = 0; // extravalue1 is used to know what "state set" to use.
+		/*
+			0 = idle
+			1 = forwards
+			2 = hurt
+			3 = win
+			4 = lose
+			5 = hitconfirm (< this one uses ->movecount as timer to know when to end, and goes back to normal states afterwards, unless hurt)
+		*/
+	}
+	else // follower exists, woo!
+	{
+		// Safety net (2)
+
+		if (P_MobjWasRemoved(player->follower))
+		{
+			P_SetTarget(&player->follower, NULL); // Remove this and respawn one, don't crash the game if Lua decides to P_RemoveMobj this thing.
+			return;
+		}
+
+		// first of all, handle states following the same model as above:
+		if (player->follower->tics == 1)
+		{
+			K_SetFollowerState(player->follower, player->follower->state->nextstate);
+		}
+
+		// move the follower next to us (yes, this is really basic maths but it looks pretty damn clean in practice)!
+		player->follower->momx = FixedDiv(sx - player->follower->x, fl.horzlag);
+		player->follower->momy = FixedDiv(sy - player->follower->y, fl.horzlag);
+
+		player->follower->z += FixedDiv(deltaz, fl.vertlag);
+
+		if (fl.mode == FOLLOWERMODE_GROUND)
+		{
+			sector_t *sec = R_PointInSubsector(sx, sy)->sector;
+
+			fh = min(fh, P_GetFloorZ(player->follower, sec, sx, sy, NULL));
+			ch = max(ch, P_GetCeilingZ(player->follower, sec, sx, sy, NULL) - ourheight);
+
+			if (P_IsObjectOnGround(player->mo) == false)
+			{
+				// In the air, match their momentum.
+				player->follower->momz = player->mo->momz;
+			}
+			else
+			{
+				fixed_t fg = P_GetMobjGravity(player->mo);
+				fixed_t fz = P_GetMobjZMovement(player->follower);
+
+				player->follower->momz = fz;
+
+				// Player is on the ground ... try to get the follower
+				// back to the ground also if it is above it.
+				player->follower->momz += FixedDiv(fg * 6, fl.vertlag); // Scaled against the default value of vertlag
+			}
+		}
+		else
+		{
+			player->follower->momz = FixedDiv(sz - player->follower->z, fl.vertlag);
+		}
+
+		if (player->mo->colorized)
+		{
+			player->follower->color = player->mo->color;
+		}
+		else
+		{
+			player->follower->color = color;
+		}
+
+		player->follower->colorized = player->mo->colorized;
+
+		P_SetScale(player->follower, FixedMul(fl.scale, player->mo->scale));
+		K_GenericExtraFlagsNoZAdjust(player->follower, player->mo); // Not K_MatchGenericExtraFlag because the Z adjust it has only works properly if master & mo have the same Z height.
+
+		// Match how the player is being drawn
+		player->follower->renderflags = player->mo->renderflags;
+
+		// Make the follower invisible if we no contest'd rather than removing it. No one will notice the diff seriously.
+		if (player->pflags & PF_NOCONTEST)
+		{
+			player->follower->renderflags |= RF_DONTDRAW;
+		}
+
+		// if we're moving let's make the angle the direction we're moving towards. This is to avoid drifting / reverse looking awkward.
+		if (FixedHypot(player->follower->momx, player->follower->momy) >= player->mo->scale)
+		{
+			destAngle = R_PointToAngle2(0, 0, player->follower->momx, player->follower->momy);
+		}
+		else
+		{
+			// Face the player's angle when standing still.
+			destAngle = player->mo->angle;
+		}
+
+		// Sal: Turn the follower around when looking backwards.
+		if ( player->cmd.buttons & BT_LOOKBACK )
+		{
+			destAngle += ANGLE_180;
+		}
+
+		// Sal: Smoothly rotate angle to the destination value.
+		angleDiff = AngleDeltaSigned(destAngle, player->follower->angle);
+
+		if (angleDiff != 0)
+		{
+			player->follower->angle += FixedDiv(angleDiff, fl.anglelag);
+		}
+
+		// Ground follower slope rotation
+		if (fl.mode == FOLLOWERMODE_GROUND)
+		{
+			if (player->follower->z <= fh)
+			{
+				player->follower->z = fh;
+				if (player->follower->momz < 0)
+				{
+					player->follower->momz = 0;
+				}
+			}
+			else if (player->follower->z >= ch)
+			{
+				player->follower->z = ch;
+				if (player->follower->momz > 0)
+				{
+					player->follower->momz = 0;
+				}
+			}
+
+			K_CalculateBananaSlope(
+				player->follower,
+				player->follower->x, player->follower->y, player->follower->z,
+				player->follower->radius, ourheight,
+				(player->mo->eflags & MFE_VERTICALFLIP),
+				false
+			);
+		}
+
+		// Finally, if the follower has bubbles, move them, set their scale, etc....
+		// This is what I meant earlier by it being easier, now we can just use this weird lil loop to get the job done!
+
+		bmobj = player->follower->hnext; // will be NULL if there's no bubble
+
+		while (bmobj != NULL && P_MobjWasRemoved(bmobj) == false)
+		{
+			// match follower's momentums and (e)flags(2).
+			bmobj->momx = player->follower->momx;
+			bmobj->momy = player->follower->momy;
+			bmobj->z += FixedDiv(deltaz, fl.vertlag);
+			bmobj->momz = player->follower->momz;
+
+			P_SetScale(bmobj, FixedMul(bubble, player->mo->scale));
+			K_GenericExtraFlagsNoZAdjust(bmobj, player->follower);
+			bmobj->renderflags = player->mo->renderflags;
+
+			if (player->follower->threshold)
+			{
+				// threshold means the follower was "despawned" with S_NULL (is actually just set to S_INVISIBLE)
+				P_SetMobjState(bmobj, S_INVISIBLE); // sooooo... let's do the same!
+			}
+
+			// switch to other bubble layer or exit
+			bmobj = bmobj->hnext;
+		}
+
+		if (player->follower->threshold)
+		{
+			// Threshold means the follower was "despanwed" with S_NULL.
+			return;
+		}
+
+		// However with how the code is factored, this is just a special case of S_INVISBLE to avoid having to add other player variables.
+
+		// handle follower animations. Could probably be better...
+		// hurt or dead
+		if (P_PlayerInPain(player) == true || player->mo->state == &states[S_KART_SPINOUT] || player->mo->health <= 0)
+		{
+			// cancel hit confirm.
+			player->follower->movecount = 0;
+
+			// spin out
+			player->follower->angle = player->drawangle;
+
+			if (player->follower->extravalue1 != 2)
+			{
+				player->follower->extravalue1 = 2;
+				K_SetFollowerState(player->follower, fl.hurtstate);
+			}
+
+			if (player->mo->health <= 0)
+			{
+				// if dead, follow the player's z momentum exactly so they both look like they die at the same speed.
+				player->follower->momz = player->mo->momz;
+			}
+		}
+		else if (player->follower->movecount)
+		{
+			if (player->follower->extravalue1 != 5)
+			{
+				player->follower->extravalue1 = 5;
+				K_SetFollowerState(player->follower, fl.hitconfirmstate);
+			}
+
+			player->follower->movecount--;
+		}
+		else if (player->speed > 10*player->mo->scale)	// animation for moving fast enough
+		{
+			if (player->follower->extravalue1 != 1)
+			{
+				player->follower->extravalue1 = 1;
+				K_SetFollowerState(player->follower, fl.followstate);
+			}
+		}
+		else
+		{
+			// animations when nearly still. This includes winning and losing.
+			if (player->follower->extravalue1 != 0)
+			{
+				if (player->exiting)
+				{
+					// win/ loss animations
+					if (K_IsPlayerLosing(player))
+					{
+						// L
+						if (player->follower->extravalue1 != 4)
+						{
+							player->follower->extravalue1 = 4;
+							K_SetFollowerState(player->follower, fl.losestate);
+						}
+					}
+					else
+					{
+						// W
+						if (player->follower->extravalue1 != 3)
+						{
+							player->follower->extravalue1 = 3;
+							K_SetFollowerState(player->follower, fl.winstate);
+						}
+					}
+				}
+				else
+				{
+					// normal standstill
+					player->follower->extravalue1 = 0;
+					K_SetFollowerState(player->follower, fl.idlestate);
+				}
+			}
+		}
+	}
+}
diff --git a/src/k_follower.h b/src/k_follower.h
new file mode 100644
index 0000000000000000000000000000000000000000..27e070814b0503bafd598ae4a08e17e7189b7952
--- /dev/null
+++ b/src/k_follower.h
@@ -0,0 +1,143 @@
+// DR. ROBOTNIK'S RING RACERS
+//-----------------------------------------------------------------------------
+// Copyright (C) 2018-2022 by "Lat'"
+// Copyright (C) 2018-2022 by Kart Krew
+//
+// 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  k_follower.h
+/// \brief Code relating to the follower system
+
+#ifndef __K_FOLLOWER__
+#define __K_FOLLOWER__
+
+#include "doomdef.h"
+#include "doomstat.h"
+#include "r_skins.h"
+
+#define FOLLOWERCOLOR_MATCH UINT16_MAX
+#define FOLLOWERCOLOR_OPPOSITE (UINT16_MAX-1)
+
+extern CV_PossibleValue_t Followercolor_cons_t[]; // follower colours table, not a duplicate because of the "Match" option.
+
+typedef enum
+{
+	FOLLOWERMODE_FLOAT,		// Default behavior, floats in the position you set it to.
+	FOLLOWERMODE_GROUND,	// Snaps to the ground & rotates with slopes.
+	FOLLOWERMODE__MAX
+} followermode_t;
+
+//
+// We'll define these here because they're really just a mobj that'll follow some rules behind a player
+//
+typedef struct follower_s
+{
+	char skinname[SKINNAMESIZE+1];	// Skin Name. This is what to refer to when asking the commands anything.
+	char name[SKINNAMESIZE+1];		// Name. This is used for the menus. We'll just follow the same rules as skins for this.
+
+	skincolornum_t defaultcolor;	// default color for menus.
+	followermode_t mode;			// Follower behavior modifier.
+
+	fixed_t scale;			// Scale relative to the player's.
+	fixed_t bubblescale;	// Bubble scale relative to the player scale. If not set, no bubble will spawn (default)
+
+	// some position shenanigans:
+	angle_t atangle;		// angle the object will be at around the player. The object itself will always face the same direction as the player.
+	fixed_t dist;			// distance relative to the player. (In a circle)
+	fixed_t height;			// height of the follower, this is mostly important for Z flipping.
+	fixed_t zoffs;			// Z offset relative to the player's height. Cannot be negative.
+
+	// movement options
+
+	fixed_t horzlag;		// Lag for X/Y displacement. Default is 3. Must be > 0 because we divide by this number.
+	fixed_t vertlag;		// Z displacement lag. Default is 6. Must be > 0 because we divide by this number.
+	fixed_t anglelag;		// Angle rotation lag. Default is 8. Must be > 0 because we divide by this number.
+
+	fixed_t bobamp;			// Bob amplitude. Default is 4.
+	tic_t bobspeed;			// Arbitrary modifier for bobbing speed. Default is TICRATE*2 (70)
+
+	// from there on out, everything is STATES to allow customization
+	// these are only set once when the action is performed and are then free to animate however they want.
+
+	statenum_t idlestate;		// state when the player is at a standstill
+	statenum_t followstate;		// state when the player is moving
+	statenum_t hurtstate;		// state when the player is being hurt
+	statenum_t winstate;		// state when the player has won
+	statenum_t losestate;		// state when the player has lost
+	statenum_t hitconfirmstate;	// state for hit confirm
+	tic_t hitconfirmtime;		// time to keep the above playing for
+} follower_t;
+
+extern INT32 numfollowers;
+extern follower_t followers[MAXSKINS];
+
+/*--------------------------------------------------
+	INT32 K_FollowerAvailable(const char *name)
+
+		Check if a follower with the specified name
+		exists or not.
+
+	Input Arguments:-
+		name - The skin name of the follower to check for.
+
+	Return:-
+		The follower numerical ID of the follower,
+		or -1 if it doesn't exist.
+--------------------------------------------------*/
+
+INT32 K_FollowerAvailable(const char *name);
+
+
+/*--------------------------------------------------
+	boolean K_SetFollowerByName(INT32 playernum, const char *skinname)
+
+		Updates a player's follower type via a named value.
+		Calls "K_SetFollowerByNum" internally.
+
+	Input Arguments:-
+		playernum - The player ID to update
+		skinname - The follower's skin name
+
+	Return:-
+		true if it was a valid name for a follower,
+		otherwise false.
+--------------------------------------------------*/
+
+boolean K_SetFollowerByName(INT32 playernum, const char *skinname);
+
+
+/*--------------------------------------------------
+	void K_SetFollowerByNum(INT32 playernum, INT32 skinnum)
+
+		Updates a player's follower type via a numerical ID.
+
+	Input Arguments:-
+		playernum - The player ID to update.
+		skinnum - The follower's skin ID
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void K_SetFollowerByNum(INT32 playernum, INT32 skinnum);
+
+
+/*--------------------------------------------------
+	void K_HandleFollower(player_t *player)
+
+		Updates a player's follower pointer, and does
+		its positioning and animations.
+
+	Input Arguments:-
+		player - The player who we want to update the follower of.
+
+	Return:-
+		None
+--------------------------------------------------*/
+
+void K_HandleFollower(player_t *player);
+
+
+#endif // __K_FOLLOWER__
diff --git a/src/k_kart.c b/src/k_kart.c
index 91d73c25425158af67db2264a1f3a44ab67f1939..a39543711eb144b561afdb5f7c007ce4ff40fb22 100644
--- a/src/k_kart.c
+++ b/src/k_kart.c
@@ -37,6 +37,8 @@
 #include "k_terrain.h"
 #include "k_director.h"
 #include "k_collide.h"
+#include "k_follower.h"
+#include "k_objects.h"
 
 // SOME IMPORTANT VARIABLES DEFINED IN DOOMDEF.H:
 // gamespeed is cc (0 for easy, 1 for normal, 2 for hard)
@@ -352,25 +354,25 @@ static INT32 K_KartItemOddsRace[NUMKARTRESULTS-1][8] =
 				//P-Odds	 0  1  2  3  4  5  6  7
 			   /*Sneaker*/ { 0, 0, 2, 4, 6, 0, 0, 0 }, // Sneaker
 		/*Rocket Sneaker*/ { 0, 0, 0, 0, 0, 2, 4, 6 }, // Rocket Sneaker
-		 /*Invincibility*/ { 0, 0, 0, 0, 2, 4, 6, 9 }, // Invincibility
-				/*Banana*/ { 4, 3, 1, 0, 0, 0, 0, 0 }, // Banana
+		 /*Invincibility*/ { 0, 0, 0, 0, 3, 4, 6, 9 }, // Invincibility
+				/*Banana*/ { 2, 3, 1, 0, 0, 0, 0, 0 }, // Banana
 		/*Eggman Monitor*/ { 1, 2, 0, 0, 0, 0, 0, 0 }, // Eggman Monitor
 			  /*Orbinaut*/ { 5, 4, 2, 2, 0, 0, 0, 0 }, // Orbinaut
 				  /*Jawz*/ { 0, 3, 2, 1, 1, 0, 0, 0 }, // Jawz
 				  /*Mine*/ { 0, 2, 3, 1, 0, 0, 0, 0 }, // Mine
 			 /*Land Mine*/ { 3, 0, 0, 0, 0, 0, 0, 0 }, // Land Mine
-			   /*Ballhog*/ { 0, 0, 2, 1, 0, 0, 0, 0 }, // Ballhog
+			   /*Ballhog*/ { 0, 0, 2, 2, 0, 0, 0, 0 }, // Ballhog
    /*Self-Propelled Bomb*/ { 0, 0, 0, 0, 0, 2, 4, 0 }, // Self-Propelled Bomb
 				  /*Grow*/ { 0, 0, 0, 1, 2, 3, 0, 0 }, // Grow
 				/*Shrink*/ { 0, 0, 0, 0, 0, 0, 2, 0 }, // Shrink
 	  /*Lightning Shield*/ { 1, 2, 0, 0, 0, 0, 0, 0 }, // Lightning Shield
 		 /*Bubble Shield*/ { 0, 1, 2, 1, 0, 0, 0, 0 }, // Bubble Shield
 		  /*Flame Shield*/ { 0, 0, 0, 0, 0, 1, 3, 5 }, // Flame Shield
-			   /*Hyudoro*/ { 0, 0, 0, 1, 1, 0, 0, 0 }, // Hyudoro
+			   /*Hyudoro*/ { 3, 0, 0, 0, 0, 0, 0, 0 }, // Hyudoro
 		   /*Pogo Spring*/ { 0, 0, 0, 0, 0, 0, 0, 0 }, // Pogo Spring
 			/*Super Ring*/ { 2, 1, 1, 0, 0, 0, 0, 0 }, // Super Ring
 		  /*Kitchen Sink*/ { 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink
-		   /*Drop Target*/ { 4, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target
+		   /*Drop Target*/ { 3, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target
 			/*Sneaker x2*/ { 0, 0, 2, 2, 1, 0, 0, 0 }, // Sneaker x2
 			/*Sneaker x3*/ { 0, 0, 0, 2, 6,10, 5, 0 }, // Sneaker x3
 			 /*Banana x3*/ { 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3
@@ -457,9 +459,6 @@ static void K_KartGetItemResult(player_t *player, SINT8 getitem)
 	if (getitem == KITEM_SPB || getitem == KITEM_SHRINK) // Indirect items
 		indirectitemcooldown = 20*TICRATE;
 
-	if (getitem == KITEM_HYUDORO) // Hyudoro cooldown
-		hyubgone = 5*TICRATE;
-
 	player->botvars.itemdelay = TICRATE;
 	player->botvars.itemconfirm = 0;
 
@@ -681,6 +680,7 @@ INT32 K_KartGetItemOdds(
 		case KITEM_LANDMINE:
 		case KITEM_DROPTARGET:
 		case KITEM_BALLHOG:
+		case KITEM_HYUDORO:
 		case KRITEM_TRIPLESNEAKER:
 		case KRITEM_TRIPLEORBINAUT:
 		case KRITEM_QUADORBINAUT:
@@ -741,13 +741,6 @@ INT32 K_KartGetItemOdds(
 			if (spbplace != -1)
 				newodds = 0;
 			break;
-		case KITEM_HYUDORO:
-			cooldownOnStart = true;
-			notNearEnd = true;
-
-			if (hyubgone > 0)
-				newodds = 0;
-			break;
 		default:
 			break;
 	}
@@ -2812,7 +2805,7 @@ void K_PlayHitEmSound(mobj_t *source, mobj_t *victim)
 	if (source->player->follower)
 	{
 		follower_t fl = followers[source->player->followerskin];
-		source->player->follower->movecount = fl.hitconfirmtime;	// movecount is used to play the hitconfirm animation for followers.
+		source->player->follower->movecount = fl.hitconfirmtime; // movecount is used to play the hitconfirm animation for followers.
 	}
 
 	if (cv_kartvoices.value)
@@ -2922,6 +2915,7 @@ boolean K_TripwirePassConditions(player_t *player)
 			player->sneakertimer ||
 			player->growshrinktimer > 0 ||
 			player->flamedash ||
+			player->hyudorotimer ||
 			player->speed > 2 * K_GetKartSpeed(player, false, true)
 	)
 		return true;
@@ -3346,7 +3340,7 @@ fixed_t K_3dKartMovement(player_t *player)
 
 angle_t K_MomentumAngle(mobj_t *mo)
 {
-	if (mo->momx || mo->momy)
+	if (FixedHypot(mo->momx, mo->momy) >= mo->scale)
 	{
 		return R_PointToAngle2(0, 0, mo->momx, mo->momy);
 	}
@@ -3551,8 +3545,7 @@ static void K_RemoveGrowShrink(player_t *player)
 		else if (player->growshrinktimer < 0) // Play Grow noise
 			S_StartSound(player->mo, sfx_kc5a);
 
-		if (player->invincibilitytimer == 0)
-			player->mo->color = player->skincolor;
+		K_KartResetPlayerColor(player);
 
 		player->mo->scalespeed = mapobjectscale/TICRATE;
 		player->mo->destscale = mapobjectscale;
@@ -5398,6 +5391,7 @@ static void K_FlameDashLeftoverSmoke(mobj_t *src)
 	}
 }
 
+#if 0
 static void K_DoHyudoroSteal(player_t *player)
 {
 	INT32 i, numplayers = 0;
@@ -5475,6 +5469,7 @@ static void K_DoHyudoroSteal(player_t *player)
 			S_StartSound(NULL, sfx_s3k92);
 	}
 }
+#endif
 
 void K_DoSneaker(player_t *player, INT32 type)
 {
@@ -5619,7 +5614,6 @@ static void K_DoShrink(player_t *user)
 
 void K_DoPogoSpring(mobj_t *mo, fixed_t vertispeed, UINT8 sound)
 {
-	const fixed_t vscale = mapobjectscale + (mo->scale - mapobjectscale);
 	fixed_t thrust = 0;
 
 	if (mo->player && mo->player->spectator)
@@ -5660,7 +5654,7 @@ void K_DoPogoSpring(mobj_t *mo, fixed_t vertispeed, UINT8 sound)
 		//CONS_Printf("Got boost: %d%\n", mo->player->trickboostpower*100 / FRACUNIT);
 	}
 
-	mo->momz = FixedMul(thrust, vscale);
+	mo->momz = FixedMul(thrust, mapobjectscale);
 
 	if (mo->eflags & MFE_UNDERWATER)
 	{
@@ -6248,7 +6242,7 @@ static fixed_t K_BananaSlopeZ(pslope_t *slope, fixed_t x, fixed_t y, fixed_t z,
 	return P_GetZAt(slope, testx, testy, z);
 }
 
-static void K_CalculateBananaSlope(mobj_t *mobj, fixed_t x, fixed_t y, fixed_t z, fixed_t radius, fixed_t height, boolean flip, boolean player)
+void K_CalculateBananaSlope(mobj_t *mobj, fixed_t x, fixed_t y, fixed_t z, fixed_t radius, fixed_t height, boolean flip, boolean player)
 {
 	fixed_t newz;
 	sector_t *sec;
@@ -6637,12 +6631,10 @@ static void K_MoveHeldObjects(player_t *player)
 					targy = player->mo->y + P_ReturnThrustY(cur, cur->angle + angoffset, cur->extravalue1);
 
 					{ // bobbing, copy pasted from my kimokawaiii entry
-						const fixed_t pi = (22<<FRACBITS) / 7; // loose approximation, this doesn't need to be incredibly precise
-						fixed_t sine = FixedMul(player->mo->scale, 8 * FINESINE((((2*pi*(4*TICRATE)) * leveltime)>>ANGLETOFINESHIFT) & FINEMASK));
+						fixed_t sine = FixedMul(player->mo->scale, 8 * FINESINE((((M_TAU_FIXED * (4*TICRATE)) * leveltime) >> ANGLETOFINESHIFT) & FINEMASK));
 						targz = (player->mo->z + (player->mo->height/2)) + sine;
 						if (player->mo->eflags & MFE_VERTICALFLIP)
 							targz += (player->mo->height/2 - 32*player->mo->scale)*6;
-
 					}
 
 					if (cur->tracer)
@@ -7069,57 +7061,57 @@ static mobj_t *attractmo;
 static fixed_t attractdist;
 static fixed_t attractzdist;
 
-static inline boolean PIT_AttractingRings(mobj_t *thing)
+static inline BlockItReturn_t PIT_AttractingRings(mobj_t *thing)
 {
 	if (attractmo == NULL || P_MobjWasRemoved(attractmo) || attractmo->player == NULL)
 	{
-		return false;
+		return BMIT_ABORT;
 	}
 
 	if (thing == NULL || P_MobjWasRemoved(thing))
 	{
-		return true; // invalid
+		return BMIT_CONTINUE; // invalid
 	}
 
 	if (thing == attractmo)
 	{
-		return true; // invalid
+		return BMIT_CONTINUE; // invalid
 	}
 
 	if (!(thing->type == MT_RING || thing->type == MT_FLINGRING))
 	{
-		return true; // not a ring
+		return BMIT_CONTINUE; // not a ring
 	}
 
 	if (thing->health <= 0)
 	{
-		return true; // dead
+		return BMIT_CONTINUE; // dead
 	}
 
 	if (thing->extravalue1)
 	{
-		return true; // in special ring animation
+		return BMIT_CONTINUE; // in special ring animation
 	}
 
 	if (thing->tracer != NULL && P_MobjWasRemoved(thing->tracer) == false)
 	{
-		return true; // already attracted
+		return BMIT_CONTINUE; // already attracted
 	}
 
 	// see if it went over / under
 	if (attractmo->z - attractzdist > thing->z + thing->height)
 	{
-		return true; // overhead
+		return BMIT_CONTINUE; // overhead
 	}
 
 	if (attractmo->z + attractmo->height + attractzdist < thing->z)
 	{
-		return true; // underneath
+		return BMIT_CONTINUE; // underneath
 	}
 
 	if (P_AproxDistance(attractmo->x - thing->x, attractmo->y - thing->y) > attractdist + thing->radius)
 	{
-		return true; // Too far away
+		return BMIT_CONTINUE; // Too far away
 	}
 
 	if (RINGTOTAL(attractmo->player) >= 20 || (attractmo->player->pflags & PF_RINGLOCK))
@@ -7146,7 +7138,7 @@ static inline boolean PIT_AttractingRings(mobj_t *thing)
 		P_SetTarget(&thing->tracer, attractmo);
 	}
 
-	return true; // find other rings
+	return BMIT_CONTINUE; // find other rings
 }
 
 /** Looks for rings near a player in the blockmap.
@@ -7568,6 +7560,15 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
 	if (player->instashield)
 		player->instashield--;
 
+	if (player->justDI)
+	{
+		player->justDI--;
+
+		// return turning if player is fully actionable, no matter when!
+		if (!P_PlayerInPain(player))
+			player->justDI = 0;
+	}
+
 	if (player->eggmanexplode)
 	{
 		if (player->spectator || (gametype == GT_BATTLE && !player->bumpers))
@@ -7578,6 +7579,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
 			if (player->eggmanexplode <= 0)
 			{
 				mobj_t *eggsexplode;
+
+				K_KartResetPlayerColor(player);
+
 				//player->flashing = 0;
 				eggsexplode = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_SPBEXPLOSION);
 				if (player->eggmanblame >= 0
@@ -7657,37 +7661,45 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
 	K_HandleDelayedHitByEm(player);
 }
 
-void K_KartPlayerAfterThink(player_t *player)
+void K_KartResetPlayerColor(player_t *player)
 {
+	boolean forcereset = false;
 	boolean fullbright = false;
 
-	if (player->playerstate == PST_DEAD || (player->respawn.state == RESPAWNST_MOVE)) // Ensure these are set correctly here
+	if (!player->mo || P_MobjWasRemoved(player->mo)) // Can't do anything
+		return;
+
+	if (player->mo->health <= 0 || player->playerstate == PST_DEAD || (player->respawn.state == RESPAWNST_MOVE)) // Override everything
 	{
 		player->mo->colorized = (player->dye != 0);
 		player->mo->color = player->dye ? player->dye : player->skincolor;
+		goto finalise;
 	}
-	else if (player->eggmanexplode) // You're gonna diiiiie
+
+	if (player->eggmanexplode) // You're gonna diiiiie
 	{
 		const INT32 flashtime = 4<<(player->eggmanexplode/TICRATE);
 		if (player->eggmanexplode == 1 || (player->eggmanexplode % (flashtime/2) != 0))
 		{
-			player->mo->colorized = (player->dye != 0);
-			player->mo->color = player->dye ? player->dye : player->skincolor;
+			forcereset = true;
 		}
 		else if (player->eggmanexplode % flashtime == 0)
 		{
 			player->mo->colorized = true;
 			player->mo->color = SKINCOLOR_BLACK;
 			fullbright = true;
+			goto finalise;
 		}
 		else
 		{
 			player->mo->colorized = true;
 			player->mo->color = SKINCOLOR_CRIMSON;
 			fullbright = true;
+			goto finalise;
 		}
 	}
-	else if (player->invincibilitytimer)
+
+	if (player->invincibilitytimer) // You're gonna kiiiiill
 	{
 		const tic_t defaultTime = itemtime+(2*TICRATE);
 		tic_t flicker = 2;
@@ -7698,45 +7710,57 @@ void K_KartPlayerAfterThink(player_t *player)
 		{
 			player->mo->color = K_RainbowColor(leveltime / 2);
 			player->mo->colorized = true;
+			forcereset = false;
 		}
 		else
 		{
-			player->mo->color = player->skincolor;
-			player->mo->colorized = false;
-
 			flicker += (defaultTime - player->invincibilitytimer) / TICRATE / 2;
+			forcereset = true;
 		}
 
 		if (leveltime % flicker == 0)
 		{
 			player->mo->color = SKINCOLOR_INVINCFLASH;
 			player->mo->colorized = true;
+			forcereset = false;
+		}
+
+		if (!forcereset)
+		{
+			goto finalise;
 		}
 	}
-	else if (player->growshrinktimer) // Ditto, for grow/shrink
+
+	if (player->growshrinktimer) // Ditto, for grow/shrink
 	{
 		if (player->growshrinktimer % 5 == 0)
 		{
 			player->mo->colorized = true;
 			player->mo->color = (player->growshrinktimer < 0 ? SKINCOLOR_CREAMSICLE : SKINCOLOR_PERIWINKLE);
 			fullbright = true;
+			goto finalise;
 		}
-		else
-		{
-			player->mo->colorized = (player->dye != 0);
-			player->mo->color = player->dye ? player->dye : player->skincolor;
-		}
+
+		forcereset = true;
 	}
-	else if (player->ringboost && (leveltime & 1)) // ring boosting
+
+	if (player->ringboost && (leveltime & 1)) // ring boosting
 	{
 		player->mo->colorized = true;
 		fullbright = true;
+		goto finalise;
 	}
 	else
 	{
 		player->mo->colorized = (player->dye != 0);
+		if (forcereset)
+		{
+			player->mo->color = player->dye ? player->dye : player->skincolor;
+		}
 	}
 
+finalise:
+
 	if (player->curshield)
 	{
 		fullbright = true;
@@ -7751,6 +7775,11 @@ void K_KartPlayerAfterThink(player_t *player)
 		if (!(player->mo->state->frame & FF_FULLBRIGHT))
 			player->mo->frame &= ~FF_FULLBRIGHT;
 	}
+}
+
+void K_KartPlayerAfterThink(player_t *player)
+{
+	K_KartResetPlayerColor(player);
 
 	// Move held objects (Bananas, Orbinaut, etc)
 	K_MoveHeldObjects(player);
@@ -8315,7 +8344,7 @@ INT16 K_GetKartTurnValue(player_t *player, INT16 turnvalue)
 		return 0;
 	}
 
-	if (player->justDI == true)
+	if (player->justDI > 0)
 	{
 		// No turning until you let go after DI-ing.
 		return 0;
@@ -8877,6 +8906,8 @@ void K_StripOther(player_t *player)
 	{
 		player->eggmanexplode = 0;
 		player->eggmanblame = -1;
+
+		K_KartResetPlayerColor(player);
 	}
 }
 
@@ -10042,7 +10073,9 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 							if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
 							{
 								player->itemamount--;
-								K_DoHyudoroSteal(player); // yes. yes they do.
+								//K_DoHyudoroSteal(player); // yes. yes they do.
+								Obj_HyudoroDeploy(player->mo);
+								K_PlayAttackTaunt(player->mo);
 							}
 							break;
 						case KITEM_POGOSPRING:
@@ -10226,77 +10259,71 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 
 			else if (!(player->pflags & PF_TRICKDELAY))	// don't allow tricking at the same frame you tumble obv
 			{
+				INT16 aimingcompare = abs(cmd->throwdir) - abs(cmd->turning);
 
-				// "COOL" timing n shit.
-				if (cmd->turning || player->throwdir)
+				// Uses cmd->turning over steering intentionally.
+#define TRICKTHRESHOLD (KART_FULLTURN/4)
+				if (aimingcompare < -TRICKTHRESHOLD) // side trick
 				{
-					if (abs(momz) < FRACUNIT*99)	// Let's use that as baseline for PERFECT trick.
+					if (cmd->turning > 0)
 					{
-						player->karthud[khud_trickcool] = TICRATE;
+						P_InstaThrust(player->mo, player->mo->angle + lr, max(basespeed, speed*5/2));
+						player->trickpanel = 2;
+					}
+					else
+					{
+						P_InstaThrust(player->mo, player->mo->angle - lr, max(basespeed, speed*5/2));
+						player->trickpanel = 3;
 					}
 				}
-
-				// Uses cmd->turning over steering intentionally.
-				if (cmd->turning > 0)
+				else if (aimingcompare > TRICKTHRESHOLD) // forward/back trick
 				{
-					P_InstaThrust(player->mo, player->mo->angle + lr, max(basespeed, speed*5/2));
-					player->trickpanel = 2;
+					if (cmd->throwdir > 0) // back trick
+					{
+						if (player->mo->momz * P_MobjFlip(player->mo) > 0)
+						{
+							player->mo->momz = 0;
+						}
 
-					player->mo->hitlag = TRICKLAG;
-					player->mo->eflags &= ~MFE_DAMAGEHITLAG;
+						P_InstaThrust(player->mo, player->mo->angle, max(basespeed, speed*3));
+						player->trickpanel = 2;
+					}
+					else if (cmd->throwdir < 0)
+					{
+						boolean relative = true;
 
-					K_trickPanelTimingVisual(player, momz);
-				}
-				else if (cmd->turning < 0)
-				{
-					P_InstaThrust(player->mo, player->mo->angle - lr, max(basespeed, speed*5/2));
-					player->trickpanel = 3;
+						player->mo->momx /= 3;
+						player->mo->momy /= 3;
 
-					player->mo->hitlag = TRICKLAG;
-					player->mo->eflags &= ~MFE_DAMAGEHITLAG;
+						if (player->mo->momz * P_MobjFlip(player->mo) <= 0)
+						{
+							relative = false;
+						}
 
-					K_trickPanelTimingVisual(player, momz);
-				}
-				else if (cmd->throwdir > 0)
-				{
-					if (player->mo->momz * P_MobjFlip(player->mo) > 0)
-					{
-						player->mo->momz = 0;
-					}
+						// Calculate speed boost decay:
+						// Base speed boost duration is 35 tics.
+						// At most, lose 3/4th of your boost.
+						player->trickboostdecay = min(TICRATE*3/4, abs(momz/FRACUNIT));
+						//CONS_Printf("decay: %d\n", player->trickboostdecay);
 
-					P_InstaThrust(player->mo, player->mo->angle, max(basespeed, speed*3));
-					player->trickpanel = 2;
+						P_SetObjectMomZ(player->mo, 48*FRACUNIT, relative);
+						player->trickpanel = 4;
+					}
+				}
+#undef TRICKTHRESHOLD
 
+				// Finalise everything.
+				if (player->trickpanel != 1) // just changed from 1?
+				{
 					player->mo->hitlag = TRICKLAG;
 					player->mo->eflags &= ~MFE_DAMAGEHITLAG;
 
 					K_trickPanelTimingVisual(player, momz);
-				}
-				else if (cmd->throwdir < 0)
-				{
-					boolean relative = true;
-
-					player->mo->momx /= 3;
-					player->mo->momy /= 3;
 
-					if (player->mo->momz * P_MobjFlip(player->mo) <= 0)
+					if (abs(momz) < FRACUNIT*99)	// Let's use that as baseline for PERFECT trick.
 					{
-						relative = false;
+						player->karthud[khud_trickcool] = TICRATE;
 					}
-
-					// Calculate speed boost decay:
-					// Base speed boost duration is 35 tics.
-					// At most, lose 3/4th of your boost.
-					player->trickboostdecay = min(TICRATE*3/4, abs(momz/FRACUNIT));
-					//CONS_Printf("decay: %d\n", player->trickboostdecay);
-
-					P_SetObjectMomZ(player->mo, 48*FRACUNIT, relative);
-					player->trickpanel = 4;
-
-					player->mo->hitlag = TRICKLAG;
-					player->mo->eflags &= ~MFE_DAMAGEHITLAG;
-
-					K_trickPanelTimingVisual(player, momz);
 				}
 			}
 		}
@@ -10507,7 +10534,7 @@ void K_HandleDirectionalInfluence(player_t *player)
 	}
 
 	// DI attempted!!
-	player->justDI = true;
+	player->justDI = MAXHITLAGTICS;
 
 	cmd = &player->cmd;
 
diff --git a/src/k_kart.h b/src/k_kart.h
index 567cfda99cba1fa581543393a2dd3e234faa3448..5a6555db008a54547dd6f28f8c8dff407d9bd1cc 100644
--- a/src/k_kart.h
+++ b/src/k_kart.h
@@ -61,6 +61,7 @@ void K_SpawnInvincibilitySpeedLines(mobj_t *mo);
 void K_SpawnBumpEffect(mobj_t *mo);
 void K_KartMoveAnimation(player_t *player);
 void K_KartPlayerHUDUpdate(player_t *player);
+void K_KartResetPlayerColor(player_t *player);
 void K_KartPlayerThink(player_t *player, ticcmd_t *cmd);
 void K_KartPlayerAfterThink(player_t *player);
 angle_t K_MomentumAngle(mobj_t *mo);
@@ -94,6 +95,7 @@ void K_KillBananaChain(mobj_t *banana, mobj_t *inflictor, mobj_t *source);
 void K_UpdateHnextList(player_t *player, boolean clean);
 void K_DropHnextList(player_t *player, boolean keepshields);
 void K_RepairOrbitChain(mobj_t *orbit);
+void K_CalculateBananaSlope(mobj_t *mobj, fixed_t x, fixed_t y, fixed_t z, fixed_t radius, fixed_t height, boolean flip, boolean player);
 player_t *K_FindJawzTarget(mobj_t *actor, player_t *source);
 INT32 K_GetKartRingPower(player_t *player, boolean boosted);
 void K_UpdateDistanceFromFinishLine(player_t *const player);
diff --git a/src/k_objects.h b/src/k_objects.h
new file mode 100644
index 0000000000000000000000000000000000000000..45f9df78c0012705ff1b10afab3e7ebfc89ce711
--- /dev/null
+++ b/src/k_objects.h
@@ -0,0 +1,11 @@
+/* object-specific code */
+#ifndef k_objects_H
+#define k_objects_H
+
+/* Hyudoro */
+void Obj_HyudoroDeploy(mobj_t *master);
+void Obj_HyudoroThink(mobj_t *actor);
+void Obj_HyudoroCenterThink(mobj_t *actor);
+void Obj_HyudoroCollide(mobj_t *special, mobj_t *toucher);
+
+#endif/*k_objects_H*/
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index b4a971d996adc41843633917bb8551d83964ae8a..4b2bad33d19b17c42459dd33363c572f58b793ba 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -239,7 +239,7 @@ static int player_get(lua_State *L)
 	else if (fastcmp(field,"tumbleHeight"))
 		lua_pushinteger(L, plr->tumbleHeight);
 	else if (fastcmp(field,"justDI"))
-		lua_pushboolean(L, plr->justDI);
+		lua_pushinteger(L, plr->justDI);
 	else if (fastcmp(field,"flipDI"))
 		lua_pushboolean(L, plr->flipDI);
 	else if (fastcmp(field,"drift"))
@@ -593,7 +593,7 @@ static int player_set(lua_State *L)
 	else if (fastcmp(field,"tumbleHeight"))
 		plr->tumbleHeight = luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"justDI"))
-		plr->justDI = luaL_checkboolean(L, 3);
+		plr->justDI = luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"flipDI"))
 		plr->flipDI = luaL_checkboolean(L, 3);
 	else if (fastcmp(field,"drift"))
diff --git a/src/lua_script.c b/src/lua_script.c
index 3f919439f8ce6c6a52c989a3619ec1c6d2ebf425..8aea589db697d7e7cc2be3d797d1b6a180ac1d0b 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -378,9 +378,6 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"indirectitemcooldown")) {
 		lua_pushinteger(L, indirectitemcooldown);
 		return 1;
-	} else if (fastcmp(word,"hyubgone")) {
-		lua_pushinteger(L, hyubgone);
-		return 1;
 	} else if (fastcmp(word,"thwompsactive")) {
 		lua_pushboolean(L, thwompsactive);
 		return 1;
@@ -468,8 +465,6 @@ int LUA_WriteGlobals(lua_State *L, const char *word)
 		exitcountdown = (tic_t)luaL_checkinteger(L, 2);
 	else if (fastcmp(word,"indirectitemcooldown"))
 		indirectitemcooldown = (tic_t)luaL_checkinteger(L, 2);
-	else if (fastcmp(word,"hyubgone"))
-		hyubgone = (tic_t)luaL_checkinteger(L, 2);
 	else
 		return 0;
 
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 49108ab931c2b72f10c138b489a0e77aaeefe22f..9f3bb29107ce41ff7871a3d3fa13b6226c207803 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -37,6 +37,8 @@
 #define M_TAU_FIXED 411769
 #endif
 
+#define M_PI_FIXED (M_TAU_FIXED >> 1)
+
 typedef INT32 fixed_t;
 
 /*!
diff --git a/src/m_menu.c b/src/m_menu.c
index 8255e5f35bd1b7357bf88c121ad709f1e726b4e2..a19504864ffd91864032dc0b0fef735885d03786 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -65,6 +65,7 @@
 #include "d_player.h" // KITEM_ constants
 #include "k_color.h"
 #include "k_grandprix.h"
+#include "k_follower.h"
 #include "r_fps.h"
 
 #include "i_joy.h" // for joystick menu controls
@@ -3119,26 +3120,6 @@ void M_Drawer(void)
 			M_GetGametypeColor();
 			currentMenu->drawroutine(); // call current menu Draw routine
 		}
-
-		// Draw version down in corner
-		// ... but only in the MAIN MENU.  I'm a picky bastard.
-		if (currentMenu == &MainDef)
-		{
-			if (customversionstring[0] != '\0')
-			{
-				V_DrawThinString(vid.dupx, vid.height - 20*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT, "Mod version:");
-				V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, customversionstring);
-			}
-			else
-			{
-#ifdef DEVELOP // Development -- show revision / branch info
-				V_DrawThinString(vid.dupx, vid.height - 20*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, compbranch);
-				V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, comprevision);
-#else // Regular build
-				V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, va("%s", VERSIONSTRING));
-#endif
-			}
-		}
 	}
 
 	// focus lost notification goes on top of everything, even the former everything
@@ -9665,12 +9646,12 @@ static void M_DrawSetupMultiPlayerMenu(void)
 
 			// Fake the follower's in game appearance by now also applying some of its variables! coolio, eh?
 			follower_t fl = followers[setupm_fakefollower];	// shortcut for our sanity
+
 			// smooth floating, totally not stolen from rocket sneakers.
-			const fixed_t pi = (22<<FRACBITS) / 7; // loose approximation, this doesn't need to be incredibly precise
-			fixed_t sine = fl.bobamp * FINESINE((((8*pi*(fl.bobspeed)) * followertimer)>>ANGLETOFINESHIFT) & FINEMASK);
+			fixed_t sine = FixedMul(fl.bobamp, FINESINE(((FixedMul(4 * M_TAU_FIXED, fl.bobspeed) * followertimer)>>ANGLETOFINESHIFT) & FINEMASK));
 
-			UINT8 *colormap = R_GetTranslationColormap(-1, setupm_fakecolor->color, 0);
-			V_DrawFixedPatch((mx+65)*FRACUNIT, (my+131-fl.zoffs)*FRACUNIT+sine, fl.scale, flags, patch, colormap);
+			UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, setupm_fakecolor->color, 0); // why does GTC_MENUCACHE not work here...?
+			V_DrawFixedPatch((mx+65)*FRACUNIT, ((my+131)*FRACUNIT)-fl.zoffs+sine, fl.scale, flags, patch, colormap);
 			Z_Free(colormap);
 		}
 	}
@@ -9682,7 +9663,7 @@ static void M_DrawSetupMultiPlayerMenu(void)
 static void M_GetFollowerState(void)
 {
 
-	if (setupm_fakefollower <= -1 || setupm_fakefollower > numfollowers-1)	// yikes, there's none!
+	if (setupm_fakefollower <= -1 || setupm_fakefollower >= numfollowers) // yikes, there's none!
 		return;
 	// ^ we don't actually need to set anything since it won't be displayed anyway.
 
@@ -9830,13 +9811,13 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 	// check followers:
 	if (setupm_fakefollower < -1)
 	{
-		setupm_fakefollower = numfollowers-1;
-		M_GetFollowerState();	// update follower state
+		setupm_fakefollower = numfollowers;
+		M_GetFollowerState(); // update follower state
 	}
-	if (setupm_fakefollower > numfollowers-1)
+	if (setupm_fakefollower >= numfollowers)
 	{
 		setupm_fakefollower = -1;
-		M_GetFollowerState();	// update follower state
+		M_GetFollowerState(); // update follower state
 	}
 
 	// check color
@@ -9878,7 +9859,7 @@ static void M_SetupMultiPlayer(INT32 choice)
 	setupm_fakefollower = atoi(setupm_cvfollower->string);	// update fake follower value
 
 	// yikes, we don't want none of that...
-	if (setupm_fakefollower > numfollowers-1)
+	if (setupm_fakefollower >= numfollowers)
 		setupm_fakefollower = -1;
 
 	M_GetFollowerState();	// update follower state
@@ -9921,7 +9902,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 	setupm_fakefollower = atoi(setupm_cvfollower->string);	// update fake follower value
 
 	// yikes, we don't want none of that...
-	if (setupm_fakefollower > numfollowers-1)
+	if (setupm_fakefollower >= numfollowers)
 		setupm_fakefollower = -1;
 
 	M_GetFollowerState();	// update follower state
@@ -9964,7 +9945,7 @@ static void M_SetupMultiPlayer3(INT32 choice)
 	setupm_fakefollower = atoi(setupm_cvfollower->string);	// update fake follower value
 
 	// yikes, we don't want none of that...
-	if (setupm_fakefollower > numfollowers-1)
+	if (setupm_fakefollower >= numfollowers)
 		setupm_fakefollower = -1;
 
 	M_GetFollowerState();	// update follower state
@@ -10007,7 +9988,7 @@ static void M_SetupMultiPlayer4(INT32 choice)
 	setupm_fakefollower = atoi(setupm_cvfollower->string);	// update fake follower value
 
 	// yikes, we don't want none of that...
-	if (setupm_fakefollower > numfollowers-1)
+	if (setupm_fakefollower >= numfollowers)
 		setupm_fakefollower = -1;
 
 	M_GetFollowerState();	// update follower state
diff --git a/src/objects/Sourcefile b/src/objects/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..f7e4f2491c434b7ff6dceda87a8cbe8063d082ef
--- /dev/null
+++ b/src/objects/Sourcefile
@@ -0,0 +1 @@
+hyudoro.c
diff --git a/src/objects/hyudoro.c b/src/objects/hyudoro.c
new file mode 100644
index 0000000000000000000000000000000000000000..af0241fe34e85665ab7f95570c76f1a3c5080238
--- /dev/null
+++ b/src/objects/hyudoro.c
@@ -0,0 +1,481 @@
+#include "../doomdef.h"
+#include "../doomstat.h"
+#include "../info.h"
+#include "../k_kart.h"
+#include "../k_objects.h"
+#include "../m_random.h"
+#include "../p_local.h"
+#include "../r_main.h"
+#include "../s_sound.h"
+
+enum {
+	HYU_PATROL,
+	HYU_RETURN,
+	HYU_HOVER,
+};
+
+// TODO: make these general functions
+
+static fixed_t
+K_GetSpeed (mobj_t *mobj)
+{
+	return FixedHypot(mobj->momx, mobj->momy);
+}
+
+static void
+K_ChangePlayerItem
+(		player_t * player,
+		INT32 itemtype,
+		INT32 itemamount)
+{
+	player->itemtype = itemtype;
+	player->itemamount = itemamount;
+	K_UnsetItemOut(player);
+}
+
+#define hyudoro_mode(o) ((o)->extravalue1)
+#define hyudoro_itemtype(o) ((o)->movefactor)
+#define hyudoro_itemcount(o) ((o)->movecount)
+#define hyudoro_hover_stack(o) ((o)->threshold)
+#define hyudoro_next(o) ((o)->tracer)
+#define hyudoro_stackpos(o) ((o)->reactiontime)
+
+// cannot be combined
+#define hyudoro_center(o) ((o)->target)
+#define hyudoro_target(o) ((o)->target)
+
+#define hyudoro_center_max_radius(o) ((o)->threshold)
+#define hyudoro_center_master(o) ((o)->target)
+
+static angle_t
+trace_angle (mobj_t *hyu)
+{
+	mobj_t *center = hyu->target;
+
+	if (hyu->x != center->x || hyu->y != center->y)
+	{
+		return R_PointToAngle2(
+				center->x, center->y, hyu->x, hyu->y);
+	}
+	else
+		return hyu->angle;
+}
+
+static angle_t
+get_look_angle (mobj_t *thing)
+{
+	player_t *player = thing->player;
+
+	return player ? player->angleturn : thing->angle;
+}
+
+static boolean
+is_hyudoro (mobj_t *thing)
+{
+	return thing && thing->type == MT_HYUDORO;
+}
+
+static mobj_t *
+get_hyudoro_master (mobj_t *hyu)
+{
+	mobj_t *center = hyudoro_center(hyu);
+
+	return center ? hyudoro_center_master(center) : NULL;
+}
+
+static player_t *
+get_hyudoro_target_player (mobj_t *hyu)
+{
+	mobj_t *target = hyudoro_target(hyu);
+
+	return target ? target->player : NULL;
+}
+
+static void
+sine_bob
+(		mobj_t * hyu,
+		angle_t a,
+		fixed_t sineofs)
+{
+	hyu->sprzoff = FixedMul(hyu->height,
+			sineofs + FINESINE(a >> ANGLETOFINESHIFT));
+}
+
+static void
+project_hyudoro (mobj_t *hyu)
+{
+	mobj_t *center = hyudoro_center(hyu);
+
+	angle_t angleStep = FixedMul(5 * ANG1,
+			FixedDiv(hyudoro_center_max_radius(center),
+				center->radius));
+
+	angle_t angle = trace_angle(hyu) + angleStep;
+
+	fixed_t d = center->radius;
+
+	fixed_t x = P_ReturnThrustX(center, angle, d);
+	fixed_t y = P_ReturnThrustY(center, angle, d);
+
+	hyu->momx = (center->x + x) - hyu->x;
+	hyu->momy = (center->y + y) - hyu->y;
+	hyu->angle = angle + ANGLE_90;
+
+	sine_bob(hyu, angle, FRACUNIT);
+}
+
+static void
+project_hyudoro_hover (mobj_t *hyu)
+{
+	const INT32 bob_speed = 64;
+
+	mobj_t *target = hyudoro_target(hyu);
+
+	// Turns a bit toward its target
+	angle_t ang = get_look_angle(target) + ANGLE_67h;
+	fixed_t rad = (target->radius * 2) + hyu->radius;
+
+	fixed_t zofs = hyudoro_stackpos(hyu) *
+		((target->height / 2) + (hyu->height * 2));
+
+	P_MoveOrigin(hyu,
+			target->x - P_ReturnThrustX(hyu, ang, rad),
+			target->y - P_ReturnThrustY(hyu, ang, rad),
+			target->z + (zofs * P_MobjFlip(target)));
+
+	// Cancel momentum from HYU_RETURN.
+	// (And anything else! I don't trust this game!!)
+	hyu->momx = 0;
+	hyu->momy = 0;
+
+	hyu->angle = ang;
+
+	// copies sprite tilting
+	hyu->pitch = target->pitch;
+	hyu->roll = target->roll;
+
+	sine_bob(hyu,
+			(leveltime & (bob_speed - 1)) *
+			(ANGLE_MAX / bob_speed), -(3*FRACUNIT/4));
+}
+
+static void
+spawn_hyudoro_shadow (mobj_t *hyu)
+{
+	mobj_t *shadow = P_SpawnMobjFromMobj(
+			hyu, 0, 0, 0, MT_SHADOW);
+
+	shadow->whiteshadow = true;
+
+	shadow->shadowscale = hyu->shadowscale;
+	hyu->shadowscale = 0;
+
+	P_SetTarget(&shadow->tracer, hyu);
+}
+
+static void
+move_to_player (mobj_t *hyu)
+{
+	mobj_t *target = hyudoro_target(hyu);
+
+	angle_t angle;
+
+	if (!target)
+		return;
+
+	angle = R_PointToAngle2(
+			hyu->x, hyu->y, target->x, target->y);
+
+	P_InstaThrust(hyu, angle, (hyu->radius / 2) +
+			max(hyu->radius, K_GetSpeed(target)));
+
+	hyu->z = target->z; // stay level with target
+	hyu->angle = angle;
+}
+
+static void
+deliver_item (mobj_t *hyu)
+{
+	mobj_t *target = hyudoro_target(hyu);
+	player_t *player = target->player;
+
+	P_SetTarget(&hyudoro_target(hyu), NULL);
+
+	if (player)
+	{
+		K_ChangePlayerItem(player,
+				hyudoro_itemtype(hyu),
+				player->itemamount + hyudoro_itemcount(hyu));
+	}
+
+	S_StartSound(target, sfx_itpick);
+
+	// Stop moving here
+	hyu->momx = 0;
+	hyu->momy = 0;
+
+	hyu->tics = 4;
+
+	hyu->destscale = target->scale / 4;
+	hyu->scalespeed =
+		abs(hyu->scale - hyu->destscale) / hyu->tics;
+}
+
+static void
+append_hyudoro
+(		mobj_t ** head,
+		mobj_t * hyu)
+{
+	INT32 lastpos = 0;
+
+	while (is_hyudoro(*head))
+	{
+		lastpos = hyudoro_stackpos(*head);
+		head = &hyudoro_next(*head);
+	}
+
+	hyudoro_stackpos(hyu) = lastpos + 1;
+	*head = hyu;
+}
+
+static void
+pop_hyudoro (mobj_t **head)
+{
+	mobj_t *hyu = *head;
+
+	INT32 lastpos;
+	INT32 thispos;
+
+	if (is_hyudoro(hyu))
+	{
+		lastpos = hyudoro_stackpos(hyu);
+		hyu = hyudoro_next(hyu);
+
+		while (is_hyudoro(hyu))
+		{
+			thispos = hyudoro_stackpos(hyu);
+
+			hyudoro_stackpos(hyu) = lastpos;
+			lastpos = thispos;
+
+			hyu = hyudoro_next(hyu);
+		}
+	}
+
+	*head = hyu;
+}
+
+static boolean
+hyudoro_patrol_hit_player
+(		mobj_t * hyu,
+		mobj_t * toucher)
+{
+	player_t *player = toucher->player;
+
+	mobj_t *center = hyudoro_center(hyu);
+
+	if (!player)
+		return false;
+
+	// Cannot hit its master
+	if (toucher == get_hyudoro_master(hyu))
+		return false;
+
+	// Don't punish a punished player
+	if (player->hyudorotimer)
+		return false;
+
+	// NO ITEM?
+	if (!player->itemamount)
+		return false;
+
+	K_AddHitLag(toucher, TICRATE/2, true);
+
+	hyudoro_mode(hyu) = HYU_RETURN;
+	hyudoro_itemtype(hyu) = player->itemtype;
+	hyudoro_itemcount(hyu) = player->itemamount;
+
+	K_StripItems(player);
+
+	player->hyudorotimer = hyudorotime;
+	player->stealingtimer = hyudorotime;
+
+	P_SetTarget(&hyudoro_target(hyu),
+			hyudoro_center_master(center));
+
+	if (center)
+		P_RemoveMobj(center);
+
+	hyu->renderflags &= ~(RF_DONTDRAW);
+
+	return true;
+}
+
+static boolean
+award_immediately (mobj_t *hyu)
+{
+	player_t *player = get_hyudoro_target_player(hyu);
+
+	if (player)
+	{
+		if (player->itemamount &&
+				player->itemtype != hyudoro_itemtype(hyu))
+		{
+			return false;
+		}
+
+		// Same as picking up paper items; get stacks
+		// immediately
+		if (!P_CanPickupItem(player, 3))
+			return false;
+	}
+
+	deliver_item(hyu);
+
+	return true;
+}
+
+static boolean
+hyudoro_return_hit_player
+(		mobj_t * hyu,
+		mobj_t * toucher)
+{
+	if (toucher != hyudoro_target(hyu))
+		return false;
+
+	// If the player already has an item, just hover beside
+	// them until they use/lose it.
+	if (!award_immediately(hyu))
+	{
+		hyudoro_mode(hyu) = HYU_HOVER;
+		append_hyudoro(&toucher->player->hoverhyudoro, hyu);
+	}
+
+	return true;
+}
+
+static boolean
+hyudoro_hover_await_stack (mobj_t *hyu)
+{
+	player_t *player = get_hyudoro_target_player(hyu);
+
+	if (!player)
+		return false;
+
+	// First in stack goes first
+	if (hyu != player->hoverhyudoro)
+		return false;
+
+	if (!award_immediately(hyu))
+		return false;
+
+	pop_hyudoro(&player->hoverhyudoro);
+
+	return true;
+}
+
+void
+Obj_HyudoroDeploy (mobj_t *master)
+{
+	mobj_t *center = P_SpawnMobjFromMobj(
+			master, 0, 0, 0, MT_HYUDORO_CENTER);
+
+	mobj_t *hyu = P_SpawnMobjFromMobj(
+			center, 0, 0, 0, MT_HYUDORO);
+
+	// This allows a Lua override
+	if (!hyudoro_center_max_radius(center))
+	{
+		hyudoro_center_max_radius(center) =
+			128 * center->scale;
+	}
+
+	center->radius = hyu->radius;
+
+	P_InitAngle(hyu, master->angle);
+	P_SetTarget(&hyudoro_center(hyu), center);
+	P_SetTarget(&hyudoro_center_master(center), master);
+
+	hyudoro_mode(hyu) = HYU_PATROL;
+
+	// Set splitscreen player visibility
+	if (master->player)
+	{
+		hyu->renderflags |= RF_DONTDRAW &
+			~(K_GetPlayerDontDrawFlag(master->player));
+	}
+
+	spawn_hyudoro_shadow(hyu); // this sucks btw
+
+	S_StartSound(master, sfx_s3k92); // scary ghost noise
+}
+
+void
+Obj_HyudoroThink (mobj_t *hyu)
+{
+	// Might get set from clipping slopes
+	hyu->momz = 0;
+
+	switch (hyudoro_mode(hyu))
+	{
+		case HYU_PATROL:
+			if (hyudoro_center(hyu))
+				project_hyudoro(hyu);
+
+			if (leveltime & 1)
+			{
+				mobj_t *ghost = P_SpawnGhostMobj(hyu);
+
+				// Flickers every frame
+				ghost->extravalue1 = 1;
+				ghost->extravalue2 = 2;
+
+				// copy per-splitscreen-player visibility
+				ghost->renderflags =
+					(hyu->renderflags & RF_DONTDRAW);
+
+				ghost->tics = 8;
+
+				P_SetTarget(&ghost->tracer, hyu);
+			}
+			break;
+
+		case HYU_RETURN:
+			move_to_player(hyu);
+			break;
+
+		case HYU_HOVER:
+			if (hyudoro_target(hyu))
+			{
+				project_hyudoro_hover(hyu);
+				hyudoro_hover_await_stack(hyu);
+			}
+			break;
+	}
+}
+
+void
+Obj_HyudoroCenterThink (mobj_t *center)
+{
+	fixed_t max_radius = hyudoro_center_max_radius(center);
+
+	if (center->radius < max_radius)
+		center->radius += max_radius / 64;
+}
+
+void
+Obj_HyudoroCollide
+(		mobj_t * hyu,
+		mobj_t * toucher)
+{
+	switch (hyudoro_mode(hyu))
+	{
+		case HYU_PATROL:
+			hyudoro_patrol_hit_player(hyu, toucher);
+			break;
+
+		case HYU_RETURN:
+			hyudoro_return_hit_player(hyu, toucher);
+			break;
+	}
+}
diff --git a/src/p_enemy.c b/src/p_enemy.c
index efc83aca3a34db93cfa8cdb782a18dc89db37787..c4e8bceb4be5de5143d19c5e557b2253c812f76c 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -4018,7 +4018,7 @@ void A_AttractChase(mobj_t *actor)
 			{
 				fixed_t dist = (4*actor->target->scale) * (16 - actor->extravalue1);
 
-				P_SetScale(actor, (actor->destscale = actor->target->scale - ((actor->target->scale/14) * actor->extravalue1)));
+				P_SetScale(actor, (actor->destscale = mapobjectscale - ((mapobjectscale/14) * actor->extravalue1)));
 				actor->z = actor->target->z;
 				K_MatchGenericExtraFlags(actor, actor->target);
 				P_MoveOrigin(actor,
@@ -4573,26 +4573,26 @@ void A_ShootBullet(mobj_t *actor)
 
 static mobj_t *minus;
 
-static boolean PIT_MinusCarry(mobj_t *thing)
+static BlockItReturn_t PIT_MinusCarry(mobj_t *thing)
 {
 	if (minus->tracer)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (minus->type == thing->type)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (!(thing->flags & (MF_PUSHABLE|MF_ENEMY)))
-		return true;
+		return BMIT_CONTINUE;
 
 	if (P_AproxDistance(minus->x - thing->x, minus->y - thing->y) >= minus->radius*3)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (abs(thing->z - minus->z) > minus->height)
-		return true;
+		return BMIT_CONTINUE;
 
 	P_SetTarget(&minus->tracer, thing);
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 // Function: A_MinusDigging
@@ -12145,13 +12145,13 @@ static mobj_t *barrel;
 static fixed_t exploderadius;
 static fixed_t explodethrust;
 
-static boolean PIT_TNTExplode(mobj_t *nearby)
+static BlockItReturn_t PIT_TNTExplode(mobj_t *nearby)
 {
 	fixed_t dx, dy, dz;
 	fixed_t dm;
 
 	if (nearby == barrel)
-		return true;
+		return BMIT_CONTINUE;
 
 	dx = nearby->x - barrel->x;
 	dy = nearby->y - barrel->y;
@@ -12159,7 +12159,7 @@ static boolean PIT_TNTExplode(mobj_t *nearby)
 	dm = P_AproxDistance(P_AproxDistance(dx, dy), dz);
 
 	if (dm >= exploderadius || !P_CheckSight(barrel, nearby)) // out of range or not visible
-		return true;
+		return BMIT_CONTINUE;
 
 	if (barrel->type == nearby->type) // nearby is also a barrel
 	{
@@ -12200,7 +12200,7 @@ static boolean PIT_TNTExplode(mobj_t *nearby)
 		}
 	}
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 // Function: A_TNTExplode
diff --git a/src/p_inter.c b/src/p_inter.c
index 8a4d6754ceda90ca992d35c8a5e02546d0b0cf32..27455872def7c0c126bd1711abf13350a2f29d32 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -37,6 +37,7 @@
 #include "k_boss.h"
 #include "k_respawn.h"
 #include "p_spec.h"
+#include "k_objects.h"
 
 // CTF player names
 #define CTFTEAMCODE(pl) pl->ctfteam ? (pl->ctfteam == 1 ? "\x85" : "\x84") : ""
@@ -483,6 +484,10 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			S_StartSound(toucher, sfx_s1b2);
 			return;
 
+		case MT_HYUDORO:
+			Obj_HyudoroCollide(special, toucher);
+			return;
+
 		case MT_RING:
 		case MT_FLINGRING:
 			if (special->extravalue1)
@@ -1767,8 +1772,7 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source,
 
 	player->carry = CR_NONE;
 
-	player->mo->color = player->skincolor;
-	player->mo->colorized = false;
+	K_KartResetPlayerColor(player);
 
 	P_ResetPlayer(player);
 
diff --git a/src/p_local.h b/src/p_local.h
index 4940daf81d4e252812cd4e0981dc435e8a10fa82..82b1d445cd9296ed73b2061f996785955b692868 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -435,7 +435,7 @@ void P_RadiusAttack(mobj_t *spot, mobj_t *source, fixed_t damagedist, UINT8 dama
 
 fixed_t P_FloorzAtPos(fixed_t x, fixed_t y, fixed_t z, fixed_t height);
 fixed_t P_CeilingzAtPos(fixed_t x, fixed_t y, fixed_t z, fixed_t height);
-boolean PIT_PushableMoved(mobj_t *thing);
+BlockItReturn_t PIT_PushableMoved(mobj_t *thing);
 
 boolean P_DoSpring(mobj_t *spring, mobj_t *object);
 
diff --git a/src/p_map.c b/src/p_map.c
index f7596816957ddd58d07fdb59fef256e37837a2f5..473ded5fe3f86bc50a8d655c2047c30a0bac831e 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -523,37 +523,37 @@ static void P_DoFanAndGasJet(mobj_t *spring, mobj_t *object)
 //
 // PIT_CheckThing
 //
-static boolean PIT_CheckThing(mobj_t *thing)
+static BlockItReturn_t PIT_CheckThing(mobj_t *thing)
 {
 	fixed_t blockdist;
 
-	// don't clip against self
-	if (thing == tmthing)
-		return true;
+	if (tmthing == NULL || P_MobjWasRemoved(tmthing) == true)
+		return BMIT_STOP; // func just popped our tmthing, cannot continue.
 
 	// Ignore... things.
-	if (!tmthing || !thing || P_MobjWasRemoved(thing))
-		return true;
+	if (thing == NULL || P_MobjWasRemoved(thing) == true)
+		return BMIT_CONTINUE;
 
-	I_Assert(!P_MobjWasRemoved(tmthing));
-	I_Assert(!P_MobjWasRemoved(thing));
+	// don't clip against self
+	if (thing == tmthing)
+		return BMIT_CONTINUE;
 
 	// Ignore spectators
 	if ((tmthing->player && tmthing->player->spectator)
 	|| (thing->player && thing->player->spectator))
-		return true;
+		return BMIT_CONTINUE;
 
 	// Ignore the collision if BOTH things are in hitlag.
 	if (thing->hitlag > 0 && tmthing->hitlag > 0)
-		return true;
+		return BMIT_CONTINUE;
 
 	if ((thing->flags & MF_NOCLIPTHING) || !(thing->flags & (MF_SOLID|MF_SPECIAL|MF_PAIN|MF_SHOOTABLE|MF_SPRING)))
-		return true;
+		return BMIT_CONTINUE;
 
 	blockdist = thing->radius + tmthing->radius;
 
 	if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-		return true; // didn't hit it
+		return BMIT_CONTINUE; // didn't hit it
 
 	if (thing->flags & MF_PAPERCOLLISION) // CAUTION! Very easy to get stuck inside MF_SOLID objects. Giving the player MF_PAPERCOLLISION is a bad idea unless you know what you're doing.
 	{
@@ -580,23 +580,23 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			fixed_t tmcosradius = FixedMul(tmthing->radius, FINECOSINE(tmthing->angle>>ANGLETOFINESHIFT));
 			fixed_t tmsinradius = FixedMul(tmthing->radius, FINESINE(tmthing->angle>>ANGLETOFINESHIFT));
 			if (abs(thing->x - tmx) >= (abs(tmcosradius) + abs(cosradius)) || abs(thing->y - tmy) >= (abs(tmsinradius) + abs(sinradius)))
-				return true; // didn't hit it
+				return BMIT_CONTINUE; // didn't hit it
 			check1 = P_PointOnLineSide(tmx - tmcosradius, tmy - tmsinradius, &junk);
 			check2 = P_PointOnLineSide(tmx + tmcosradius, tmy + tmsinradius, &junk);
 			check3 = P_PointOnLineSide(tmx + tmthing->momx - tmcosradius, tmy + tmthing->momy - tmsinradius, &junk);
 			check4 = P_PointOnLineSide(tmx + tmthing->momx + tmcosradius, tmy + tmthing->momy + tmsinradius, &junk);
 			if ((check1 == check2) && (check2 == check3) && (check3 == check4))
-				return true; // the line doesn't cross between collider's start or end
+				return BMIT_CONTINUE; // the line doesn't cross between collider's start or end
 		}
 		else
 		{
 			if (abs(thing->x - tmx) >= (tmthing->radius + abs(cosradius)) || abs(thing->y - tmy) >= (tmthing->radius + abs(sinradius)))
-				return true; // didn't hit it
+				return BMIT_CONTINUE; // didn't hit it
 			if ((P_PointOnLineSide(tmx - tmthing->radius, tmy - tmthing->radius, &junk)
 			== P_PointOnLineSide(tmx + tmthing->radius, tmy + tmthing->radius, &junk))
 			&& (P_PointOnLineSide(tmx + tmthing->radius, tmy - tmthing->radius, &junk)
 			== P_PointOnLineSide(tmx - tmthing->radius, tmy + tmthing->radius, &junk)))
-				return true; // the line doesn't cross between either pair of opposite corners
+				return BMIT_CONTINUE; // the line doesn't cross between either pair of opposite corners
 		}
 	}
 	else if (tmthing->flags & MF_PAPERCOLLISION)
@@ -609,7 +609,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		tmsinradius = FixedMul(tmthing->radius, FINESINE(tmthing->angle>>ANGLETOFINESHIFT));
 
 		if (abs(thing->x - tmx) >= (thing->radius + abs(tmcosradius)) || abs(thing->y - tmy) >= (thing->radius + abs(tmsinradius)))
-			return true; // didn't hit it
+			return BMIT_CONTINUE; // didn't hit it
 
 		v1.x = tmx - tmcosradius;
 		v1.y = tmy - tmsinradius;
@@ -626,34 +626,34 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		== P_PointOnLineSide(thing->x + thing->radius, thing->y + thing->radius, &junk))
 		&& (P_PointOnLineSide(thing->x + thing->radius, thing->y - thing->radius, &junk)
 		== P_PointOnLineSide(thing->x - thing->radius, thing->y + thing->radius, &junk)))
-			return true; // the line doesn't cross between either pair of opposite corners
+			return BMIT_CONTINUE; // the line doesn't cross between either pair of opposite corners
 	}
 
 	{
 		UINT8 shouldCollide = LUAh_MobjCollide(thing, tmthing); // checks hook for thing's type
 		if (P_MobjWasRemoved(tmthing) || P_MobjWasRemoved(thing))
-			return true; // one of them was removed???
+			return BMIT_CONTINUE; // one of them was removed???
 		if (shouldCollide == 1)
-			return false; // force collide
+			return BMIT_ABORT; // force collide
 		else if (shouldCollide == 2)
-			return true; // force no collide
+			return BMIT_CONTINUE; // force no collide
 
 		shouldCollide = LUAh_MobjMoveCollide(tmthing, thing); // checks hook for tmthing's type
 		if (P_MobjWasRemoved(tmthing) || P_MobjWasRemoved(thing))
-			return true; // one of them was removed???
+			return BMIT_CONTINUE; // one of them was removed???
 		if (shouldCollide == 1)
-			return false; // force collide
+			return BMIT_ABORT; // force collide
 		else if (shouldCollide == 2)
-			return true; // force no collide
+			return BMIT_CONTINUE; // force no collide
 	}
 
 	// When solid spikes move, assume they just popped up and teleport things on top of them to hurt.
 	if (tmthing->type == MT_SPIKE && tmthing->flags & MF_SOLID)
 	{
 		if (thing->z > tmthing->z + tmthing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (thing->z + thing->height < tmthing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
 		if (tmthing->eflags & MFE_VERTICALFLIP)
 			P_SetOrigin(thing, thing->x, thing->y, tmthing->z - thing->height - FixedMul(FRACUNIT, tmthing->scale));
@@ -661,16 +661,16 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_SetOrigin(thing, thing->x, thing->y, tmthing->z + tmthing->height + FixedMul(FRACUNIT, tmthing->scale));
 		if (thing->flags & MF_SHOOTABLE)
 			P_DamageMobj(thing, tmthing, tmthing, 1, 0);
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (thing->flags & MF_PAIN)
 	{ // Player touches painful thing sitting on the floor
 		// see if it went over / under
 		if (thing->z > tmthing->z + tmthing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (thing->z + thing->height < tmthing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 		if (tmthing->flags & MF_SHOOTABLE && thing->health > 0)
 		{
 			UINT32 damagetype = (thing->info->mass & 0xFF);
@@ -678,15 +678,15 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if (P_DamageMobj(tmthing, thing, thing, 1, damagetype) && (damagetype = (thing->info->mass>>8)))
 				S_StartSound(thing, damagetype);
 		}
-		return true;
+		return BMIT_CONTINUE;
 	}
 	else if (tmthing->flags & MF_PAIN && thing->player)
 	{ // Painful thing splats player in the face
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 		if (thing->flags & MF_SHOOTABLE && tmthing->health > 0)
 		{
 			UINT32 damagetype = (tmthing->info->mass & 0xFF);
@@ -694,7 +694,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if (P_DamageMobj(thing, tmthing, tmthing, 1, damagetype) && (damagetype = (tmthing->info->mass>>8)))
 				S_StartSound(tmthing, damagetype);
 		}
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	// check for skulls slamming into things
@@ -702,13 +702,13 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
 		tmthing->flags2 &= ~MF2_SKULLFLY;
 		tmthing->momx = tmthing->momy = tmthing->momz = 0;
-		return false; // stop moving
+		return BMIT_ABORT; // stop moving
 	}
 
 	// SRB2kart 011617 - Colission[sic] code for kart items //{
@@ -717,46 +717,46 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_SMKIceBlockCollide(tmthing, thing);
+		return K_SMKIceBlockCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_SMK_ICEBLOCK)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_SMKIceBlockCollide(thing, tmthing);
+		return K_SMKIceBlockCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_EGGMANITEM || tmthing->type == MT_EGGMANITEM_SHIELD)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_EggItemCollide(tmthing, thing);
+		return K_EggItemCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_EGGMANITEM || thing->type == MT_EGGMANITEM_SHIELD)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_EggItemCollide(thing, tmthing);
+		return K_EggItemCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_RANDOMITEM)
-		return true;
+		return BMIT_CONTINUE;
 
 	// Bubble Shield reflect
 	if (((thing->type == MT_BUBBLESHIELD && thing->target->player && thing->target->player->bubbleblowup)
@@ -768,11 +768,11 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_BubbleShieldCollide(thing, tmthing);
+		return K_BubbleShieldCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (((tmthing->type == MT_BUBBLESHIELD && tmthing->target->player && tmthing->target->player->bubbleblowup)
 		|| (tmthing->player && tmthing->player->bubbleblowup))
@@ -783,16 +783,16 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_BubbleShieldCollide(tmthing, thing);
+		return K_BubbleShieldCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	// double make sure bubbles won't collide with anything else
 	if (thing->type == MT_BUBBLESHIELD || tmthing->type == MT_BUBBLESHIELD)
-		return true;
+		return BMIT_CONTINUE;
 
 	// Droptarget reflect
 	if ((thing->type == MT_DROPTARGET || thing->type == MT_DROPTARGET_SHIELD)
@@ -803,11 +803,11 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_DropTargetCollide(thing, tmthing);
+		return K_DropTargetCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if ((tmthing->type == MT_DROPTARGET || tmthing->type == MT_DROPTARGET_SHIELD)
 		&& (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_JAWZ_DUD
@@ -817,144 +817,144 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_DropTargetCollide(tmthing, thing);
+		return K_DropTargetCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	// double make sure drop targets won't collide with anything else
 	if (thing->type == MT_DROPTARGET || tmthing->type == MT_DROPTARGET
 		|| thing->type == MT_DROPTARGET_SHIELD || tmthing->type == MT_DROPTARGET_SHIELD)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (tmthing->type == MT_ORBINAUT || tmthing->type == MT_JAWZ || tmthing->type == MT_JAWZ_DUD
 		|| tmthing->type == MT_ORBINAUT_SHIELD || tmthing->type == MT_JAWZ_SHIELD)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_OrbinautJawzCollide(tmthing, thing);
+		return K_OrbinautJawzCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_JAWZ_DUD
 		|| thing->type == MT_ORBINAUT_SHIELD || thing->type == MT_JAWZ_SHIELD)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_OrbinautJawzCollide(thing, tmthing);
+		return K_OrbinautJawzCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_BANANA || tmthing->type == MT_BANANA_SHIELD || tmthing->type == MT_BALLHOG)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_BananaBallhogCollide(tmthing, thing);
+		return K_BananaBallhogCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_BANANA || thing->type == MT_BANANA_SHIELD || thing->type == MT_BALLHOG)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_BananaBallhogCollide(thing, tmthing);
+		return K_BananaBallhogCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_SSMINE || tmthing->type == MT_SSMINE_SHIELD)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_MineCollide(tmthing, thing);
+		return K_MineCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_SSMINE || thing->type == MT_SSMINE_SHIELD)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_MineCollide(thing, tmthing);
+		return K_MineCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_LANDMINE)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_LandMineCollide(tmthing, thing);
+		return K_LandMineCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_LANDMINE)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_LandMineCollide(thing, tmthing);
+		return K_LandMineCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_SINK)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_KitchenSinkCollide(tmthing, thing);
+		return K_KitchenSinkCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_SINK)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_KitchenSinkCollide(thing, tmthing);
+		return K_KitchenSinkCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	if (tmthing->type == MT_FALLINGROCK)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_FallingRockCollide(tmthing, thing);
+		return K_FallingRockCollide(tmthing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 	else if (thing->type == MT_FALLINGROCK)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
-		return K_FallingRockCollide(thing, tmthing);
+		return K_FallingRockCollide(thing, tmthing) ? BMIT_CONTINUE : BMIT_ABORT;
 	}
 
 	//}
@@ -972,10 +972,10 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (tmznext <= thzh)
 		{
 			P_DoSpring(thing, tmthing);
-		//	return true;
+		//	return BMIT_CONTINUE;
 		}
 		//else if (tmz > thzh - sprarea && tmz < thzh) // Don't damage people springing up / down
-			return true;
+			return BMIT_CONTINUE;
 	}
 
 	// missiles can hit other things
@@ -985,35 +985,35 @@ static boolean PIT_CheckThing(mobj_t *thing)
 
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return BMIT_CONTINUE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return BMIT_CONTINUE; // underneath
 
 		if (tmthing->target && tmthing->target->type == thing->type)
 		{
 			// Don't hit same species as originator.
 			if (thing == tmthing->target)
-				return true;
+				return BMIT_CONTINUE;
 
 			if (thing->type != MT_PLAYER)
 			{
 				// Explode, but do no damage.
 				// Let players missile other players.
-				return false;
+				return BMIT_ABORT;
 			}
 		}
 
 		if (!(thing->flags & MF_SHOOTABLE))
 		{
 			// didn't do any damage
-			return !(thing->flags & MF_SOLID);
+			return (thing->flags & MF_SOLID) ? BMIT_ABORT : BMIT_CONTINUE;
 		}
 
 		// damage / explode
 		P_DamageMobj(thing, tmthing, tmthing->target, 1, damagetype);
 
 		// don't traverse any more
-		return false;
+		return BMIT_ABORT;
 	}
 
 	if (thing->flags & MF_PUSHABLE && (tmthing->player || tmthing->flags & MF_PUSHABLE)
@@ -1080,13 +1080,13 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (thing->flags & MF_SPECIAL && tmthing->player)
 	{
 		P_TouchSpecialThing(thing, tmthing, true); // can remove thing
-		return true;
+		return BMIT_CONTINUE;
 	}
 	// check again for special pickup
 	if (tmthing->flags & MF_SPECIAL && thing->player)
 	{
 		P_TouchSpecialThing(tmthing, thing, true); // can remove thing
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	// Sprite Spikes!
@@ -1142,7 +1142,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if (playerangle > ANGLE_180)
 				playerangle = InvAngle(playerangle);
 			if (playerangle < ANGLE_90)
-				return true; // Yes, this is intentionally outside the z-height check. No standing on spikes whilst moving away from them.
+				return BMIT_CONTINUE; // Yes, this is intentionally outside the z-height check. No standing on spikes whilst moving away from them.
 		}
 
 		bottomz = thing->z;
@@ -1176,15 +1176,15 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (thing->type == MT_FAN || thing->type == MT_STEAM)
 		{
 			P_DoFanAndGasJet(thing, tmthing);
-			return true;
+			return BMIT_CONTINUE;
 		}
 		else if (thing->flags & MF_SPRING)
 		{
 			if ( thing->z <= tmthing->z + tmthing->height
 			&& tmthing->z <= thing->z + thing->height)
 				if (P_DoSpring(thing, tmthing))
-					return false;
-			return true;
+					return BMIT_ABORT;
+			return BMIT_CONTINUE;
 		}
 	}
 
@@ -1193,7 +1193,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		if ((thing->z + thing->height >= tmthing->z)
 		&& (tmthing->z + tmthing->height >= thing->z))
-			return false;
+			return BMIT_ABORT;
 	}
 
 	if (thing->player)
@@ -1205,7 +1205,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (tmthing->player) // Is the moving/interacting object the player?
 	{
 		if (!tmthing->health)
-			return true;
+			return BMIT_CONTINUE;
 
 		if (thing->type == MT_FAN || thing->type == MT_STEAM)
 			P_DoFanAndGasJet(thing, tmthing);
@@ -1214,27 +1214,27 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if ( thing->z <= tmthing->z + tmthing->height
 			&& tmthing->z <= thing->z + thing->height)
 				if (P_DoSpring(thing, tmthing))
-					return false;
-			return true;
+					return BMIT_ABORT;
+			return BMIT_CONTINUE;
 		}
 		else if (thing->player) // bounce when players collide
 		{
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return BMIT_CONTINUE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return BMIT_CONTINUE; // underneath
 
 			if (thing->player->hyudorotimer || tmthing->player->hyudorotimer)
 			{
-				return true;
+				return BMIT_CONTINUE;
 			}
 
 			if ((gametyperules & GTR_BUMPERS)
 				&& ((thing->player->bumpers && !tmthing->player->bumpers)
 				|| (tmthing->player->bumpers && !thing->player->bumpers)))
 			{
-				return true;
+				return BMIT_CONTINUE;
 			}
 
 			// The bump has to happen last
@@ -1252,18 +1252,18 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				K_PvPTouchDamage(tmthing, thing);
 			}
 
-			return true;
+			return BMIT_CONTINUE;
 		}
 		else if (thing->type == MT_BLUEROBRA_HEAD || thing->type == MT_BLUEROBRA_JOINT)
 		{
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return BMIT_CONTINUE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return BMIT_CONTINUE; // underneath
 
 			if (!thing->health)
-				return true; // dead
+				return BMIT_CONTINUE; // dead
 
 			if (tmthing->player->invincibilitytimer > 0
 				|| tmthing->player->growshrinktimer > 0)
@@ -1272,42 +1272,42 @@ static boolean PIT_CheckThing(mobj_t *thing)
 					P_KillMobj(thing->target, tmthing, tmthing, DMG_NORMAL);
 				else
 					P_KillMobj(thing, tmthing, tmthing, DMG_NORMAL);
-				return true;
+				return BMIT_CONTINUE;
 			}
 			else
 			{
 				K_KartSolidBounce(tmthing, thing);
-				return true;
+				return BMIT_CONTINUE;
 			}
 		}
 		else if (thing->type == MT_SMK_PIPE)
 		{
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return BMIT_CONTINUE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return BMIT_CONTINUE; // underneath
 
 			if (!thing->health)
-				return true; // dead
+				return BMIT_CONTINUE; // dead
 
 			if (tmthing->player->invincibilitytimer > 0
 				|| tmthing->player->growshrinktimer > 0)
 			{
 				P_KillMobj(thing, tmthing, tmthing, DMG_NORMAL);
-				return true; // kill
+				return BMIT_CONTINUE; // kill
 			}
 
 			K_KartSolidBounce(tmthing, thing);
-			return true;
+			return BMIT_CONTINUE;
 		}
 		else if (thing->type == MT_SMK_THWOMP)
 		{
 			if (!thing->health)
-				return true; // dead
+				return BMIT_CONTINUE; // dead
 
 			if (!thwompsactive)
-				return true; // not active yet
+				return BMIT_CONTINUE; // not active yet
 
 			if ((tmthing->z < thing->z) && (thing->z >= thing->movefactor-(256<<FRACBITS)))
 			{
@@ -1317,21 +1317,21 @@ static boolean PIT_CheckThing(mobj_t *thing)
 
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return BMIT_CONTINUE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return BMIT_CONTINUE; // underneath
 
 			// kill
 			if (tmthing->player->invincibilitytimer > 0
 				|| tmthing->player->growshrinktimer > 0)
 			{
 				P_KillMobj(thing, tmthing, tmthing, DMG_NORMAL);
-				return true;
+				return BMIT_CONTINUE;
 			}
 
 			// no interaction
 			if (tmthing->player->flashing > 0 || tmthing->player->hyudorotimer > 0 || tmthing->player->spinouttimer > 0)
-				return true;
+				return BMIT_CONTINUE;
 
 			// collide
 			if (tmthing->z < thing->z && thing->momz < 0)
@@ -1344,29 +1344,29 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				}
 			}
 
-			return true;
+			return BMIT_CONTINUE;
 		}
 		else if (thing->type == MT_KART_LEFTOVER)
 		{
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return BMIT_CONTINUE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return BMIT_CONTINUE; // underneath
 
 			K_KartBouncing(tmthing, thing);
-			return true;
+			return BMIT_CONTINUE;
 		}
 		else if (thing->flags & MF_SOLID)
 		{
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return BMIT_CONTINUE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return BMIT_CONTINUE; // underneath
 
 			K_KartSolidBounce(tmthing, thing);
-			return true;
+			return BMIT_CONTINUE;
 		}
 	}
 
@@ -1393,7 +1393,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 					tmfloorslope = NULL;
 					tmfloorpic = -1;
 				}
-				return true;
+				return BMIT_CONTINUE;
 			}
 
 			topz = thing->z - thing->scale; // FixedMul(FRACUNIT, thing->scale), but thing->scale == FRACUNIT in base scale anyways
@@ -1406,7 +1406,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				&& tmthing->z + tmthing->height < tmthing->ceilingz)
 			{
 				if (thing->flags & MF_GRENADEBOUNCE && (thing->flags & MF_MONITOR || thing->info->flags & MF_MONITOR)) // Gold monitor hack...
-					return false;
+					return BMIT_ABORT;
 
 				tmfloorz = tmceilingz = topz; // block while in air
 				tmceilingrover = NULL;
@@ -1437,7 +1437,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 					tmceilingslope = NULL;
 					tmceilingpic = -1;
 				}
-				return true;
+				return BMIT_CONTINUE;
 			}
 
 			topz = thing->z + thing->height + thing->scale; // FixedMul(FRACUNIT, thing->scale), but thing->scale == FRACUNIT in base scale anyways
@@ -1450,7 +1450,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				&& tmthing->z > tmthing->floorz)
 			{
 				if (thing->flags & MF_GRENADEBOUNCE && (thing->flags & MF_MONITOR || thing->info->flags & MF_MONITOR)) // Gold monitor hack...
-					return false;
+					return BMIT_ABORT;
 
 				tmfloorz = tmceilingz = topz; // block while in air
 				tmfloorrover = NULL;
@@ -1470,24 +1470,24 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	}
 
 	// not solid not blocked
-	return true;
+	return BMIT_CONTINUE;
 }
 
 // PIT_CheckCameraLine
 // Adjusts tmfloorz and tmceilingz as lines are contacted - FOR CAMERA ONLY
-static boolean PIT_CheckCameraLine(line_t *ld)
+static BlockItReturn_t PIT_CheckCameraLine(line_t *ld)
 {
 	if (ld->polyobj && !(ld->polyobj->flags & POF_SOLID))
-		return true;
+		return BMIT_CONTINUE;
 
 	if (tmbbox[BOXRIGHT] <= ld->bbox[BOXLEFT] || tmbbox[BOXLEFT] >= ld->bbox[BOXRIGHT]
 		|| tmbbox[BOXTOP] <= ld->bbox[BOXBOTTOM] || tmbbox[BOXBOTTOM] >= ld->bbox[BOXTOP])
 	{
-		return true;
+		return BMIT_CONTINUE;
 	}
 
 	if (P_BoxOnLineSide(tmbbox, ld) != -1)
-		return true;
+		return BMIT_CONTINUE;
 
 	// A line has been hit
 
@@ -1505,8 +1505,8 @@ static boolean PIT_CheckCameraLine(line_t *ld)
 	if (!ld->backsector) // one sided line
 	{
 		if (P_PointOnLineSide(mapcampointer->x, mapcampointer->y, ld))
-			return true; // don't hit the back side
-		return false;
+			return BMIT_CONTINUE; // don't hit the back side
+		return BMIT_ABORT;
 	}
 
 	// set openrange, opentop, openbottom
@@ -1530,7 +1530,7 @@ static boolean PIT_CheckCameraLine(line_t *ld)
 	if (lowfloor < tmdropoffz)
 		tmdropoffz = lowfloor;
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 boolean P_IsLineBlocking(const line_t *ld, const mobj_t *thing)
@@ -1559,19 +1559,19 @@ boolean P_IsLineTripWire(const line_t *ld)
 // PIT_CheckLine
 // Adjusts tmfloorz and tmceilingz as lines are contacted
 //
-static boolean PIT_CheckLine(line_t *ld)
+static BlockItReturn_t PIT_CheckLine(line_t *ld)
 {
 	const fixed_t thingtop = tmthing->z + tmthing->height;
 
 	if (ld->polyobj && !(ld->polyobj->flags & POF_SOLID))
-		return true;
+		return BMIT_CONTINUE;
 
 	if (tmbbox[BOXRIGHT] <= ld->bbox[BOXLEFT] || tmbbox[BOXLEFT] >= ld->bbox[BOXRIGHT]
 	|| tmbbox[BOXTOP] <= ld->bbox[BOXBOTTOM] || tmbbox[BOXBOTTOM] >= ld->bbox[BOXTOP])
-		return true;
+		return BMIT_CONTINUE;
 
 	if (P_BoxOnLineSide(tmbbox, ld) != -1)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (tmthing->flags & MF_PAPERCOLLISION) // Caution! Turning whilst up against a wall will get you stuck. You probably shouldn't give the player this flag.
 	{
@@ -1580,7 +1580,7 @@ static boolean PIT_CheckLine(line_t *ld)
 		sinradius = FixedMul(tmthing->radius, FINESINE(tmthing->angle>>ANGLETOFINESHIFT));
 		if (P_PointOnLineSide(tmx - cosradius, tmy - sinradius, ld)
 		== P_PointOnLineSide(tmx + cosradius, tmy + sinradius, ld))
-			return true; // the line doesn't cross between collider's start or end
+			return BMIT_CONTINUE; // the line doesn't cross between collider's start or end
 #ifdef PAPER_COLLISIONCORRECTION
 		{
 			fixed_t dist;
@@ -1616,22 +1616,22 @@ static boolean PIT_CheckLine(line_t *ld)
 	{
 		UINT8 shouldCollide = LUAh_MobjLineCollide(tmthing, blockingline); // checks hook for thing's type
 		if (P_MobjWasRemoved(tmthing))
-			return true; // one of them was removed???
+			return BMIT_CONTINUE; // one of them was removed???
 		if (shouldCollide == 1)
-			return false; // force collide
+			return BMIT_ABORT; // force collide
 		else if (shouldCollide == 2)
-			return true; // force no collide
+			return BMIT_CONTINUE; // force no collide
 	}
 
 	if (!ld->backsector) // one sided line
 	{
 		if (P_PointOnLineSide(tmthing->x, tmthing->y, ld))
-			return true; // don't hit the back side
-		return false;
+			return BMIT_CONTINUE; // don't hit the back side
+		return BMIT_ABORT;
 	}
 
 	if (P_IsLineBlocking(ld, tmthing))
-		return false;
+		return BMIT_ABORT;
 
 	// set openrange, opentop, openbottom
 	P_LineOpening(ld, tmthing);
@@ -1676,7 +1676,7 @@ static boolean PIT_CheckLine(line_t *ld)
 		add_spechit(ld);
 	}
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 // =========================================================================
@@ -2322,32 +2322,32 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 static mobj_t *stand;
 static fixed_t standx, standy;
 
-boolean PIT_PushableMoved(mobj_t *thing)
+BlockItReturn_t PIT_PushableMoved(mobj_t *thing)
 {
 	fixed_t blockdist;
 
 	if (!(thing->flags & MF_SOLID)
 		|| (thing->flags & MF_NOGRAVITY))
-		return true; // Don't move something non-solid!
+		return BMIT_CONTINUE; // Don't move something non-solid!
 
 	// Only pushables are supported... in 2.0. Now players can be moved too!
 	if (!(thing->flags & MF_PUSHABLE || thing->player))
-		return true;
+		return BMIT_CONTINUE;
 
 	if (thing == stand)
-		return true;
+		return BMIT_CONTINUE;
 
 	blockdist = stand->radius + thing->radius;
 
 	if (abs(thing->x - stand->x) >= blockdist || abs(thing->y - stand->y) >= blockdist)
-		return true; // didn't hit it
+		return BMIT_CONTINUE; // didn't hit it
 
 	if ((!(stand->eflags & MFE_VERTICALFLIP) && thing->z != stand->z + stand->height + FixedMul(FRACUNIT, stand->scale))
 	|| ((stand->eflags & MFE_VERTICALFLIP) && thing->z + thing->height != stand->z - FixedMul(FRACUNIT, stand->scale)))
-		return true; // Not standing on top
+		return BMIT_CONTINUE; // Not standing on top
 
 	if (!stand->momx && !stand->momy)
-		return true;
+		return BMIT_CONTINUE;
 
 	// Move this guy!
 	if (thing->player)
@@ -2396,7 +2396,7 @@ boolean PIT_PushableMoved(mobj_t *thing)
 		thing->momy = stand->momy;
 		thing->momz = stand->momz;
 	}
-	return true;
+	return BMIT_CONTINUE;
 }
 
 static boolean P_WaterRunning(mobj_t *thing)
@@ -3952,18 +3952,18 @@ static boolean bombsightcheck;
 // "bombsource" is the creature
 // that caused the explosion at "bombspot".
 //
-static boolean PIT_RadiusAttack(mobj_t *thing)
+static BlockItReturn_t PIT_RadiusAttack(mobj_t *thing)
 {
 	fixed_t dx, dy, dz, dist;
 
 	if (thing == bombspot) // ignore the bomb itself (Deton fix)
-		return true;
+		return BMIT_CONTINUE;
 
 	if ((bombdamagetype & DMG_CANTHURTSELF) && bombsource && thing->type == bombsource->type) // ignore the type of guys who dropped the bomb (Jetty-Syn Bomber or Skim can bomb eachother, but not themselves.)
-		return true;
+		return BMIT_CONTINUE;
 
 	if ((thing->flags & (MF_MONITOR|MF_SHOOTABLE)) != MF_SHOOTABLE)
-		return true;
+		return BMIT_CONTINUE;
 
 	dx = abs(thing->x - bombspot->x);
 	dy = abs(thing->y - bombspot->y);
@@ -3976,20 +3976,20 @@ static boolean PIT_RadiusAttack(mobj_t *thing)
 		dist = 0;
 
 	if (dist >= bombdamage)
-		return true; // out of range
+		return BMIT_CONTINUE; // out of range
 
 	if (thing->floorz > bombspot->z && bombspot->ceilingz < thing->z)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (thing->ceilingz < bombspot->z && bombspot->floorz > thing->z)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (!bombsightcheck || P_CheckSight(thing, bombspot))
 	{	// must be in direct path
 		P_DamageMobj(thing, bombspot, bombsource, 1, bombdamagetype); // Tails 01-11-2001
 	}
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 //
@@ -4623,19 +4623,19 @@ void P_DelPrecipSeclist(mprecipsecnode_t *node)
 // at this location, so don't bother with checking impassable or
 // blocking lines.
 
-static inline boolean PIT_GetSectors(line_t *ld)
+static inline BlockItReturn_t PIT_GetSectors(line_t *ld)
 {
 	if (tmbbox[BOXRIGHT] <= ld->bbox[BOXLEFT] ||
 		tmbbox[BOXLEFT] >= ld->bbox[BOXRIGHT] ||
 		tmbbox[BOXTOP] <= ld->bbox[BOXBOTTOM] ||
 		tmbbox[BOXBOTTOM] >= ld->bbox[BOXTOP])
-	return true;
+	return BMIT_CONTINUE;
 
 	if (P_BoxOnLineSide(tmbbox, ld) != -1)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (ld->polyobj) // line belongs to a polyobject, don't add it
-		return true;
+		return BMIT_CONTINUE;
 
 	// This line crosses through the object.
 
@@ -4654,23 +4654,23 @@ static inline boolean PIT_GetSectors(line_t *ld)
 	if (ld->backsector)
 		sector_list = P_AddSecnode(ld->backsector, tmthing, sector_list);
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 // Tails 08-25-2002
-static inline boolean PIT_GetPrecipSectors(line_t *ld)
+static inline BlockItReturn_t PIT_GetPrecipSectors(line_t *ld)
 {
 	if (preciptmbbox[BOXRIGHT] <= ld->bbox[BOXLEFT] ||
 		preciptmbbox[BOXLEFT] >= ld->bbox[BOXRIGHT] ||
 		preciptmbbox[BOXTOP] <= ld->bbox[BOXBOTTOM] ||
 		preciptmbbox[BOXBOTTOM] >= ld->bbox[BOXTOP])
-	return true;
+	return BMIT_CONTINUE;
 
 	if (P_BoxOnLineSide(preciptmbbox, ld) != -1)
-		return true;
+		return BMIT_CONTINUE;
 
 	if (ld->polyobj) // line belongs to a polyobject, don't add it
-		return true;
+		return BMIT_CONTINUE;
 
 	// This line crosses through the object.
 
@@ -4689,7 +4689,7 @@ static inline boolean PIT_GetPrecipSectors(line_t *ld)
 	if (ld->backsector)
 		precipsector_list = P_AddPrecipSecnode(ld->backsector, tmprecipthing, precipsector_list);
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 // P_CreateSecNodeList alters/creates the sector_list that shows what sectors
diff --git a/src/p_maputl.c b/src/p_maputl.c
index 555928a3ac05635d235063ba2be447d85ea741d7..37a24267cdfb03e0b93a7421787b453bb40514b8 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -1096,7 +1096,7 @@ void P_SetPrecipitationThingPosition(precipmobj_t *thing)
 // to P_BlockLinesIterator, then make one or more calls
 // to it.
 //
-boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean (*func)(line_t *))
+boolean P_BlockLinesIterator(INT32 x, INT32 y, BlockItReturn_t (*func)(line_t *))
 {
 	INT32 offset;
 	const INT32 *list; // Big blockmap
@@ -1122,11 +1122,22 @@ boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean (*func)(line_t *))
 
 			for (i = 0; i < po->numLines; ++i)
 			{
+				BlockItReturn_t ret = BMIT_CONTINUE;
+
 				if (po->lines[i]->validcount == validcount) // line has been checked
 					continue;
+
 				po->lines[i]->validcount = validcount;
-				if (!func(po->lines[i]))
+				ret = func(po->lines[i]);
+
+				if (ret == BMIT_ABORT)
+				{
 					return false;
+				}
+				else if (ret == BMIT_STOP)
+				{
+					return true;
+				}
 			}
 		}
 		plink = (polymaplink_t *)(plink->link.next);
@@ -1137,15 +1148,24 @@ boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean (*func)(line_t *))
 	// First index is really empty, so +1 it.
 	for (list = blockmaplump + offset + 1; *list != -1; list++)
 	{
+		BlockItReturn_t ret = BMIT_CONTINUE;
+
 		ld = &lines[*list];
 
 		if (ld->validcount == validcount)
 			continue; // Line has already been checked.
 
 		ld->validcount = validcount;
+		ret = func(ld);
 
-		if (!func(ld))
+		if (ret == BMIT_ABORT)
+		{
 			return false;
+		}
+		else if (ret == BMIT_STOP)
+		{
+			return true;
+		}
 	}
 	return true; // Everything was checked.
 }
@@ -1154,7 +1174,7 @@ boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean (*func)(line_t *))
 //
 // P_BlockThingsIterator
 //
-boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean (*func)(mobj_t *))
+boolean P_BlockThingsIterator(INT32 x, INT32 y, BlockItReturn_t (*func)(mobj_t *))
 {
 	mobj_t *mobj, *bnext = NULL;
 
@@ -1164,19 +1184,25 @@ boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean (*func)(mobj_t *))
 	// Check interaction with the objects in the blockmap.
 	for (mobj = blocklinks[y*bmapwidth + x]; mobj; mobj = bnext)
 	{
+		BlockItReturn_t ret = BMIT_CONTINUE;
+
 		P_SetTarget(&bnext, mobj->bnext); // We want to note our reference to bnext here incase it is MF_NOTHINK and gets removed!
-		if (!func(mobj))
+		ret = func(mobj);
+
+		if (ret == BMIT_ABORT)
 		{
 			P_SetTarget(&bnext, NULL);
-			return false;
+			return false; // failure
 		}
-		if (P_MobjWasRemoved(tmthing) // func just popped our tmthing, cannot continue.
-		|| (bnext && P_MobjWasRemoved(bnext))) // func just broke blockmap chain, cannot continue.
+
+		if ((ret == BMIT_STOP)
+			|| (bnext && P_MobjWasRemoved(bnext))) // func just broke blockmap chain, cannot continue.
 		{
 			P_SetTarget(&bnext, NULL);
-			return true;
+			return true; // success
 		}
 	}
+
 	return true;
 }
 
@@ -1220,7 +1246,7 @@ static void P_CheckIntercepts(void)
 // are on opposite sides of the trace.
 // Returns true if earlyout and a solid line hit.
 //
-static boolean PIT_AddLineIntercepts(line_t *ld)
+static BlockItReturn_t PIT_AddLineIntercepts(line_t *ld)
 {
 	INT32 s1, s2;
 	fixed_t frac;
@@ -1243,18 +1269,18 @@ static boolean PIT_AddLineIntercepts(line_t *ld)
 	}
 
 	if (s1 == s2)
-		return true; // Line isn't crossed.
+		return BMIT_CONTINUE; // Line isn't crossed.
 
 	// Hit the line.
 	P_MakeDivline(ld, &dl);
 	frac = P_InterceptVector(&trace, &dl);
 
 	if (frac < 0)
-		return true; // Behind source.
+		return BMIT_CONTINUE; // Behind source.
 
 	// Try to take an early out of the check.
 	if (earlyout && frac < FRACUNIT && !ld->backsector)
-		return false; // stop checking
+		return BMIT_ABORT; // stop checking
 
 	P_CheckIntercepts();
 
@@ -1263,13 +1289,13 @@ static boolean PIT_AddLineIntercepts(line_t *ld)
 	intercept_p->d.line = ld;
 	intercept_p++;
 
-	return true; // continue
+	return BMIT_CONTINUE; // continue
 }
 
 //
 // PIT_AddThingIntercepts
 //
-static boolean PIT_AddThingIntercepts(mobj_t *thing)
+static BlockItReturn_t PIT_AddThingIntercepts(mobj_t *thing)
 {
 	fixed_t px1, py1, px2, py2, frac;
 	INT32 s1, s2;
@@ -1300,7 +1326,7 @@ static boolean PIT_AddThingIntercepts(mobj_t *thing)
 	s2 = P_PointOnDivlineSide(px2, py2, &trace);
 
 	if (s1 == s2)
-		return true; // Line isn't crossed.
+		return BMIT_CONTINUE; // Line isn't crossed.
 
 	dl.x = px1;
 	dl.y = py1;
@@ -1310,7 +1336,7 @@ static boolean PIT_AddThingIntercepts(mobj_t *thing)
 	frac = P_InterceptVector(&trace, &dl);
 
 	if (frac < 0)
-		return true; // Behind source.
+		return BMIT_CONTINUE; // Behind source.
 
 	P_CheckIntercepts();
 
@@ -1319,7 +1345,7 @@ static boolean PIT_AddThingIntercepts(mobj_t *thing)
 	intercept_p->d.thing = thing;
 	intercept_p++;
 
-	return true; // Keep going.
+	return BMIT_CONTINUE; // Keep going.
 }
 
 //
diff --git a/src/p_maputl.h b/src/p_maputl.h
index 43a7fb5076360d06ed97e3cc4a16757edee989f6..c17ad5d332d80eb51fc70e5e633b14e33850a949 100644
--- a/src/p_maputl.h
+++ b/src/p_maputl.h
@@ -67,8 +67,15 @@ extern INT32 opentoppic, openbottompic;
 
 void P_LineOpening(line_t *plinedef, mobj_t *mobj);
 
-boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean(*func)(line_t *));
-boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean(*func)(mobj_t *));
+typedef enum
+{
+	BMIT_CONTINUE, // Continue blockmap search
+	BMIT_STOP, // End blockmap search with success
+	BMIT_ABORT // End blockmap search with failure
+} BlockItReturn_t;
+
+boolean P_BlockLinesIterator(INT32 x, INT32 y, BlockItReturn_t(*func)(line_t *));
+boolean P_BlockThingsIterator(INT32 x, INT32 y, BlockItReturn_t(*func)(mobj_t *));
 
 #define PT_ADDLINES     1
 #define PT_ADDTHINGS    2
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 8315f76652b2388fa01631adaa62cd505487122b..5219fb707395ef9b9c4fc4e76fb69a9bacebd325 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -44,6 +44,7 @@
 #include "k_bot.h"
 #include "k_terrain.h"
 #include "k_collide.h"
+#include "k_objects.h"
 
 static CV_PossibleValue_t CV_BobSpeed[] = {{0, "MIN"}, {4*FRACUNIT, "MAX"}, {0, NULL}};
 consvar_t cv_movebob = CVAR_INIT ("movebob", "1.0", CV_FLOAT|CV_SAVE, CV_BobSpeed, NULL);
@@ -5300,6 +5301,20 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 
 	switch (mobj->type)
 	{
+	case MT_SHADOW:
+		if (mobj->tracer)
+		{
+			P_MoveOrigin(mobj,
+					mobj->tracer->x,
+					mobj->tracer->y,
+					mobj->tracer->z);
+		}
+		else
+		{
+			P_RemoveMobj(mobj);
+			return;
+		}
+		break;
 	case MT_BOSSJUNK:
 		mobj->renderflags ^= RF_DONTDRAW;
 		break;
@@ -7671,6 +7686,16 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 		}
 		break;
 	}
+	case MT_HYUDORO:
+	{
+		Obj_HyudoroThink(mobj);
+		break;
+	}
+	case MT_HYUDORO_CENTER:
+	{
+		Obj_HyudoroCenterThink(mobj);
+		break;
+	}
 	case MT_ROCKETSNEAKER:
 		if (!mobj->target || !mobj->target->health)
 		{
@@ -8597,8 +8622,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 			if (mobj->extravalue1)
 			{
 				const INT32 speed = 6*TICRATE; // longer is slower
-				const fixed_t pi = 22*FRACUNIT/7; // Inaccurate, but is close enough for our usage
-				fixed_t sine = FINESINE((((2*pi*speed) * leveltime) >> ANGLETOFINESHIFT) & FINEMASK) * flip;
+				fixed_t sine = FINESINE((((M_TAU_FIXED * speed) * leveltime) >> ANGLETOFINESHIFT) & FINEMASK) * flip;
 
 				// Flying capsules are flipped upside-down, like S3K
 				flip = -flip;
@@ -9186,14 +9210,19 @@ void P_MobjThinker(mobj_t *mobj)
 		{
 			if (mobj->extravalue2 >= 2)
 			{
+				UINT32 dontdraw = RF_DONTDRAW;
+
+				if (mobj->tracer)
+					dontdraw &= ~(mobj->tracer->renderflags);
+
 				if (mobj->extravalue2 == 2) // I don't know why the normal logic doesn't work for this.
-					mobj->renderflags ^= RF_DONTDRAW;
+					mobj->renderflags ^= dontdraw;
 				else
 				{
 					if (mobj->fuse == mobj->extravalue2)
-						mobj->renderflags &= ~RF_DONTDRAW;
+						mobj->renderflags &= ~(dontdraw);
 					else
-						mobj->renderflags |= RF_DONTDRAW;
+						mobj->renderflags |= dontdraw;
 				}
 			}
 		}
@@ -9654,6 +9683,7 @@ static void P_DefaultMobjShadowScale(mobj_t *thing)
 		case MT_SSMINE_SHIELD:
 		case MT_LANDMINE:
 		case MT_BALLHOG:
+		case MT_HYUDORO:
 		case MT_SINK:
 		case MT_ROCKETSNEAKER:
 		case MT_SPB:
diff --git a/src/p_saveg.c b/src/p_saveg.c
index bcd12e8e3018f9eabdd3750ec8c6fb7f754d4175..50440bd6d45d4edf942d0130a703c470797dd0be 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -63,6 +63,7 @@ typedef enum
 	FOLLOWER   = 0x04,
 	SKYBOXVIEW = 0x08,
 	SKYBOXCENTER = 0x10,
+	HOVERHYUDORO = 0x20,
 } player_saveflags;
 
 static inline void P_ArchivePlayer(void)
@@ -196,6 +197,9 @@ static void P_NetArchivePlayers(void)
 		if (players[i].skybox.centerpoint)
 			flags |= SKYBOXCENTER;
 
+		if (players[i].hoverhyudoro)
+			flags |= HOVERHYUDORO;
+
 		WRITEUINT16(save_p, flags);
 
 		if (flags & SKYBOXVIEW)
@@ -210,6 +214,9 @@ static void P_NetArchivePlayers(void)
 		if (flags & FOLLOWITEM)
 			WRITEUINT32(save_p, players[i].followmobj->mobjnum);
 
+		if (flags & HOVERHYUDORO)
+			WRITEUINT32(save_p, players[i].hoverhyudoro->mobjnum);
+
 		WRITEUINT32(save_p, (UINT32)players[i].followitem);
 
 		WRITEUINT32(save_p, players[i].charflags);
@@ -487,6 +494,9 @@ static void P_NetUnArchivePlayers(void)
 		if (flags & FOLLOWITEM)
 			players[i].followmobj = (mobj_t *)(size_t)READUINT32(save_p);
 
+		if (flags & HOVERHYUDORO)
+			players[i].hoverhyudoro = (mobj_t *)(size_t)READUINT32(save_p);
+
 		players[i].followitem = (mobjtype_t)READUINT32(save_p);
 
 		//SetPlayerSkinByNum(i, players[i].skin);
@@ -523,7 +533,7 @@ static void P_NetUnArchivePlayers(void)
 		players[i].tumbleBounces = READUINT8(save_p);
 		players[i].tumbleHeight = READUINT16(save_p);
 
-		players[i].justDI = (boolean)READUINT8(save_p);
+		players[i].justDI = READUINT8(save_p);
 		players[i].flipDI = (boolean)READUINT8(save_p);
 
 		players[i].drift = READSINT8(save_p);
@@ -4240,6 +4250,13 @@ static void P_RelinkPointers(void)
 					CONS_Debug(DBG_GAMELOGIC, "respawn.wp not found on %d\n", mobj->type);
 				}
 			}
+			if (mobj->player->hoverhyudoro)
+			{
+				temp = (UINT32)(size_t)mobj->player->hoverhyudoro;
+				mobj->player->hoverhyudoro = NULL;
+				if (!P_SetTarget(&mobj->player->hoverhyudoro, P_FindNewPosition(temp)))
+					CONS_Debug(DBG_GAMELOGIC, "hoverhyudoro not found on %d\n", mobj->type);
+			}
 		}
 	}
 }
@@ -4492,7 +4509,6 @@ static void P_NetArchiveMisc(boolean resending)
 
 	WRITEUINT32(save_p, wantedcalcdelay);
 	WRITEUINT32(save_p, indirectitemcooldown);
-	WRITEUINT32(save_p, hyubgone);
 	WRITEUINT32(save_p, mapreset);
 
 	for (i = 0; i < MAXPLAYERS; i++)
@@ -4642,7 +4658,6 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 
 	wantedcalcdelay = READUINT32(save_p);
 	indirectitemcooldown = READUINT32(save_p);
-	hyubgone = READUINT32(save_p);
 	mapreset = READUINT32(save_p);
 
 	for (i = 0; i < MAXPLAYERS; i++)
diff --git a/src/p_setup.c b/src/p_setup.c
index c1da43eef71ce99b5fd5b0990aac8d20f1f057cb..2f23d6c1f87fb1e4c1e127f6f5eabff009d32602 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -4405,7 +4405,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 	wantedcalcdelay = wantedfrequency*2;
 	indirectitemcooldown = 0;
-	hyubgone = 0;
 	mapreset = 0;
 
 	for (i = 0; i < MAXPLAYERS; i++)
diff --git a/src/p_spec.c b/src/p_spec.c
index b6079e6cface33999791ad7a64448d6df3a1a6a8..56e7ae455524bc1a1be04db2f48bbea7c532ce33 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -8390,13 +8390,13 @@ static pusher_t *tmpusher; // pusher structure for blockmap searches
   *       ::tmpusher won't need to be used.
   * \sa T_Pusher
   */
-static inline boolean PIT_PushThing(mobj_t *thing)
+static inline BlockItReturn_t PIT_PushThing(mobj_t *thing)
 {
 	if (thing->eflags & MFE_PUSHED)
-		return false;
+		return BMIT_ABORT;
 
 	if (!tmpusher->source)
-		return false;
+		return BMIT_ABORT;
 
 	// Allow this to affect pushable objects at some point?
 	if (thing->player && !(thing->flags & (MF_NOGRAVITY | MF_NOCLIP)))
@@ -8416,7 +8416,7 @@ static inline boolean PIT_PushThing(mobj_t *thing)
 		{
 			// Make sure the Z is in range
 			if (thing->z < sz - tmpusher->radius || thing->z > sz + tmpusher->radius)
-				return false;
+				return BMIT_ABORT;
 
 			dist = P_AproxDistance(P_AproxDistance(thing->x - sx, thing->y - sy),
 				thing->z - sz);
@@ -8482,7 +8482,7 @@ static inline boolean PIT_PushThing(mobj_t *thing)
 	if (tmpusher->exclusive)
 		thing->eflags |= MFE_PUSHED;
 
-	return true;
+	return BMIT_CONTINUE;
 }
 
 /** Applies a pusher to all affected objects.
diff --git a/src/p_tick.c b/src/p_tick.c
index c7dae9030770ee8477c5b87f7643deb0f54cebd7..843461db60b56f1318b1ed24ff4d805ca1a6a7a3 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -685,8 +685,6 @@ void P_Ticker(boolean run)
 
 		if (indirectitemcooldown > 0)
 			indirectitemcooldown--;
-		if (hyubgone > 0)
-			hyubgone--;
 
 		K_BossInfoTicker();
 
diff --git a/src/p_user.c b/src/p_user.c
index fde4465c378c42b9b76bad5cade3ad7365ebb24f..ffc53012b53c2cdf8efdcd5c32ed7e867a7a44b8 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -56,6 +56,7 @@
 #include "k_boss.h"
 #include "k_terrain.h" // K_SpawnSplashForMobj
 #include "k_color.h"
+#include "k_follower.h"
 
 #ifdef HW3SOUND
 #include "hardware/hw3sound.h"
@@ -1691,8 +1692,7 @@ static void P_CheckInvincibilityTimer(player_t *player)
 	// Resume normal music stuff.
 	if (player->invincibilitytimer == 1)
 	{
-		player->mo->color = player->skincolor;
-		player->mo->colorized = false;
+		//K_KartResetPlayerColor(player); -- this gets called every tic anyways
 		G_GhostAddColor((INT32) (player - players), GHC_NORMAL);
 
 		P_RestoreMusic(player);
@@ -2143,7 +2143,7 @@ void P_MovePlayer(player_t *player)
 
 	if (cmd->turning == 0)
 	{
-		player->justDI = false;
+		player->justDI = 0;
 	}
 
 	// Kart frames
@@ -2680,8 +2680,7 @@ static void P_DeathThink(player_t *player)
 	if (!player->mo)
 		return;
 
-	player->mo->colorized = false;
-	player->mo->color = player->skincolor;
+	//K_KartResetPlayerColor(player); -- called at death, don't think we need to re-establish
 
 	P_CalcHeight(player);
 }
@@ -3962,289 +3961,6 @@ static void P_ParabolicMove(mobj_t *mo, fixed_t x, fixed_t y, fixed_t z, fixed_t
 
 #endif
 
-/* 	set follower state with our weird hacks
-	the reason we do this is to avoid followers ever using actions (majormods, yikes!)
-	without having to touch p_mobj.c.
-	so we give it 1more tic and change the state when tic == 1 instead of 0
-	cool beans?
-	cool beans.
-*/
-static void P_SetFollowerState(mobj_t *f, INT32 state)
-{
-
-	if (!f || P_MobjWasRemoved(f))
-		return;		// safety net
-
-	// No, do NOT set the follower to S_NULL. Set it to S_INVISIBLE.
-	if (state == S_NULL)
-	{
-		state = S_INVISIBLE;
-		f->threshold = 1;	// Threshold = 1 means stop doing anything related to setting states, so that we don't get out of S_INVISIBLE
-	}
-
-	// extravalue2 stores the last "first state" we used.
-	// because states default to idlestates, if we use an animation that uses an "ongoing" state line, don't reset it!
-	// this prevents it from looking very dumb
-	if (state == f->extravalue2)
-		return;
-
-	// we will save the state into extravalue2.
-	f->extravalue2 = state;
-
-	P_SetMobjStateNF(f, state);
-	if (f->state->tics > 0)
-		f->tics++;
-}
-
-//
-//P_HandleFollower
-//
-//Handle the follower's spawning and moving along with the player. Do note that some of the stuff like the removal if a player doesn't exist anymore is handled in MT_FOLLOWER's thinker.
-static void P_HandleFollower(player_t *player)
-{
-	follower_t fl;
-	angle_t an;
-	fixed_t zoffs;
-	fixed_t sx, sy, sz, deltaz;
-	UINT16 color;
-
-	fixed_t bubble;	// bubble scale (0 if no bubble)
-	mobj_t *bmobj;	// temp bubble mobj
-
-
-	if (!player->followerready)
-		return;	// we aren't ready to perform anything follower related yet.
-
-	// How about making sure our follower exists and is added before trying to spawn it n' all?
-	if (player->followerskin > numfollowers-1 || player->followerskin < -1)
-	{
-		//CONS_Printf("Follower skin invlaid. Setting to -1.\n");
-		player->followerskin = -1;
-		return;
-	}
-
-	// don't do anything if we can't have a follower to begin with. (It gets removed under those conditions)
-	if (player->spectator)
-		return;
-	if (player->followerskin < 0)
-		return;
-	// Before we do anything, let's be sure of where we're supposed to be
-	fl = followers[player->followerskin];
-
-	an = player->mo->angle + (fl.atangle)*ANG1;		// it's aproximative but it really doesn't matter in the grand scheme of things...
-	zoffs = (fl.zoffs)*FRACUNIT;
-	bubble = fl.bubblescale;	// 0 if no bubble to spawn.
-
-	// do you like angle maths? I certainly don't...
-	sx = player->mo->x + FixedMul((player->mo->scale*fl.dist), FINECOSINE((an)>>ANGLETOFINESHIFT));
-	sy = player->mo->y + FixedMul((player->mo->scale*fl.dist), FINESINE((an)>>ANGLETOFINESHIFT));
-
-	// interp info helps with stretchy fix
-	deltaz = (player->mo->z - player->mo->old_z);
-
-	// for the z coordinate, don't be a doof like Steel and forget that MFE_VERTICALFLIP exists :P
-	sz = player->mo->z + FixedMul(player->mo->scale, zoffs)*P_MobjFlip(player->mo);
-	if (player->mo->eflags & MFE_VERTICALFLIP)
-		sz += fl.height*player->mo->scale;
-
-	// finally, add a cool floating effect to the z height.
-	// not stolen from k_kart I swear!!
-	{
-		const fixed_t pi = (22<<FRACBITS) / 7; // loose approximation, this doesn't need to be incredibly precise
-		fixed_t sine = fl.bobamp * FINESINE((((8*pi*(fl.bobspeed)) * leveltime)>>ANGLETOFINESHIFT) & FINEMASK);
-		sz += FixedMul(player->mo->scale, sine)*P_MobjFlip(player->mo);
-	}
-
-	// Set follower colour
-	switch (player->followercolor)
-	{
-		case FOLLOWERCOLOR_MATCH: // "Match"
-			color = player->skincolor;
-			break;
-		case FOLLOWERCOLOR_OPPOSITE: // "Opposite"
-			color = skincolors[player->skincolor].invcolor;
-			break;
-		default:
-
-			color = player->followercolor;
-			if (!color || color > MAXSKINCOLORS+2) // Make sure this isn't garbage
-				color = player->skincolor; // "Match" as fallback.
-
-			break;
-	}
-
-
-
-	if (!player->follower)	// follower doesn't exist / isn't valid
-	{
-		//CONS_Printf("Spawning follower...\n");
-		// so let's spawn one!
-		P_SetTarget(&player->follower, P_SpawnMobj(sx, sy, sz, MT_FOLLOWER));
-		P_SetFollowerState(player->follower, fl.idlestate);
-		P_SetTarget(&player->follower->target, player->mo);	// we need that to know when we need to disappear
-		P_InitAngle(player->follower, player->mo->angle);
-
-		// This is safe to only spawn it here, the follower is removed then respawned when switched.
-		if (bubble)
-		{
-			bmobj = P_SpawnMobj(player->follower->x, player->follower->y, player->follower->z, MT_FOLLOWERBUBBLE_FRONT);
-			P_SetTarget(&player->follower->hnext, bmobj);
-			P_SetTarget(&bmobj->target, player->follower);	// Used to know if we have to despawn at some point.
-
-			bmobj = P_SpawnMobj(player->follower->x, player->follower->y, player->follower->z, MT_FOLLOWERBUBBLE_BACK);
-			P_SetTarget(&player->follower->hnext->hnext, bmobj);	// this seems absolutely stupid, I know, but this will make updating the momentums/flags of these a bit easier.
-			P_SetTarget(&bmobj->target, player->follower);	// Ditto
-		}
-
-		player->follower->extravalue1 = 0;	// extravalue1 is used to know what "state set" to use.
-		/*
-			0 = idle
-			1 = forwards
-			2 = hurt
-			3 = win
-			4 = lose
-			5 = hitconfirm (< this one uses ->movecount as timer to know when to end, and goes back to normal states afterwards, unless hurt)
-		*/
-	}
-	else	// follower exists, woo!
-	{
-
-		// Safety net (2)
-
-		if (P_MobjWasRemoved(player->follower))
-		{
-			P_SetTarget(&player->follower, NULL);	// Remove this and respawn one, don't crash the game if Lua decides to P_RemoveMobj this thing.
-			return;
-		}
-
-		// first of all, handle states following the same model as above:
-		if (player->follower->tics == 1)
-			P_SetFollowerState(player->follower, player->follower->state->nextstate);
-
-		// move the follower next to us (yes, this is really basic maths but it looks pretty damn clean in practice)!
-		// 02/09/2021: cast lag to int32 otherwise funny things happen since it was changed to uint32 in the struct
-		player->follower->momx = (sx - player->follower->x)/ (INT32)fl.horzlag;
-		player->follower->momy = (sy - player->follower->y)/ (INT32)fl.horzlag;
-		player->follower->z += (deltaz/ (INT32)fl.vertlag);
-		player->follower->momz = (sz - player->follower->z)/ (INT32)fl.vertlag;
-		player->follower->angle = player->mo->angle;
-
-		if (player->mo->colorized)
-			player->follower->color = player->mo->color;
-		else
-			player->follower->color = color;
-
-		player->follower->colorized = player->mo->colorized;
-
-		P_SetScale(player->follower, FixedMul(fl.scale, player->mo->scale));
-		K_GenericExtraFlagsNoZAdjust(player->follower, player->mo);	// Not K_MatchGenericExtraFlag because the Z adjust it has only works properly if master & mo have the same Z height.
-
-		// Match how the player is being drawn
-		player->follower->renderflags = player->mo->renderflags;
-
-		// Make the follower invisible if we no contest'd rather than removing it. No one will notice the diff seriously.
-		if (player->pflags & PF_NOCONTEST)
-			player->follower->renderflags |= RF_DONTDRAW;
-
-		// if we're moving let's make the angle the direction we're moving towards. This is to avoid drifting / reverse looking awkward.
-		player->follower->angle = K_MomentumAngle(player->follower);
-
-		// Finally, if the follower has bubbles, move them, set their scale, etc....
-		// This is what I meant earlier by it being easier, now we can just use this weird lil loop to get the job done!
-
-		bmobj = player->follower->hnext;	// will be NULL if there's no bubble
-
-		while (bmobj && !P_MobjWasRemoved(bmobj))
-		{
-			// match follower's momentums and (e)flags(2).
-			bmobj->momx = player->follower->momx;
-			bmobj->momy = player->follower->momy;
-			bmobj->z += (deltaz/ (INT32)fl.vertlag);
-			bmobj->momz = player->follower->momz;
-
-			P_SetScale(bmobj, FixedMul(bubble, player->mo->scale));
-			K_GenericExtraFlagsNoZAdjust(bmobj, player->follower);
-			bmobj->renderflags = player->mo->renderflags;
-
-			if (player->follower->threshold)	// threshold means the follower was "despawned" with S_NULL (is actually just set to S_INVISIBLE)
-				P_SetMobjState(bmobj, S_INVISIBLE);	// sooooo... let's do the same!
-
-			bmobj = bmobj->hnext;	// switch to other bubble layer or exit
-		}
-
-
-		if (player->follower->threshold)
-			return;	// Threshold means the follower was "despanwed" with S_NULL.
-
-		// However with how the code is factored, this is just a special case of S_INVISBLE to avoid having to add other player variables.
-
-
-		// handle follower animations. Could probably be better...
-		// hurt or dead
-		if (player->spinouttimer || player->mo->state == &states[S_KART_SPINOUT] || player->mo->health <= 0)
-		{
-			player->follower->movecount = 0;	// cancel hit confirm.
-			player->follower->angle = player->drawangle;	// spin out
-			if (player->follower->extravalue1 != 2)
-			{
-				player->follower->extravalue1 = 2;
-				P_SetFollowerState(player->follower, fl.hurtstate);
-			}
-			if (player->mo->health <= 0)	// if dead, follow the player's z momentum exactly so they both look like they die at the same speed.
-				player->follower->momz = player->mo->momz;
-		}
-		else if (player->follower->movecount)
-		{
-			if (player->follower->extravalue1 != 5)
-			{
-				player->follower->extravalue1 = 5;
-				P_SetFollowerState(player->follower, fl.hitconfirmstate);
-			}
-			player->follower->movecount--;
-		}
-		else if (player->speed > 10*player->mo->scale)	// animation for moving fast enough
-		{
-
-			if (player->follower->extravalue1 != 1)
-			{
-				player->follower->extravalue1 = 1;
-				P_SetFollowerState(player->follower, fl.followstate);
-			}
-		}
-		else	// animations when nearly still. This includes winning and losing.
-		{
-			if (player->follower->extravalue1 != 0)
-			{
-
-				if (player->exiting)	// win/ loss animations
-				{
-					if (K_IsPlayerLosing(player))	// L
-					{
-						if (player->follower->extravalue1 != 4)
-						{
-							player->follower->extravalue1 = 4;
-							P_SetFollowerState(player->follower, fl.losestate);
-						}
-					}
-					else	// W
-					{
-						if (player->follower->extravalue1 != 3)
-						{
-							player->follower->extravalue1 = 3;
-							P_SetFollowerState(player->follower, fl.winstate);
-						}
-					}
-				}
-				else	// normal standstill
-				{
-					player->follower->extravalue1 = 0;
-					P_SetFollowerState(player->follower, fl.idlestate);
-				}
-			}
-		}
-	}
-}
-
 	/* gaysed script from me, based on Golden's sprite slope roll */
 
 // holy SHIT
@@ -4371,9 +4087,6 @@ void P_PlayerThink(player_t *player)
 		player->awayviewtics = 0; // reset to zero
 	}
 
-	// Run followes here. We need them to run even when we're dead to follow through what we're doing.
-	P_HandleFollower(player);
-
 	if (player->flashcount)
 		player->flashcount--;
 
@@ -4788,11 +4501,15 @@ void P_PlayerAfterThink(player_t *player)
 
 	if (player->playerstate == PST_DEAD)
 	{
+		// Followers need handled while dead.
+		K_HandleFollower(player);
+
 		if (player->followmobj)
 		{
 			P_RemoveMobj(player->followmobj);
 			P_SetTarget(&player->followmobj, NULL);
 		}
+
 		return;
 	}
 
@@ -4865,6 +4582,10 @@ void P_PlayerAfterThink(player_t *player)
 			}
 		}
 	}
+
+	// Run followers in AfterThink, after the players have moved,
+	// so a lag value of 1 is exactly attached to the player.
+	K_HandleFollower(player);
 }
 
 void P_SetPlayerAngle(player_t *player, angle_t angle)
diff --git a/src/r_data.h b/src/r_data.h
index 1228f2420b1b889ef3f20e9bc38f939ca3984048..3c8908a597d38b6b6c353096302949e06e1372f0 100644
--- a/src/r_data.h
+++ b/src/r_data.h
@@ -40,9 +40,6 @@ extern INT16 color8to16[256]; // remap color index to highcolor
 extern INT16 *hicolormaps; // remap high colors to high colors..
 
 extern CV_PossibleValue_t Color_cons_t[];
-extern CV_PossibleValue_t Followercolor_cons_t[];	// follower colours table, not a duplicate because of the "Match" option.
-#define FOLLOWERCOLOR_MATCH UINT16_MAX
-#define FOLLOWERCOLOR_OPPOSITE (UINT16_MAX-1)
 
 // I/O, setting up the stuff.
 void R_InitTextureData(void);
diff --git a/src/r_draw.c b/src/r_draw.c
index d9f89242349803152b18e7c10348b5c2268875e9..7488ab5aadce61a31a2fb9359e208b19dd1c975a 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -194,7 +194,6 @@ static INT32 CacheIndexToSkin(INT32 ttc)
 }
 
 CV_PossibleValue_t Color_cons_t[MAXSKINCOLORS+1];
-CV_PossibleValue_t Followercolor_cons_t[MAXSKINCOLORS+3];	// +3 to account for "Match", "Opposite" & NULL
 
 #define TRANSTAB_AMTMUL10 (255.0f / 10.0f)
 
diff --git a/src/r_skins.c b/src/r_skins.c
index ba7ed08110e8ee63f282b55397eff77b3ac0506b..ae5e0363cd3091ba4c4613e2ac865e2c50824653 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -27,6 +27,9 @@
 #include "p_local.h"
 #include "dehacked.h" // get_number (for thok)
 #include "m_cond.h"
+#if 0
+#include "k_kart.h" // K_KartResetPlayerColor
+#endif
 #ifdef HWRENDER
 #include "hardware/hw_md2.h"
 #endif
@@ -34,8 +37,6 @@
 INT32 numskins = 0;
 skin_t skins[MAXSKINS];
 
-INT32 numfollowers = 0;
-
 // FIXTHIS: don't work because it must be inistilised before the config load
 //#define SKINVALUES
 #ifdef SKINVALUES
@@ -44,9 +45,6 @@ CV_PossibleValue_t skin_cons_t[MAXSKINS+1];
 
 CV_PossibleValue_t Forceskin_cons_t[MAXSKINS+2];
 
-// SRB2Kart followers
-follower_t followers[MAXSKINS];
-
 //
 // 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.
@@ -285,7 +283,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
 {
 	player_t *player = &players[playernum];
 	skin_t *skin = &skins[skinnum];
-	UINT16 newcolor = 0;
+	//UINT16 newcolor = 0;
 	//UINT8 i;
 
 	if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum)) // Make sure it exists!
@@ -311,6 +309,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
 			}
 
 			player->skincolor = newcolor = skin->prefcolor;
+			K_KartResetPlayerColor(player);
 		}
 #endif
 
@@ -323,12 +322,6 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
 		if (player->mo)
 		{
 			player->mo->skin = skin;
-
-			if (newcolor)
-			{
-				player->mo->color = newcolor;
-			}
-
 			P_SetScale(player->mo, player->mo->scale);
 			P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames
 		}
@@ -860,97 +853,3 @@ next_token:
 }
 
 #undef SYMBOLCONVERT
-
-// SRB2Kart: Followers!
-// TODO: put this stuff in its own file?
-
-// same thing as R_SkinAvailable, but for followers
-INT32 R_FollowerAvailable(const char *name)
-{
-	INT32 i;
-
-	for (i = 0; i < numfollowers; i++)
-	{
-		if (stricmp(followers[i].skinname,name)==0)
-			return i;
-	}
-	return -1;
-}
-
-// same thing as SetPlayerSkin, but for followers
-boolean SetPlayerFollower(INT32 playernum, const char *skinname)
-{
-	INT32 i;
-	player_t *player = &players[playernum];
-
-	if (stricmp("None", skinname) == 0)
-	{
-		SetFollower(playernum, -1);	// reminder that -1 is nothing
-		return true;
-	}
-	for (i = 0; i < numfollowers; i++)
-	{
-		// search in the skin list
-		if (stricmp(followers[i].skinname, skinname) == 0)
-		{
-			SetFollower(playernum, i);
-			return true;
-		}
-	}
-
-	if (P_IsLocalPlayer(player))
-		CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found.\n"), skinname);
-	else if(server || IsPlayerAdmin(consoleplayer))
-		CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) follower '%s' not found\n"), playernum, player_names[playernum], skinname);
-
-	SetFollower(playernum, -1);	// reminder that -1 is nothing
-	return false;
-}
-
-// SetPlayerSkinByNum, for followers
-void SetFollower(INT32 playernum, INT32 skinnum)
-{
-	player_t *player = &players[playernum];
-	mobj_t *bub;
-	mobj_t *tmp;
-
-	player->followerready = true; // we are ready to perform follower related actions in the player thinker, now.
-
-	if (skinnum >= -1 && skinnum <= numfollowers) // Make sure it exists!
-	{
-		/*
-			We don't spawn the follower here since it'll be easier to handle all of it in the Player thinker itself.
-			However, we will despawn it right here if there's any to make it easy for the player thinker to replace it or delete it.
-		*/
-
-		if (player->follower && skinnum != player->followerskin)	// this is also called when we change colour so don't respawn the follower unless we changed skins
-		{
-			// Remove follower's possible hnext list (bubble)
-			bub = player->follower->hnext;
-
-			while (bub && !P_MobjWasRemoved(bub))
-			{
-				tmp = bub->hnext;
-				P_RemoveMobj(bub);
-				bub = tmp;
-			}
-
-			P_RemoveMobj(player->follower);
-			P_SetTarget(&player->follower, NULL);
-		}
-
-		player->followerskin = skinnum;
-
-		// for replays: We have changed our follower mid-game; let the game know so it can do the same in the replay!
-		demo_extradata[playernum] |= DXD_FOLLOWER;
-
-		return;
-	}
-
-	if (P_IsLocalPlayer(player))
-		CONS_Alert(CONS_WARNING, M_GetText("Follower %d not found\n"), skinnum);
-	else if(server || IsPlayerAdmin(consoleplayer))
-		CONS_Alert(CONS_WARNING, "Player %d (%s) follower %d not found\n", playernum, player_names[playernum], skinnum);
-
-	SetFollower(playernum, -1); // Not found, then set -1 (nothing) as our follower.
-}
diff --git a/src/r_skins.h b/src/r_skins.h
index 2d29af1881265ea2d71e1d6f97e70d6ac1bd0b53..cffd54da83496ff1c9232a9e6d1f817f0fe9eb35 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -90,51 +90,4 @@ void R_AddSkins(UINT16 wadnum);
 
 UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player);
 
-// SRB2Kart Followers
-
-//
-// We'll define these here because they're really just a mobj that'll follow some rules behind a player
-//
-typedef struct follower_s
-{
-	char skinname[SKINNAMESIZE+1];	// Skin Name. This is what to refer to when asking the commands anything.
-	char name[SKINNAMESIZE+1];		// Name. This is used for the menus. We'll just follow the same rules as skins for this.
-
-	UINT16 defaultcolor;	// default color for menus.
-
-	fixed_t scale;			// Scale relative to the player's.
-	fixed_t bubblescale;	// Bubble scale relative to the player scale. If not set, no bubble will spawn (default)
-
-	// some position shenanigans:
-	INT32 atangle;			// angle the object will be at around the player. The object itself will always face the same direction as the player.
-	INT32 dist;				// distance relative to the player. (In a circle)
-	INT32 height;			// height of the follower, this is mostly important for Z flipping.
-	INT32 zoffs;			// Z offset relative to the player's height. Cannot be negative.
-
-	// movement options
-
-	UINT32 horzlag;			// Lag for X/Y displacement. Default is 2. Must be > 0 because we divide by this number.
-	UINT32 vertlag;			// not Vert from Neptunia lagging, this is for Z displacement lag Default is 6. Must be > 0 because we divide by this number.
-	INT32 bobamp;			// Bob amplitude. Default is 4.
-	INT32 bobspeed;			// Arbitrary modifier for bobbing speed, default is TICRATE*2 (70).
-
-	// from there on out, everything is STATES to allow customization
-	// these are only set once when the action is performed and are then free to animate however they want.
-
-	INT32 idlestate;		// state when the player is at a standstill
-	INT32 followstate;		// state when the player is moving
-	INT32 hurtstate;		// state when the player is being hurt
-	INT32 winstate;			// state when the player has won
-	INT32 losestate;		// state when the player has lost
-	INT32 hitconfirmstate;	// state for hit confirm
-	UINT32 hitconfirmtime;	// time to keep the above playing for
-} follower_t;
-
-extern INT32 numfollowers;
-extern follower_t followers[MAXSKINS]; // again, use the same rules as skins, no reason not to.
-
-INT32 R_FollowerAvailable(const char *name);
-boolean SetPlayerFollower(INT32 playernum,const char *skinname);
-void SetFollower(INT32 playernum,INT32 skinnum);
-
 #endif //__R_SKINS__