diff --git a/src/cvars.cpp b/src/cvars.cpp
index 5eb4c6a0be1d6d7cb6837b9e65af172877a91ba2..85085e14739497919ddf91870cdda9b2e2906da0 100644
--- a/src/cvars.cpp
+++ b/src/cvars.cpp
@@ -975,6 +975,7 @@ consvar_t cv_dummymenuplayer = MenuDummy("dummymenuplayer", "P1").onchange(Dummy
 consvar_t cv_dummyprofileautoroulette = MenuDummy("dummyprofileautoroulette", "Off").on_off();
 consvar_t cv_dummyprofilefov = MenuDummy("dummyprofilefov", "100").min_max(70, 110);
 consvar_t cv_dummyprofilelitesteer = MenuDummy("dummyprofilelitesteer", "Off").on_off();
+consvar_t cv_dummyprofileautoring = MenuDummy("dummyprofileautoring", "Off").on_off();
 consvar_t cv_dummyprofilekickstart = MenuDummy("dummyprofilekickstart", "Off").on_off();
 consvar_t cv_dummyprofilename = MenuDummy("dummyprofilename", "");
 consvar_t cv_dummyprofileplayername = MenuDummy("dummyprofileplayername", "");
@@ -1087,6 +1088,13 @@ consvar_t cv_litesteer[MAXSPLITSCREENPLAYERS] = {
 	Player("litesteer4", "Off").on_off().onchange(weaponPrefChange4),
 };
 
+consvar_t cv_autoring[MAXSPLITSCREENPLAYERS] = {
+	Player("autoring", "Off").on_off().onchange(weaponPrefChange),
+	Player("autoring2", "Off").on_off().onchange(weaponPrefChange2),
+	Player("autoring3", "Off").on_off().onchange(weaponPrefChange3),
+	Player("autoring4", "Off").on_off().onchange(weaponPrefChange4),
+};
+
 consvar_t cv_cam_dist[MAXSPLITSCREENPLAYERS] = {
 	Player("cam_dist", "190").floating_point(),
 	Player("cam2_dist", "190").floating_point(),
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 5853e7936beecb3f530ba9aaafd5a0d63914a265..d887aaf733d57ac627f50411e4de11668b300eaa 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -1258,6 +1258,7 @@ enum {
 	WP_SHRINKME = 1<<1,
 	WP_AUTOROULETTE = 1<<2,
 	WP_ANALOGSTICK = 1<<3,
+	WP_AUTORING = 1<<4,
 };
 
 void WeaponPref_Send(UINT8 ssplayer)
@@ -1276,6 +1277,9 @@ void WeaponPref_Send(UINT8 ssplayer)
 	if (gamecontrolflags[ssplayer] & GCF_ANALOGSTICK)
 		prefs |= WP_ANALOGSTICK;
 
+	if (cv_autoring[ssplayer].value)
+		prefs |= WP_AUTORING;
+
 	UINT8 buf[2];
 	buf[0] = prefs;
 	buf[1] = cv_mindelay.value;
@@ -1301,6 +1305,9 @@ void WeaponPref_Save(UINT8 **cp, INT32 playernum)
 	if (player->pflags & PF_ANALOGSTICK)
 		prefs |= WP_ANALOGSTICK;
 
+	if (player->pflags & PF_AUTORING)
+		prefs |= WP_AUTORING;
+
 	WRITEUINT8(*cp, prefs);
 }
 
@@ -1311,7 +1318,7 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
 
 	UINT8 prefs = READUINT8(p);
 
-	player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE);
+	player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE|PF_AUTORING);
 
 	if (prefs & WP_KICKSTARTACCEL)
 		player->pflags |= PF_KICKSTARTACCEL;
@@ -1327,6 +1334,9 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
 	else
 		player->pflags &= ~PF_ANALOGSTICK;
 
+	if (prefs & WP_AUTORING)
+		player->pflags |= PF_AUTORING;
+
 	if (leveltime < 2)
 	{
 		// BAD HACK: No other place I tried to slot this in
diff --git a/src/d_player.h b/src/d_player.h
index c17b8d48d62a662b32494117e77fa65c84f130bd..8932313776f4578afa8bc8edcfc1d91461eeef99 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -109,7 +109,7 @@ typedef enum
 	PF_TRUSTWAYPOINTS	= 1<<15, // Do not activate lap cheat prevention next time finish line distance is updated
 	PF_FREEZEWAYPOINTS	= 1<<16, // Skip the next waypoint/finish line distance update
 
-	//1<<17 free, was previously itemflags stuff
+	PF_AUTORING			= 1<<17, // Accessibility: Non-deterministic item box, no manual stop.
 
 	PF_DRIFTINPUT		= 1<<18, // Drifting!
 	PF_GETSPARKS		= 1<<19, // Can get sparks
@@ -955,6 +955,7 @@ struct player_t
 	UINT8 typing_duration; // How long since resumed timer
 
 	UINT8 kickstartaccel;
+	boolean autoring;	// did we autoring this tic?
 
 	UINT8 stairjank;
 	UINT8 topdriftheld;
diff --git a/src/deh_soc.c b/src/deh_soc.c
index b7ccb9187ca86c1c94487c88850b501ca2717331..b0e7a473a9181fab7b712459bbf82c989d34fc5c 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -4152,6 +4152,7 @@ void readfollower(MYFILE *f)
 	followers[numfollowers].bobspeed = TICRATE*2;
 	followers[numfollowers].bobamp = 4*FRACUNIT;
 	followers[numfollowers].hitconfirmtime = TICRATE;
+	followers[numfollowers].ringtime = 10;
 	followers[numfollowers].defaultcolor = FOLLOWERCOLOR_MATCH;
 	followers[numfollowers].category = UINT8_MAX;
 	followers[numfollowers].hornsound = sfx_horn00;
@@ -4329,6 +4330,16 @@ void readfollower(MYFILE *f)
 			{
 				followers[numfollowers].hitconfirmtime = (tic_t)get_number(word2);
 			}
+			else if (fastcmp(word, "RINGSTATE"))
+			{
+				if (word2)
+					strupr(word2);
+				followers[numfollowers].ringstate = get_number(word2);
+			}
+			else if (fastcmp(word, "RINGTIME"))
+			{
+				followers[numfollowers].ringtime = (tic_t)get_number(word2);
+			}
 			else
 			{
 				deh_warning("Follower %d: unknown word '%s'", numfollowers, word);
@@ -4386,6 +4397,7 @@ if ((signed)followers[numfollowers].field < threshold) \
 	FALLBACK(bobamp, "BOBAMP", 0, 0);
 	FALLBACK(bobspeed, "BOBSPEED", 0, 0);
 	FALLBACK(hitconfirmtime, "HITCONFIRMTIME", 1, 1);
+	FALLBACK(ringtime, "RINGTIME", 1, 1);
 	FALLBACK(scale, "SCALE", 1, 1);				// No null/negative scale
 	FALLBACK(bubblescale, "BUBBLESCALE", 0, 0);	// No negative scale
 
@@ -4408,6 +4420,7 @@ if (!followers[numfollowers].field) \
 	NOSTATE(losestate, "LOSESTATE");
 	NOSTATE(winstate, "WINSTATE");
 	NOSTATE(hitconfirmstate, "HITCONFIRMSTATE");
+	NOSTATE(ringstate, "RINGSTATE");
 #undef NOSTATE
 
 	if (!followers[numfollowers].hornsound)
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 3122a1387164220fab70e8e5b5ea402342ec12a9..df818c09456301efb412c9a7aebc7fbc2cd73a8c 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -4066,7 +4066,7 @@ const char *const PLAYERFLAG_LIST[] = {
 	"ANALOGSTICK", // This player is using an analog joystick
 	"TRUSTWAYPOINTS", // Do not activate lap cheat prevention next time finish line distance is updated
 	"FREEZEWAYPOINTS", // Skip the next waypoint/finish line distance update
-	"\x01", // Free
+	"AUTORING", // Accessibility: Non-deterministic item box, no manual stop.
 
 	"DRIFTINPUT", // Drifting!
 	"GETSPARKS", // Can get sparks
diff --git a/src/g_demo.cpp b/src/g_demo.cpp
index 13eba5aa308a4b5e8569597ec04fa6ca839ed09d..2e21a7d7ed5839d7f5d96c057fef29ffc922be98 100644
--- a/src/g_demo.cpp
+++ b/src/g_demo.cpp
@@ -205,6 +205,7 @@ boolean G_CompatLevel(UINT16 level)
 #define DEMO_SHRINKME		0x04
 #define DEMO_BOT			0x08
 #define DEMO_AUTOROULETTE	0x10
+#define DEMO_AUTORING		0x20
 
 // For demos
 #define ZT_FWD		0x0001
@@ -2310,6 +2311,8 @@ void G_BeginRecording(void)
 				i |= DEMO_KICKSTART;
 			if (player->pflags & PF_AUTOROULETTE)
 				i |= DEMO_AUTOROULETTE;
+			if (player->pflags & PF_AUTORING)
+				i |= DEMO_AUTORING;
 			if (player->pflags & PF_SHRINKME)
 				i |= DEMO_SHRINKME;
 			if (player->bot == true)
@@ -3383,6 +3386,11 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum)
 		else
 			players[p].pflags &= ~PF_AUTOROULETTE;
 
+		if (flags & DEMO_AUTORING)
+			players[p].pflags |= PF_AUTORING;
+		else
+			players[p].pflags &= ~PF_AUTORING;
+
 		if (flags & DEMO_SHRINKME)
 			players[p].pflags |= PF_SHRINKME;
 		else
diff --git a/src/g_game.c b/src/g_game.c
index c9fdfc91f5767564ac466f869ce2f643c083cd93..04daab555d5563e888d71b2cd930e3f93861c9fc 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2251,7 +2251,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	totalring = players[player].totalring;
 	xtralife = players[player].xtralife;
 
-	pflags = (players[player].pflags & (PF_WANTSTOJOIN|PF_KICKSTARTACCEL|PF_SHRINKME|PF_SHRINKACTIVE|PF_AUTOROULETTE|PF_ANALOGSTICK));
+	pflags = (players[player].pflags & (PF_WANTSTOJOIN|PF_KICKSTARTACCEL|PF_SHRINKME|PF_SHRINKACTIVE|PF_AUTOROULETTE|PF_ANALOGSTICK|PF_AUTORING));
 
 	// SRB2kart
 	memcpy(&itemRoulette, &players[player].itemRoulette, sizeof (itemRoulette));
diff --git a/src/g_game.h b/src/g_game.h
index 335294950c7459944d96887b5343389094691217..ff49c49b47628eb2363b885a9914a270665dfbea 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -99,6 +99,7 @@ extern consvar_t cv_pauseifunfocused;
 extern consvar_t cv_kickstartaccel[MAXSPLITSCREENPLAYERS];
 extern consvar_t cv_autoroulette[MAXSPLITSCREENPLAYERS];
 extern consvar_t cv_litesteer[MAXSPLITSCREENPLAYERS];
+extern consvar_t cv_autoring[MAXSPLITSCREENPLAYERS];
 extern consvar_t cv_shrinkme[MAXSPLITSCREENPLAYERS];
 
 extern consvar_t cv_deadzone[MAXSPLITSCREENPLAYERS];
diff --git a/src/k_follower.c b/src/k_follower.c
index 1617c8d456a792a2877784ae7caff8c1f9cb5bed..dc02685b6f7b109855f25fa31e690ee337e1f402 100644
--- a/src/k_follower.c
+++ b/src/k_follower.c
@@ -338,6 +338,8 @@ void K_HandleFollower(player_t *player)
 	angle_t destAngle;
 	INT32 angleDiff;
 
+	INT32 followerskin;
+
 	if (player->followerready == false)
 	{
 		// we aren't ready to perform anything follower related yet.
@@ -351,9 +353,11 @@ void K_HandleFollower(player_t *player)
 		player->followerskin = -1;
 	}
 
+	followerskin = K_GetEffectiveFollowerSkin(player);
+
 	// don't do anything if we can't have a follower to begin with.
 	// (It gets removed under those conditions)
-	if (player->spectator || player->followerskin < 0
+	if (player->spectator || followerskin < 0
 	|| player->mo == NULL || P_MobjWasRemoved(player->mo))
 	{
 		if (player->follower)
@@ -364,7 +368,7 @@ void K_HandleFollower(player_t *player)
 	}
 
 	// Before we do anything, let's be sure of where we're supposed to be
-	fl = &followers[player->followerskin];
+	fl = &followers[followerskin];
 
 	an = player->mo->angle + fl->atangle;
 	zoffs = fl->zoffs;
@@ -417,7 +421,10 @@ void K_HandleFollower(player_t *player)
 	}
 
 	// Set follower colour
-	color = K_GetEffectiveFollowerColor(player->followercolor, fl, player->skincolor, &skins[player->skin]);
+	if (player->followerskin < 0) // using a fallback follower
+		color = fl->defaultcolor;
+	else
+		color = K_GetEffectiveFollowerColor(player->followercolor, fl, player->skincolor, &skins[player->skin]);
 
 	if (player->follower == NULL || P_MobjWasRemoved(player->follower)) // follower doesn't exist / isn't valid
 	{
@@ -519,6 +526,13 @@ void K_HandleFollower(player_t *player)
 		{
 			player->follower->renderflags |= RF_DONTDRAW;
 		}
+		else
+		{
+			if ((player->pflags & PF_AUTORING) && !(K_PlayerCanUseItem(player) && (player->itemflags & IF_USERINGS)))
+			{
+				player->follower->renderflags |= RF_TRANS50;
+			}
+		}
 
 		// 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)
@@ -537,6 +551,12 @@ void K_HandleFollower(player_t *player)
 			destAngle += ANGLE_180;
 		}
 
+		// Using auto-ring, face towards the player while throwing your rings.
+		if (player->follower->cvmem)
+		{
+			destAngle = player->mo->angle + ANGLE_180;
+		}
+
 		// Sal: Smoothly rotate angle to the destination value.
 		angleDiff = AngleDeltaSigned(destAngle, player->follower->angle);
 
@@ -628,8 +648,9 @@ void K_HandleFollower(player_t *player)
 		// hurt or dead
 		if (P_PlayerInPain(player) == true || player->mo->state == &states[S_KART_SPINOUT] || player->mo->health <= 0)
 		{
-			// cancel hit confirm.
+			// cancel hit confirm / rings
 			player->follower->movecount = 0;
+			player->follower->cvmem = 0;
 
 			// spin out
 			player->follower->angle = player->drawangle;
@@ -661,6 +682,11 @@ void K_HandleFollower(player_t *player)
 			K_UpdateFollowerState(player->follower, fl->hitconfirmstate, FOLLOWERSTATE_HITCONFIRM);
 			player->follower->movecount--;
 		}
+		else if (player->follower->cvmem)
+		{
+			K_UpdateFollowerState(player->follower, fl->ringstate, FOLLOWERSTATE_RING);
+			player->follower->cvmem--;
+		}
 		else if (player->speed > 10*player->mo->scale) // animation for moving fast enough
 		{
 			K_UpdateFollowerState(player->follower, fl->followstate, FOLLOWERSTATE_FOLLOW);
@@ -730,18 +756,21 @@ void K_HandleFollower(player_t *player)
 --------------------------------------------------*/
 void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial)
 {
+	// special case for checking for fallback follower for autoring
+	const INT32 followerskin = K_GetEffectiveFollowerSkin(taunter);
+
 	// Basic checks
 	if (
 		taunter == NULL
 		|| victim == NULL
-		|| taunter->followerskin < 0
-		|| taunter->followerskin >= numfollowers
+		|| followerskin < 0
+		|| followerskin >= numfollowers
 	)
 	{
 		return;
 	}
 
-	const follower_t *fl = &followers[taunter->followerskin];
+	const follower_t *fl = &followers[followerskin];
 
 	// Restrict mystic melody special status
 	if (mysticmelodyspecial == true)
@@ -866,3 +895,16 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelo
 		}
 	}
 }
+
+/*--------------------------------------------------
+	INT32 K_GetEffectiveFollowerSkin(const player_t *player);
+
+		See header file for description.
+--------------------------------------------------*/
+INT32 K_GetEffectiveFollowerSkin(const player_t *player)
+{
+	if ((player->pflags & PF_AUTORING) && player->followerskin == -1)
+		return K_FollowerAvailable("Goddess");
+	else
+		return player->followerskin;
+}
diff --git a/src/k_follower.h b/src/k_follower.h
index dc3c6cb8b8ab936b7d07c52605b13009a31f145d..edab92f8ef4d680296021120365db685fa651366 100644
--- a/src/k_follower.h
+++ b/src/k_follower.h
@@ -49,6 +49,7 @@ typedef enum
 	FOLLOWERSTATE_WIN,
 	FOLLOWERSTATE_LOSE,
 	FOLLOWERSTATE_HITCONFIRM, // Uses movecount as a timer for how long to play this state.
+	FOLLOWERSTATE_RING, // Uses cvmem as a timer for how long to play this state.
 	FOLLOWERSTATE__MAX
 } followerstate_t;
 
@@ -93,6 +94,8 @@ struct follower_t
 	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
+	statenum_t ringstate;		// state for giving an auto-ring
+	tic_t ringtime;				// time to keep the above playing for
 
 	sfxenum_t hornsound;		// Press (B) to announce you are pressing (B)
 };
@@ -243,6 +246,21 @@ void K_RemoveFollower(player_t *player);
 
 void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial);
 
+/*--------------------------------------------------
+	INT32 K_GetEffectiveFollowerSkin(const player_t *player)
+
+		Returns the player's follower, set by profile or as
+		a fallback.
+
+	Input Arguments:-
+		player - The player.
+
+	Return:-
+		The resultant skin id for the follower, or -1 for None
+--------------------------------------------------*/
+
+INT32 K_GetEffectiveFollowerSkin(const player_t *player);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
diff --git a/src/k_hud.cpp b/src/k_hud.cpp
index 4aa19ac05746bd18ac28c3d557812da9e33adf7a..cc03a30e7a4b76b78032c764a31a5080a9276f6d 100644
--- a/src/k_hud.cpp
+++ b/src/k_hud.cpp
@@ -212,6 +212,7 @@ static patch_t *kp_bossret[4];
 static patch_t *kp_trickcool[2];
 
 patch_t *kp_autoroulette;
+patch_t *kp_autoring;
 
 patch_t *kp_capsuletarget_arrow[2][2];
 patch_t *kp_capsuletarget_icon[2];
@@ -781,6 +782,7 @@ void K_LoadKartHUDGraphics(void)
 	HU_UpdatePatch(&kp_trickcool[1], "K_COOL2");
 
 	HU_UpdatePatch(&kp_autoroulette, "A11YITEM");
+	HU_UpdatePatch(&kp_autoring, "A11YRING");
 
 	sprintf(buffer, "K_BOSB0x");
 	for (i = 0; i < 8; i++)
@@ -3250,6 +3252,19 @@ static void K_drawKartAccessibilityIcons(boolean gametypeinfoshown, INT32 fx)
         else
             fx += 12 + 1;
     }
+
+	if (stplyr->pflags & PF_AUTORING)
+    {
+        if (mirror)
+            fx -= 14;
+
+        V_DrawScaledPatch(fx, fy-1, V_SLIDEIN|splitflags, kp_autoring);
+
+        if (mirror)
+            fx--;
+        else
+            fx += 14 + 1;
+    }
 }
 
 static void K_drawKartSpeedometer(boolean gametypeinfoshown)
diff --git a/src/k_hud.h b/src/k_hud.h
index a4e92c728258aa0ef21eac12d045a61226bbc174..07cea47742a470d84e60462f85f285a3022a607b 100644
--- a/src/k_hud.h
+++ b/src/k_hud.h
@@ -78,6 +78,7 @@ extern patch_t *kp_spraycantarget_far[2][6];
 extern patch_t *kp_spraycantarget_near[2][6];
 
 extern patch_t *kp_autoroulette;
+extern patch_t *kp_autoring;
 
 extern patch_t *kp_button_a[2][2];
 extern patch_t *kp_button_b[2][2];
diff --git a/src/k_kart.c b/src/k_kart.c
index 9c71770bab3ab03575e09471d26f184326cc66d3..bfd837f240fddbf49826f65fbc558cce30c153b2 100644
--- a/src/k_kart.c
+++ b/src/k_kart.c
@@ -2865,11 +2865,12 @@ void K_TryHurtSoundExchange(mobj_t *victim, mobj_t *attacker)
 	attacker->player->confirmVictim = (victim->player - players);
 	attacker->player->confirmVictimDelay = TICRATE/2;
 
+	const INT32 followerskin = K_GetEffectiveFollowerSkin(attacker->player);
 	if (attacker->player->follower != NULL
-		&& attacker->player->followerskin >= 0
-		&& attacker->player->followerskin < numfollowers)
+		&& followerskin >= 0
+		&& followerskin < numfollowers)
 	{
-		const follower_t *fl = &followers[attacker->player->followerskin];
+		const follower_t *fl = &followers[followerskin];
 		attacker->player->follower->movecount = fl->hitconfirmtime; // movecount is used to play the hitconfirm animation for followers.
 	}
 }
@@ -12599,14 +12600,45 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 		player->instaWhipCharge = min(player->instaWhipCharge, INSTAWHIP_CHARGETIME - 2);
 	}
 
-	if (player && player->mo && player->mo->health > 0 && !player->spectator && !P_PlayerInPain(player) && !mapreset && leveltime > introtime)
+	if (player && player->mo && K_PlayerCanUseItem(player))
 	{
 		// First, the really specific, finicky items that function without the item being directly in your item slot.
 		{
 			// Ring boosting
 			if (player->itemflags & IF_USERINGS)
 			{
-				if ((cmd->buttons & BT_ATTACK) && !player->ringdelay && player->rings > 0)
+				// Auto-Ring
+				UINT8 tiereddelay = 5;
+				player->autoring = false;
+				if (
+					player->pflags & PF_AUTORING
+					&& leveltime > starttime
+					&& K_GetForwardMove(player) > 0
+					&& P_IsObjectOnGround(player->mo)
+				)
+				{
+					fixed_t pspeed = FixedDiv(player->speed * 100, K_GetKartSpeed(player, false, true));
+
+					if (player->rings >= 18 && pspeed < 100*FRACUNIT)
+					{
+						player->autoring = true;
+						tiereddelay = 3;
+					}
+					else if (player->rings >= 10 && pspeed < 85*FRACUNIT)
+					{
+						player->autoring = true;
+						tiereddelay = 4;
+					}
+					else if (player->rings >= 4 && pspeed < 35*FRACUNIT)
+					{
+						player->autoring = true;
+						tiereddelay = 5;
+					}
+					else
+						player->autoring = false;
+				}
+
+				if (((cmd->buttons & BT_ATTACK) || player->autoring) && !player->ringdelay && player->rings > 0)
 				{
 					mobj_t *ring = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_RING);
 					P_SetMobjState(ring, S_FASTRING1);
@@ -12628,9 +12660,41 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 					ring->extravalue2 = 1; // Ring use animation flag
 					ring->shadowscale = 0;
 					P_SetTarget(&ring->target, player->mo); // user
+
+					const INT32 followerskin = K_GetEffectiveFollowerSkin(player);
+					if (player->autoring
+						&& player->follower != NULL
+						&& P_MobjWasRemoved(player->follower) == false
+						&& followerskin >= 0
+						&& followerskin < numfollowers)
+					{
+						const follower_t *fl = &followers[followerskin];
+
+						ring->cusval = player->follower->x - player->mo->x;
+						ring->cvmem = player->follower->y - player->mo->y;
+						ring->movefactor = P_GetMobjHead(player->follower) - P_GetMobjHead(player->mo);
+
+						// cvmem is used to play the ring animation for followers
+						player->follower->cvmem = fl->ringtime;
+					}
+					else
+					{
+						ring->cusval = ring->cvmem = ring->movefactor = 0;
+					}
+
+					// really silly stupid dumb HACK to fix interp
+					// without needing to duplicate any code
+					A_AttractChase(ring);
+					P_SetOrigin(ring, ring->x, ring->y, ring->z);
+					ring->extravalue1 = 1;
+
 					player->rings--;
-					player->ringdelay = 3;
+					if (player->autoring && !(cmd->buttons & BT_ATTACK))
+						player->ringdelay = tiereddelay;
+					else
+						player->ringdelay = 3;
 				}
+
 			}
 			// Other items
 			else
@@ -14416,4 +14480,9 @@ void K_MakeObjectReappear(mobj_t *mo)
 	(!P_MobjWasRemoved(mo->punt_ref) ? mo->punt_ref : mo)->reappear = leveltime + (30*TICRATE);
 }
 
+boolean K_PlayerCanUseItem(player_t *player)
+{
+	return (player->mo->health > 0 && !player->spectator && !P_PlayerInPain(player) && !mapreset && leveltime > introtime);
+}
+
 //}
diff --git a/src/k_kart.h b/src/k_kart.h
index 6556138e16969aa88d669b7d17d823ac5b64888b..4c67b23df53206d4d22498b90f094b19e79ee06c 100644
--- a/src/k_kart.h
+++ b/src/k_kart.h
@@ -277,6 +277,8 @@ void K_BumperInflate(player_t *player);
 
 boolean K_ThunderDome(void);
 
+boolean K_PlayerCanUseItem(player_t *player);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
diff --git a/src/k_menu.h b/src/k_menu.h
index feb465404d7fb0db16dca8957b4e98b91feb6da3..643170b627c7447347fd0c49d9e7ae767bb463e5 100644
--- a/src/k_menu.h
+++ b/src/k_menu.h
@@ -1124,6 +1124,7 @@ extern consvar_t cv_dummyprofileplayername;
 extern consvar_t cv_dummyprofilekickstart;
 extern consvar_t cv_dummyprofileautoroulette;
 extern consvar_t cv_dummyprofilelitesteer;
+extern consvar_t cv_dummyprofileautoring;
 extern consvar_t cv_dummyprofilerumble;
 extern consvar_t cv_dummyprofilefov;
 
diff --git a/src/k_profiles.cpp b/src/k_profiles.cpp
index 1076cfc7814c8443eec00c0f8768bd5cede1bbea..3491d70fc2256da62b53fd1e7edf8d52cea44518 100644
--- a/src/k_profiles.cpp
+++ b/src/k_profiles.cpp
@@ -82,6 +82,7 @@ profile_t* PR_MakeProfile(
 	newprofile->kickstartaccel = false;
 	newprofile->autoroulette = false;
 	newprofile->litesteer = false;
+	newprofile->autoring = false;
 	newprofile->rumble = true;
 	newprofile->fov = atoi(cv_dummyprofilefov.defaultvalue);
 
@@ -103,6 +104,7 @@ profile_t* PR_MakeProfileFromPlayer(const char *prname, const char *pname, const
 	newprofile->kickstartaccel = cv_kickstartaccel[pnum].value;
 	newprofile->autoroulette = cv_autoroulette[pnum].value;
 	newprofile->litesteer = cv_litesteer[pnum].value;
+	newprofile->autoring = cv_autoring[pnum].value;
 	newprofile->rumble = cv_rumble[pnum].value;
 	newprofile->fov = cv_fov[pnum].value / FRACUNIT;
 
@@ -299,6 +301,7 @@ void PR_SaveProfiles(void)
 		jsonprof.preferences.kickstartaccel = cprof->kickstartaccel;
 		jsonprof.preferences.autoroulette = cprof->autoroulette;
 		jsonprof.preferences.litesteer = cprof->litesteer;
+		jsonprof.preferences.autoring = cprof->autoring;
 		jsonprof.preferences.rumble = cprof->rumble;
 		jsonprof.preferences.fov = cprof->fov;
 
@@ -473,6 +476,7 @@ void PR_LoadProfiles(void)
 		newprof->kickstartaccel = jsprof.preferences.kickstartaccel;
 		newprof->autoroulette = jsprof.preferences.autoroulette;
 		newprof->litesteer = jsprof.preferences.litesteer;
+		newprof->autoring = jsprof.preferences.autoring;
 		newprof->rumble = jsprof.preferences.rumble;
 		newprof->fov = jsprof.preferences.fov;
 
@@ -554,6 +558,7 @@ static void PR_ApplyProfile_Settings(profile_t *p, UINT8 playernum)
 	CV_StealthSetValue(&cv_kickstartaccel[playernum], p->kickstartaccel);
 	CV_StealthSetValue(&cv_autoroulette[playernum], p->autoroulette);
 	CV_StealthSetValue(&cv_litesteer[playernum], p->litesteer);
+	CV_StealthSetValue(&cv_autoring[playernum], p->autoring);
 	CV_StealthSetValue(&cv_rumble[playernum], p->rumble);
 	CV_StealthSetValue(&cv_fov[playernum], p->fov);
 
diff --git a/src/k_profiles.h b/src/k_profiles.h
index 70d3abd6899016a52a88acff02e2c2089f1ac45c..247a08e9ffe440f6531b31bed5eb7f84561d81ea 100644
--- a/src/k_profiles.h
+++ b/src/k_profiles.h
@@ -46,6 +46,7 @@ struct ProfilePreferencesJson
 	bool kickstartaccel;
 	bool autoroulette;
 	bool litesteer;
+	bool autoring;
 	bool rumble;
 	uint8_t fov;
 	tm test;
@@ -55,6 +56,7 @@ struct ProfilePreferencesJson
 		kickstartaccel,
 		autoroulette,
 		litesteer,
+		autoring,
 		rumble,
 		fov
 	)
@@ -160,6 +162,7 @@ struct profile_t
 	boolean kickstartaccel;				// cv_kickstartaccel
 	boolean autoroulette;				// cv_autoroulette
 	boolean litesteer;					// cv_litesteer
+	boolean autoring;					// cv_autoring
 	boolean rumble;						// cv_rumble
 	UINT8 fov;							// cv_fov
 
diff --git a/src/menus/options-profiles-1.c b/src/menus/options-profiles-1.c
index be2584e63f7acdfc93bf9b40057134f090ab0428..e7c31449c8e5f337de30c7f26091e2b97a6fcd55 100644
--- a/src/menus/options-profiles-1.c
+++ b/src/menus/options-profiles-1.c
@@ -102,6 +102,7 @@ void M_StartEditProfile(INT32 c)
 			CV_StealthSetValue(&cv_dummyprofilekickstart, optionsmenu.profile->kickstartaccel);
 			CV_StealthSetValue(&cv_dummyprofileautoroulette, optionsmenu.profile->autoroulette);
 			CV_StealthSetValue(&cv_dummyprofilelitesteer, optionsmenu.profile->litesteer);
+			CV_StealthSetValue(&cv_dummyprofileautoring, optionsmenu.profile->autoring);
 			CV_StealthSetValue(&cv_dummyprofilerumble, optionsmenu.profile->rumble);
 			CV_StealthSetValue(&cv_dummyprofilefov, optionsmenu.profile->fov);
 		}
@@ -112,6 +113,7 @@ void M_StartEditProfile(INT32 c)
 			CV_StealthSetValue(&cv_dummyprofilekickstart, 0);	// off
 			CV_StealthSetValue(&cv_dummyprofileautoroulette, 0); // off
 			CV_StealthSetValue(&cv_dummyprofilelitesteer, 1); // on
+			CV_StealthSetValue(&cv_dummyprofileautoring, 0); // on
 			CV_StealthSetValue(&cv_dummyprofilerumble, 1);	// on
 			CV_StealthSetValue(&cv_dummyprofilefov, 90);
 		}
diff --git a/src/menus/options-profiles-edit-1.c b/src/menus/options-profiles-edit-1.c
index 9599ae456da90c7f6f290315c195933446ad13cb..93d44049a8eb27da002626442632194e218d47f4 100644
--- a/src/menus/options-profiles-edit-1.c
+++ b/src/menus/options-profiles-edit-1.c
@@ -98,6 +98,7 @@ static void M_ProfileEditApply(void)
 	optionsmenu.profile->kickstartaccel = cv_dummyprofilekickstart.value;
 	optionsmenu.profile->autoroulette = cv_dummyprofileautoroulette.value;
 	optionsmenu.profile->litesteer = cv_dummyprofilelitesteer.value;
+	optionsmenu.profile->autoring = cv_dummyprofileautoring.value;
 	optionsmenu.profile->rumble = cv_dummyprofilerumble.value;
 	optionsmenu.profile->fov = cv_dummyprofilefov.value;
 
@@ -109,6 +110,7 @@ static void M_ProfileEditApply(void)
 		CV_SetValue(&cv_kickstartaccel[belongsto], cv_dummyprofilekickstart.value);
 		CV_SetValue(&cv_autoroulette[belongsto], cv_dummyprofileautoroulette.value);
 		CV_SetValue(&cv_litesteer[belongsto], cv_dummyprofilelitesteer.value);
+		CV_SetValue(&cv_autoring[belongsto], cv_dummyprofileautoring.value);
 		CV_SetValue(&cv_rumble[belongsto], cv_dummyprofilerumble.value);
 		CV_SetValue(&cv_fov[belongsto], cv_dummyprofilefov.value);
 	}
diff --git a/src/menus/options-profiles-edit-accessibility.cpp b/src/menus/options-profiles-edit-accessibility.cpp
index 9a236dae8bc614896ba7dd51c8a775aa64f3a618..14dd0643d1d4bfd8fb68cc1ef7bf1ab26fb917a8 100644
--- a/src/menus/options-profiles-edit-accessibility.cpp
+++ b/src/menus/options-profiles-edit-accessibility.cpp
@@ -105,6 +105,9 @@ menuitem_t OPTIONS_ProfileAccessibility[] = {
 	{IT_STRING | IT_CVAR, "Auto Roulette", "Item roulette auto-stops on a random result.",
 		NULL, {.cvar = &cv_dummyprofileautoroulette}, 0, 0},
 
+	{IT_STRING | IT_CVAR, "Auto Ring", "Auto-use rings to maintain momentum.",
+		NULL, {.cvar = &cv_dummyprofileautoring}, 0, 0},
+
 	{IT_STRING | IT_CVAR, "Kickstart Accel", "Hold A to auto-accel. Tap it to cancel.",
 		NULL, {.cvar = &cv_dummyprofilekickstart}, 0, 0},
 
diff --git a/src/p_enemy.c b/src/p_enemy.c
index f2f50e8c5083346d9d8d5dc4f7ecb4ebbcae10b6..0cfc0d5e57a8c8e51b20cca0cee178f6ca786001 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3547,13 +3547,26 @@ void A_AttractChase(mobj_t *actor)
 			}
 			else
 			{
-				fixed_t offz = FixedMul(80*actor->target->scale, FINESINE(FixedAngle((90 - (9 * abs(10 - actor->extravalue1))) << FRACBITS) >> ANGLETOFINESHIFT));
+				fixed_t hop = FixedMul(
+					80 * actor->target->scale,
+					FINESINE(FixedAngle((90 - (9 * abs(10 - actor->extravalue1))) << FRACBITS) >> ANGLETOFINESHIFT)
+				);
+
+				fixed_t offsFrac = (20 - (actor->extravalue1 - 1)) * FRACUNIT / 20;
+				fixed_t offsX = FixedMul(actor->cusval, offsFrac);
+				fixed_t offsY = FixedMul(actor->cvmem, offsFrac);
+				fixed_t offsZ = FixedMul(actor->movefactor, offsFrac);
+
 				//P_SetScale(actor, (actor->destscale = actor->target->scale));
-				actor->z = actor->target->z;
 				K_MatchGenericExtraFlags(actor, actor->target);
-				P_MoveOrigin(actor, actor->target->x, actor->target->y,
-						actor->z +
-						( actor->target->height + offz )* P_MobjFlip(actor));
+
+				P_MoveOrigin(
+					actor,
+					actor->target->x + offsX,
+					actor->target->y + offsY,
+					actor->target->z + offsZ + ( actor->target->height + hop ) * P_MobjFlip(actor)
+				);
+
 				actor->extravalue1++;
 			}
 		}
diff --git a/src/p_saveg.c b/src/p_saveg.c
index bef745fdbdb696ea41341e140b6338e6bc20a383..6a54dfb58a2a2de081be9a1d2ac39813f7f43c06 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -565,6 +565,7 @@ static void P_NetArchivePlayers(savebuffer_t *save)
 		WRITEUINT8(save->p, players[i].typing_duration);
 
 		WRITEUINT8(save->p, players[i].kickstartaccel);
+		WRITEUINT8(save->p, players[i].autoring);
 
 		WRITEUINT8(save->p, players[i].stairjank);
 		WRITEUINT8(save->p, players[i].topdriftheld);
@@ -1166,6 +1167,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save)
 		players[i].typing_duration = READUINT8(save->p);
 
 		players[i].kickstartaccel = READUINT8(save->p);
+		players[i].autoring = READUINT8(save->p);
 
 		players[i].stairjank = READUINT8(save->p);
 		players[i].topdriftheld = READUINT8(save->p);