diff --git a/src/d_main.c b/src/d_main.c
index 605c3a677761620c9193bd6649faf686bbd99357..22369d1a4541faf7aabef1197463c9fbedc26b22 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -71,6 +71,7 @@ int	snprintf(char *str, size_t n, const char *fmt, ...);
 #include "fastcmp.h"
 #include "keys.h"
 #include "filesrch.h" // refreshdirmenu, mainwadstally
+#include "g_input.h" // tutorial mode control scheming
 
 #ifdef CMAKECONFIG
 #include "config.h"
@@ -733,6 +734,19 @@ void D_StartTitle(void)
 	// Reset the palette
 	if (rendermode != render_none)
 		V_SetPaletteLump("PLAYPAL");
+
+	// The title screen is obviously not a tutorial! (Unless I'm mistaken)
+	if (tutorialmode && tutorialgcs)
+	{
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gcl_tutorial_full, num_gcl_tutorial_full); // using gcs_custom as temp storage
+		CV_SetValue(&cv_usemouse, tutorialusemouse);
+		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
+		CV_SetValue(&cv_mousemove, tutorialmousemove);
+		CV_SetValue(&cv_analog, tutorialanalog);
+		M_StartMessage("Do you want to \x82save the recommended \x82movement controls?\x80\n\nPress 'Y' or 'Enter' to confirm\nPress 'N' or any key to keep \nyour current controls",
+			M_TutorialSaveControlResponse, MM_YESNO);
+	}
+	tutorialmode = false;
 }
 
 //
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index f38e097bd42d683b4682fd1e89c571d443c8250e..bb33eb133ed5df2f2cb16481253e78824f3f8a82 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -712,6 +712,7 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_crosshair2);
 	CV_RegisterVar(&cv_alwaysfreelook);
 	CV_RegisterVar(&cv_alwaysfreelook2);
+	CV_RegisterVar(&cv_tutorialprompt);
 
 	// g_input.c
 	CV_RegisterVar(&cv_sideaxis);
@@ -1808,6 +1809,16 @@ static void Command_Map_f(void)
 	else
 		fromlevelselect = ((netgame || multiplayer) && ((gametype == newgametype) && (newgametype == GT_COOP)));
 
+	if (tutorialmode && tutorialgcs)
+	{
+		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gcl_tutorial_full, num_gcl_tutorial_full); // using gcs_custom as temp storage
+		CV_SetValue(&cv_usemouse, tutorialusemouse);
+		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
+		CV_SetValue(&cv_mousemove, tutorialmousemove);
+		CV_SetValue(&cv_analog, tutorialanalog);
+	}
+	tutorialmode = false; // warping takes us out of tutorial mode
+
 	D_MapChange(newmapnum, newgametype, false, newresetplayers, 0, false, fromlevelselect);
 }
 
diff --git a/src/dehacked.c b/src/dehacked.c
index aa5d84954591ad63e9620e7327d2e7c237bb566e..ff7b603d9b151943937b7388a09c3862b327e01f 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -2892,6 +2892,19 @@ static void readmaincfg(MYFILE *f)
 				startchar = (INT16)value;
 				char_on = -1;
 			}
+			else if (fastcmp(word, "TUTORIALMAP"))
+			{
+				// Support using the actual map name,
+				// i.e., Level AB, Level FZ, etc.
+
+				// Convert to map number
+				if (word2[0] >= 'A' && word2[0] <= 'Z')
+					value = M_MapNumber(word2[0], word2[1]);
+				else
+					value = get_number(word2);
+
+				tutorialmap = (INT16)value;
+			}
 			else
 				deh_warning("Maincfg: unknown word '%s'", word);
 		}
diff --git a/src/doomstat.h b/src/doomstat.h
index c4fda93ae553c1ae161a963beb6aa1e78af28f18..7660547685426bd01cfb84ef5aa5f28b49b6fad2 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -132,6 +132,10 @@ extern INT16 bootmap; //bootmap for loading a map on startup
 extern INT16 tutorialmap; // map to load for tutorial
 extern boolean tutorialmode; // are we in a tutorial right now?
 extern INT32 tutorialgcs; // which control scheme is loaded?
+extern INT32 tutorialusemouse; // store cv_usemouse user value
+extern INT32 tutorialfreelook; // store cv_alwaysfreelook user value
+extern INT32 tutorialmousemove; // store cv_mousemove user value
+extern INT32 tutorialanalog; // store cv_analog user value
 
 extern boolean looptitle;
 
diff --git a/src/g_game.c b/src/g_game.c
index 28a8a8d0a9853d499140ea5abd9970536fe81191..4fb56abaf95e1a5e12d9eb748120e10e478dc04f 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -130,6 +130,10 @@ INT16 bootmap; //bootmap for loading a map on startup
 INT16 tutorialmap = 0; // map to load for tutorial
 boolean tutorialmode = false; // are we in a tutorial right now?
 INT32 tutorialgcs = gcs_custom; // which control scheme is loaded?
+INT32 tutorialusemouse = 0; // store cv_usemouse user value
+INT32 tutorialfreelook = 0; // store cv_alwaysfreelook user value
+INT32 tutorialmousemove = 0; // store cv_mousemove user value
+INT32 tutorialanalog = 0; // store cv_analog user value
 
 boolean looptitle = false;
 
diff --git a/src/g_game.h b/src/g_game.h
index d6b41830e8c674e52bf0cb3374e526709df3e93b..9e8580d139c8e94b0c31183746723b62bbf32f65 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -55,6 +55,7 @@ extern tic_t timeinmap; // Ticker for time spent in level (used for levelcard di
 extern INT16 rw_maximums[NUM_WEAPONS];
 
 // used in game menu
+extern consvar_t cv_tutorialprompt;
 extern consvar_t cv_crosshair, cv_crosshair2;
 extern consvar_t cv_invertmouse, cv_alwaysfreelook, cv_mousemove;
 extern consvar_t cv_invertmouse2, cv_alwaysfreelook2, cv_mousemove2;
diff --git a/src/m_menu.c b/src/m_menu.c
index 5e2cc48d51fe8ac688639284f2e14c95463d6ee6..e973cea551ab692d44f76a62ee44f9d9b4460c61 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -273,6 +273,7 @@ menu_t SP_MainDef, OP_MainDef;
 menu_t MISC_ScrambleTeamDef, MISC_ChangeTeamDef;
 
 // Single Player
+static void M_StartTutorial(INT32 choice);
 static void M_LoadGame(INT32 choice);
 static void M_TimeAttackLevelSelect(INT32 choice);
 static void M_TimeAttack(INT32 choice);
@@ -440,6 +441,9 @@ static CV_PossibleValue_t serversort_cons_t[] = {
 };
 consvar_t cv_serversort = {"serversort", "Ping", CV_HIDEN | CV_CALL, serversort_cons_t, M_SortServerList, 0, NULL, NULL, 0, 0, NULL};
 
+// first time memory
+consvar_t cv_tutorialprompt = {"tutorialprompt", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 // autorecord demos for time attack
 static consvar_t cv_autorecord = {"autorecord", "Yes", 0, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
@@ -731,6 +735,7 @@ static menuitem_t SR_EmblemHintMenu[] =
 // Single Player Main
 static menuitem_t SP_MainMenu[] =
 {
+	{IT_CALL | IT_STRING,                       NULL, "Tutorial",      M_StartTutorial,            84},
 	{IT_CALL | IT_STRING,                       NULL, "Start Game",    M_LoadGame,                 92},
 	{IT_SECRET,                                 NULL, "Record Attack", M_TimeAttack,              100},
 	{IT_SECRET,                                 NULL, "NiGHTS Mode",   M_NightsAttack,            108},
@@ -739,6 +744,7 @@ static menuitem_t SP_MainMenu[] =
 
 enum
 {
+	sptutorial,
 	sploadgame,
 	sprecordattack,
 	spnightsmode,
@@ -1554,7 +1560,18 @@ menu_t SR_EmblemHintDef =
 };
 
 // Single Player
-menu_t SP_MainDef = CENTERMENUSTYLE(NULL, SP_MainMenu, &MainDef, 72);
+menu_t SP_MainDef = //CENTERMENUSTYLE(NULL, SP_MainMenu, &MainDef, 72);
+{
+	NULL,
+	sizeof(SP_MainMenu)/sizeof(menuitem_t),
+	&MainDef,
+	SP_MainMenu,
+	M_DrawCenteredMenu,
+	BASEVIDWIDTH/2, 72,
+	1, // start at "Start Game" on first entry
+	NULL
+};
+
 menu_t SP_LoadDef =
 {
 	"M_PICKG",
@@ -6093,6 +6110,8 @@ static void M_CustomLevelSelect(INT32 choice)
 static void M_SinglePlayerMenu(INT32 choice)
 {
 	(void)choice;
+	SP_MainMenu[sptutorial].status =
+		tutorialmap ? IT_CALL|IT_STRING : IT_NOTHING|IT_DISABLED;
 	SP_MainMenu[sprecordattack].status =
 		(M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING : IT_SECRET;
 	SP_MainMenu[spnightsmode].status =
@@ -6118,6 +6137,80 @@ static void M_LoadGameLevelSelect(INT32 choice)
 	M_SetupNextMenu(&SP_LevelSelectDef);
 }
 
+void M_TutorialSaveControlResponse(INT32 ch)
+{
+	if (ch == 'y' || ch == KEY_ENTER)
+	{
+		G_CopyControls(gamecontrol, gamecontroldefault[tutorialgcs], gcl_tutorial_full, num_gcl_tutorial_full);
+		CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
+		CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
+		CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
+		CV_Set(&cv_analog, cv_analog.defaultvalue);
+		S_StartSound(NULL, sfx_itemup);
+	}
+	else
+		S_StartSound(NULL, sfx_menu1);
+}
+
+static void M_TutorialControlResponse(INT32 ch)
+{
+	if (ch != KEY_ESCAPE)
+	{
+		G_CopyControls(gamecontroldefault[gcs_custom], gamecontrol, NULL, 0); // using gcs_custom as temp storage for old controls
+		if (ch == 'y' || ch == KEY_ENTER)
+		{
+			tutorialgcs = gcs_fps;
+			tutorialusemouse = cv_usemouse.value;
+			tutorialfreelook = cv_alwaysfreelook.value;
+			tutorialmousemove = cv_mousemove.value;
+			tutorialanalog = cv_analog.value;
+
+			G_CopyControls(gamecontrol, gamecontroldefault[tutorialgcs], gcl_tutorial_full, num_gcl_tutorial_full);
+			CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
+			CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
+			CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
+			CV_Set(&cv_analog, cv_analog.defaultvalue);
+
+			//S_StartSound(NULL, sfx_itemup);
+		}
+		else
+		{
+			tutorialgcs = gcs_custom;
+			S_StartSound(NULL, sfx_menu1);
+		}
+		M_StartTutorial(INT32_MAX);
+	}
+	else
+		S_StartSound(NULL, sfx_menu1);
+
+	MessageDef.prevMenu = &SP_MainDef; // if FirstPrompt -> ControlsPrompt -> ESC, we would go to the main menu unless we force this
+}
+
+// Starts up the tutorial immediately (tbh I wasn't sure where else to put this)
+static void M_StartTutorial(INT32 choice)
+{
+	if (!tutorialmap)
+		return; // no map to go to, don't bother
+
+	if (choice != INT32_MAX && G_GetControlScheme(gamecontrol, gcl_tutorial_check, num_gcl_tutorial_check) != gcs_fps)
+	{
+		M_StartMessage("Do you want to try the \202recommended \202movement controls\x80?\n\nWe will set them just for this tutorial.\n\nPress 'Y' or 'Enter' to confirm\nPress 'N' or any key to keep \nyour current controls.\n",M_TutorialControlResponse,MM_YESNO);
+		return;
+	}
+	else if (choice != INT32_MAX)
+		tutorialgcs = gcs_custom;
+
+	CV_SetValue(&cv_tutorialprompt, 0); // first-time prompt
+
+	tutorialmode = true; // turn on tutorial mode
+
+	emeralds = 0;
+	M_ClearMenus(true);
+	gamecomplete = false;
+	cursaveslot = 0;
+	G_DeferedInitNew(false, G_BuildMapName(tutorialmap), 0, false, false);
+}
+
 // ==============
 // LOAD GAME MENU
 // ==============
@@ -6725,6 +6818,26 @@ static void M_HandleLoadSave(INT32 choice)
 	}
 }
 
+static void M_FirstTimeResponse(INT32 ch)
+{
+	S_StartSound(NULL, sfx_menu1);
+
+	if (ch == KEY_ESCAPE)
+		return;
+
+	if (ch != 'y' && ch != KEY_ENTER)
+	{
+		CV_SetValue(&cv_tutorialprompt, 0);
+		M_ReadSaveStrings();
+		MessageDef.prevMenu = &SP_LoadDef; // calls M_SetupNextMenu
+	}
+	else
+	{
+		M_StartTutorial(0);
+		MessageDef.prevMenu = &MessageDef; // otherwise, the controls prompt won't fire
+	}
+}
+
 //
 // Selected from SRB2 menu
 //
@@ -6732,6 +6845,13 @@ static void M_LoadGame(INT32 choice)
 {
 	(void)choice;
 
+	if (tutorialmap && cv_tutorialprompt.value)
+	{
+		M_StartMessage("Do you want to \x82play a brief Tutorial\x80?\n\nWe highly recommend this because \nthe controls are slightly different \nfrom other games.\n\nPress 'Y' or 'Enter' to go\nPress 'N' or any key to skip\n",
+			M_FirstTimeResponse, MM_YESNO);
+		return;
+	}
+
 	M_ReadSaveStrings();
 	M_SetupNextMenu(&SP_LoadDef);
 }
@@ -9198,9 +9318,17 @@ static void M_DrawControl(void)
 	// draw title (or big pic)
 	M_DrawMenuTitle();
 
-	M_CentreText(30,
-		 (setupcontrols_secondaryplayer ? "SET CONTROLS FOR SECONDARY PLAYER" :
-		                                  "PRESS ENTER TO CHANGE, BACKSPACE TO CLEAR"));
+	if (tutorialmode && tutorialgcs)
+	{
+		if ((gametic / TICRATE) % 2)
+			M_CentreText(30, "\202EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
+		else
+			M_CentreText(30, "EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
+	}
+	else
+		M_CentreText(30,
+		    (setupcontrols_secondaryplayer ? "SET CONTROLS FOR SECONDARY PLAYER" :
+		                                     "PRESS ENTER TO CHANGE, BACKSPACE TO CLEAR"));
 
 	if (i)
 		V_DrawString(currentMenu->x - 16, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A"); // up arrow
@@ -9335,6 +9463,9 @@ static void M_ChangeControl(INT32 choice)
 {
 	static char tmp[55];
 
+	if (tutorialmode && tutorialgcs) // don't allow control changes if temp control override is active
+		return;
+
 	controltochange = currentMenu->menuitems[choice].alphaKey;
 	sprintf(tmp, M_GetText("Hit the new key for\n%s\nESC for Cancel"),
 		currentMenu->menuitems[choice].text);
diff --git a/src/m_menu.h b/src/m_menu.h
index 9df56e897690f2044951fe85d16e96f4ee36f202..ad32de1b155120b2d82cacb74a17c28b7ee51207 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -240,6 +240,8 @@ extern INT16 char_on, startchar;
 
 #define BwehHehHe() S_StartSound(NULL, sfx_bewar1+M_RandomKey(4)) // Bweh heh he
 
+void M_TutorialSaveControlResponse(INT32 ch);
+
 void M_ForceSaveSlotSelected(INT32 sslot);
 
 void M_CheatActivationResponder(INT32 ch);
diff --git a/src/m_misc.c b/src/m_misc.c
index 7bc2d75cb3ba6cfb04668d6c4ac9f6726eef1fa6..92c2aeea9dd3458d66edc0f8495119542859998c 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -539,7 +539,21 @@ void M_SaveConfig(const char *filename)
 
 	// FIXME: save key aliases if ever implemented..
 
-	CV_SaveVariables(f);
+	if (tutorialmode && tutorialgcs)
+	{
+		CV_SetValue(&cv_usemouse, tutorialusemouse);
+		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
+		CV_SetValue(&cv_mousemove, tutorialmousemove);
+		CV_SetValue(&cv_analog, tutorialanalog);
+		CV_SaveVariables(f);
+		CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
+		CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
+		CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
+		CV_Set(&cv_analog, cv_analog.defaultvalue);
+	}
+	else
+		CV_SaveVariables(f);
+
 	if (!dedicated)
 	{
 		if (tutorialmode && tutorialgcs)
diff --git a/src/p_user.c b/src/p_user.c
index 57ee9169769a782bfb751ca962284d0efc8cc32a..f4bfd4c610282aad30be8c7ca5e4f2bf7b86ec8d 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -8785,7 +8785,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 
 	cameranoclip = (player->powers[pw_carry] == CR_NIGHTSMODE || player->pflags & PF_NOCLIP) || (mo->flags & (MF_NOCLIP|MF_NOCLIPHEIGHT)); // Noclipping player camera noclips too!!
 
-	if (!(player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD))
+	if (!(player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || tutorialmode))
 	{
 		if (player->spectator) // force cam off for spectators
 			return true;
@@ -8848,7 +8848,16 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	if (P_CameraThinker(player, thiscam, resetcalled))
 		return true;
 
-	if (thiscam == &camera)
+	if (tutorialmode)
+	{
+		// force defaults because we have a camera look section
+		camspeed = (INT32)(atof(cv_cam_speed.defaultvalue) * FRACUNIT);
+		camstill = (!stricmp(cv_cam_still.defaultvalue, "off")) ? false : true;
+		camrotate = atoi(cv_cam_rotate.defaultvalue);
+		camdist = FixedMul((INT32)(atof(cv_cam_dist.defaultvalue) * FRACUNIT), mo->scale);
+		camheight = FixedMul((INT32)(atof(cv_cam_height.defaultvalue) * FRACUNIT), FixedMul(player->camerascale, mo->scale));
+	}
+	else if (thiscam == &camera)
 	{
 		camspeed = cv_cam_speed.value;
 		camstill = cv_cam_still.value;
diff --git a/src/r_main.c b/src/r_main.c
index bfca180d0011ed9dc34d68bf9948b448b12827b8..98ce978a693b73acb894b46cfb9fe682990f7fe5 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -913,7 +913,7 @@ void R_SetupFrame(player_t *player, boolean skybox)
 		chasecam = (cv_chasecam.value != 0);
 	}
 
-	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN)
+	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
 		chasecam = true; // force chasecam on
 	else if (player->spectator) // no spectator chasecam
 		chasecam = false; // force chasecam off