diff --git a/src/m_menu.c b/src/m_menu.c
index 38165472e53459d93dc3f3cffa2a81ecce2d9de7..c4fccb372c376e72f15e54cc1410999f3648f878 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -140,6 +140,7 @@ static char *char_notes = NULL;
 
 boolean menuactive = false;
 boolean fromlevelselect = false;
+tic_t shieldprompt_timer = 0; // Show a prompt about the new Shield button for old configs // TODO: 2.3: Remove
 
 typedef enum
 {
@@ -3161,6 +3162,7 @@ static void Command_Manual_f(void)
 	if (modeattacking)
 		return;
 	M_StartControlPanel();
+	if (shieldprompt_timer) return; // TODO: 2.3: Delete this line
 	currentMenu = &MISC_HelpDef;
 	itemOn = 0;
 }
@@ -3340,6 +3342,7 @@ boolean M_Responder(event_t *ev)
 				if (modeattacking)
 					return true;
 				M_StartControlPanel();
+				if (shieldprompt_timer) return true; // TODO: 2.3: Delete this line
 				M_Options(0);
 				// Uncomment the below if you want the menu to reset to the top each time like before. M_SetupNextMenu will fix it automatically.
 				//OP_SoundOptionsDef.lastOn = 0;
@@ -3350,6 +3353,7 @@ boolean M_Responder(event_t *ev)
 				if (modeattacking)
 					return true;
 				M_StartControlPanel();
+				if (shieldprompt_timer) return true; // TODO: 2.3: Delete this line
 				M_Options(0);
 				M_VideoModeMenu(0);
 				return true;
@@ -3361,6 +3365,7 @@ boolean M_Responder(event_t *ev)
 				if (modeattacking)
 					return true;
 				M_StartControlPanel();
+				if (shieldprompt_timer) return true; // TODO: 2.3: Delete this line
 				M_Options(0);
 				M_SetupNextMenu(&OP_MainDef);
 				return true;
@@ -3631,6 +3636,230 @@ void M_Drawer(void)
 	}
 }
 
+// Handle the "Do you want to assign Shield Ability now?" pop-up for old configs // TODO: 2.3: Remove this line...
+static UINT8 shieldprompt_currentchoice = 0; // ...and this line...
+
+static void M_ShieldPromptUseDefaults(void) // ...and this function
+{
+	// With a default config from v2.2.10 to v2.2.13, the B button will be set to Custom 1,
+	// and Controls per Key defaults to "One", so it will override the default Shield button.
+	// A default config from v2.2.0 to v2.2.9 has Next Weapon on B, so it suffers from this too.
+
+	// So for "Use default Shield Ability buttons", we should update old configs to mitigate gamepad conflicts
+	// (even with "Several" Controls per Key!), and show a message with the default bindings
+
+	for (setupcontrols = gamecontrol; true; setupcontrols = gamecontrolbis) // Do stuff for both P1 and P2
+	{
+		INT32 JOY1 = (setupcontrols == gamecontrol) ? KEY_JOY1 : KEY_2JOY1; // Is this for P1 or for P2?
+
+		if ((setupcontrols[GC_CUSTOM1][0] == JOY1+1 || setupcontrols[GC_CUSTOM1][1] == JOY1+1)
+		&&  (setupcontrols[GC_CUSTOM2][0] == JOY1+3 || setupcontrols[GC_CUSTOM2][1] == JOY1+3)
+		&&  (setupcontrols[GC_CUSTOM3][0] == JOY1+8 || setupcontrols[GC_CUSTOM3][1] == JOY1+8))
+		{
+			// If the player has v2.2.13's default gamepad Custom 1/2/3 buttons,
+			// shuffle Custom 1/2/3 around to make room for Shield Ability on B
+			UINT8 shield_slot  = (setupcontrols[GC_SHIELD ][0] == KEY_NULL  ) ? 0 : 1;
+			UINT8 custom1_slot = (setupcontrols[GC_CUSTOM1][0] == JOY1+1) ? 0 : 1;
+			UINT8 custom2_slot = (setupcontrols[GC_CUSTOM2][0] == JOY1+3) ? 0 : 1;
+			UINT8 custom3_slot = (setupcontrols[GC_CUSTOM3][0] == JOY1+8) ? 0 : 1;
+
+			setupcontrols[GC_SHIELD ][shield_slot ] = JOY1+1; // Assign Shield Ability to B
+			setupcontrols[GC_CUSTOM1][custom1_slot] = JOY1+3; // Move Custom 1 from B to Y
+			setupcontrols[GC_CUSTOM2][custom2_slot] = JOY1+8; // Move Custom 2 from Y to LS
+			setupcontrols[GC_CUSTOM3][custom3_slot] = KEY_NULL; // Unassign Custom 3 from LS...
+			// (The alternative would be to check and update the ENTIRE gamepad layout.
+			// That'd be nice, but it would mess with people that are used to the old defaults.)
+		}
+		else if ((setupcontrols[GC_WEAPONNEXT][0] == JOY1+1 || setupcontrols[GC_WEAPONNEXT][1] == JOY1+1)
+		&&       (setupcontrols[GC_WEAPONPREV][0] == JOY1+2 || setupcontrols[GC_WEAPONPREV][1] == JOY1+2))
+		{
+			// Or if the user has a default config from v2.2.0 to v2.2.9,
+			// the B button will be Next Weapon, and X will be Previous Weapon.
+			// It's "safe" to discard one of them, you just have to press X multiple times to select in the other direction
+			UINT8 shield_slot  = (setupcontrols[GC_SHIELD    ][0] == KEY_NULL  ) ? 0 : 1;
+			UINT8 nweapon_slot = (setupcontrols[GC_WEAPONNEXT][0] == JOY1+1) ? 0 : 1;
+			UINT8 pweapon_slot = (setupcontrols[GC_WEAPONPREV][0] == JOY1+2) ? 0 : 1;
+
+			setupcontrols[GC_SHIELD    ][shield_slot ] = JOY1+1; // Assign Shield Ability to B
+			setupcontrols[GC_WEAPONNEXT][nweapon_slot] = JOY1+3; // Move Next Weapon from B to X
+			setupcontrols[GC_WEAPONPREV][pweapon_slot] = KEY_NULL; // Unassign Previous Weapon from X
+		}
+
+		if (setupcontrols == gamecontrolbis) // If we've already updated both players, break out
+			break;
+	}
+
+
+	// Now, show a message about the default Shield Ability bindings
+	if ((gamecontrol[GC_SHIELD][0] == KEY_LALT && gamecontrol[GC_SHIELD][1] == KEY_JOY1+1)
+	||  (gamecontrol[GC_SHIELD][0] == KEY_JOY1+1 && gamecontrol[GC_SHIELD][1] == KEY_LALT))
+	{
+		// Left Alt and the B button are both assigned
+		M_StartMessage(M_GetText("Shield Ability defaults to\nthe \x82""Left Alt\x80"" key on keyboard,\nand the \x85""B button\x80"" on gamepads."
+		"\n\nYou can always reassign it\nin the Options menu later."
+		"\n\n\nPress 'Enter' to continue\n"),
+			NULL, MM_NOTHING);
+		MessageDef.x = 43; // Change the pop-up message's background position/width
+		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 27;
+	}
+	else if (gamecontrol[GC_SHIELD][0] == KEY_LALT || gamecontrol[GC_SHIELD][1] == KEY_LALT)
+	{
+		// Left Alt is assigned, but the B button isn't.
+		M_StartMessage(M_GetText("Shield Ability defaults to\nthe \x82""Left Alt\x80"" key on keyboard.\nThe \x85""B button\x80"" on gamepads was taken."
+		"\n\nYou can always reassign it\nin the Options menu later."
+		"\n\n\nPress 'Enter' to continue\n"),
+			NULL, MM_NOTHING);
+		MessageDef.x = 24; // Change the pop-up message's background position/width
+		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 32;
+	}
+	else if (gamecontrol[GC_SHIELD][0] == KEY_JOY1+1 || gamecontrol[GC_SHIELD][1] == KEY_JOY1+1)
+	{
+		// The B button is assigned, but Left Alt isn't
+		M_StartMessage(M_GetText("Shield Ability defaults to\nthe \x85""B button\x80"" on gamepads.\nThe \x82""Left Alt\x80"" key on keyboard was taken."
+		"\n\nYou can always reassign it\nin the Options menu later."
+		"\n\n\nPress 'Enter' to continue\n"),
+			NULL, MM_NOTHING);
+		MessageDef.x = 8; // Change the pop-up message's background position/width
+		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 36;
+	}
+	else if (gamecontrol[GC_SHIELD][0] == KEY_NULL && gamecontrol[GC_SHIELD][1] == KEY_NULL)
+	{
+		// Neither Left Alt nor the B button are assigned
+		M_StartMessage(M_GetText("Shield Ability is unassigned!\nThe \x82""Left Alt\x80"" key on keyboard and\nthe \x85""B button\x80"" on gamepads were taken."
+		"\n\nYou should assign Shield Ability\nin the Options menu later."
+		"\n\n\nPress 'Enter' to continue\n"),
+			NULL, MM_NOTHING);
+		MessageDef.x = 19; // Change the pop-up message's background position/width
+		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 33;
+	}
+	else
+	{
+		// Neither Left Alt nor the B button are assigned... but something else is???
+		// (This can technically happen if you edit your config or use setcontrol in the console before opening the menu)
+		char keystr[16+16+2+7+1]; // Two 16-char keys + two colour codes + "' and '" + null
+
+		if (gamecontrol[GC_SHIELD][0] != KEY_NULL && gamecontrol[GC_SHIELD][1] != KEY_NULL)
+			STRBUFCPY(keystr, va("%s\x80""' and '\x82""%s",
+				G_KeyNumToName(gamecontrol[GC_SHIELD][0]),
+				G_KeyNumToName(gamecontrol[GC_SHIELD][1])));
+		else if (gamecontrol[GC_SHIELD][0] != KEY_NULL)
+			STRBUFCPY(keystr, G_KeyNumToName(gamecontrol[GC_SHIELD][0]));
+		else //if (gamecontrol[GC_SHIELD][1] != KEY_NULL)
+			STRBUFCPY(keystr, G_KeyNumToName(gamecontrol[GC_SHIELD][1]));
+
+		M_StartMessage(va("Shield Ability is assigned to\n'\x82""%s\x80""'."
+		"\n\nYou can always reassign it\nin the Options menu later."
+		"\n\n\nPress 'Enter' to continue\n",
+			keystr), NULL, MM_NOTHING);
+		MessageDef.x = 23; // Change the pop-up message's background position/width
+		MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 32;
+	}
+}
+
+static void M_HandleShieldPromptMenu(INT32 choice) // TODO: 2.3: Remove
+{
+	switch (choice)
+	{
+		case KEY_ESCAPE:
+			if (I_GetTime() <= shieldprompt_timer) // Don't mash past the pop-up by accident!
+				break;
+
+			S_StartSound(NULL, sfx_menu1);
+			noFurtherInput = true;
+			shieldprompt_timer = 0;
+			M_ShieldPromptUseDefaults();
+			break;
+
+		case KEY_ENTER:
+			if (I_GetTime() <= shieldprompt_timer) // Don't mash past the pop-up by accident!
+				break;
+
+			S_StartSound(NULL, sfx_menu1);
+			noFurtherInput = true;
+			shieldprompt_timer = 0;
+
+			if (shieldprompt_currentchoice == 0)
+			{
+				OP_ChangeControlsDef.lastOn = 8; // Highlight Shield Ability in the controls menu
+				M_Setup1PControlsMenu(0); // Set up P1's controls menu and call M_SetupNextMenu
+			}
+			else if (shieldprompt_currentchoice == 1) // Copy the Spin buttons to the Shield buttons
+			{
+				CV_SetValue(&cv_controlperkey, 2); // Make sure that Controls per Key is "Several"
+
+				gamecontrol   [GC_SHIELD][0] = gamecontrol   [GC_SPIN][0];
+				gamecontrol   [GC_SHIELD][1] = gamecontrol   [GC_SPIN][1];
+				gamecontrolbis[GC_SHIELD][0] = gamecontrolbis[GC_SPIN][0];
+				gamecontrolbis[GC_SHIELD][1] = gamecontrolbis[GC_SPIN][1];
+				CV_SetValue(&cv_shieldaxis,  cv_spinaxis.value);
+				CV_SetValue(&cv_shieldaxis2, cv_spinaxis2.value);
+
+				M_StartMessage(M_GetText("Spin and Shield Ability are now\nthe same button."
+				"\n\nYou can always reassign them\nin the Options menu later."
+				"\n\n\nPress 'Enter' to continue\n"),
+					NULL, MM_NOTHING);
+				MessageDef.x = 36; // Change the pop-up message's background position/width
+				MessageDef.lastOn = (MessageDef.lastOn & ~0xFF) | 29;
+			}
+			else
+				M_ShieldPromptUseDefaults();
+			break;
+
+		case KEY_UPARROW:
+			S_StartSound(NULL, sfx_menu1);
+			shieldprompt_currentchoice = (shieldprompt_currentchoice+2)%3;
+			break;
+
+		case KEY_DOWNARROW:
+			S_StartSound(NULL, sfx_menu1);
+			shieldprompt_currentchoice = (shieldprompt_currentchoice+1)%3;
+			break;
+	}
+
+	MessageDef.prevMenu = &MainDef;
+}
+
+static void M_DrawShieldPromptMenu(void) // TODO: 2.3: Remove
+{
+	INT16 cursorx = (BASEVIDWIDTH/2) - 24;
+
+	V_DrawFill(10-3, 68-3, 300+6, 40+6, 159);
+	// V_DrawCenteredString doesn't centre newlines, so we have to draw each line separately
+	V_DrawCenteredString(BASEVIDWIDTH/2, 68, V_ALLOWLOWERCASE, "Welcome back! Since you last played,");
+	V_DrawCenteredString(BASEVIDWIDTH/2, 76, V_ALLOWLOWERCASE, "Spin has been split into separate");
+	V_DrawCenteredString(BASEVIDWIDTH/2, 84, V_ALLOWLOWERCASE, "\"Spin\" and \"Shield Ability\" controls.");
+
+	V_DrawCenteredString(BASEVIDWIDTH/2, 98, V_ALLOWLOWERCASE, "Do you want to assign Shield Ability now?");
+
+
+	V_DrawCenteredString(BASEVIDWIDTH/2, 164,
+		(shieldprompt_currentchoice == 0) ? V_YELLOWMAP : 0, "Open Control Setup");
+	V_DrawCenteredString(BASEVIDWIDTH/2, 172,
+		(shieldprompt_currentchoice == 1) ? V_YELLOWMAP : 0, "Keep the old behaviour");
+	V_DrawCenteredString(BASEVIDWIDTH/2, 180,
+		(shieldprompt_currentchoice == 2) ? V_YELLOWMAP : 0, "Use default Shield Ability buttons");
+
+	switch (shieldprompt_currentchoice)
+	{
+		case 0:  cursorx -= V_StringWidth("Open Control Setup",                 0)/2; break;
+		case 1:  cursorx -= V_StringWidth("Keep the old behaviour",             0)/2; break;
+		default: cursorx -= V_StringWidth("Use default Shield Ability buttons", 0)/2; break;
+	}
+	V_DrawScaledPatch(cursorx, 164 + (shieldprompt_currentchoice*8), 0, W_CachePatchName("M_CURSOR", PU_PATCH));
+}
+
+static menuitem_t OP_ShieldPromptMenu[] = {{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleShieldPromptMenu, 0}}; // TODO: 2.3: Remove
+
+menu_t OP_ShieldPromptDef = { // TODO: 2.3: Remove
+	MN_SPECIAL,
+	NULL,
+	1,
+	&MainDef,
+	OP_ShieldPromptMenu,
+	M_DrawShieldPromptMenu,
+	0, 0, 0, NULL
+};
+
 //
 // M_StartControlPanel
 //
@@ -3662,6 +3891,15 @@ void M_StartControlPanel(void)
 		currentMenu = &MainDef;
 		itemOn = singleplr;
 		M_UpdateItemOn();
+
+		if (shieldprompt_timer) // For old configs, show a pop-up about the new Shield button // TODO: 2.3: Remove
+		{
+			S_StartSound(NULL, sfx_strpst);
+			noFurtherInput = true;
+			shieldprompt_timer = I_GetTime() + TICRATE; // Don't mash past the pop-up by accident!
+
+			M_SetupNextMenu(&OP_ShieldPromptDef);
+		}
 	}
 	else if (modeattacking)
 	{
diff --git a/src/m_menu.h b/src/m_menu.h
index 99a5b6de4266be02b1cba0e98a26addac5524548..55c0b4060820df361c0eb97c5729efeda9f60d4b 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -176,6 +176,7 @@ typedef struct
 extern menupres_t menupres[NUMMENUTYPES];
 extern UINT32 prevMenuId;
 extern UINT32 activeMenuId;
+extern tic_t shieldprompt_timer; // Show a prompt about the new Shield button for old configs // TODO: 2.3: Remove
 
 void M_InitMenuPresTables(void);
 UINT8 M_GetYoungestChildMenu(void);
diff --git a/src/m_misc.c b/src/m_misc.c
index a60bbea98d2e2b47cf3fea0dae652d26b6aa4763..724717aa681597b33a8b7bc573dc0be91220337f 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -560,6 +560,11 @@ void M_FirstLoadConfig(void)
 	COM_BufInsertText(va("exec \"%s\"\n", configfile));
 	// no COM_BufExecute() needed; that does it right away
 
+	// For configs loaded at startup only, check for pre-Shield-button configs // TODO: 2.3: Remove
+	if (GETMAJOREXECVERSION(cv_execversion.value) < 55 // Pre-v2.2.14 configs
+	&& cv_execversion.value != 25) // Make sure that the config exists, too
+		shieldprompt_timer = 1;
+
 	// don't filter anymore vars and don't let this convsvar be changed
 	COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, EXECVERSION));
 	CV_ToggleExecVersion(false);