diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8bbf46945f0eb0f561702ef014e96dfd3d72ce96..855393de1fdbe1eb436282d17e96134149b5c00d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0)
 # DO NOT CHANGE THIS SRB2 STRING! Some variable names depend on this string.
 # Version change is fine.
 project(SRB2
-	VERSION 2.2.0
+	VERSION 2.2.1
 	LANGUAGES C)
 
 if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
diff --git a/appveyor.yml b/appveyor.yml
index d33d3d3a3ef5239e4e7e55fc41e1918f5286a8d1..20b18d7d5ffd61c73817833b6fd41c3f66243aab 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.2.0.{branch}-{build}
+version: 2.2.1.{branch}-{build}
 os: MinGW
 
 environment:
diff --git a/src/am_map.c b/src/am_map.c
index 7dfbf4ba43d8e38109271cf35310d2eca054ba44..b2c7de442f21d90699dd77cec9596cf90931e0d7 100644
--- a/src/am_map.c
+++ b/src/am_map.c
@@ -160,6 +160,7 @@ static boolean am_stopped = true;
 static INT32 f_x, f_y;	// location of window on screen (always zero for both)
 static INT32 f_w, f_h;	// size of window on screen (always the screen width and height respectively)
 
+static boolean m_keydown[4]; // which window panning keys are being pressed down?
 static mpoint_t m_paninc; // how far the window pans each tic (map coords)
 static fixed_t mtof_zoommul; // how far the window zooms in each tic (map coords)
 static fixed_t ftom_zoommul; // how far the window zooms in each tic (fb coords)
@@ -205,6 +206,7 @@ static boolean followplayer = true; // specifies whether to follow the player ar
 typedef void (*AMDRAWFLINEFUNC) (const fline_t *fl, INT32 color);
 static AMDRAWFLINEFUNC AM_drawFline;
 
+static void AM_drawPixel(INT32 xx, INT32 yy, INT32 cc);
 static void AM_drawFline_soft(const fline_t *fl, INT32 color);
 
 static void AM_activateNewScale(void)
@@ -345,21 +347,21 @@ static void AM_initVariables(void)
 }
 
 //
-// should be called at the start of every level
-// right now, i figure it out myself
+// Called when the screen size changes.
 //
-static void AM_LevelInit(void)
+static void AM_FrameBufferInit(void)
 {
 	f_x = f_y = 0;
 	f_w = vid.width;
 	f_h = vid.height;
+}
 
-	AM_drawFline = AM_drawFline_soft;
-#ifdef HWRENDER
-	if (rendermode == render_opengl)
-		AM_drawFline = HWR_drawAMline;
-#endif
-
+//
+// should be called at the start of every level
+// right now, i figure it out myself
+//
+static void AM_LevelInit(void)
+{
 	AM_findMinMaxBoundaries();
 	scale_mtof = FixedDiv(min_scale_mtof*10, 7*FRACUNIT);
 	if (scale_mtof > max_scale_mtof)
@@ -381,7 +383,7 @@ void AM_Stop(void)
   *
   * \sa AM_Stop
   */
-static inline void AM_Start(void)
+void AM_Start(void)
 {
 	static INT32 lastlevel = -1;
 
@@ -390,8 +392,12 @@ static inline void AM_Start(void)
 	am_stopped = false;
 	if (lastlevel != gamemap || am_recalc) // screen size changed
 	{
-		AM_LevelInit();
-		lastlevel = gamemap;
+		AM_FrameBufferInit();
+		if (lastlevel != gamemap)
+		{
+			AM_LevelInit();
+			lastlevel = gamemap;
+		}
 		am_recalc = false;
 	}
 	AM_initVariables();
@@ -417,6 +423,28 @@ static void AM_maxOutWindowScale(void)
 	AM_activateNewScale();
 }
 
+//
+// set window panning
+//
+static void AM_setWindowPanning(void)
+{
+	// up and down
+	if (m_keydown[2]) // pan up
+		m_paninc.y = FTOM(F_PANINC);
+	else if (m_keydown[3]) // pan down
+		m_paninc.y = -FTOM(F_PANINC);
+	else
+		m_paninc.y = 0;
+
+	// left and right
+	if (m_keydown[0]) // pan right
+		m_paninc.x = FTOM(F_PANINC);
+	else if (m_keydown[1]) // pan left
+		m_paninc.x = -FTOM(F_PANINC);
+	else
+		m_paninc.x = 0;
+}
+
 /** Responds to user inputs in automap mode.
   *
   * \param ev Event to possibly respond to.
@@ -449,35 +477,49 @@ boolean AM_Responder(event_t *ev)
 			{
 				case AM_PANRIGHTKEY: // pan right
 					if (!followplayer)
-						m_paninc.x = FTOM(F_PANINC);
+					{
+						m_keydown[0] = true;
+						AM_setWindowPanning();
+					}
 					else
 						rc = false;
 					break;
 				case AM_PANLEFTKEY: // pan left
 					if (!followplayer)
-						m_paninc.x = -FTOM(F_PANINC);
+					{
+						m_keydown[1] = true;
+						AM_setWindowPanning();
+					}
 					else
 						rc = false;
 					break;
 				case AM_PANUPKEY: // pan up
 					if (!followplayer)
-						m_paninc.y = FTOM(F_PANINC);
+					{
+						m_keydown[2] = true;
+						AM_setWindowPanning();
+					}
 					else
 						rc = false;
 					break;
 				case AM_PANDOWNKEY: // pan down
 					if (!followplayer)
-						m_paninc.y = -FTOM(F_PANINC);
+					{
+						m_keydown[3] = true;
+						AM_setWindowPanning();
+					}
 					else
 						rc = false;
 					break;
 				case AM_ZOOMOUTKEY: // zoom out
 					mtof_zoommul = M_ZOOMOUT;
 					ftom_zoommul = M_ZOOMIN;
+					AM_setWindowPanning();
 					break;
 				case AM_ZOOMINKEY: // zoom in
 					mtof_zoommul = M_ZOOMIN;
 					ftom_zoommul = M_ZOOMOUT;
+					AM_setWindowPanning();
 					break;
 				case AM_TOGGLEKEY:
 					AM_Stop();
@@ -491,6 +533,7 @@ boolean AM_Responder(event_t *ev)
 					}
 					else
 						AM_restoreScaleAndLoc();
+					AM_setWindowPanning();
 					break;
 				case AM_FOLLOWKEY:
 					followplayer = !followplayer;
@@ -510,14 +553,32 @@ boolean AM_Responder(event_t *ev)
 			switch (ev->data1)
 			{
 				case AM_PANRIGHTKEY:
+					if (!followplayer)
+					{
+						m_keydown[0] = false;
+						AM_setWindowPanning();
+					}
+					break;
 				case AM_PANLEFTKEY:
 					if (!followplayer)
-						m_paninc.x = 0;
+					{
+						m_keydown[1] = false;
+						AM_setWindowPanning();
+					}
 					break;
 				case AM_PANUPKEY:
+					if (!followplayer)
+					{
+						m_keydown[2] = false;
+						AM_setWindowPanning();
+					}
+					break;
 				case AM_PANDOWNKEY:
 					if (!followplayer)
-						m_paninc.y = 0;
+					{
+						m_keydown[3] = false;
+						AM_setWindowPanning();
+					}
 					break;
 				case AM_ZOOMOUTKEY:
 				case AM_ZOOMINKEY:
@@ -718,6 +779,17 @@ static boolean AM_clipMline(const mline_t *ml, fline_t *fl)
 }
 #undef DOOUTCODE
 
+//
+// Draws a pixel.
+//
+static void AM_drawPixel(INT32 xx, INT32 yy, INT32 cc)
+{
+	UINT8 *dest = screens[0];
+	if (xx < 0 || yy < 0 || xx >= vid.width || yy >= vid.height)
+		return; // off the screen
+	dest[(yy*vid.width) + xx] = cc;
+}
+
 //
 // Classic Bresenham w/ whatever optimizations needed for speed
 //
@@ -739,8 +811,6 @@ static void AM_drawFline_soft(const fline_t *fl, INT32 color)
 	}
 #endif
 
-	#define PUTDOT(xx,yy,cc) V_DrawFill(xx,yy,1,1,cc|V_NOSCALESTART);
-
 	dx = fl->b.x - fl->a.x;
 	ax = 2 * (dx < 0 ? -dx : dx);
 	sx = dx < 0 ? -1 : 1;
@@ -757,7 +827,7 @@ static void AM_drawFline_soft(const fline_t *fl, INT32 color)
 		d = ay - ax/2;
 		for (;;)
 		{
-			PUTDOT(x, y, color)
+			AM_drawPixel(x, y, color);
 			if (x == fl->b.x)
 				return;
 			if (d >= 0)
@@ -774,7 +844,7 @@ static void AM_drawFline_soft(const fline_t *fl, INT32 color)
 		d = ax - ay/2;
 		for (;;)
 		{
-			PUTDOT(x, y, color)
+			AM_drawPixel(x, y, color);
 			if (y == fl->b.y)
 				return;
 			if (d >= 0)
@@ -786,8 +856,6 @@ static void AM_drawFline_soft(const fline_t *fl, INT32 color)
 			d += ax;
 		}
 	}
-
-	#undef PUTDOT
 }
 
 //
@@ -1100,6 +1168,12 @@ void AM_Drawer(void)
 	if (!automapactive)
 		return;
 
+	AM_drawFline = AM_drawFline_soft;
+#ifdef HWRENDER
+	if (rendermode == render_opengl)
+		AM_drawFline = HWR_drawAMline;
+#endif
+
 	AM_clearFB(BACKGROUND);
 	if (draw_grid) AM_drawGrid(GRIDCOLORS);
 	AM_drawWalls();
diff --git a/src/am_map.h b/src/am_map.h
index 462bb3d6d7b0adf15140f55705fbb76642725089..0560207bb41821f7ce09eb14d63ee3a62dfcfb97 100644
--- a/src/am_map.h
+++ b/src/am_map.h
@@ -38,6 +38,9 @@ void AM_Ticker(void);
 // Called by main loop, instead of view drawer if automap is active.
 void AM_Drawer(void);
 
+// Enables the automap.
+void AM_Start(void);
+
 // Called to force the automap to quit if the level is completed while it is up.
 void AM_Stop(void);
 
diff --git a/src/command.c b/src/command.c
index dbdaceab5560f21aa2229d9e2ecaaef69b97c224..cb6803b571cc70a37c015caa91a430be265396ee 100644
--- a/src/command.c
+++ b/src/command.c
@@ -823,8 +823,13 @@ static void COM_Help_f(void)
 					if (!stricmp(cvar->PossibleValue[MINVAL].strvalue, "MIN"))
 					{
 						if (floatmode)
-							CONS_Printf("  range from %f to %f\n", FIXED_TO_FLOAT(cvar->PossibleValue[MINVAL].value),
-								FIXED_TO_FLOAT(cvar->PossibleValue[MAXVAL].value));
+						{
+							float fu = FIXED_TO_FLOAT(cvar->PossibleValue[MINVAL].value);
+							float ck = FIXED_TO_FLOAT(cvar->PossibleValue[MAXVAL].value);
+							CONS_Printf("  range from %ld%s to %ld%s\n",
+									(long)fu, M_Ftrim(fu),
+									(long)ck, M_Ftrim(ck));
+						}
 						else
 							CONS_Printf("  range from %d to %d\n", cvar->PossibleValue[MINVAL].value,
 								cvar->PossibleValue[MAXVAL].value);
@@ -973,7 +978,10 @@ static void COM_Add_f(void)
 	}
 
 	if (( cvar->flags & CV_FLOAT ))
-		CV_Set(cvar, va("%f", FIXED_TO_FLOAT (cvar->value) + atof(COM_Argv(2))));
+	{
+		float n =FIXED_TO_FLOAT (cvar->value) + atof(COM_Argv(2));
+		CV_Set(cvar, va("%ld%s", (long)n, M_Ftrim(n)));
+	}
 	else
 		CV_AddValue(cvar, atoi(COM_Argv(2)));
 }
diff --git a/src/config.h.in b/src/config.h.in
index 233cbdc53349cd4a97a9bf1f96607600138c5867..498f3086d3279655264160139dde53287356fac3 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -26,12 +26,12 @@
 #else
 
 /* Manually defined asset hashes for non-CMake builds
- * Last updated 2019 / 12 / 06 - v2.2.0 - main assets
+ * Last updated 2020 / 02 / 15 - v2.2.1 - main assets
  * Last updated 20?? / ?? / ?? - v2.2.? - patch.pk3
  */
-#define ASSET_HASH_SRB2_PK3   "51419a33b4982d840c6772c159ba7c0a"
-#define ASSET_HASH_ZONES_PK3  "df74843919fd51af26a0baa8e21e4c19"
-#define ASSET_HASH_PLAYER_DTA "56a247e074dd0dc794b6617efef1e918"
+#define ASSET_HASH_SRB2_PK3   "0277c9416756627004e83cbb5b2e3e28"
+#define ASSET_HASH_ZONES_PK3  "f7e88afb6af7996a834c7d663144bead"
+#define ASSET_HASH_PLAYER_DTA "ad49e07b17cc662f1ad70c454910b4ae"
 #ifdef USE_PATCH_DTA
 #define ASSET_HASH_PATCH_PK3  "there is no patch.pk3, only zuul"
 #endif
diff --git a/src/console.c b/src/console.c
index 890d424fa697251f35f73a5e9630a53b5606c78b..83df15a2825eb61d64bb6edd003d1da4bab54a56 100644
--- a/src/console.c
+++ b/src/console.c
@@ -613,6 +613,15 @@ void CON_Ticker(void)
 	con_tick++;
 	con_tick &= 7;
 
+	// if the menu is open then close the console.
+	if (menuactive && con_destlines)
+	{
+		consoletoggle = false;
+		con_destlines = 0;
+		CON_ClearHUD();
+		I_UpdateMouseGrab();
+	}
+
 	// console key was pushed
 	if (consoletoggle)
 	{
@@ -769,7 +778,7 @@ boolean CON_Responder(event_t *ev)
 	// check for console toggle key
 	if (ev->type != ev_console)
 	{
-		if (modeattacking || metalrecording || menuactive)
+		if (modeattacking || metalrecording)
 			return false;
 
 		if (key == gamecontrol[gc_console][0] || key == gamecontrol[gc_console][1])
@@ -784,7 +793,7 @@ boolean CON_Responder(event_t *ev)
 		// check other keys only if console prompt is active
 		if (!consoleready && key < NUMINPUTS) // metzgermeister: boundary check!!
 		{
-			if (! menuactive && bindtable[key])
+			if (bindtable[key])
 			{
 				COM_BufAddText(bindtable[key]);
 				COM_BufAddText("\n");
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 79b63bb8172466c0dff2c52736c8c0af65e26c23..77118110e41417149d0d953511f1b9d7e386c360 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -404,8 +404,8 @@ static void ExtraDataTicker(void)
 		}
 
 	// If you are a client, you can safely forget the net commands for this tic
-	// If you are the server, you need to remember them until every client has been aknowledged,
-	// because if you need to resend a PT_SERVERTICS packet, you need to put the commands in it
+	// If you are the server, you need to remember them until every client has been acknowledged,
+	// because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it
 	if (client)
 		D_FreeTextcmd(gametic);
 }
@@ -1278,8 +1278,14 @@ static boolean CL_SendJoin(void)
 	netbuffer->u.clientcfg.subversion = SUBVERSION;
 	strncpy(netbuffer->u.clientcfg.application, SRB2APPLICATION,
 			sizeof netbuffer->u.clientcfg.application);
+
+	CleanupPlayerName(consoleplayer, cv_playername.zstring);
+	if (splitscreen)
+		CleanupPlayerName(1, cv_playername2.zstring);/* 1 is a HACK? oh no */
+
 	strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
 	strncpy(netbuffer->u.clientcfg.names[1], cv_playername2.zstring, MAXPLAYERNAME);
+
 	return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
 }
 
@@ -4504,7 +4510,7 @@ static void CL_SendClientCmd(void)
 		packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16);
 		HSendPacket(servernode, false, 0, packetsize);
 	}
-	else if (gamestate != GS_NULL && addedtogame)
+	else if (gamestate != GS_NULL && (addedtogame || dedicated))
 	{
 		G_MoveTiccmd(&netbuffer->u.clientpak.cmd, &localcmds, 1);
 		netbuffer->u.clientpak.consistancy = SHORT(consistancy[gametic%BACKUPTICS]);
diff --git a/src/d_main.c b/src/d_main.c
index cbf9c8091c4ede1aa1d4254a4fa70c4f18b0cce4..82aad739b272dcb1b7097ea2296ac2b5a9805c75 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -188,14 +188,14 @@ void D_ProcessEvents(void)
 				continue;
 		}
 
-		// console input
-		if (CON_Responder(ev))
-			continue; // ate the event
-
 		// Menu input
 		if (M_Responder(ev))
 			continue; // menu ate the event
 
+		// console input
+		if (CON_Responder(ev))
+			continue; // ate the event
+
 		G_Responder(ev);
 	}
 }
@@ -502,12 +502,13 @@ static void D_Display(void)
 	// vid size change is now finished if it was on...
 	vid.recalc = 0;
 
-	M_Drawer(); // menu is drawn even on top of everything
-	// focus lost moved to M_Drawer
-
+	// FIXME: draw either console or menu, not the two
 	if (gamestate != GS_TIMEATTACK)
 		CON_Drawer();
 
+	M_Drawer(); // menu is drawn even on top of everything
+	// focus lost moved to M_Drawer
+
 	//
 	// wipe update
 	//
@@ -1214,7 +1215,7 @@ void D_SRB2Main(void)
 #endif
 	D_CleanFile();
 
-#ifndef DEVELOP // md5s last updated 06/12/19 (ddmmyy)
+#ifndef DEVELOP // md5s last updated 16/02/20 (ddmmyy)
 
 	// Check MD5s of autoloaded files
 	W_VerifyFileMD5(0, ASSET_HASH_SRB2_PK3); // srb2.pk3
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 6b3662710d99d1d23495838a269f975ea13e03f2..bac297f8e10a4183d30347ec4068afa5e7e37b05 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -632,7 +632,7 @@ void D_RegisterClientCommands(void)
 	// Set default player names
 	// Monster Iestyn (12/08/19): not sure where else I could have actually put this, but oh well
 	for (i = 0; i < MAXPLAYERS; i++)
-		sprintf(player_names[i], "Player %d", i);
+		sprintf(player_names[i], "Player %d", 1 + i);
 
 	if (dedicated)
 		return;
@@ -677,6 +677,7 @@ void D_RegisterClientCommands(void)
 	// GIF variables
 	CV_RegisterVar(&cv_gif_optimize);
 	CV_RegisterVar(&cv_gif_downscale);
+	CV_RegisterVar(&cv_gif_localcolortable);
 
 #ifdef WALLSPLATS
 	CV_RegisterVar(&cv_splats);
@@ -1008,7 +1009,7 @@ boolean EnsurePlayerNameIsGood(char *name, INT32 playernum)
   *     SetPlayerName
   * \author Graue <graue@oceanbase.org>
   */
-static void CleanupPlayerName(INT32 playernum, const char *newname)
+void CleanupPlayerName(INT32 playernum, const char *newname)
 {
 	char *buf;
 	char *p;
@@ -1036,6 +1037,17 @@ static void CleanupPlayerName(INT32 playernum, const char *newname)
 
 		tmpname = p;
 
+		do
+		{
+			/* from EnsurePlayerNameIsGood */
+			if (!isprint(*p) || *p == ';' || (UINT8)*p >= 0x80)
+				break;
+		}
+		while (*++p) ;
+
+		if (*p)/* bad char found */
+			break;
+
 		// Remove trailing spaces.
 		p = &tmpname[strlen(tmpname)-1]; // last character
 		while (*p == ' ' && p >= tmpname)
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 8f857c6dbc5a309119b2fac1d4a334959fbabd24..f258cde6275ec719c3545c37e7440dbf20449742 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -194,6 +194,7 @@ typedef union {
 // add game commands, needs cleanup
 void D_RegisterServerCommands(void);
 void D_RegisterClientCommands(void);
+void CleanupPlayerName(INT32 playernum, const char *newname);
 boolean EnsurePlayerNameIsGood(char *name, INT32 playernum);
 void D_SendPlayerConfig(void);
 void Command_ExitGame_f(void);
diff --git a/src/dehacked.c b/src/dehacked.c
index 45f00b8cf60e81c0f0906ba31c9bb720baf0dc7b..4d114727627d672eaa1adce3f4c21e272fcaaacf 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -585,21 +585,30 @@ static void readfreeslots(MYFILE *f)
 					continue;
 				// Copy in the spr2 name and increment free_spr2.
 				if (free_spr2 < NUMPLAYERSPRITES) {
-					CONS_Printf("Sprite SPR2_%s allocated.\n",word);
 					strncpy(spr2names[free_spr2],word,4);
 					spr2defaults[free_spr2] = 0;
 					spr2names[free_spr2++][4] = 0;
 				} else
-					CONS_Alert(CONS_WARNING, "Ran out of free SPR2 slots!\n");
+					deh_warning("Ran out of free SPR2 slots!\n");
 			}
 			else if (fastcmp(type, "TOL"))
 			{
-				if (lastcustomtol > 31)
-					CONS_Alert(CONS_WARNING, "Ran out of free typeoflevel slots!\n");
+				// Search if we already have a typeoflevel by that name...
+				for (i = 0; TYPEOFLEVEL[i].name; i++)
+					if (fastcmp(word, TYPEOFLEVEL[i].name))
+						break;
+
+				// We found it? Then don't allocate another one.
+				if (TYPEOFLEVEL[i].name)
+					continue;
+
+				// We don't, so freeslot it.
+				if (lastcustomtol == (UINT32)MAXTOL) // Unless you have way too many, since they're flags.
+					deh_warning("Ran out of free typeoflevel slots!\n");
 				else
 				{
-					G_AddTOL((1<<lastcustomtol), word);
-					lastcustomtol++;
+					G_AddTOL(lastcustomtol, word);
+					lastcustomtol <<= 1;
 				}
 			}
 			else
@@ -1105,38 +1114,7 @@ static void readsprite2(MYFILE *f, INT32 num)
 	Z_Free(s);
 }
 
-INT32 numtolinfo = NUMBASETOL;
-UINT32 lastcustomtol = 13;
-
-tolinfo_t TYPEOFLEVEL[NUMMAXTOL] = {
-	{"SOLO",TOL_SP},
-	{"SP",TOL_SP},
-	{"SINGLEPLAYER",TOL_SP},
-	{"SINGLE",TOL_SP},
-
-	{"COOP",TOL_COOP},
-	{"CO-OP",TOL_COOP},
-
-	{"COMPETITION",TOL_COMPETITION},
-	{"RACE",TOL_RACE},
-
-	{"MATCH",TOL_MATCH},
-	{"TAG",TOL_TAG},
-	{"CTF",TOL_CTF},
-
-	{"2D",TOL_2D},
-	{"MARIO",TOL_MARIO},
-	{"NIGHTS",TOL_NIGHTS},
-	{"OLDBRAK",TOL_ERZ3},
-
-	{"XMAS",TOL_XMAS},
-	{"CHRISTMAS",TOL_XMAS},
-	{"WINTER",TOL_XMAS},
-
-	{NULL, 0}
-};
-
-// copypasted from readPlayer :sleep:
+// copypasted from readPlayer :]
 static const char *const GAMETYPERULE_LIST[];
 static void readgametype(MYFILE *f, char *gtname)
 {
@@ -1293,10 +1271,7 @@ static void readgametype(MYFILE *f, char *gtname)
 				UINT32 wordgt = 0;
 				for (j = 0; GAMETYPERULE_LIST[j]; j++)
 					if (fastcmp(word, GAMETYPERULE_LIST[j])) {
-						if (!j) // GTR_CAMPAIGN
-							wordgt |= 1;
-						else
-							wordgt |= (1<<j);
+						wordgt |= (1<<j);
 						if (i || word2[0] == 'T' || word2[0] == 'Y')
 							newgtrules |= wordgt;
 						break;
@@ -4945,19 +4920,19 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_PLAY_SUPER_TRANS3",
 	"S_PLAY_SUPER_TRANS4",
 	"S_PLAY_SUPER_TRANS5",
-	"S_PLAY_SUPER_TRANS6", // This has special significance in the code. If you add more frames, search for it and make the appropriate changes.
+	"S_PLAY_SUPER_TRANS6",
 
 	// technically the player goes here but it's an infinite tic state
 	"S_OBJPLACE_DUMMY",
 
-	// 1-Up Box Sprites (uses player sprite)
+	// 1-Up Box Sprites overlay (uses player sprite)
 	"S_PLAY_BOX1",
 	"S_PLAY_BOX2",
 	"S_PLAY_ICON1",
 	"S_PLAY_ICON2",
 	"S_PLAY_ICON3",
 
-	// Level end sign (uses player sprite)
+	// Level end sign overlay (uses player sprite)
 	"S_PLAY_SIGN",
 
 	// NiGHTS character (uses player sprite)
@@ -5205,7 +5180,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_ROBOHOOD_JUMP2",
 	"S_ROBOHOOD_JUMP3",
 
-	// CastleBot FaceStabber
+	// Castlebot Facestabber
 	"S_FACESTABBER_STND1",
 	"S_FACESTABBER_STND2",
 	"S_FACESTABBER_STND3",
@@ -5425,6 +5400,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_EGGMOBILE_FLEE2",
 	"S_EGGMOBILE_BALL",
 	"S_EGGMOBILE_TARGET",
+
 	"S_BOSSEGLZ1",
 	"S_BOSSEGLZ2",
 
@@ -5477,7 +5453,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_EGGMOBILE3_FLEE1",
 	"S_EGGMOBILE3_FLEE2",
 
-	// Boss 3 pinch
+	// Boss 3 Pinch
 	"S_FAKEMOBILE_INIT",
 	"S_FAKEMOBILE",
 	"S_FAKEMOBILE_ATK1",
@@ -5493,7 +5469,6 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_BOSSSEBH2",
 
 	// Boss 3 Shockwave
-
 	"S_SHOCKWAVE1",
 	"S_SHOCKWAVE2",
 
@@ -5530,9 +5505,9 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_JETFLAME",
 
 	// Boss 4 Spectator Eggrobo
-	"S_EGGROBO1_IDLE",
+	"S_EGGROBO1_STND",
 	"S_EGGROBO1_BSLAP1",
-	"S_EGGROBO2_BSLAP2",
+	"S_EGGROBO1_BSLAP2",
 	"S_EGGROBO1_PISSED",
 
 	// Boss 4 Spectator Eggrobo jet flame
@@ -5776,7 +5751,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_CYBRAKDEMON_NAPALM_ATTACK1",
 	"S_CYBRAKDEMON_NAPALM_ATTACK2",
 	"S_CYBRAKDEMON_NAPALM_ATTACK3",
-	"S_CYBRAKDEMON_FINISH_ATTACK", // If just attacked, remove MF2_FRET w/out going back to spawnstate
+	"S_CYBRAKDEMON_FINISH_ATTACK1", // If just attacked, remove MF2_FRET w/out going back to spawnstate
 	"S_CYBRAKDEMON_FINISH_ATTACK2", // Force a delay between attacks so you don't get bombarded with them back-to-back
 	"S_CYBRAKDEMON_PAIN1",
 	"S_CYBRAKDEMON_PAIN2",
@@ -6470,7 +6445,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_LITTLETUMBLEWEED_ROLL7",
 	"S_LITTLETUMBLEWEED_ROLL8",
 
-	// Cacti Sprites
+	// Cacti
 	"S_CACTI1",
 	"S_CACTI2",
 	"S_CACTI3",
@@ -6485,7 +6460,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_CACTITINYSEG",
 	"S_CACTISMALLSEG",
 
-	// Warning signs sprites
+	// Warning signs
 	"S_ARIDSIGN_CAUTION",
 	"S_ARIDSIGN_CACTI",
 	"S_ARIDSIGN_SHARPTURN",
@@ -6502,6 +6477,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_TNTBARREL_EXPL4",
 	"S_TNTBARREL_EXPL5",
 	"S_TNTBARREL_EXPL6",
+	"S_TNTBARREL_EXPL7",
 	"S_TNTBARREL_FLYING",
 
 	// TNT proximity shell
@@ -7047,7 +7023,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_ZAPSB10",
 	"S_ZAPSB11", // blank frame
 
-	// Thunder spark
+	//Thunder spark
 	"S_THUNDERCOIN_SPARK",
 
 	// Invincibility Sparkles
@@ -7348,6 +7324,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_BHORIZ7",
 	"S_BHORIZ8",
 
+	// Booster
 	"S_BOOSTERSOUND",
 	"S_YELLOWBOOSTERROLLER",
 	"S_YELLOWBOOSTERSEG_LEFT",
@@ -7378,7 +7355,7 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_SPLISH8",
 	"S_SPLISH9",
 
-	// Lava splish
+	// Lava Splish
 	"S_LAVASPLISH",
 
 	// added water splash
@@ -7974,6 +7951,8 @@ static const char *const STATE_LIST[] = { // array length left dynamic for sanit
 	"S_ROCKCRUMBLEN",
 	"S_ROCKCRUMBLEO",
 	"S_ROCKCRUMBLEP",
+
+	// Level debris
 	"S_GFZDEBRIS",
 	"S_BRICKDEBRIS",
 	"S_WOODDEBRIS",
@@ -7993,7 +7972,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_THOK", // Thok! mobj
 	"MT_PLAYER",
 	"MT_TAILSOVERLAY", // c:
-	"MT_METALJETFUME", // [:
+	"MT_METALJETFUME",
 
 	// Enemies
 	"MT_BLUECRAWLA", // Crawla (Blue)
@@ -8110,7 +8089,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_CYBRAKDEMON_NAPALM_FLAMES",
 	"MT_CYBRAKDEMON_VILE_EXPLOSION",
 
-	// Metal Sonic
+	// Metal Sonic (Boss 9)
 	"MT_METALSONIC_RACE",
 	"MT_METALSONIC_BATTLE",
 	"MT_MSSHIELD_FRONT",
@@ -8124,7 +8103,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_BOMBSPHERE",
 	"MT_REDTEAMRING",  //Rings collectable by red team.
 	"MT_BLUETEAMRING", //Rings collectable by blue team.
-	"MT_TOKEN", // Special Stage Token
+	"MT_TOKEN", // Special Stage token for special stage
 	"MT_REDFLAG", // Red CTF Flag
 	"MT_BLUEFLAG", // Blue CTF Flag
 	"MT_EMBLEM",
@@ -8350,22 +8329,22 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	// Arid Canyon Scenery
 	"MT_BIGTUMBLEWEED",
 	"MT_LITTLETUMBLEWEED",
-	"MT_CACTI1",
-	"MT_CACTI2",
-	"MT_CACTI3",
-	"MT_CACTI4",
-	"MT_CACTI5",
-	"MT_CACTI6",
-	"MT_CACTI7",
-	"MT_CACTI8",
-	"MT_CACTI9",
-	"MT_CACTI10",
-	"MT_CACTI11",
-	"MT_CACTITINYSEG",
-	"MT_CACTISMALLSEG",
-	"MT_ARIDSIGN_CAUTION",
-	"MT_ARIDSIGN_CACTI",
-	"MT_ARIDSIGN_SHARPTURN",
+	"MT_CACTI1", // Tiny Red Flower Cactus
+	"MT_CACTI2", // Small Red Flower Cactus
+	"MT_CACTI3", // Tiny Blue Flower Cactus
+	"MT_CACTI4", // Small Blue Flower Cactus
+	"MT_CACTI5", // Prickly Pear
+	"MT_CACTI6", // Barrel Cactus
+	"MT_CACTI7", // Tall Barrel Cactus
+	"MT_CACTI8", // Armed Cactus
+	"MT_CACTI9", // Ball Cactus
+	"MT_CACTI10", // Tiny Cactus
+	"MT_CACTI11", // Small Cactus
+	"MT_CACTITINYSEG", // Tiny Cactus Segment
+	"MT_CACTISMALLSEG", // Small Cactus Segment
+	"MT_ARIDSIGN_CAUTION", // Caution Sign
+	"MT_ARIDSIGN_CACTI", // Cacti Sign
+	"MT_ARIDSIGN_SHARPTURN", // Sharp Turn Sign
 	"MT_OILLAMP",
 	"MT_TNTBARREL",
 	"MT_PROXIMITYTNT",
@@ -8420,7 +8399,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_GLAREGOYLEUP",
 	"MT_GLAREGOYLEDOWN",
 	"MT_GLAREGOYLELONG",
-	"MT_TARGET",
+	"MT_TARGET", // AKA Red Crystal
 	"MT_GREENFLAME",
 	"MT_BLUEGARGOYLE",
 
@@ -8471,7 +8450,7 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_HHZSTALAGMITE_TALL",
 	"MT_HHZSTALAGMITE_SHORT",
 
-	// Botanic Serenity
+	// Botanic Serenity scenery
 	"MT_BSZTALLFLOWER_RED",
 	"MT_BSZTALLFLOWER_PURPLE",
 	"MT_BSZTALLFLOWER_BLUE",
@@ -8751,6 +8730,8 @@ static const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for s
 	"MT_ROCKCRUMBLE14",
 	"MT_ROCKCRUMBLE15",
 	"MT_ROCKCRUMBLE16",
+
+	// Level debris
 	"MT_GFZDEBRIS",
 	"MT_BRICKDEBRIS",
 	"MT_WOODDEBRIS",
@@ -10465,16 +10446,23 @@ static inline int lib_freeslot(lua_State *L)
 		}
 		else if (fastcmp(type, "TOL"))
 		{
-			if (lastcustomtol > 31)
-				CONS_Alert(CONS_WARNING, "Ran out of free typeoflevel slots!\n");
-			else
-			{
-				UINT32 newtol = (1<<lastcustomtol);
-				CONS_Printf("TypeOfLevel TOL_%s allocated.\n",word);
-				G_AddTOL(newtol, word);
-				lua_pushinteger(L, newtol);
-				lastcustomtol++;
-				r++;
+			// Search if we already have a typeoflevel by that name...
+			int i;
+			for (i = 0; TYPEOFLEVEL[i].name; i++)
+				if (fastcmp(word, TYPEOFLEVEL[i].name))
+					break;
+
+			// We don't, so allocate a new one.
+			if (TYPEOFLEVEL[i].name == NULL) {
+				if (lastcustomtol == (UINT32)MAXTOL) // Unless you have way too many, since they're flags.
+					CONS_Alert(CONS_WARNING, "Ran out of free typeoflevel slots!\n");
+				else {
+					CONS_Printf("TypeOfLevel TOL_%s allocated.\n",word);
+					G_AddTOL(lastcustomtol, word);
+					lua_pushinteger(L, lastcustomtol);
+					lastcustomtol <<= 1;
+					r++;
+				}
 			}
 		}
 		Z_Free(s);
diff --git a/src/doomdef.h b/src/doomdef.h
index 533546303899a26503bdb70057ed6ccde2acd912..4d3204d67c29c6ad2c8fcafdb9c5efd48f78b0ad 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -143,9 +143,9 @@ extern char logfilename[1024];
 // we use comprevision and compbranch instead.
 #else
 #define VERSION    202 // Game version
-#define SUBVERSION 0  // more precise version number
-#define VERSIONSTRING "v2.2.0"
-#define VERSIONSTRINGW L"v2.2.0"
+#define SUBVERSION 1  // more precise version number
+#define VERSIONSTRING "v2.2.1"
+#define VERSIONSTRINGW L"v2.2.1"
 // Hey! If you change this, add 1 to the MODVERSION below!
 // Otherwise we can't force updates!
 #endif
@@ -210,7 +210,7 @@ extern char logfilename[1024];
 // it's only for detection of the version the player is using so the MS can alert them of an update.
 // Only set it higher, not lower, obviously.
 // Note that we use this to help keep internal testing in check; this is why v2.2.0 is not version "1".
-#define MODVERSION 40
+#define MODVERSION 41
 
 // To version config.cfg, MAJOREXECVERSION is set equal to MODVERSION automatically.
 // Increment MINOREXECVERSION whenever a config change is needed that does not correspond
diff --git a/src/doomstat.h b/src/doomstat.h
index c7c12632ab1dcf45c8ab5040ccb5095c91d799dc..cf02e438900c7996ed5a058139f5b6bfc278b8e2 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -437,6 +437,7 @@ extern const char *Gametype_ConstantNames[NUMGAMETYPES];
 extern INT32 pointlimits[NUMGAMETYPES];
 extern INT32 timelimits[NUMGAMETYPES];
 
+// TypeOfLevel things
 enum TypeOfLevel
 {
 	TOL_SP          = 0x01, ///< Single Player
@@ -461,16 +462,16 @@ enum TypeOfLevel
 	TOL_XMAS   = 0x1000, ///< Christmas NiGHTS
 };
 
-#define NUMBASETOL 18
-#define NUMMAXTOL (18 + NUMGAMETYPEFREESLOTS)
+#define MAXTOL             (1<<31)
+#define NUMBASETOLNAMES    (19)
+#define NUMTOLNAMES        (NUMBASETOLNAMES + NUMGAMETYPEFREESLOTS)
 
 typedef struct
 {
 	const char *name;
 	UINT32 flag;
 } tolinfo_t;
-extern tolinfo_t TYPEOFLEVEL[NUMMAXTOL];
-extern INT32 numtolinfo;
+extern tolinfo_t TYPEOFLEVEL[NUMTOLNAMES];
 extern UINT32 lastcustomtol;
 
 extern tic_t totalplaytime;
diff --git a/src/f_finale.c b/src/f_finale.c
index ce4ec0eb4a637fd7157511be1e45bc6901dc9692..66f963bbbec415a1cdb4ed20ed641a7da35bb47d 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -1120,6 +1120,9 @@ static const char *credits[] = {
 	"\1Sonic Robo Blast II",
 	"\1Credits",
 	"",
+	"\1Producer",
+	"Rob Tisdell",
+	"",
 	"\1Game Design",
 	"Ben \"Mystic\" Geyer",
 	"\"SSNTails\"",
@@ -1234,7 +1237,7 @@ static const char *credits[] = {
 	"Thomas \"Shadow Hog\" Igoe",
 	"Alexander \"DrTapeworm\" Moench-Ford",
 	"\"Kaito Sinclaire\"",
-	"\"QueenDelta\"",
+	"Anna \"QueenDelta\" Sandlin",
 	"Wessel \"sphere\" Smit",
 	"\"Spazzo\"",
 	"\"SSNTails\"",
@@ -1258,7 +1261,7 @@ static const char *credits[] = {
 	"Cody \"SRB2 Playah\" Koester",
 	"Skye \"OmegaVelocity\" Meredith",
 	"Stephen \"HEDGESMFG\" Moellering",
-	"Nick \"ST218\" Molina",
+	"Rosalie \"ST218\" Molina",
 	"Samuel \"Prime 2.0\" Peters",
 	"Colin \"Sonict\" Pfaff",
 	"Bill \"Tets\" Reed",
diff --git a/src/g_game.c b/src/g_game.c
index 8383782cb4de0a7797087e499c2b3ff4c6d59cef..9897220105c69b0eab054e3efd4da75936ad47e8 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -3256,8 +3256,8 @@ void G_AddGametypeConstant(INT16 gtype, const char *newgtconst)
 {
 	size_t r = 0; // read
 	size_t w = 0; // write
-	char *gtconst = Z_Calloc(strlen(newgtconst) + 3, PU_STATIC, NULL);
-	char *tmpconst = Z_Calloc(strlen(newgtconst), PU_STATIC, NULL);
+	char *gtconst = Z_Calloc(strlen(newgtconst) + 4, PU_STATIC, NULL);
+	char *tmpconst = Z_Calloc(strlen(newgtconst) + 1, PU_STATIC, NULL);
 
 	// Copy the gametype name.
 	strcpy(tmpconst, newgtconst);
@@ -3384,6 +3384,36 @@ UINT32 gametypetol[NUMGAMETYPES] =
 	TOL_CTF, // CTF
 };
 
+tolinfo_t TYPEOFLEVEL[NUMTOLNAMES] = {
+	{"SOLO",TOL_SP},
+	{"SP",TOL_SP},
+	{"SINGLEPLAYER",TOL_SP},
+	{"SINGLE",TOL_SP},
+
+	{"COOP",TOL_COOP},
+	{"CO-OP",TOL_COOP},
+
+	{"COMPETITION",TOL_COMPETITION},
+	{"RACE",TOL_RACE},
+
+	{"MATCH",TOL_MATCH},
+	{"TAG",TOL_TAG},
+	{"CTF",TOL_CTF},
+
+	{"2D",TOL_2D},
+	{"MARIO",TOL_MARIO},
+	{"NIGHTS",TOL_NIGHTS},
+	{"OLDBRAK",TOL_ERZ3},
+
+	{"XMAS",TOL_XMAS},
+	{"CHRISTMAS",TOL_XMAS},
+	{"WINTER",TOL_XMAS},
+
+	{NULL, 0}
+};
+
+UINT32 lastcustomtol = (TOL_XMAS<<1);
+
 //
 // G_AddTOL
 //
@@ -3391,16 +3421,16 @@ UINT32 gametypetol[NUMGAMETYPES] =
 //
 void G_AddTOL(UINT32 newtol, const char *tolname)
 {
-	TYPEOFLEVEL[numtolinfo].name = Z_StrDup(tolname);
-	TYPEOFLEVEL[numtolinfo].flag = newtol;
-	numtolinfo++;
+	INT32 i;
+	for (i = 0; TYPEOFLEVEL[i].name; i++)
+		;
 
-	TYPEOFLEVEL[numtolinfo].name = NULL;
-	TYPEOFLEVEL[numtolinfo].flag = 0;
+	TYPEOFLEVEL[i].name = Z_StrDup(tolname);
+	TYPEOFLEVEL[i].flag = newtol;
 }
 
 //
-// G_AddTOL
+// G_AddGametypeTOL
 //
 // Assigns a type of level to a gametype.
 //
@@ -3739,7 +3769,10 @@ static void G_DoCompleted(void)
 			}
 
 		if (i == 7)
+		{
 			gottoken = false;
+			token = 0;
+		}
 	}
 
 	if (spec && !gottoken)
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index f4313307e2496bf3e219f8c5a946d38e216075cd..2729640ff10cd8ae0c644a9acd4b20073bc47d35 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -455,8 +455,9 @@ void HWR_InitModels(void)
 	size_t i;
 	INT32 s;
 	FILE *f;
-	char name[18], filename[32];
+	char name[24], filename[32];
 	float scale, offset;
+	size_t prefixlen;
 
 	CONS_Printf("HWR_InitModels()...\n");
 	for (s = 0; s < MAXSKINS; s++)
@@ -488,37 +489,54 @@ void HWR_InitModels(void)
 		nomd2s = true;
 		return;
 	}
-	while (fscanf(f, "%19s %31s %f %f", name, filename, &scale, &offset) == 4)
+
+	// length of the player model prefix
+	prefixlen = strlen(PLAYERMODELPREFIX);
+
+	while (fscanf(f, "%25s %31s %f %f", name, filename, &scale, &offset) == 4)
 	{
-		if (stricmp(name, "PLAY") == 0)
+		char *skinname = name;
+		size_t len = strlen(name);
+
+		// check for the player model prefix.
+		if (!strnicmp(name, PLAYERMODELPREFIX, prefixlen) && (len > prefixlen))
 		{
-			CONS_Printf("Model for sprite PLAY detected in models.dat, use a player skin instead!\n");
-			continue;
+			skinname += prefixlen;
+			goto addskinmodel;
 		}
 
-		for (i = 0; i < NUMSPRITES; i++)
+		// add sprite model
+		if (len == 4) // must be 4 characters long exactly. otherwise it's not a sprite name.
 		{
-			if (stricmp(name, sprnames[i]) == 0)
+			for (i = 0; i < NUMSPRITES; i++)
 			{
-				md2_models[i].scale = scale;
-				md2_models[i].offset = offset;
-				md2_models[i].notfound = false;
-				strcpy(md2_models[i].filename, filename);
+				if (stricmp(name, sprnames[i]) == 0)
+				{
+					md2_models[i].scale = scale;
+					md2_models[i].offset = offset;
+					md2_models[i].notfound = false;
+					strcpy(md2_models[i].filename, filename);
+					goto modelfound;
+				}
 			}
 		}
 
+addskinmodel:
+		// add player model
 		for (s = 0; s < MAXSKINS; s++)
 		{
-			if (stricmp(name, skins[s].name) == 0)
+			if (stricmp(skinname, skins[s].name) == 0)
 			{
 				md2_playermodels[s].skin = s;
 				md2_playermodels[s].scale = scale;
 				md2_playermodels[s].offset = offset;
 				md2_playermodels[s].notfound = false;
 				strcpy(md2_playermodels[s].filename, filename);
+				goto modelfound;
 			}
 		}
 
+modelfound:
 		// move on to next line...
 		continue;
 	}
@@ -528,8 +546,9 @@ void HWR_InitModels(void)
 void HWR_AddPlayerModel(int skin) // For skins that were added after startup
 {
 	FILE *f;
-	char name[18], filename[32];
+	char name[24], filename[32];
 	float scale, offset;
+	size_t prefixlen;
 
 	if (nomd2s)
 		return;
@@ -547,31 +566,42 @@ void HWR_AddPlayerModel(int skin) // For skins that were added after startup
 		return;
 	}
 
-	// Check for any model that match the names of player skins!
-	while (fscanf(f, "%19s %31s %f %f", name, filename, &scale, &offset) == 4)
+	// length of the player model prefix
+	prefixlen = strlen(PLAYERMODELPREFIX);
+
+	// Check for any models that match the names of player skins!
+	while (fscanf(f, "%25s %31s %f %f", name, filename, &scale, &offset) == 4)
 	{
-		if (stricmp(name, skins[skin].name) == 0)
+		char *skinname = name;
+		size_t len = strlen(name);
+
+		// ignore the player model prefix.
+		if (!strnicmp(name, PLAYERMODELPREFIX, prefixlen) && (len > prefixlen))
+			skinname += prefixlen;
+
+		if (stricmp(skinname, skins[skin].name) == 0)
 		{
 			md2_playermodels[skin].skin = skin;
 			md2_playermodels[skin].scale = scale;
 			md2_playermodels[skin].offset = offset;
 			md2_playermodels[skin].notfound = false;
 			strcpy(md2_playermodels[skin].filename, filename);
-			goto playermd2found;
+			goto playermodelfound;
 		}
 	}
 
 	md2_playermodels[skin].notfound = true;
-playermd2found:
+playermodelfound:
 	fclose(f);
 }
 
 void HWR_AddSpriteModel(size_t spritenum) // For sprites that were added after startup
 {
 	FILE *f;
-	// name[18] is used to check for names in the models.dat file that match with sprites or player skins
+	// name[24] is used to check for names in the models.dat file that match with sprites or player skins
 	// sprite names are always 4 characters long, and names is for player skins can be up to 19 characters long
-	char name[18], filename[32];
+	// PLAYERMODELPREFIX is 6 characters long
+	char name[24], filename[32];
 	float scale, offset;
 
 	if (nomd2s)
@@ -591,21 +621,30 @@ void HWR_AddSpriteModel(size_t spritenum) // For sprites that were added after s
 		return;
 	}
 
-	// Check for any MD2s that match the names of sprite names!
-	while (fscanf(f, "%19s %31s %f %f", name, filename, &scale, &offset) == 4)
+	// Check for any models that match the names of sprite names!
+	while (fscanf(f, "%25s %31s %f %f", name, filename, &scale, &offset) == 4)
 	{
+		// length of the sprite name
+		size_t len = strlen(name);
+		if (len != 4) // must be 4 characters long exactly. otherwise it's not a sprite name.
+			continue;
+
+		// check for the player model prefix.
+		if (!strnicmp(name, PLAYERMODELPREFIX, strlen(PLAYERMODELPREFIX)))
+			continue; // that's not a sprite...
+
 		if (stricmp(name, sprnames[spritenum]) == 0)
 		{
 			md2_models[spritenum].scale = scale;
 			md2_models[spritenum].offset = offset;
 			md2_models[spritenum].notfound = false;
 			strcpy(md2_models[spritenum].filename, filename);
-			goto spritemd2found;
+			goto spritemodelfound;
 		}
 	}
 
 	md2_models[spritenum].notfound = true;
-spritemd2found:
+spritemodelfound:
 	fclose(f);
 }
 
diff --git a/src/hardware/hw_md2.h b/src/hardware/hw_md2.h
index 479c68e5b2d8504493abfefb8f01d35f25ef56f8..faa91caeee42a2ab47321442663584829685c5db 100644
--- a/src/hardware/hw_md2.h
+++ b/src/hardware/hw_md2.h
@@ -42,4 +42,6 @@ void HWR_AddPlayerModel(INT32 skin);
 void HWR_AddSpriteModel(size_t spritenum);
 boolean HWR_DrawModel(gr_vissprite_t *spr);
 
+#define PLAYERMODELPREFIX "PLAYER"
+
 #endif // _HW_MD2_H_
diff --git a/src/info.c b/src/info.c
index 45034fe371e0b3e96edcc2360f3ee2473cdaaebd..20bd73498db9cf923429a64a34c44d026eafa844 100644
--- a/src/info.c
+++ b/src/info.c
@@ -762,7 +762,7 @@ state_t states[NUMSTATES] =
 	{SPR_PLAY, SPR2_LIFE, 20, {NULL}, 0,  4, S_NULL},       // S_PLAY_ICON3
 
 	// Level end sign (uses player sprite)
-	{SPR_PLAY, SPR2_SIGN|FF_PAPERSPRITE, -1, {NULL}, 0, 29, S_PLAY_SIGN},         // S_PLAY_SIGN
+	{SPR_PLAY, SPR2_SIGN|FF_PAPERSPRITE, 2, {NULL}, 0, 29, S_PLAY_SIGN},         // S_PLAY_SIGN
 
 	// NiGHTS Player, transforming
 	{SPR_PLAY, SPR2_TRNS|FF_ANIMATE,     7, {NULL},          0, 4, S_PLAY_NIGHTS_TRANS2}, // S_PLAY_NIGHTS_TRANS1
@@ -2343,12 +2343,13 @@ state_t states[NUMSTATES] =
 
 	// TNT barrel
 	{SPR_BARR, 0, -1, {NULL}, 0, 0, S_NULL}, // S_TNTBARREL_STND1
-	{SPR_BARX, 0|FF_FULLBRIGHT, 3, {A_SetObjectFlags}, MF_NOCLIP|MF_NOGRAVITY|MF_NOBLOCKMAP, 0, S_TNTBARREL_EXPL2}, // S_TNTBARREL_EXPL1
-	{SPR_BARX, 1|FF_FULLBRIGHT, 2, {A_TNTExplode}, MT_TNTDUST, 0, S_TNTBARREL_EXPL3}, // S_TNTBARREL_EXPL2
-	{SPR_BARX, 1|FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_TNTBARREL_EXPL4}, // S_TNTBARREL_EXPL3
-	{SPR_BARX, 2|FF_FULLBRIGHT, 3, {NULL}, 0, 0, S_TNTBARREL_EXPL5}, // S_TNTBARREL_EXPL4
-	{SPR_BARX, 3|FF_FULLBRIGHT, 3, {NULL}, 0, 0, S_TNTBARREL_EXPL6}, // S_TNTBARREL_EXPL5
-	{SPR_NULL, 0, 35, {NULL}, 0, 0, S_NULL}, // S_TNTBARREL_EXPL6
+	{SPR_BARX, 0, 0, {A_RollAngle}, 0, 1, S_TNTBARREL_EXPL2}, // S_TNTBARREL_EXPL1
+	{SPR_BARX, 0|FF_FULLBRIGHT, 3, {A_SetObjectFlags}, MF_NOCLIP|MF_NOGRAVITY|MF_NOBLOCKMAP, 0, S_TNTBARREL_EXPL3}, // S_TNTBARREL_EXPL2
+	{SPR_BARX, 1|FF_FULLBRIGHT, 2, {A_TNTExplode}, MT_TNTDUST, 0, S_TNTBARREL_EXPL4}, // S_TNTBARREL_EXPL3
+	{SPR_BARX, 1|FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_TNTBARREL_EXPL5}, // S_TNTBARREL_EXPL4
+	{SPR_BARX, 2|FF_FULLBRIGHT, 3, {NULL}, 0, 0, S_TNTBARREL_EXPL6}, // S_TNTBARREL_EXPL5
+	{SPR_BARX, 3|FF_FULLBRIGHT, 3, {NULL}, 0, 0, S_TNTBARREL_EXPL7}, // S_TNTBARREL_EXPL6
+	{SPR_NULL, 0, 35, {NULL}, 0, 0, S_NULL}, // S_TNTBARREL_EXPL7
 #ifndef ROTSPRITE
 	{SPR_BARR, 1|FF_ANIMATE, -1, {NULL}, 7, 2, S_NULL}, // S_TNTBARREL_FLYING
 #else
@@ -2394,7 +2395,7 @@ state_t states[NUMSTATES] =
 
 	// Minecart
 	{SPR_NULL, 0,                            1, {NULL},                 0, 0, S_MINECART_IDLE},   // S_MINECART_IDLE
-	{SPR_NULL, 0,                            0, {A_KillSegments},       0, 0, S_TNTBARREL_EXPL3}, // S_MINECART_DTH1
+	{SPR_NULL, 0,                            0, {A_KillSegments},       0, 0, S_TNTBARREL_EXPL4}, // S_MINECART_DTH1
 	{SPR_MCRT, 8|FF_PAPERSPRITE,            -1, {NULL},                 0, 0, S_NULL},            // S_MINECARTEND
 	{SPR_MCRT, 0|FF_PAPERSPRITE,            -1, {NULL},                 0, 0, S_NULL},            // S_MINECARTSEG_FRONT
 	{SPR_MCRT, 1|FF_PAPERSPRITE,            -1, {NULL},                 0, 0, S_NULL},            // S_MINECARTSEG_BACK
diff --git a/src/info.h b/src/info.h
index 628335635da599b7f49766d693b2105bd831d08c..59b8eb68d74b41bc62c658e1f2e0eaa04b4afde9 100644
--- a/src/info.h
+++ b/src/info.h
@@ -2503,6 +2503,7 @@ typedef enum state
 	S_TNTBARREL_EXPL4,
 	S_TNTBARREL_EXPL5,
 	S_TNTBARREL_EXPL6,
+	S_TNTBARREL_EXPL7,
 	S_TNTBARREL_FLYING,
 
 	// TNT proximity shell
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index f451944e3d95f6c6b40c990f6c120e6ee196c965..dbab801dfa662d43886319353a91c5bc954bd305 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -268,10 +268,14 @@ static int patch_get(lua_State *L)
 #endif
 	enum patch field = luaL_checkoption(L, 2, NULL, patch_opt);
 
-	// patches are CURRENTLY always valid, expected to be cached with PU_STATIC
-	// this may change in the future, so patch.valid still exists
-	if (!patch)
+	// patches are invalidated when switching renderers
+	if (!patch) {
+		if (field == patch_valid) {
+			lua_pushboolean(L, 0);
+			return 1;
+		}
 		return LUA_ErrInvalid(L, "patch_t");
+	}
 
 	switch (field)
 	{
@@ -1214,7 +1218,7 @@ void LUAh_GameHUD(player_t *stplayr)
 
 	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
 	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 2); // HUD[2] = rendering funcs
+	lua_rawgeti(gL, -1, 2+hudhook_game); // HUD[2] = rendering funcs
 	I_Assert(lua_istable(gL, -1));
 
 	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
@@ -1248,7 +1252,7 @@ void LUAh_ScoresHUD(void)
 
 	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
 	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 3); // HUD[3] = rendering funcs
+	lua_rawgeti(gL, -1, 2+hudhook_scores); // HUD[3] = rendering funcs
 	I_Assert(lua_istable(gL, -1));
 
 	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
@@ -1273,7 +1277,7 @@ void LUAh_TitleHUD(void)
 
 	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
 	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 4); // HUD[4] = rendering funcs
+	lua_rawgeti(gL, -1, 2+hudhook_title); // HUD[5] = rendering funcs
 	I_Assert(lua_istable(gL, -1));
 
 	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
@@ -1298,7 +1302,7 @@ void LUAh_TitleCardHUD(player_t *stplayr)
 
 	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
 	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 5); // HUD[5] = rendering funcs
+	lua_rawgeti(gL, -1, 2+hudhook_titlecard); // HUD[6] = rendering funcs
 	I_Assert(lua_istable(gL, -1));
 
 	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
@@ -1332,7 +1336,7 @@ void LUAh_IntermissionHUD(void)
 
 	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
 	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 4); // HUD[4] = rendering funcs
+	lua_rawgeti(gL, -1, 2+hudhook_intermission); // HUD[4] = rendering funcs
 	I_Assert(lua_istable(gL, -1));
 
 	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
diff --git a/src/m_anigif.c b/src/m_anigif.c
index 32fc2746da54caa831cda304053c73a2519b55db..faa8f29e16b6a9b9d0545692e94e8f0cda334105 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -19,6 +19,7 @@
 #include "v_video.h"
 #include "i_video.h"
 #include "m_misc.h"
+#include "st_stuff.h" // st_palette
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -29,11 +30,17 @@
 
 consvar_t cv_gif_optimize = {"gif_optimize", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_gif_downscale =  {"gif_downscale", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_gif_localcolortable =  {"gif_localcolortable", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 #ifdef HAVE_ANIGIF
 static boolean gif_optimize = false; // So nobody can do something dumb
 static boolean gif_downscale = false; // like changing cvars mid output
-static RGBA_t *gif_palette = NULL;
+
+// Palette handling
+static boolean gif_localcolortable = false;
+static boolean gif_colorprofile = false;
+static RGBA_t *gif_headerpalette = NULL;
+static RGBA_t *gif_framepalette = NULL;
 
 static FILE *gif_out = NULL;
 static INT32 gif_frames = 0;
@@ -393,16 +400,47 @@ const UINT8 gifhead_nsid[19] = {0x21,0xFF,0x0B, // extension block + size
 	0x4E,0x45,0x54,0x53,0x43,0x41,0x50,0x45,0x32,0x2E,0x30, // NETSCAPE2.0
 	0x03,0x01,0xFF,0xFF,0x00}; // sub-block, repetitions
 
+
+//
+// GIF_getpalette
+// determine the palette for the current frame.
+//
+static RGBA_t *GIF_getpalette(size_t palnum)
+{
+	// In hardware mode, always returns the local palette
+#ifdef HWRENDER
+	if (rendermode == render_opengl)
+		return pLocalPalette;
+	else
+#endif
+		return (gif_colorprofile ? &pLocalPalette[palnum*256] : &pMasterPalette[palnum*256]);
+}
+
+//
+// GIF_palwrite
+// writes the gif palette.
+// used both for the header and local color tables.
+//
+static UINT8 *GIF_palwrite(UINT8 *p, RGBA_t *pal)
+{
+	INT32 i;
+	for (i = 0; i < 256; i++)
+	{
+		WRITEUINT8(p, pal[i].s.red);
+		WRITEUINT8(p, pal[i].s.green);
+		WRITEUINT8(p, pal[i].s.blue);
+	}
+	return p;
+}
+
 //
 // GIF_headwrite
 // writes the gif header to the currently open output file.
-// NOTE that this code does not accomodate for palette changes.
 //
 static void GIF_headwrite(void)
 {
 	UINT8 *gifhead = Z_Malloc(800, PU_STATIC, NULL);
 	UINT8 *p = gifhead;
-	INT32 i;
 	UINT16 rwidth, rheight;
 
 	if (!gif_out)
@@ -423,24 +461,17 @@ static void GIF_headwrite(void)
 		rwidth = vid.width;
 		rheight = vid.height;
 	}
+
 	WRITEUINT16(p, rwidth);
 	WRITEUINT16(p, rheight);
 
 	// colors, aspect, etc
-	WRITEUINT8(p, 0xF7);
+	WRITEUINT8(p, 0xF7); // (0xF7 = 1111 0111)
 	WRITEUINT8(p, 0x00);
 	WRITEUINT8(p, 0x00);
 
 	// write color table
-	{
-		RGBA_t *pal = gif_palette;
-		for (i = 0; i < 256; i++)
-		{
-			WRITEUINT8(p, pal[i].s.red);
-			WRITEUINT8(p, pal[i].s.green);
-			WRITEUINT8(p, pal[i].s.blue);
-		}
-	}
+	p = GIF_palwrite(p, gif_headerpalette);
 
 	// write extension block
 	WRITEMEM(p, gifhead_nsid, sizeof(gifhead_nsid));
@@ -468,7 +499,7 @@ static void hwrconvert(void)
 	INT32 x, y;
 	size_t i = 0;
 
-	InitColorLUT(gif_palette);
+	InitColorLUT(gif_framepalette);
 
 	for (y = 0; y < vid.height; y++)
 	{
@@ -494,6 +525,7 @@ static void GIF_framewrite(void)
 	UINT8 *p;
 	UINT8 *movie_screen = screens[2];
 	INT32 blitx, blity, blitw, blith;
+	boolean palchanged;
 
 	if (!gifframe_data)
 		gifframe_data = Z_Malloc(gifframe_size, PU_STATIC, NULL);
@@ -502,8 +534,18 @@ static void GIF_framewrite(void)
 	if (!gif_out)
 		return;
 
+	// Lactozilla: Compare the header's palette with the current frame's palette and see if it changed.
+	if (gif_localcolortable)
+	{
+		gif_framepalette = GIF_getpalette(max(st_palette, 0));
+		palchanged = memcmp(gif_headerpalette, gif_framepalette, sizeof(RGBA_t) * 256);
+	}
+	else
+		palchanged = false;
+
 	// Compare image data (for optimizing GIF)
-	if (gif_optimize && gif_frames > 0)
+	// If the palette has changed, the entire frame is considered to be different.
+	if (gif_optimize && gif_frames > 0 && (!palchanged))
 	{
 		// before blit movie_screen points to last frame, cur_screen points to this frame
 		UINT8 *cur_screen = screens[0];
@@ -566,7 +608,20 @@ static void GIF_framewrite(void)
 		WRITEUINT16(p, (UINT16)(blity / scrbuf_downscaleamt));
 		WRITEUINT16(p, (UINT16)(blitw / scrbuf_downscaleamt));
 		WRITEUINT16(p, (UINT16)(blith / scrbuf_downscaleamt));
-		WRITEUINT8(p, 0); // no local table of colors
+
+		if (!gif_localcolortable)
+			WRITEUINT8(p, 0); // no local table of colors
+		else
+		{
+			if (palchanged)
+			{
+				// The palettes are different, so write the Local Color Table!
+				WRITEUINT8(p, 0x87); // (0x87 = 1000 0111)
+				p = GIF_palwrite(p, gif_framepalette);
+			}
+			else
+				WRITEUINT8(p, 0); // They are equal, no Local Color Table needed.
+		}
 
 		scrbuf_pos = movie_screen + blitx + (blity * vid.width);
 		scrbuf_writeend = scrbuf_pos + (blitw - 1) + ((blith - 1) * vid.width);
@@ -624,29 +679,15 @@ static void GIF_framewrite(void)
 //
 INT32 GIF_open(const char *filename)
 {
-#if 0
-	if (rendermode != render_soft)
-	{
-		CONS_Alert(CONS_WARNING, M_GetText("GIFs cannot be taken in non-software modes!\n"));
-		return 0;
-	}
-#endif
-
 	gif_out = fopen(filename, "wb");
 	if (!gif_out)
 		return 0;
 
 	gif_optimize = (!!cv_gif_optimize.value);
 	gif_downscale = (!!cv_gif_downscale.value);
-
-	// GIF color table
-	// In hardware mode, uses the master palette
-	gif_palette = ((cv_screenshot_colorprofile.value
-#ifdef HWRENDER
-	&& (rendermode == render_soft)
-#endif
-	) ? pLocalPalette
-	: pMasterPalette);
+	gif_localcolortable = (!!cv_gif_localcolortable.value);
+	gif_colorprofile = (!!cv_screenshot_colorprofile.value);
+	gif_headerpalette = GIF_getpalette(0);
 
 	GIF_headwrite();
 	gif_frames = 0;
diff --git a/src/m_anigif.h b/src/m_anigif.h
index 9bdf2cc7f22701c6a2a35eff4580ba299379a565..9eb6faa75e0748a71cf1d6890adbac0fe41a43ba 100644
--- a/src/m_anigif.h
+++ b/src/m_anigif.h
@@ -27,6 +27,6 @@ void GIF_frame(void);
 INT32 GIF_close(void);
 #endif
 
-extern consvar_t cv_gif_optimize, cv_gif_downscale;
+extern consvar_t cv_gif_optimize, cv_gif_downscale, cv_gif_localcolortable;
 
 #endif
diff --git a/src/m_argv.c b/src/m_argv.c
index 935f4b9e3f2786b4a369af8b9b06a949c0961036..129706127087254d82362e75bc5169b858736cc2 100644
--- a/src/m_argv.c
+++ b/src/m_argv.c
@@ -92,36 +92,21 @@ const char *M_GetNextParm(void)
 void M_PushSpecialParameters(void)
 {
 	INT32 i;
-	char s[256];
-	boolean onetime = false;
-
 	for (i = 1; i < myargc; i++)
 	{
 		if (myargv[i][0] == '+')
 		{
-			strcpy(s, &myargv[i][1]);
+			COM_BufAddText(&myargv[i][1]);
 			i++;
 
 			// get the parameters of the command too
 			for (; i < myargc && myargv[i][0] != '+' && myargv[i][0] != '-'; i++)
 			{
-				strcat(s, " ");
-				if (!onetime)
-				{
-					strcat(s, "\"");
-					onetime = true;
-				}
-				strcat(s, myargv[i]);
-			}
-			if (onetime)
-			{
-				strcat(s, "\"");
-				onetime = false;
+				COM_BufAddText(va(" \"%s\"", myargv[i]));
 			}
-			strcat(s, "\n");
 
 			// push it
-			COM_BufAddText(s);
+			COM_BufAddText("\n");
 			i--;
 		}
 	}
diff --git a/src/m_menu.c b/src/m_menu.c
index 5e3cde2cda4cb11ad6e1c4c91e49963442b22183..0a5238bfd65daa88bd082c1889d2781f39061b77 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -326,6 +326,7 @@ menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef;
 menu_t OP_ServerOptionsDef;
 menu_t OP_MonitorToggleDef;
 static void M_ScreenshotOptions(INT32 choice);
+static void M_SetupScreenshotMenu(void);
 static void M_EraseData(INT32 choice);
 
 static void M_Addons(INT32 choice);
@@ -364,7 +365,6 @@ static void M_DrawColorMenu(void);
 static void M_DrawScreenshotMenu(void);
 static void M_DrawMonitorToggles(void);
 #ifndef NONET
-static void M_DrawScreenshotMenu(void);
 static void M_DrawConnectMenu(void);
 static void M_DrawMPMainMenu(void);
 static void M_DrawRoomMenu(void);
@@ -1505,6 +1505,7 @@ static menuitem_t OP_ScreenshotOptionsMenu[] =
 
 	{IT_STRING|IT_CVAR, NULL, "Region Optimizing", &cv_gif_optimize,              95},
 	{IT_STRING|IT_CVAR, NULL, "Downscaling",       &cv_gif_downscale,             100},
+	{IT_STRING|IT_CVAR, NULL, "Local Color Table", &cv_gif_localcolortable,       105},
 
 	{IT_STRING|IT_CVAR, NULL, "Memory Level",      &cv_zlib_memorya,              95},
 	{IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_levela,               100},
@@ -1515,13 +1516,14 @@ static menuitem_t OP_ScreenshotOptionsMenu[] =
 enum
 {
 	op_screenshot_colorprofile = 1,
+	op_screenshot_storagelocation = 3,
 	op_screenshot_folder = 4,
 	op_movie_folder = 11,
 	op_screenshot_capture = 12,
 	op_screenshot_gif_start = 13,
-	op_screenshot_gif_end = 14,
-	op_screenshot_apng_start = 15,
-	op_screenshot_apng_end = 18,
+	op_screenshot_gif_end = 15,
+	op_screenshot_apng_start = 16,
+	op_screenshot_apng_end = 19,
 };
 
 static menuitem_t OP_EraseDataMenu[] =
@@ -3008,7 +3010,8 @@ static void M_ChangeCvar(INT32 choice)
 			|| !(currentMenu->menuitems[itemOn].status & IT_CV_INTEGERSTEP))
 		{
 			char s[20];
-			sprintf(s,"%f",FIXED_TO_FLOAT(cv->value)+(choice)*(1.0f/16.0f));
+			float n = FIXED_TO_FLOAT(cv->value)+(choice)*(1.0f/16.0f);
+			sprintf(s,"%ld%s",(long)n,M_Ftrim(n));
 			CV_Set(cv,s);
 		}
 		else
@@ -3765,6 +3768,9 @@ void M_Ticker(void)
 		if (--vidm_testingmode == 0)
 			setmodeneeded = vidm_previousmode + 1;
 	}
+
+	if (currentMenu == &OP_ScreenshotOptionsDef)
+		M_SetupScreenshotMenu();
 }
 
 //
@@ -10826,7 +10832,6 @@ static void M_HandleConnectIP(INT32 choice)
 					default: // otherwise do nothing.
 						break;
 				}
-				break; // don't check for typed keys
 			}
 
 			if (l >= 28-1)
@@ -11330,9 +11335,27 @@ static void M_ScreenshotOptions(INT32 choice)
 	Screenshot_option_Onchange();
 	Moviemode_mode_Onchange();
 
+	M_SetupScreenshotMenu();
 	M_SetupNextMenu(&OP_ScreenshotOptionsDef);
 }
 
+static void M_SetupScreenshotMenu(void)
+{
+	menuitem_t *item = &OP_ScreenshotOptionsMenu[op_screenshot_colorprofile];
+
+#ifdef HWRENDER
+	// Hide some options based on render mode
+	if (rendermode == render_opengl)
+	{
+		item->status = IT_GRAYEDOUT;
+		if ((currentMenu == &OP_ScreenshotOptionsDef) && (itemOn == op_screenshot_colorprofile)) // Can't select that
+			itemOn = op_screenshot_storagelocation;
+	}
+	else
+#endif
+		item->status = (IT_STRING | IT_CVAR);
+}
+
 // =============
 // JOYSTICK MENU
 // =============
@@ -12275,6 +12298,15 @@ static void M_HandleVideoMode(INT32 ch)
 static void M_DrawScreenshotMenu(void)
 {
 	M_DrawGenericScrollMenu();
+#ifdef HWRENDER
+	if ((rendermode == render_opengl) && (itemOn < 7)) // where it starts to go offscreen; change this number if you change the layout of the screenshot menu
+	{
+		INT32 y = currentMenu->y+currentMenu->menuitems[op_screenshot_colorprofile].alphaKey*2;
+		if (itemOn == 6)
+			y -= 10;
+		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, y, V_REDMAP, "Yes");
+	}
+#endif
 }
 
 // ===============
diff --git a/src/m_misc.c b/src/m_misc.c
index 3026f66548c88bf33d06e64701c891e69db25d20..3dfeef81ec941129cadbdfd145394e1ad49f8452 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -2612,3 +2612,20 @@ int M_JumpWordReverse(const char *line, int offset)
 		offset--;
 	return offset;
 }
+
+const char * M_Ftrim (double f)
+{
+	static char dig[9];/* "0." + 6 digits (6 is printf's default) */
+	int i;
+	/* I know I said it's the default, but just in case... */
+	sprintf(dig, "%.6g", modf(f, &f));
+	if (dig[0])
+	{
+		for (i = strlen(dig); dig[i] == '0'; --i)
+			;
+		dig[i + 1] = '\0';
+		return &dig[1];/* skip the 0 */
+	}
+	else
+		return "";
+}
diff --git a/src/m_misc.h b/src/m_misc.h
index 3fb9f031a460f007e4d539837905072547b367fd..ad12517c18782b63793effbfd4a009d2bda4e9b0 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -109,6 +109,12 @@ int M_JumpWord (const char *s);
 /* E.g. cursor = M_JumpWordReverse(line, cursor); */
 int M_JumpWordReverse (const char *line, int offset);
 
+/*
+Return dot and then the fractional part of a float, without
+trailing zeros, or "" if the fractional part is zero.
+*/
+const char * M_Ftrim (double);
+
 // counting bits, for weapon ammo code, usually
 FUNCMATH UINT8 M_CountBits(UINT32 num, UINT8 size);
 
diff --git a/src/p_enemy.c b/src/p_enemy.c
index db297e684eecb15b57b63e39fc06f5c6aefd50f8..2ddbd8d29ba6cbd7ffc9890772bd53d52f3e3449 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -5198,8 +5198,8 @@ void A_SignPlayer(mobj_t *actor)
 			player_t *player = actor->target ? actor->target->player : NULL;
 			UINT8 skinnum;
 			UINT8 skincount = 0;
-			for (skincount = 0; skincount < numskins; skincount++)
-				if (!skincheck(skincount))
+			for (skinnum = 0; skinnum < numskins; skinnum++)
+				if (!skincheck(skinnum))
 					skincount++;
 			skinnum = P_RandomKey(skincount);
 			for (skincount = 0; skincount < numskins; skincount++)
@@ -5232,20 +5232,23 @@ void A_SignPlayer(mobj_t *actor)
 		{
 			ov->color = facecolor;
 			ov->skin = skin;
-			P_SetMobjState(ov, actor->info->seestate); // S_PLAY_SIGN
+			if ((statenum_t)(ov->state-states) != actor->info->seestate)
+				P_SetMobjState(ov, actor->info->seestate); // S_PLAY_SIGN
 		}
 		else // CLEAR! sign
 		{
 			ov->color = SKINCOLOR_NONE;
 			ov->skin = NULL; // needs to be NULL in the case of SF_HIRES characters
-			P_SetMobjState(ov, actor->info->missilestate); // S_CLEARSIGN
+			if ((statenum_t)(ov->state-states) != actor->info->missilestate)
+				P_SetMobjState(ov, actor->info->missilestate); // S_CLEARSIGN
 		}
 	}
 	else // Eggman face
 	{
 		ov->color = SKINCOLOR_NONE;
 		ov->skin = NULL;
-		P_SetMobjState(ov, actor->info->meleestate); // S_EGGMANSIGN
+		if ((statenum_t)(ov->state-states) != actor->info->meleestate)
+			P_SetMobjState(ov, actor->info->meleestate); // S_EGGMANSIGN
 		if (!signcolor)
 			signcolor = SKINCOLOR_CARBON;
 	}
diff --git a/src/p_inter.c b/src/p_inter.c
index 8090b7406f9e177f6630d2a58e162218d9aaa22c..667245f039ede6a946bc6e4d7854c2a74fa251a4 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -1428,98 +1428,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 // Misc touchables //
 // *************** //
 		case MT_STARPOST:
-			if (player->bot)
-				return;
-			// In circuit, player must have touched all previous starposts
-			if (circuitmap
-				&& special->health - player->starpostnum > 1)
-			{
-				// blatant reuse of a variable that's normally unused in circuit
-				if (!player->tossdelay)
-					S_StartSound(toucher, sfx_lose);
-				player->tossdelay = 3;
-				return;
-			}
-
-			// We could technically have 91.1 Star Posts. 90 is cleaner.
-			if (special->health > 90)
-			{
-				CONS_Debug(DBG_GAMELOGIC, "Bad Starpost Number!\n");
-				return;
-			}
-
-			if (player->starpostnum >= special->health)
-				return; // Already hit this post
-
-			if (cv_coopstarposts.value && G_GametypeUsesCoopStarposts() && (netgame || multiplayer))
-			{
-				for (i = 0; i < MAXPLAYERS; i++)
-				{
-					if (playeringame[i])
-					{
-						if (players[i].bot) // ignore dumb, stupid tails
-							continue;
-
-						players[i].starposttime = leveltime;
-						players[i].starpostx = player->mo->x>>FRACBITS;
-						players[i].starposty = player->mo->y>>FRACBITS;
-						players[i].starpostz = special->z>>FRACBITS;
-						players[i].starpostangle = special->angle;
-						players[i].starpostscale = player->mo->destscale;
-						if (special->flags2 & MF2_OBJECTFLIP)
-						{
-							players[i].starpostscale *= -1;
-							players[i].starpostz += special->height>>FRACBITS;
-						}
-						players[i].starpostnum = special->health;
-
-						if (cv_coopstarposts.value == 2 && (players[i].playerstate == PST_DEAD || players[i].spectator) && P_GetLives(&players[i]))
-							P_SpectatorJoinGame(&players[i]); //players[i].playerstate = PST_REBORN;
-					}
-				}
-				S_StartSound(NULL, special->info->painsound);
-			}
-			else
-			{
-				// Save the player's time and position.
-				player->starposttime = leveltime;
-				player->starpostx = toucher->x>>FRACBITS;
-				player->starposty = toucher->y>>FRACBITS;
-				player->starpostz = special->z>>FRACBITS;
-				player->starpostangle = special->angle;
-				player->starpostscale = player->mo->destscale;
-				if (special->flags2 & MF2_OBJECTFLIP)
-				{
-					player->starpostscale *= -1;
-					player->starpostz += special->height>>FRACBITS;
-				}
-				player->starpostnum = special->health;
-				S_StartSound(toucher, special->info->painsound);
-			}
-
-			P_ClearStarPost(special->health);
-
-			// Find all starposts in the level with this value - INCLUDING this one!
-			if (!(netgame && circuitmap && player != &players[consoleplayer]))
-			{
-				thinker_t *th;
-				mobj_t *mo2;
-
-				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
-				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-						continue;
-
-					mo2 = (mobj_t *)th;
-
-					if (mo2->type != MT_STARPOST)
-						continue;
-					if (mo2->health != special->health)
-						continue;
-
-					P_SetMobjState(mo2, mo2->info->painstate);
-				}
-			}
+			P_TouchStarPost(special, player, special->spawnpoint && (special->spawnpoint->options & MTF_OBJECTSPECIAL));
 			return;
 
 		case MT_FAKEMOBILE:
@@ -1872,6 +1781,112 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 	special->shadowscale = 0;
 }
 
+/** Saves a player's level progress at a star post
+  *
+  * \param post The star post to trigger
+  * \param player The player that should receive the checkpoint
+  * \param snaptopost If true, the respawn point will use the star post's position, otherwise player x/y and star post z
+  */
+void P_TouchStarPost(mobj_t *post, player_t *player, boolean snaptopost)
+{
+	size_t i;
+	mobj_t *toucher = player->mo;
+	mobj_t *checkbase = snaptopost ? post : toucher;
+
+	if (player->bot)
+		return;
+	// In circuit, player must have touched all previous starposts
+	if (circuitmap
+		&& post->health - player->starpostnum > 1)
+	{
+		// blatant reuse of a variable that's normally unused in circuit
+		if (!player->tossdelay)
+			S_StartSound(toucher, sfx_lose);
+		player->tossdelay = 3;
+		return;
+	}
+
+	// With the parameter + angle setup, we can go up to 1365 star posts. Who needs that many?
+	if (post->health > 1365)
+	{
+		CONS_Debug(DBG_GAMELOGIC, "Bad Starpost Number!\n");
+		return;
+	}
+
+	if (player->starpostnum >= post->health)
+		return; // Already hit this post
+
+	if (cv_coopstarposts.value && G_GametypeUsesCoopStarposts() && (netgame || multiplayer))
+	{
+		for (i = 0; i < MAXPLAYERS; i++)
+		{
+			if (playeringame[i])
+			{
+				if (players[i].bot) // ignore dumb, stupid tails
+					continue;
+
+				players[i].starposttime = leveltime;
+				players[i].starpostx = checkbase->x>>FRACBITS;
+				players[i].starposty = checkbase->y>>FRACBITS;
+				players[i].starpostz = post->z>>FRACBITS;
+				players[i].starpostangle = post->angle;
+				players[i].starpostscale = player->mo->destscale;
+				if (post->flags2 & MF2_OBJECTFLIP)
+				{
+					players[i].starpostscale *= -1;
+					players[i].starpostz += post->height>>FRACBITS;
+				}
+				players[i].starpostnum = post->health;
+
+				if (cv_coopstarposts.value == 2 && (players[i].playerstate == PST_DEAD || players[i].spectator) && P_GetLives(&players[i]))
+					P_SpectatorJoinGame(&players[i]); //players[i].playerstate = PST_REBORN;
+			}
+		}
+		S_StartSound(NULL, post->info->painsound);
+	}
+	else
+	{
+		// Save the player's time and position.
+		player->starposttime = leveltime;
+		player->starpostx = checkbase->x>>FRACBITS;
+		player->starposty = checkbase->y>>FRACBITS;
+		player->starpostz = post->z>>FRACBITS;
+		player->starpostangle = post->angle;
+		player->starpostscale = player->mo->destscale;
+		if (post->flags2 & MF2_OBJECTFLIP)
+		{
+			player->starpostscale *= -1;
+			player->starpostz += post->height>>FRACBITS;
+		}
+		player->starpostnum = post->health;
+		S_StartSound(toucher, post->info->painsound);
+	}
+
+	P_ClearStarPost(post->health);
+
+	// Find all starposts in the level with this value - INCLUDING this one!
+	if (!(netgame && circuitmap && player != &players[consoleplayer]))
+	{
+		thinker_t *th;
+		mobj_t *mo2;
+
+		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+		{
+			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				continue;
+
+			mo2 = (mobj_t *)th;
+
+			if (mo2->type != MT_STARPOST)
+				continue;
+			if (mo2->health != post->health)
+				continue;
+
+			P_SetMobjState(mo2, mo2->info->painstate);
+		}
+	}
+}
+
 /** Prints death messages relating to a dying or hit player.
   *
   * \param player    Affected player.
diff --git a/src/p_local.h b/src/p_local.h
index a5f3d313ce25c71fc50a3695ea8eafd7a01831f8..8056d137c3850b2c9071161ae5519963051c8afd 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -486,6 +486,7 @@ void P_PlayerWeaponPanelOrAmmoBurst(player_t *player);
 void P_PlayerEmeraldBurst(player_t *player, boolean toss);
 
 void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck);
+void P_TouchStarPost(mobj_t *starpost, player_t *player, boolean snaptopost);
 void P_PlayerFlagBurst(player_t *player, boolean toss);
 void P_CheckTimeLimit(void);
 void P_CheckPointLimit(void);
diff --git a/src/p_mobj.c b/src/p_mobj.c
index e6d84086cc8c280b5f3176436f05749fb8292424..10898f70e79a0bfae8ad69ca2d1e6f9d129c93ad 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -2350,6 +2350,7 @@ static void P_RingZMovement(mobj_t *mo)
 	if (mo->eflags & MFE_APPLYPMOMZ && !P_IsObjectOnGround(mo))
 	{
 		mo->momz += mo->pmomz;
+		mo->pmomz = 0;
 		mo->eflags &= ~MFE_APPLYPMOMZ;
 	}
 	mo->z += mo->momz;
@@ -2419,6 +2420,7 @@ static boolean P_ZMovement(mobj_t *mo)
 	if (mo->eflags & MFE_APPLYPMOMZ && !P_IsObjectOnGround(mo))
 	{
 		mo->momz += mo->pmomz;
+		mo->pmomz = 0;
 		mo->eflags &= ~MFE_APPLYPMOMZ;
 	}
 	mo->z += mo->momz;
@@ -2907,6 +2909,7 @@ static void P_PlayerZMovement(mobj_t *mo)
 	if (mo->eflags & MFE_APPLYPMOMZ && !P_IsObjectOnGround(mo))
 	{
 		mo->momz += mo->pmomz;
+		mo->pmomz = 0;
 		mo->eflags &= ~MFE_APPLYPMOMZ;
 	}
 
@@ -3157,6 +3160,7 @@ static boolean P_SceneryZMovement(mobj_t *mo)
 	if (mo->eflags & MFE_APPLYPMOMZ && !P_IsObjectOnGround(mo))
 	{
 		mo->momz += mo->pmomz;
+		mo->pmomz = 0;
 		mo->eflags &= ~MFE_APPLYPMOMZ;
 	}
 	mo->z += mo->momz;
@@ -10457,6 +10461,61 @@ void P_SceneryThinker(mobj_t *mobj)
 // GAME SPAWN FUNCTIONS
 //
 
+static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
+{
+	switch (thing->type)
+	{
+		case MT_PLAYER:
+		case MT_ROLLOUTROCK:
+
+		case MT_EGGMOBILE4_MACE:
+		case MT_SMALLMACE:
+		case MT_BIGMACE:
+
+		case MT_SMALLGRABCHAIN:
+		case MT_BIGGRABCHAIN:
+
+		case MT_YELLOWSPRINGBALL:
+		case MT_REDSPRINGBALL:
+
+			return FRACUNIT;
+
+		case MT_RING:
+		case MT_FLINGRING:
+
+		case MT_BLUESPHERE:
+		case MT_FLINGBLUESPHERE:
+		case MT_BOMBSPHERE:
+
+		case MT_REDTEAMRING:
+		case MT_BLUETEAMRING:
+		case MT_REDFLAG:
+		case MT_BLUEFLAG:
+
+		case MT_EMBLEM:
+
+		case MT_TOKEN:
+		case MT_EMERALD1:
+		case MT_EMERALD2:
+		case MT_EMERALD3:
+		case MT_EMERALD4:
+		case MT_EMERALD5:
+		case MT_EMERALD6:
+		case MT_EMERALD7:
+		case MT_EMERHUNT:
+		case MT_FLINGEMERALD:
+
+			return 2*FRACUNIT/3;
+
+		default:
+
+			if (thing->flags & (MF_ENEMY|MF_BOSS))
+				return FRACUNIT;
+			else
+				return 0;
+	}
+}
+
 //
 // P_SpawnMobj
 //
@@ -10558,20 +10617,7 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		mobj->z = z;
 
 	// Set shadowscale here, before spawn hook so that Lua can change it
-	if (
-		type == MT_PLAYER ||
-		type == MT_ROLLOUTROCK ||
-		type == MT_EGGMOBILE4_MACE ||
-		(type >= MT_SMALLMACE && type <= MT_REDSPRINGBALL) ||
-		(mobj->flags & (MF_ENEMY|MF_BOSS))
-	)
-		mobj->shadowscale = FRACUNIT;
-	else if (
-		type >= MT_RING && type <= MT_FLINGEMERALD && type != MT_EMERALDSPAWN
-	)
-		mobj->shadowscale = 2*FRACUNIT/3;
-	else
-		mobj->shadowscale = 0;
+	mobj->shadowscale = P_DefaultMobjShadowScale(mobj);
 
 #ifdef HAVE_BLUA
 	// DANGER! This can cause P_SpawnMobj to return NULL!
@@ -12931,7 +12977,16 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		thinker_t* th;
 		mobj_t* mo2;
 		boolean foundanother = false;
-		mobj->health = (mthing->angle/360) + 1;
+
+		if (mthing->extrainfo)
+			// Allow thing Parameter to define star post num too!
+			// For starposts above param 15 (the 16th), add 360 to the angle like before and start parameter from 1 (NOT 0)!
+			// So the 16th starpost is angle=0 param=15, the 17th would be angle=360 param=1.
+			// This seems more intuitive for mappers to use until UDMF is ready, since most SP maps won't have over 16 consecutive star posts.
+			mobj->health = mthing->extrainfo + (mthing->angle/360)*15 + 1;
+		else
+			// Old behavior if Parameter is 0; add 360 to the angle for each consecutive star post.
+			mobj->health = (mthing->angle/360) + 1;
 
 		// See if other starposts exist in this level that have the same value.
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index cd0a44bb4a78e913addbd4e91a64cc1791c30b2a..a206d01712aac0eb960191604bcbd1f8d8793e40 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -1001,15 +1001,35 @@ static void Polyobj_pushThing(polyobj_t *po, line_t *line, mobj_t *mo)
 //
 static void Polyobj_slideThing(mobj_t *mo, fixed_t dx, fixed_t dy)
 {
-	if (mo->player) { // Do something similar to conveyor movement. -Red
-		mo->player->cmomx += dx;
-		mo->player->cmomy += dy;
+	if (mo->player) { // Finally this doesn't suck eggs -fickle
+		fixed_t cdx, cdy;
 
-		dx = FixedMul(dx, CARRYFACTOR);
-		dy = FixedMul(dy, CARRYFACTOR);
+		cdx = FixedMul(dx, FRACUNIT-CARRYFACTOR);
+		cdy = FixedMul(dy, FRACUNIT-CARRYFACTOR);
 
-		mo->player->cmomx -= dx;
-		mo->player->cmomy -= dy;
+		if (mo->player->onconveyor == 1)
+		{
+			mo->momx += cdx;
+			mo->momy += cdy;
+
+			// Multiple slides in the same tic, somehow
+			mo->player->cmomx += cdx;
+			mo->player->cmomy += cdy;
+		}
+		else
+		{
+			if (mo->player->onconveyor == 3)
+			{
+				mo->momx += cdx - mo->player->cmomx;
+				mo->momy += cdy - mo->player->cmomy;
+			}
+
+			mo->player->cmomx = cdx;
+			mo->player->cmomy = cdy;
+		}
+
+		dx = FixedMul(dx, FRACUNIT - mo->friction);
+		dy = FixedMul(dy, FRACUNIT - mo->friction);
 
 		if (mo->player->pflags & PF_SPINNING && (mo->player->rmomx || mo->player->rmomy) && !(mo->player->pflags & PF_STARTDASH)) {
 #define SPINMULT 5184 // Consider this a substitute for properly calculating FRACUNIT-friction. I'm tired. -Red
@@ -1282,7 +1302,8 @@ static void Polyobj_rotateThings(polyobj_t *po, vertex_t origin, angle_t delta,
 {
 	static INT32 pomovecount = 10000;
 	INT32 x, y;
-	angle_t deltafine = delta >> ANGLETOFINESHIFT;
+	angle_t deltafine = (((po->angle + delta) >> ANGLETOFINESHIFT) - (po->angle >> ANGLETOFINESHIFT)) & FINEMASK;
+	// This fineshift trickery replaces the old delta>>ANGLETOFINESHIFT; doing it this way avoids loss of precision causing objects to slide off -fickle
 
 	pomovecount++;
 
@@ -1334,19 +1355,10 @@ static void Polyobj_rotateThings(polyobj_t *po, vertex_t origin, angle_t delta,
 					oldxoff = mo->x-origin.x;
 					oldyoff = mo->y-origin.y;
 
-					if (mo->player) // Hack to fix players sliding off of spinning polys -Red
-					{
-						fixed_t temp;
-
-						temp = FixedMul(oldxoff, c)-FixedMul(oldyoff, s);
-						oldyoff = FixedMul(oldyoff, c)+FixedMul(oldxoff, s);
-						oldxoff = temp;
-					}
-
-					newxoff = FixedMul(oldxoff, c)-FixedMul(oldyoff, s);
-					newyoff = FixedMul(oldyoff, c)+FixedMul(oldxoff, s);
+					newxoff = FixedMul(oldxoff, c)-FixedMul(oldyoff, s) - oldxoff;
+					newyoff = FixedMul(oldyoff, c)+FixedMul(oldxoff, s) - oldyoff;
 
-					Polyobj_slideThing(mo, newxoff-oldxoff, newyoff-oldyoff);
+					Polyobj_slideThing(mo, newxoff, newyoff);
 
 					if (turnthings == 2 || (turnthings == 1 && !mo->player)) {
 						mo->angle += delta;
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 8538568804352af0231b2743735e75dad0f271e9..7d755edc6915af64f44847befd4478294b0de928 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -1255,6 +1255,7 @@ typedef enum
 #endif
 	MD2_COLORIZED    = 1<<12,
 	MD2_ROLLANGLE    = 1<<13,
+	MD2_SHADOWSCALE  = 1<<14,
 } mobj_diff2_t;
 
 typedef enum
@@ -1476,6 +1477,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		diff2 |= MD2_COLORIZED;
 	if (mobj->rollangle)
 		diff2 |= MD2_ROLLANGLE;
+	if (mobj->shadowscale)
+		diff2 |= MD2_SHADOWSCALE;
 	if (diff2 != 0)
 		diff |= MD_MORE;
 
@@ -1642,6 +1645,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		WRITEUINT8(save_p, mobj->colorized);
 	if (diff2 & MD2_ROLLANGLE)
 		WRITEANGLE(save_p, mobj->rollangle);
+	if (diff2 & MD2_SHADOWSCALE)
+		WRITEFIXED(save_p, mobj->shadowscale);
 
 	WRITEUINT32(save_p, mobj->mobjnum);
 }
@@ -2721,6 +2726,8 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 		mobj->colorized = READUINT8(save_p);
 	if (diff2 & MD2_ROLLANGLE)
 		mobj->rollangle = READANGLE(save_p);
+	if (diff2 & MD2_SHADOWSCALE)
+		mobj->shadowscale = READFIXED(save_p);
 
 	if (diff & MD_REDFLAG)
 	{
diff --git a/src/p_spec.c b/src/p_spec.c
index 84ccc29a57fcfcd3390dec9795ce803138b4fc7f..76a80d754af2ea2f5b41b71c151c2a05246d3575 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -4692,7 +4692,7 @@ DoneSection2:
 			if (!post)
 				break;
 
-			P_TouchSpecialThing(post, player->mo, false);
+			P_TouchStarPost(post, player, false);
 			break;
 		}
 
diff --git a/src/p_user.c b/src/p_user.c
index 0f4cf027bae9888e8acd3f4aab92f4ed28c30096..5abf01b57f10b08ccd8002f34b45219acfc0e415 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2380,6 +2380,8 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 					}
 				}
 			}
+			else if (player->charability == CA_GLIDEANDCLIMB && (player->mo->state-states == S_PLAY_GLIDE_LANDING))
+				;
 			else if (player->charability2 == CA2_GUNSLINGER && player->panim == PA_ABILITY2)
 				;
 			else if (player->panim != PA_IDLE && player->panim != PA_WALK && player->panim != PA_RUN && player->panim != PA_DASH)
@@ -4537,16 +4539,14 @@ void P_DoJump(player_t *player, boolean soundandstate)
 		player->mo->z--;
 		if (player->mo->pmomz < 0)
 			player->mo->momz += player->mo->pmomz; // Add the platform's momentum to your jump.
-		else
-			player->mo->pmomz = 0;
+		player->mo->pmomz = 0;
 	}
 	else
 	{
 		player->mo->z++;
 		if (player->mo->pmomz > 0)
 			player->mo->momz += player->mo->pmomz; // Add the platform's momentum to your jump.
-		else
-			player->mo->pmomz = 0;
+		player->mo->pmomz = 0;
 	}
 	player->mo->eflags &= ~MFE_APPLYPMOMZ;
 
@@ -5503,10 +5503,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						; // Can't do anything if you're a fish out of water!
 					else if (player->powers[pw_tailsfly]) // If currently flying, give an ascend boost.
 					{
-						if (!player->fly1)
-							player->fly1 = 20;
-						else
-							player->fly1 = 2;
+						player->fly1 = 20;
 
 						if (player->charability == CA_SWIM)
 							player->fly1 /= 2;
@@ -8487,7 +8484,11 @@ static void P_MovePlayer(player_t *player)
 			// Descend
 			if (cmd->buttons & BT_USE && !(player->pflags & PF_STASIS) && !player->exiting && !(player->mo->eflags & MFE_GOOWATER))
 				if (P_MobjFlip(player->mo)*player->mo->momz > -FixedMul(5*actionspd, player->mo->scale))
+				{
+					if (player->fly1 > 2)
+						player->fly1 = 2;
 					P_SetObjectMomZ(player->mo, -actionspd/2, true);
+				}
 
 		}
 		else
@@ -12175,7 +12176,9 @@ void P_PlayerThink(player_t *player)
 
 #ifdef POLYOBJECTS
 	if (player->onconveyor == 1)
-			player->cmomy = player->cmomx = 0;
+		player->onconveyor = 3;
+	else if (player->onconveyor == 3)
+		player->cmomy = player->cmomx = 0;
 #endif
 
 	P_DoSuperStuff(player);
diff --git a/src/r_data.c b/src/r_data.c
index a2a87d5ecdeab5acda2b925ab95408cf6b9ecea1..ef62ba0c18931cac8ac393cfed5a36a9c686e25f 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -2471,16 +2471,20 @@ extracolormap_t *R_AddColormaps(extracolormap_t *exc_augend, extracolormap_t *ex
 
 // Thanks to quake2 source!
 // utils3/qdata/images.c
-UINT8 NearestColor(UINT8 r, UINT8 g, UINT8 b)
+UINT8 NearestPaletteColor(UINT8 r, UINT8 g, UINT8 b, RGBA_t *palette)
 {
 	int dr, dg, db;
 	int distortion, bestdistortion = 256 * 256 * 4, bestcolor = 0, i;
 
+	// Use master palette if none specified
+	if (palette == NULL)
+		palette = pMasterPalette;
+
 	for (i = 0; i < 256; i++)
 	{
-		dr = r - pMasterPalette[i].s.red;
-		dg = g - pMasterPalette[i].s.green;
-		db = b - pMasterPalette[i].s.blue;
+		dr = r - palette[i].s.red;
+		dg = g - palette[i].s.green;
+		db = b - palette[i].s.blue;
 		distortion = dr*dr + dg*dg + db*db;
 		if (distortion < bestdistortion)
 		{
diff --git a/src/r_data.h b/src/r_data.h
index f028f2f5d16ef54da544d56780448adc84d8b858..145f0182b1b585cdc8a9db5cada3cbc75f73c00b 100644
--- a/src/r_data.h
+++ b/src/r_data.h
@@ -171,7 +171,8 @@ const char *R_NameForColormap(extracolormap_t *extra_colormap);
 #define R_PutRgbaRGB(r, g, b) (R_PutRgbaR(r) + R_PutRgbaG(g) + R_PutRgbaB(b))
 #define R_PutRgbaRGBA(r, g, b, a) (R_PutRgbaRGB(r, g, b) + R_PutRgbaA(a))
 
-UINT8 NearestColor(UINT8 r, UINT8 g, UINT8 b);
+UINT8 NearestPaletteColor(UINT8 r, UINT8 g, UINT8 b, RGBA_t *palette);
+#define NearestColor(r, g, b) NearestPaletteColor(r, g, b, NULL)
 
 extern INT32 numtextures;
 
diff --git a/src/r_patch.c b/src/r_patch.c
index dd5224c2693796caf7f0640c47ba1d5aeacac57a..87600d699c4cfaa2400ce76e99643e157bdb691b 100644
--- a/src/r_patch.c
+++ b/src/r_patch.c
@@ -1196,7 +1196,7 @@ void R_CacheRotSprite(spritenum_t sprnum, UINT8 frame, spriteinfo_t *sprinfo, sp
 	INT32 angle;
 	patch_t *patch;
 	patch_t *newpatch;
-	UINT16 *rawsrc, *rawdst;
+	UINT16 *rawdst;
 	size_t size;
 	INT32 bflip = (flip != 0x00);
 
@@ -1242,16 +1242,6 @@ void R_CacheRotSprite(spritenum_t sprnum, UINT8 frame, spriteinfo_t *sprinfo, sp
 			leftoffset = width - leftoffset;
 		}
 
-		// Draw the sprite to a temporary buffer.
-		size = (width*height);
-		rawsrc = Z_Malloc(size * sizeof(UINT16), PU_STATIC, NULL);
-
-		// can't memset here
-		for (i = 0; i < size; i++)
-			rawsrc[i] = 0xFF00;
-
-		R_PatchToMaskedFlat(patch, rawsrc, bflip);
-
 		// Don't cache angle = 0
 		for (angle = 1; angle < ROTANGLES; angle++)
 		{
diff --git a/src/r_things.c b/src/r_things.c
index 7f0f43281ca9f2abe6f8ad16f67f71f758a3bd33..ca285644fba5773ec9c5aff3b47a3fe956a25eee 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -3463,6 +3463,7 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
 	GETFLAG(DASHMODE)
 	GETFLAG(FASTEDGE)
 	GETFLAG(MULTIABILITY)
+	GETFLAG(NONIGHTSROTATION)
 #undef GETFLAG
 
 	else // let's check if it's a sound, otherwise error out
diff --git a/src/screen.c b/src/screen.c
index 5406ee46fa7e257b9bcdcccc268561d065648047..b5faaef7edc345e97ee1f771821204ac321f6d1a 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -360,10 +360,13 @@ void SCR_Recalc(void)
 	vid.fsmalldupy = vid.smalldupy*FRACUNIT;
 #endif
 
-	// toggle off automap because some screensize-dependent values will
+	// toggle off (then back on) the automap because some screensize-dependent values will
 	// be calculated next time the automap is activated.
 	if (automapactive)
-		AM_Stop();
+	{
+		am_recalc = true;
+		AM_Start();
+	}
 
 	// set the screen[x] ptrs on the new vidbuffers
 	V_Init();
diff --git a/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj b/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
index ab3157c44c5eab916918c6370074c70910cfb901..2b03cb8d4607bd96f53b04f8b98e1a227866bd64 100644
--- a/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
+++ b/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
@@ -1219,7 +1219,7 @@
 		C01FCF4B08A954540054247B /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.2.0;
+				CURRENT_PROJECT_VERSION = 2.2.1;
 				GCC_PREPROCESSOR_DEFINITIONS = (
 					"$(inherited)",
 					NORMALSRB2,
@@ -1231,7 +1231,7 @@
 		C01FCF4C08A954540054247B /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.2.0;
+				CURRENT_PROJECT_VERSION = 2.2.1;
 				GCC_ENABLE_FIX_AND_CONTINUE = NO;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
 				GCC_PREPROCESSOR_DEFINITIONS = (
diff --git a/src/v_video.c b/src/v_video.c
index 94f76fa58cbf7ebcdf8fd98790996e39a79d33eb..bda05966130caa8e6b18d7ce3f1aa25dd886ffce 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -3243,7 +3243,6 @@ Unoptimized version
 #endif
 }
 
-// Taken from my videos-in-SRB2 project
 // Generates a color look-up table
 // which has up to 64 colors at each channel
 // (see the defines in v_video.h)
@@ -3260,7 +3259,7 @@ void InitColorLUT(RGBA_t *palette)
 		for (r = 0; r < CLUTSIZE; r++)
 			for (g = 0; g < CLUTSIZE; g++)
 				for (b = 0; b < CLUTSIZE; b++)
-					colorlookup[r][g][b] = NearestColor(r << SHIFTCOLORBITS, g << SHIFTCOLORBITS, b << SHIFTCOLORBITS);
+					colorlookup[r][g][b] = NearestPaletteColor(r << SHIFTCOLORBITS, g << SHIFTCOLORBITS, b << SHIFTCOLORBITS, palette);
 		clutinit = true;
 		lastpalette = palette;
 	}
diff --git a/src/v_video.h b/src/v_video.h
index eb75a414ff3e31acfd8c48ded41c6d1b94ec35bb..bc19f6e9970339660082c1f1c074eb4f9e25d3c3 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -37,10 +37,7 @@ cv_allcaps;
 // Allocates buffer screens, call before R_Init.
 void V_Init(void);
 
-// Taken from my videos-in-SRB2 project
-// Generates a color look-up table
-// which has up to 64 colors at each channel
-
+// Color look-up table
 #define COLORBITS 6
 #define SHIFTCOLORBITS (8-COLORBITS)
 #define CLUTSIZE (1<<COLORBITS)
diff --git a/src/w_wad.c b/src/w_wad.c
index 01053ff652d1e3932c0fe40a0da92d9a96a5035d..59c1e50b1c0fe76b7c0405ac5f7eedf105a9379e 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -831,13 +831,11 @@ UINT16 W_InitFile(const char *filename, boolean mainfile)
   * backwards, so a later file overrides all earlier ones.
   *
   * \param filenames A null-terminated list of files to use.
-  * \return 1 if all files were loaded, 0 if at least one was missing or
+  * \return 1 if base files were loaded, 0 if at least one was missing or
   *           invalid.
   */
 INT32 W_InitMultipleFiles(char **filenames, UINT16 mainfiles)
 {
-	INT32 rc = 1;
-
 	// open all the files, load headers, and count lumps
 	numwadfiles = 0;
 
@@ -845,13 +843,15 @@ INT32 W_InitMultipleFiles(char **filenames, UINT16 mainfiles)
 	for (; *filenames; filenames++)
 	{
 		//CONS_Debug(DBG_SETUP, "Loading %s\n", *filenames);
-		rc &= (W_InitFile(*filenames, numwadfiles < mainfiles) != INT16_MAX) ? 1 : 0;
+		if (W_InitFile(*filenames, numwadfiles < mainfiles) == INT16_MAX)
+		{
+			CONS_Printf(M_GetText("Errors occurred while loading %s; not added.\n"), *filenames);
+			if (numwadfiles < mainfiles)
+				return 0;
+		}
 	}
 
-	if (!numwadfiles)
-		I_Error("W_InitMultipleFiles: no files found");
-
-	return rc;
+	return 1;
 }
 
 /** Make sure a lump number is valid.
@@ -1682,7 +1682,7 @@ W_VerifyName (const char *name, lumpchecklist_t *checklist, boolean status)
 	size_t j;
 	for (j = 0; checklist[j].len && checklist[j].name; ++j)
 	{
-		if (( strncmp(name, checklist[j].name,
+		if (( strncasecmp(name, checklist[j].name,
 						checklist[j].len) != false ) == status)
 		{
 			return true;
@@ -1737,6 +1737,19 @@ W_VerifyWAD (FILE *fp, lumpchecklist_t *checklist, boolean status)
 	return true;
 }
 
+// List of blacklisted folders to use when checking the PK3
+static lumpchecklist_t folderblacklist[] =
+{
+	{"Lua/", 4},
+	{"SOC/", 4},
+	{"Sprites/",  8},
+	{"Textures/", 9},
+	{"Patches/", 8},
+	{"Flats/", 6},
+	{"Fades/", 6},
+	{NULL, 0},
+};
+
 static int
 W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 {
@@ -1788,7 +1801,7 @@ W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 		else
 			trimname = fullname; // Care taken for root files.
 
-		if (*trimname) // Ignore directories
+		if (*trimname) // Ignore directories, well kinda
 		{
 			if ((dotpos = strrchr(trimname, '.')) == 0)
 				dotpos = fullname + strlen(fullname); // Watch for files without extension.
@@ -1798,6 +1811,10 @@ W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 
 			if (! W_VerifyName(lumpname, checklist, status))
 				return false;
+
+			// Check for directories next, if it's blacklisted it will return false
+			if (W_VerifyName(fullname, folderblacklist, status))
+				return false;
 		}
 
 		free(fullname);
diff --git a/src/z_zone.c b/src/z_zone.c
index 25e4ea4c7db2f584020ed9bd78652e4314c3df40..68045e8b741feb56565a8a877dbb45d668c2cd94 100644
--- a/src/z_zone.c
+++ b/src/z_zone.c
@@ -232,12 +232,12 @@ void Z_Free(void *ptr)
 
 	// Free the memory and get rid of the block.
 	free(block->real);
-	block->prev->next = block->next;
-	block->next->prev = block->prev;
-	free(block);
 #ifdef VALGRIND_DESTROY_MEMPOOL
 	VALGRIND_DESTROY_MEMPOOL(block);
 #endif
+	block->prev->next = block->next;
+	block->next->prev = block->prev;
+	free(block);
 }
 
 /** malloc() that doesn't accept failure.
@@ -317,13 +317,9 @@ void *Z_MallocAlign(size_t size, INT32 tag, void *user, INT32 alignbits)
 	// The mem header lives 'sizeof (memhdr_t)' bytes before given.
 	hdr = (memhdr_t *)((UINT8 *)given - sizeof *hdr);
 
-#ifdef VALGRIND_CREATE_MEMPOOL
-	VALGRIND_CREATE_MEMPOOL(block, padsize, Z_calloc);
+#ifdef HAVE_VALGRIND
 	Z_calloc = false;
 #endif
-#ifdef VALGRIND_MEMPOOL_ALLOC
-	VALGRIND_MEMPOOL_ALLOC(block, hdr, size + sizeof *hdr);
-#endif
 
 	block->next = head.next;
 	block->prev = &head;
@@ -341,6 +337,13 @@ void *Z_MallocAlign(size_t size, INT32 tag, void *user, INT32 alignbits)
 	block->size = blocksize;
 	block->realsize = size;
 
+#ifdef VALGRIND_CREATE_MEMPOOL
+	VALGRIND_CREATE_MEMPOOL(block, padsize, Z_calloc);
+#endif
+//#ifdef VALGRIND_MEMPOOL_ALLOC
+//	VALGRIND_MEMPOOL_ALLOC(block, hdr, size + sizeof *hdr);
+//#endif
+
 	hdr->id = ZONEID;
 	hdr->block = block;