diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 372341291bc0fe6118ab20a81350ad658bd5a102..b0a593bb16d760950c71623f49e7349abd1df8ad 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -16,6 +16,7 @@ set(SRB2_CORE_SOURCES
 	f_finale.c
 	f_wipe.c
 	filesrch.c
+	g_demo.c
 	g_game.c
 	g_input.c
 	hu_stuff.c
@@ -71,6 +72,7 @@ set(SRB2_CORE_HEADERS
 	f_finale.h
 	fastcmp.h
 	filesrch.h
+	g_demo.h
 	g_game.h
 	g_input.h
 	g_state.h
diff --git a/src/Makefile b/src/Makefile
index 1ebd39ac40e4d417ec74c2cc6c6ff616463e7626..fdf9c78b774b513ee5650654c32913e468d77d83 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -422,6 +422,7 @@ OBJS:=$(i_main_o) \
 		$(OBJDIR)/z_zone.o   \
 		$(OBJDIR)/f_finale.o \
 		$(OBJDIR)/f_wipe.o   \
+		$(OBJDIR)/g_demo.o   \
 		$(OBJDIR)/g_game.o   \
 		$(OBJDIR)/g_input.o  \
 		$(OBJDIR)/am_map.o   \
diff --git a/src/android/i_video.c b/src/android/i_video.c
index b8bb4fefbf408970af9c23ace95f5217548dc43e..1909cd71afbb5e2acf1efdfd5a218b44cef09316 100644
--- a/src/android/i_video.c
+++ b/src/android/i_video.c
@@ -19,10 +19,10 @@ boolean allow_fullscreen = false;
 consvar_t cv_vidwait = {"vid_wait", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 void I_StartupGraphics(void){}
-void I_StartupHardwareGraphics(void){}
-
 void I_ShutdownGraphics(void){}
 
+void VID_StartupOpenGL(void){}
+
 void I_SetPalette(RGBA_t *palette)
 {
   (void)palette;
@@ -52,10 +52,8 @@ INT32 VID_SetMode(INT32 modenum)
   return 0;
 }
 
-void VID_CheckRenderer(void)
-{
-	// ..............
-}
+void VID_CheckRenderer(void) {}
+void VID_CheckGLLoaded(rendermode_t oldrender) {}
 
 const char *VID_GetModeName(INT32 modenum)
 {
diff --git a/src/b_bot.c b/src/b_bot.c
index 4fefbdcb61b78a4972cf729d55a054cd2497f7da..9a4c20c17618ef2a7cf3f8edf475761ff0b89cc9 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -459,6 +459,21 @@ boolean B_CheckRespawn(player_t *player)
 	if (!sonic || sonic->health <= 0)
 		return false;
 
+#ifdef HAVE_BLUA
+	// B_RespawnBot doesn't do anything if the condition above this isn't met
+	{
+		UINT8 shouldForce = LUAh_BotRespawn(sonic, tails);
+
+		if (P_MobjWasRemoved(sonic) || P_MobjWasRemoved(tails))
+			return (shouldForce == 1); // mobj was removed
+
+		if (shouldForce == 1)
+			return true;
+		else if (shouldForce == 2)
+			return false;
+	}
+#endif
+
 	// Check if Sonic is busy first.
 	// If he's doing any of these things, he probably doesn't want to see us.
 	if (sonic->player->pflags & (PF_GLIDING|PF_SLIDING|PF_BOUNCING)
diff --git a/src/console.c b/src/console.c
index f8fa1314a7085179a8b99be769dad3fb9b186347..0f1ccbd33b02269e167ecfe9816b2164e77b09ca 100644
--- a/src/console.c
+++ b/src/console.c
@@ -97,6 +97,7 @@ static void CON_InputInit(void);
 static void CON_RecalcSize(void);
 static void CON_ChangeHeight(void);
 
+static void CON_DrawBackpic(void);
 static void CONS_hudlines_Change(void);
 static void CONS_backcolor_Change(void);
 
@@ -1530,6 +1531,51 @@ static void CON_DrawHudlines(void)
 	con_clearlines = y; // this is handled by HU_Erase();
 }
 
+// Lactozilla: Draws the console's background picture.
+static void CON_DrawBackpic(void)
+{
+	patch_t *con_backpic;
+	lumpnum_t piclump;
+	int x, w, h;
+
+	// Get the lumpnum for CONSBACK, or fallback into MISSING.
+	piclump = W_CheckNumForName("CONSBACK");
+	if (piclump == LUMPERROR)
+		piclump = W_GetNumForName("MISSING");
+
+	// Cache the Software patch.
+	con_backpic = W_CacheSoftwarePatchNum(piclump, PU_PATCH);
+
+	// Center the backpic, and draw a vertically cropped patch.
+	w = (con_backpic->width * vid.dupx);
+	x = (vid.width / 2) - (w / 2);
+	h = con_curlines/vid.dupy;
+
+	// If the patch doesn't fill the entire screen,
+	// then fill the sides with a solid color.
+	if (x > 0)
+	{
+		column_t *column = (column_t *)((UINT8 *)(con_backpic) + LONG(con_backpic->columnofs[0]));
+		if (!column->topdelta)
+		{
+			UINT8 *source = (UINT8 *)(column) + 3;
+			INT32 color = (source[0] | V_NOSCALESTART);
+			// left side
+			V_DrawFill(0, 0, x, con_curlines, color);
+			// right side
+			V_DrawFill((x + w), 0, (vid.width - w), con_curlines, color);
+		}
+	}
+
+	// Cache the patch normally.
+	con_backpic = W_CachePatchNum(piclump, PU_PATCH);
+	V_DrawCroppedPatch(x << FRACBITS, 0, FRACUNIT, V_NOSCALESTART, con_backpic,
+			0, ( BASEVIDHEIGHT - h ), BASEVIDWIDTH, h);
+
+	// Unlock the cached patch.
+	W_UnlockCachedPatch(con_backpic);
+}
+
 // draw the console background, text, and prompt if enough place
 //
 static void CON_DrawConsole(void)
@@ -1551,19 +1597,7 @@ static void CON_DrawConsole(void)
 
 	// draw console background
 	if (cons_backpic.value || con_forcepic)
-	{
-		patch_t *con_backpic = W_CachePatchName("CONSBACK", PU_PATCH);
-		int h;
-
-		h = con_curlines/vid.dupy;
-
-		// Jimita: CON_DrawBackpic just called V_DrawScaledPatch
-		//V_DrawScaledPatch(0, 0, 0, con_backpic);
-		V_DrawCroppedPatch(0, 0, FRACUNIT, 0, con_backpic,
-				0, ( BASEVIDHEIGHT - h ), BASEVIDWIDTH, h);
-
-		W_UnlockCachedPatch(con_backpic);
-	}
+		CON_DrawBackpic();
 	else
 	{
 		// inu: no more width (was always 0 and vid.width)
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index aa5d4cb9a9e11a51e3eca4fad9a0638b3b33d0a5..3603bb20d62ffee551c7b1ba12170ec3384068a5 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -1338,7 +1338,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 	netbuffer->u.serverinfo.numberofplayer = (UINT8)D_NumPlayers();
 	netbuffer->u.serverinfo.maxplayer = (UINT8)cv_maxplayers.value;
 
-	if (FindRejoinerNum(node) != -1)
+	if (!node || FindRejoinerNum(node) != -1)
 		netbuffer->u.serverinfo.refusereason = 0;
 	else if (!cv_allownewplayer.value)
 		netbuffer->u.serverinfo.refusereason = 1;
@@ -4875,7 +4875,8 @@ static inline void PingUpdate(void)
 	{
 		for (i = 1; i < MAXPLAYERS; i++)
 		{
-			if (playeringame[i] && (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value))
+			if (playeringame[i] && !players[i].quittime
+			&& (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value))
 			{
 				if (players[i].jointime > 30 * TICRATE)
 					laggers[i] = true;
@@ -4894,8 +4895,8 @@ static inline void PingUpdate(void)
 				if (playeringame[i] && laggers[i])
 				{
 					pingtimeout[i]++;
+					// ok your net has been bad for too long, you deserve to die.
 					if (pingtimeout[i] > cv_pingtimeout.value)
-// ok your net has been bad for too long, you deserve to die.
 					{
 						pingtimeout[i] = 0;
 						SendKick(i, KICK_MSG_PING_HIGH | KICK_MSG_KEEP_BODY);
diff --git a/src/d_main.c b/src/d_main.c
index 40e7af22a76137170e8bd60cd0c6014d199fab59..78ebbcd410c2db50a3ca4d538ca53dc15bf105d9 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1284,11 +1284,10 @@ void D_SRB2Main(void)
 
 		// Set cv_renderer to the new render mode
 		VID_CheckRenderer();
-		SCR_ChangeRendererCVars(setrenderneeded);
+		SCR_ChangeRendererCVars(rendermode);
 
-		// check the renderer's state, and then clear setrenderneeded
+		// check the renderer's state
 		D_CheckRendererState();
-		setrenderneeded = 0;
 	}
 
 	wipegamestate = gamestate;
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index aaa01c57f8ee7525eb752364641459c65154819c..1fd53499a6043bd3cdf1bab2d2f54a40bd8a3c71 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -1252,7 +1252,7 @@ static void SendNameAndColor(void)
 
 		players[consoleplayer].skincolor = cv_playercolor.value;
 
-		if (players[consoleplayer].mo)
+		if (players[consoleplayer].mo && !players[consoleplayer].powers[pw_dye])
 			players[consoleplayer].mo->color = players[consoleplayer].skincolor;
 
 		if (metalrecording)
@@ -1364,8 +1364,9 @@ static void SendNameAndColor2(void)
 	if (botingame)
 	{
 		players[secondplaya].skincolor = botcolor;
-		if (players[secondplaya].mo)
+		if (players[secondplaya].mo && !players[secondplaya].powers[pw_dye])
 			players[secondplaya].mo->color = players[secondplaya].skincolor;
+
 		SetPlayerSkinByNum(secondplaya, botskin-1);
 		return;
 	}
@@ -1378,7 +1379,7 @@ static void SendNameAndColor2(void)
 
 		// don't use secondarydisplayplayer: the second player must be 1
 		players[secondplaya].skincolor = cv_playercolor2.value;
-		if (players[secondplaya].mo)
+		if (players[secondplaya].mo && !players[secondplaya].powers[pw_dye])
 			players[secondplaya].mo->color = players[secondplaya].skincolor;
 
 		if (cv_forceskin.value >= 0 && (netgame || multiplayer)) // Server wants everyone to use the same player
diff --git a/src/d_player.h b/src/d_player.h
index 209ff766d6732715ad3cb1d5d27123310561ef21..8697e9836929c0e269f61127d7245686e71b0232 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -48,6 +48,7 @@ typedef enum
 	SF_FASTEDGE         = 1<<12, // Faster edge teeter?
 	SF_MULTIABILITY     = 1<<13, // Revenge of Final Demo.
 	SF_NONIGHTSROTATION = 1<<14, // Disable sprite rotation for NiGHTS
+	SF_NONIGHTSSUPER    = 1<<15, // Disable super colors for NiGHTS (if you have SF_SUPER)
 	// free up to and including 1<<31
 } skinflags_t;
 
@@ -278,6 +279,9 @@ typedef enum
 	pw_nights_linkfreeze,
 
 	pw_nocontrol, //for linedef exec 427
+
+	pw_dye, // for dyes
+
 	pw_justlaunched, // Launched off a slope this tic (0=none, 1=standard launch, 2=half-pipe launch)
 
 	NUMPOWERS
diff --git a/src/dehacked.c b/src/dehacked.c
index c6cd0b9e5e3a9f8fb5e90a18e85b92de4b1efc2d..08ff13cf2e3baf4a03b0c2c34777c762b917f22e 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -2909,6 +2909,7 @@ static actionpointer_t actionpointers[] =
 	{{A_SetRandomTics},          "A_SETRANDOMTICS"},
 	{{A_ChangeColorRelative},    "A_CHANGECOLORRELATIVE"},
 	{{A_ChangeColorAbsolute},    "A_CHANGECOLORABSOLUTE"},
+	{{A_Dye},                    "A_DYE"},
 	{{A_MoveRelative},           "A_MOVERELATIVE"},
 	{{A_MoveAbsolute},           "A_MOVEABSOLUTE"},
 	{{A_Thrust},                 "A_THRUST"},
@@ -9132,7 +9133,11 @@ static const char *const POWERS_LIST[] = {
 
 	//for linedef exec 427
 	"NOCONTROL",
-	"JUSTLAUNCHED",
+
+	//for dyes
+	"DYE",
+
+	"JUSTLAUNCHED"
 };
 
 static const char *const HUDITEMS_LIST[] = {
@@ -9467,6 +9472,7 @@ struct {
 	{"SF_FASTEDGE",SF_FASTEDGE},
 	{"SF_MULTIABILITY",SF_MULTIABILITY},
 	{"SF_NONIGHTSROTATION",SF_NONIGHTSROTATION},
+	{"SF_NONIGHTSSUPER",SF_NONIGHTSSUPER},
 
 	// Dashmode constants
 	{"DASHMODE_THRESHOLD",DASHMODE_THRESHOLD},
@@ -9648,11 +9654,11 @@ struct {
 	{"FF_CUTEXTRA",FF_CUTEXTRA},               ///< Cuts out hidden translucent pixels.
 	{"FF_CUTLEVEL",FF_CUTLEVEL},               ///< Cuts out all hidden pixels.
 	{"FF_CUTSPRITES",FF_CUTSPRITES},           ///< Final step in making 3D water.
-	{"FF_BOTHPLANES",FF_BOTHPLANES},           ///< Renders both planes all the time.
+	{"FF_BOTHPLANES",FF_BOTHPLANES},           ///< Render inside and outside planes.
 	{"FF_EXTRA",FF_EXTRA},                     ///< Gets cut by ::FF_CUTEXTRA.
 	{"FF_TRANSLUCENT",FF_TRANSLUCENT},         ///< See through!
 	{"FF_FOG",FF_FOG},                         ///< Fog "brush."
-	{"FF_INVERTPLANES",FF_INVERTPLANES},       ///< Reverse the plane visibility rules.
+	{"FF_INVERTPLANES",FF_INVERTPLANES},       ///< Only render inside planes.
 	{"FF_ALLSIDES",FF_ALLSIDES},               ///< Render inside and outside sides.
 	{"FF_INVERTSIDES",FF_INVERTSIDES},         ///< Only render inside sides.
 	{"FF_DOUBLESHADOW",FF_DOUBLESHADOW},       ///< Make two lightlist entries to reset light?
diff --git a/src/djgppdos/i_video.c b/src/djgppdos/i_video.c
index 02c7a842bc2c0aa77fb7d8d7180a04d9029878b5..f525b96ca6b19475148b8c22b213e21e88b3c8b0 100644
--- a/src/djgppdos/i_video.c
+++ b/src/djgppdos/i_video.c
@@ -339,7 +339,4 @@ void I_StartupGraphics(void)
 
 }
 
-void I_StartupHardwareGraphics(void)
-{
-	// oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo y
-}
+void VID_StartupOpenGL(void) {}
diff --git a/src/djgppdos/vid_vesa.c b/src/djgppdos/vid_vesa.c
index c8ce7dae52e645372455b76cbad40ffd03c53a0e..61ed18e4b6858eb8a9374b7edfe957f06975cce0 100644
--- a/src/djgppdos/vid_vesa.c
+++ b/src/djgppdos/vid_vesa.c
@@ -378,10 +378,8 @@ INT32 VID_SetMode (INT32 modenum)  //, UINT8 *palette)
 	return 1;
 }
 
-void VID_CheckRenderer(void)
-{
-	// ..............
-}
+void VID_CheckRenderer(void) {}
+void VID_CheckGLLoaded(rendermode_t oldrender) {}
 
 
 
diff --git a/src/dummy/i_video.c b/src/dummy/i_video.c
index fafeee0001400ef868b6c8d91042870c6c2e3773..56ead3672ae8ded9510814b20c766061a5d1d668 100644
--- a/src/dummy/i_video.c
+++ b/src/dummy/i_video.c
@@ -11,10 +11,10 @@ boolean allow_fullscreen = false;
 consvar_t cv_vidwait = {"vid_wait", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 void I_StartupGraphics(void){}
-void I_StartupHardwareGraphics(void){}
-
 void I_ShutdownGraphics(void){}
 
+void VID_StartupOpenGL(void){}
+
 void I_SetPalette(RGBA_t *palette)
 {
 	(void)palette;
@@ -40,10 +40,8 @@ INT32 VID_SetMode(INT32 modenum)
 	return 0;
 }
 
-void VID_CheckRenderer(void)
-{
-	// ..............
-}
+void VID_CheckRenderer(void) {}
+void VID_CheckGLLoaded(rendermode_t oldrender) {}
 
 const char *VID_GetModeName(INT32 modenum)
 {
diff --git a/src/g_demo.c b/src/g_demo.c
new file mode 100644
index 0000000000000000000000000000000000000000..30bc8ca48f57d299afd94d47bc5faea7a3d36663
--- /dev/null
+++ b/src/g_demo.c
@@ -0,0 +1,2501 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  g_demo.c
+/// \brief Demo recording and playback
+
+#include "doomdef.h"
+#include "console.h"
+#include "d_main.h"
+#include "d_player.h"
+#include "d_clisrv.h"
+#include "p_setup.h"
+#include "i_system.h"
+#include "m_random.h"
+#include "p_local.h"
+#include "r_draw.h"
+#include "r_main.h"
+#include "g_game.h"
+#include "g_demo.h"
+#include "m_misc.h"
+#include "m_menu.h"
+#include "m_argv.h"
+#include "hu_stuff.h"
+#include "z_zone.h"
+#include "i_video.h"
+#include "byteptr.h"
+#include "i_joy.h"
+#include "r_local.h"
+#include "r_skins.h"
+#include "y_inter.h"
+#include "v_video.h"
+#include "lua_hook.h"
+#include "md5.h" // demo checksums
+
+boolean timingdemo; // if true, exit with report on completion
+boolean nodrawers; // for comparative timing purposes
+boolean noblit; // for comparative timing purposes
+tic_t demostarttime; // for comparative timing purposes
+
+static char demoname[64];
+boolean demorecording;
+boolean demoplayback;
+boolean titledemo; // Title Screen demo can be cancelled by any key
+static UINT8 *demobuffer = NULL;
+static UINT8 *demo_p, *demotime_p;
+static UINT8 *demoend;
+static UINT8 demoflags;
+static UINT16 demoversion;
+boolean singledemo; // quit after playing a demo from cmdline
+boolean demo_start; // don't start playing demo right away
+boolean demosynced = true; // console warning message
+
+boolean metalrecording; // recording as metal sonic
+mobj_t *metalplayback;
+static UINT8 *metalbuffer = NULL;
+static UINT8 *metal_p;
+static UINT16 metalversion;
+
+// extra data stuff (events registered this frame while recording)
+static struct {
+	UINT8 flags; // EZT flags
+
+	// EZT_COLOR
+	UINT8 color, lastcolor;
+
+	// EZT_SCALE
+	fixed_t scale, lastscale;
+
+	// EZT_HIT
+	UINT16 hits;
+	mobj_t **hitlist;
+} ghostext;
+
+// Your naming conventions are stupid and useless.
+// There is no conflict here.
+typedef struct demoghost {
+	UINT8 checksum[16];
+	UINT8 *buffer, *p, color, fadein;
+	UINT16 version;
+	mobj_t oldmo, *mo;
+	struct demoghost *next;
+} demoghost;
+demoghost *ghosts = NULL;
+
+//
+// DEMO RECORDING
+//
+
+#define DEMOVERSION 0x000c
+#define DEMOHEADER  "\xF0" "SRB2Replay" "\x0F"
+
+#define DF_GHOST        0x01 // This demo contains ghost data too!
+#define DF_RECORDATTACK 0x02 // This demo is from record attack and contains its final completion time, score, and rings!
+#define DF_NIGHTSATTACK 0x04 // This demo is from NiGHTS attack and contains its time left, score, and mares!
+#define DF_ATTACKMASK   0x06 // This demo is from ??? attack and contains ???
+#define DF_ATTACKSHIFT  1
+
+// For demos
+#define ZT_FWD     0x01
+#define ZT_SIDE    0x02
+#define ZT_ANGLE   0x04
+#define ZT_BUTTONS 0x08
+#define ZT_AIMING  0x10
+#define DEMOMARKER 0x80 // demoend
+#define METALDEATH 0x44
+#define METALSNICE 0x69
+
+static ticcmd_t oldcmd;
+
+// For Metal Sonic and time attack ghosts
+#define GZT_XYZ    0x01
+#define GZT_MOMXY  0x02
+#define GZT_MOMZ   0x04
+#define GZT_ANGLE  0x08
+#define GZT_FRAME  0x10 // Animation frame
+#define GZT_SPR2   0x20 // Player animations
+#define GZT_EXTRA  0x40
+#define GZT_FOLLOW 0x80 // Followmobj
+
+// GZT_EXTRA flags
+#define EZT_THOK   0x01 // Spawned a thok object
+#define EZT_SPIN   0x02 // Because one type of thok object apparently wasn't enough
+#define EZT_REV    0x03 // And two types wasn't enough either yet
+#define EZT_THOKMASK 0x03
+#define EZT_COLOR  0x04 // Changed color (Super transformation, Mario fireflowers/invulnerability, etc.)
+#define EZT_FLIP   0x08 // Reversed gravity
+#define EZT_SCALE  0x10 // Changed size
+#define EZT_HIT    0x20 // Damaged a mobj
+#define EZT_SPRITE 0x40 // Changed sprite set completely out of PLAY (NiGHTS, SOCs, whatever)
+#define EZT_HEIGHT 0x80 // Changed height
+
+// GZT_FOLLOW flags
+#define FZT_SPAWNED 0x01 // just been spawned
+#define FZT_SKIN 0x02 // has skin
+#define FZT_LINKDRAW 0x04 // has linkdraw (combine with spawned only)
+#define FZT_COLORIZED 0x08 // colorized (ditto)
+#define FZT_SCALE 0x10 // different scale to object
+// spare FZT slots 0x20 to 0x80
+
+static mobj_t oldmetal, oldghost;
+
+void G_SaveMetal(UINT8 **buffer)
+{
+	I_Assert(buffer != NULL && *buffer != NULL);
+
+	WRITEUINT32(*buffer, metal_p - metalbuffer);
+}
+
+void G_LoadMetal(UINT8 **buffer)
+{
+	I_Assert(buffer != NULL && *buffer != NULL);
+
+	G_DoPlayMetal();
+	metal_p = metalbuffer + READUINT32(*buffer);
+}
+
+
+void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
+{
+	UINT8 ziptic;
+	(void)playernum;
+
+	if (!demo_p || !demo_start)
+		return;
+	ziptic = READUINT8(demo_p);
+
+	if (ziptic & ZT_FWD)
+		oldcmd.forwardmove = READSINT8(demo_p);
+	if (ziptic & ZT_SIDE)
+		oldcmd.sidemove = READSINT8(demo_p);
+	if (ziptic & ZT_ANGLE)
+		oldcmd.angleturn = READINT16(demo_p);
+	if (ziptic & ZT_BUTTONS)
+		oldcmd.buttons = (oldcmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) | (READUINT16(demo_p) & ~(BT_CAMLEFT|BT_CAMRIGHT));
+	if (ziptic & ZT_AIMING)
+		oldcmd.aiming = READINT16(demo_p);
+
+	G_CopyTiccmd(cmd, &oldcmd, 1);
+
+	if (!(demoflags & DF_GHOST) && *demo_p == DEMOMARKER)
+	{
+		// end of demo data stream
+		G_CheckDemoStatus();
+		return;
+	}
+}
+
+void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
+{
+	char ziptic = 0;
+	UINT8 *ziptic_p;
+	(void)playernum;
+
+	if (!demo_p)
+		return;
+	ziptic_p = demo_p++; // the ziptic, written at the end of this function
+
+	if (cmd->forwardmove != oldcmd.forwardmove)
+	{
+		WRITEUINT8(demo_p,cmd->forwardmove);
+		oldcmd.forwardmove = cmd->forwardmove;
+		ziptic |= ZT_FWD;
+	}
+
+	if (cmd->sidemove != oldcmd.sidemove)
+	{
+		WRITEUINT8(demo_p,cmd->sidemove);
+		oldcmd.sidemove = cmd->sidemove;
+		ziptic |= ZT_SIDE;
+	}
+
+	if (cmd->angleturn != oldcmd.angleturn)
+	{
+		WRITEINT16(demo_p,cmd->angleturn);
+		oldcmd.angleturn = cmd->angleturn;
+		ziptic |= ZT_ANGLE;
+	}
+
+	if (cmd->buttons != oldcmd.buttons)
+	{
+		WRITEUINT16(demo_p,cmd->buttons);
+		oldcmd.buttons = cmd->buttons;
+		ziptic |= ZT_BUTTONS;
+	}
+
+	if (cmd->aiming != oldcmd.aiming)
+	{
+		WRITEINT16(demo_p,cmd->aiming);
+		oldcmd.aiming = cmd->aiming;
+		ziptic |= ZT_AIMING;
+	}
+
+	*ziptic_p = ziptic;
+
+	// attention here for the ticcmd size!
+	// latest demos with mouse aiming byte in ticcmd
+	if (!(demoflags & DF_GHOST) && ziptic_p > demoend - 9)
+	{
+		G_CheckDemoStatus(); // no more space
+		return;
+	}
+}
+
+void G_GhostAddThok(void)
+{
+	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
+		return;
+	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_THOK;
+}
+
+void G_GhostAddSpin(void)
+{
+	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
+		return;
+	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_SPIN;
+}
+
+void G_GhostAddRev(void)
+{
+	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
+		return;
+	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_REV;
+}
+
+void G_GhostAddFlip(void)
+{
+	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
+		return;
+	ghostext.flags |= EZT_FLIP;
+}
+
+void G_GhostAddColor(ghostcolor_t color)
+{
+	if (!demorecording || !(demoflags & DF_GHOST))
+		return;
+	if (ghostext.lastcolor == (UINT8)color)
+	{
+		ghostext.flags &= ~EZT_COLOR;
+		return;
+	}
+	ghostext.flags |= EZT_COLOR;
+	ghostext.color = (UINT8)color;
+}
+
+void G_GhostAddScale(fixed_t scale)
+{
+	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
+		return;
+	if (ghostext.lastscale == scale)
+	{
+		ghostext.flags &= ~EZT_SCALE;
+		return;
+	}
+	ghostext.flags |= EZT_SCALE;
+	ghostext.scale = scale;
+}
+
+void G_GhostAddHit(mobj_t *victim)
+{
+	if (!demorecording || !(demoflags & DF_GHOST))
+		return;
+	ghostext.flags |= EZT_HIT;
+	ghostext.hits++;
+	ghostext.hitlist = Z_Realloc(ghostext.hitlist, ghostext.hits * sizeof(mobj_t *), PU_LEVEL, NULL);
+	ghostext.hitlist[ghostext.hits-1] = victim;
+}
+
+void G_WriteGhostTic(mobj_t *ghost)
+{
+	char ziptic = 0;
+	UINT8 *ziptic_p;
+	UINT32 i;
+	fixed_t height;
+
+	if (!demo_p)
+		return;
+	if (!(demoflags & DF_GHOST))
+		return; // No ghost data to write.
+
+	ziptic_p = demo_p++; // the ziptic, written at the end of this function
+
+	#define MAXMOM (0xFFFF<<8)
+
+	// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
+	if (abs(ghost->x-oldghost.x) > MAXMOM
+	|| abs(ghost->y-oldghost.y) > MAXMOM
+	|| abs(ghost->z-oldghost.z) > MAXMOM)
+	{
+		oldghost.x = ghost->x;
+		oldghost.y = ghost->y;
+		oldghost.z = ghost->z;
+		ziptic |= GZT_XYZ;
+		WRITEFIXED(demo_p,oldghost.x);
+		WRITEFIXED(demo_p,oldghost.y);
+		WRITEFIXED(demo_p,oldghost.z);
+	}
+	else
+	{
+		// For moving normally:
+		// Store one full byte of movement, plus one byte of fractional movement.
+		INT16 momx = (INT16)((ghost->x-oldghost.x)>>8);
+		INT16 momy = (INT16)((ghost->y-oldghost.y)>>8);
+		if (momx != oldghost.momx
+		|| momy != oldghost.momy)
+		{
+			oldghost.momx = momx;
+			oldghost.momy = momy;
+			ziptic |= GZT_MOMXY;
+			WRITEINT16(demo_p,momx);
+			WRITEINT16(demo_p,momy);
+		}
+		momx = (INT16)((ghost->z-oldghost.z)>>8);
+		if (momx != oldghost.momz)
+		{
+			oldghost.momz = momx;
+			ziptic |= GZT_MOMZ;
+			WRITEINT16(demo_p,momx);
+		}
+
+		// This SHOULD set oldghost.x/y/z to match ghost->x/y/z
+		// but it keeps the fractional loss of one byte,
+		// so it will hopefully be made up for in future tics.
+		oldghost.x += oldghost.momx<<8;
+		oldghost.y += oldghost.momy<<8;
+		oldghost.z += oldghost.momz<<8;
+	}
+
+	#undef MAXMOM
+
+	// Only store the 8 most relevant bits of angle
+	// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
+	// and it does not affect this mode of movement at all anyway.
+	if (ghost->player && ghost->player->drawangle>>24 != oldghost.angle)
+	{
+		oldghost.angle = ghost->player->drawangle>>24;
+		ziptic |= GZT_ANGLE;
+		WRITEUINT8(demo_p,oldghost.angle);
+	}
+
+	// Store the sprite frame.
+	if ((ghost->frame & FF_FRAMEMASK) != oldghost.frame)
+	{
+		oldghost.frame = (ghost->frame & FF_FRAMEMASK);
+		ziptic |= GZT_FRAME;
+		WRITEUINT8(demo_p,oldghost.frame);
+	}
+
+	if (ghost->sprite == SPR_PLAY
+	&& ghost->sprite2 != oldghost.sprite2)
+	{
+		oldghost.sprite2 = ghost->sprite2;
+		ziptic |= GZT_SPR2;
+		WRITEUINT8(demo_p,oldghost.sprite2);
+	}
+
+	// Check for sprite set changes
+	if (ghost->sprite != oldghost.sprite)
+	{
+		oldghost.sprite = ghost->sprite;
+		ghostext.flags |= EZT_SPRITE;
+	}
+
+	if ((height = FixedDiv(ghost->height, ghost->scale)) != oldghost.height)
+	{
+		oldghost.height = height;
+		ghostext.flags |= EZT_HEIGHT;
+	}
+
+	if (ghostext.flags)
+	{
+		ziptic |= GZT_EXTRA;
+
+		if (ghostext.color == ghostext.lastcolor)
+			ghostext.flags &= ~EZT_COLOR;
+		if (ghostext.scale == ghostext.lastscale)
+			ghostext.flags &= ~EZT_SCALE;
+
+		WRITEUINT8(demo_p,ghostext.flags);
+		if (ghostext.flags & EZT_COLOR)
+		{
+			WRITEUINT8(demo_p,ghostext.color);
+			ghostext.lastcolor = ghostext.color;
+		}
+		if (ghostext.flags & EZT_SCALE)
+		{
+			WRITEFIXED(demo_p,ghostext.scale);
+			ghostext.lastscale = ghostext.scale;
+		}
+		if (ghostext.flags & EZT_HIT)
+		{
+			WRITEUINT16(demo_p,ghostext.hits);
+			for (i = 0; i < ghostext.hits; i++)
+			{
+				mobj_t *mo = ghostext.hitlist[i];
+				//WRITEUINT32(demo_p,UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.)
+				WRITEUINT32(demo_p,mo->type);
+				WRITEUINT16(demo_p,(UINT16)mo->health);
+				WRITEFIXED(demo_p,mo->x);
+				WRITEFIXED(demo_p,mo->y);
+				WRITEFIXED(demo_p,mo->z);
+				WRITEANGLE(demo_p,mo->angle);
+			}
+			Z_Free(ghostext.hitlist);
+			ghostext.hits = 0;
+			ghostext.hitlist = NULL;
+		}
+		if (ghostext.flags & EZT_SPRITE)
+			WRITEUINT16(demo_p,oldghost.sprite);
+		if (ghostext.flags & EZT_HEIGHT)
+		{
+			height >>= FRACBITS;
+			WRITEINT16(demo_p, height);
+		}
+		ghostext.flags = 0;
+	}
+
+	if (ghost->player && ghost->player->followmobj && !(ghost->player->followmobj->sprite == SPR_NULL || (ghost->player->followmobj->flags2 & MF2_DONTDRAW))) // bloats tails runs but what can ya do
+	{
+		INT16 temp;
+		UINT8 *followtic_p = demo_p++;
+		UINT8 followtic = 0;
+
+		ziptic |= GZT_FOLLOW;
+
+		if (ghost->player->followmobj->skin)
+			followtic |= FZT_SKIN;
+
+		if (!(oldghost.flags2 & MF2_AMBUSH))
+		{
+			followtic |= FZT_SPAWNED;
+			WRITEINT16(demo_p,ghost->player->followmobj->info->height>>FRACBITS);
+			if (ghost->player->followmobj->flags2 & MF2_LINKDRAW)
+				followtic |= FZT_LINKDRAW;
+			if (ghost->player->followmobj->colorized)
+				followtic |= FZT_COLORIZED;
+			if (followtic & FZT_SKIN)
+				WRITEUINT8(demo_p,(UINT8)(((skin_t *)(ghost->player->followmobj->skin))-skins));
+			oldghost.flags2 |= MF2_AMBUSH;
+		}
+
+		if (ghost->player->followmobj->scale != ghost->scale)
+		{
+			followtic |= FZT_SCALE;
+			WRITEFIXED(demo_p,ghost->player->followmobj->scale);
+		}
+
+		temp = (INT16)((ghost->player->followmobj->x-ghost->x)>>8);
+		WRITEINT16(demo_p,temp);
+		temp = (INT16)((ghost->player->followmobj->y-ghost->y)>>8);
+		WRITEINT16(demo_p,temp);
+		temp = (INT16)((ghost->player->followmobj->z-ghost->z)>>8);
+		WRITEINT16(demo_p,temp);
+		if (followtic & FZT_SKIN)
+			WRITEUINT8(demo_p,ghost->player->followmobj->sprite2);
+		WRITEUINT16(demo_p,ghost->player->followmobj->sprite);
+		WRITEUINT8(demo_p,(ghost->player->followmobj->frame & FF_FRAMEMASK));
+		WRITEUINT8(demo_p,ghost->player->followmobj->color);
+
+		*followtic_p = followtic;
+	}
+	else
+		oldghost.flags2 &= ~MF2_AMBUSH;
+
+	*ziptic_p = ziptic;
+
+	// attention here for the ticcmd size!
+	// latest demos with mouse aiming byte in ticcmd
+	if (demo_p >= demoend - (13 + 9 + 9))
+	{
+		G_CheckDemoStatus(); // no more space
+		return;
+	}
+}
+
+// Uses ghost data to do consistency checks on your position.
+// This fixes desynchronising demos when fighting eggman.
+void G_ConsGhostTic(void)
+{
+	UINT8 ziptic;
+	UINT16 px,py,pz,gx,gy,gz;
+	mobj_t *testmo;
+
+	if (!demo_p || !demo_start)
+		return;
+	if (!(demoflags & DF_GHOST))
+		return; // No ghost data to use.
+
+	testmo = players[0].mo;
+
+	// Grab ghost data.
+	ziptic = READUINT8(demo_p);
+	if (ziptic & GZT_XYZ)
+	{
+		oldghost.x = READFIXED(demo_p);
+		oldghost.y = READFIXED(demo_p);
+		oldghost.z = READFIXED(demo_p);
+	}
+	else
+	{
+		if (ziptic & GZT_MOMXY)
+		{
+			oldghost.momx = READINT16(demo_p)<<8;
+			oldghost.momy = READINT16(demo_p)<<8;
+		}
+		if (ziptic & GZT_MOMZ)
+			oldghost.momz = READINT16(demo_p)<<8;
+		oldghost.x += oldghost.momx;
+		oldghost.y += oldghost.momy;
+		oldghost.z += oldghost.momz;
+	}
+	if (ziptic & GZT_ANGLE)
+		demo_p++;
+	if (ziptic & GZT_FRAME)
+		demo_p++;
+	if (ziptic & GZT_SPR2)
+		demo_p++;
+
+	if (ziptic & GZT_EXTRA)
+	{ // But wait, there's more!
+		UINT8 xziptic = READUINT8(demo_p);
+		if (xziptic & EZT_COLOR)
+			demo_p++;
+		if (xziptic & EZT_SCALE)
+			demo_p += sizeof(fixed_t);
+		if (xziptic & EZT_HIT)
+		{ // Resync mob damage.
+			UINT16 i, count = READUINT16(demo_p);
+			thinker_t *th;
+			mobj_t *mobj;
+
+			UINT32 type;
+			UINT16 health;
+			fixed_t x;
+			fixed_t y;
+			fixed_t z;
+
+			for (i = 0; i < count; i++)
+			{
+				//demo_p += 4; // reserved.
+				type = READUINT32(demo_p);
+				health = READUINT16(demo_p);
+				x = READFIXED(demo_p);
+				y = READFIXED(demo_p);
+				z = READFIXED(demo_p);
+				demo_p += sizeof(angle_t); // angle, unnecessary for cons.
+
+				mobj = NULL;
+				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+				{
+					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+						continue;
+					mobj = (mobj_t *)th;
+					if (mobj->type == (mobjtype_t)type && mobj->x == x && mobj->y == y && mobj->z == z)
+						break;
+				}
+				if (th != &thlist[THINK_MOBJ] && mobj->health != health) // Wasn't damaged?! This is desync! Fix it!
+				{
+					if (demosynced)
+						CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
+					demosynced = false;
+					P_DamageMobj(mobj, players[0].mo, players[0].mo, 1, 0);
+				}
+			}
+		}
+		if (xziptic & EZT_SPRITE)
+			demo_p += sizeof(UINT16);
+		if (xziptic & EZT_HEIGHT)
+			demo_p += sizeof(INT16);
+	}
+
+	if (ziptic & GZT_FOLLOW)
+	{ // Even more...
+		UINT8 followtic = READUINT8(demo_p);
+		if (followtic & FZT_SPAWNED)
+		{
+			demo_p += sizeof(INT16);
+			if (followtic & FZT_SKIN)
+				demo_p++;
+		}
+		if (followtic & FZT_SCALE)
+			demo_p += sizeof(fixed_t);
+		demo_p += sizeof(INT16);
+		demo_p += sizeof(INT16);
+		demo_p += sizeof(INT16);
+		if (followtic & FZT_SKIN)
+			demo_p++;
+		demo_p += sizeof(UINT16);
+		demo_p++;
+		demo_p++;
+	}
+
+	// Re-synchronise
+	px = testmo->x>>FRACBITS;
+	py = testmo->y>>FRACBITS;
+	pz = testmo->z>>FRACBITS;
+	gx = oldghost.x>>FRACBITS;
+	gy = oldghost.y>>FRACBITS;
+	gz = oldghost.z>>FRACBITS;
+
+	if (px != gx || py != gy || pz != gz)
+	{
+		if (demosynced)
+			CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
+		demosynced = false;
+
+		P_UnsetThingPosition(testmo);
+		testmo->x = oldghost.x;
+		testmo->y = oldghost.y;
+		P_SetThingPosition(testmo);
+		testmo->z = oldghost.z;
+	}
+
+	if (*demo_p == DEMOMARKER)
+	{
+		// end of demo data stream
+		G_CheckDemoStatus();
+		return;
+	}
+}
+
+void G_GhostTicker(void)
+{
+	demoghost *g,*p;
+	for(g = ghosts, p = NULL; g; g = g->next)
+	{
+		// Skip normal demo data.
+		UINT8 ziptic = READUINT8(g->p);
+		UINT8 xziptic = 0;
+		if (ziptic & ZT_FWD)
+			g->p++;
+		if (ziptic & ZT_SIDE)
+			g->p++;
+		if (ziptic & ZT_ANGLE)
+			g->p += 2;
+		if (ziptic & ZT_BUTTONS)
+			g->p += 2;
+		if (ziptic & ZT_AIMING)
+			g->p += 2;
+
+		// Grab ghost data.
+		ziptic = READUINT8(g->p);
+		if (ziptic & GZT_XYZ)
+		{
+			g->oldmo.x = READFIXED(g->p);
+			g->oldmo.y = READFIXED(g->p);
+			g->oldmo.z = READFIXED(g->p);
+		}
+		else
+		{
+			if (ziptic & GZT_MOMXY)
+			{
+				g->oldmo.momx = READINT16(g->p)<<8;
+				g->oldmo.momy = READINT16(g->p)<<8;
+			}
+			if (ziptic & GZT_MOMZ)
+				g->oldmo.momz = READINT16(g->p)<<8;
+			g->oldmo.x += g->oldmo.momx;
+			g->oldmo.y += g->oldmo.momy;
+			g->oldmo.z += g->oldmo.momz;
+		}
+		if (ziptic & GZT_ANGLE)
+			g->mo->angle = READUINT8(g->p)<<24;
+		if (ziptic & GZT_FRAME)
+			g->oldmo.frame = READUINT8(g->p);
+		if (ziptic & GZT_SPR2)
+			g->oldmo.sprite2 = READUINT8(g->p);
+
+		// Update ghost
+		P_UnsetThingPosition(g->mo);
+		g->mo->x = g->oldmo.x;
+		g->mo->y = g->oldmo.y;
+		g->mo->z = g->oldmo.z;
+		P_SetThingPosition(g->mo);
+		g->mo->frame = g->oldmo.frame | tr_trans30<<FF_TRANSSHIFT;
+		if (g->fadein)
+		{
+			g->mo->frame += (((--g->fadein)/6)<<FF_TRANSSHIFT); // this calc never exceeds 9 unless g->fadein is bad, and it's only set once, so...
+			g->mo->flags2 &= ~MF2_DONTDRAW;
+		}
+		g->mo->sprite2 = g->oldmo.sprite2;
+
+		if (ziptic & GZT_EXTRA)
+		{ // But wait, there's more!
+			xziptic = READUINT8(g->p);
+			if (xziptic & EZT_COLOR)
+			{
+				g->color = READUINT8(g->p);
+				switch(g->color)
+				{
+				default:
+				case GHC_RETURNSKIN:
+					g->mo->skin = g->oldmo.skin;
+					/* FALLTHRU */
+				case GHC_NORMAL: // Go back to skin color
+					g->mo->color = g->oldmo.color;
+					break;
+				// Handled below
+				case GHC_SUPER:
+				case GHC_INVINCIBLE:
+					break;
+				case GHC_FIREFLOWER: // Fireflower
+					g->mo->color = SKINCOLOR_WHITE;
+					break;
+				case GHC_NIGHTSSKIN: // not actually a colour
+					g->mo->skin = &skins[DEFAULTNIGHTSSKIN];
+					break;
+				}
+			}
+			if (xziptic & EZT_FLIP)
+				g->mo->eflags ^= MFE_VERTICALFLIP;
+			if (xziptic & EZT_SCALE)
+			{
+				g->mo->destscale = READFIXED(g->p);
+				if (g->mo->destscale != g->mo->scale)
+					P_SetScale(g->mo, g->mo->destscale);
+			}
+			if (xziptic & EZT_THOKMASK)
+			{ // Let's only spawn ONE of these per frame, thanks.
+				mobj_t *mobj;
+				INT32 type = -1;
+				if (g->mo->skin)
+				{
+					skin_t *skin = (skin_t *)g->mo->skin;
+					switch (xziptic & EZT_THOKMASK)
+					{
+					case EZT_THOK:
+						type = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
+						break;
+					case EZT_SPIN:
+						type = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
+						break;
+					case EZT_REV:
+						type = skin->revitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
+						break;
+					}
+				}
+				if (type != MT_NULL)
+				{
+					if (type == MT_GHOST)
+					{
+						mobj = P_SpawnGhostMobj(g->mo); // does a large portion of the work for us
+						mobj->frame = (mobj->frame & ~FF_FRAMEMASK)|tr_trans60<<FF_TRANSSHIFT; // P_SpawnGhostMobj sets trans50, we want trans60
+					}
+					else
+					{
+						mobj = P_SpawnMobjFromMobj(g->mo, 0, 0, -FixedDiv(FixedMul(g->mo->info->height, g->mo->scale) - g->mo->height,3*FRACUNIT), MT_THOK);
+						mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
+						mobj->frame = (states[mobjinfo[type].spawnstate].frame & FF_FRAMEMASK) | tr_trans60<<FF_TRANSSHIFT;
+						mobj->color = g->mo->color;
+						mobj->skin = g->mo->skin;
+						P_SetScale(mobj, (mobj->destscale = g->mo->scale));
+
+						if (type == MT_THOK) // spintrail-specific modification for MT_THOK
+						{
+							mobj->frame = FF_TRANS80;
+							mobj->fuse = mobj->tics;
+						}
+						mobj->tics = -1; // nope.
+					}
+					mobj->floorz = mobj->z;
+					mobj->ceilingz = mobj->z+mobj->height;
+					P_UnsetThingPosition(mobj);
+					mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
+					P_SetThingPosition(mobj);
+					if (!mobj->fuse)
+						mobj->fuse = 8;
+					P_SetTarget(&mobj->target, g->mo);
+				}
+			}
+			if (xziptic & EZT_HIT)
+			{ // Spawn hit poofs for killing things!
+				UINT16 i, count = READUINT16(g->p), health;
+				UINT32 type;
+				fixed_t x,y,z;
+				angle_t angle;
+				mobj_t *poof;
+				for (i = 0; i < count; i++)
+				{
+					//g->p += 4; // reserved
+					type = READUINT32(g->p);
+					health = READUINT16(g->p);
+					x = READFIXED(g->p);
+					y = READFIXED(g->p);
+					z = READFIXED(g->p);
+					angle = READANGLE(g->p);
+					if (!(mobjinfo[type].flags & MF_SHOOTABLE)
+					|| !(mobjinfo[type].flags & (MF_ENEMY|MF_MONITOR))
+					|| health != 0 || i >= 4) // only spawn for the first 4 hits per frame, to prevent ghosts from splode-spamming too bad.
+						continue;
+					poof = P_SpawnMobj(x, y, z, MT_GHOST);
+					poof->angle = angle;
+					poof->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
+					poof->health = 0;
+					P_SetMobjStateNF(poof, S_XPLD1);
+				}
+			}
+			if (xziptic & EZT_SPRITE)
+				g->mo->sprite = READUINT16(g->p);
+			if (xziptic & EZT_HEIGHT)
+			{
+				fixed_t temp = READINT16(g->p)<<FRACBITS;
+				g->mo->height = FixedMul(temp, g->mo->scale);
+			}
+		}
+
+		// Tick ghost colors (Super and Mario Invincibility flashing)
+		switch(g->color)
+		{
+		case GHC_SUPER: // Super (P_DoSuperStuff)
+			if (g->mo->skin)
+			{
+				skin_t *skin = (skin_t *)g->mo->skin;
+				g->mo->color = skin->supercolor;
+			}
+			else
+				g->mo->color = SKINCOLOR_SUPERGOLD1;
+			g->mo->color += abs( ( (signed)( (unsigned)leveltime >> 1 ) % 9) - 4);
+			break;
+		case GHC_INVINCIBLE: // Mario invincibility (P_CheckInvincibilityTimer)
+			g->mo->color = (UINT8)(SKINCOLOR_RUBY + (leveltime % (MAXSKINCOLORS - SKINCOLOR_RUBY))); // Passes through all saturated colours
+			break;
+		default:
+			break;
+		}
+
+#define follow g->mo->tracer
+		if (ziptic & GZT_FOLLOW)
+		{ // Even more...
+			UINT8 followtic = READUINT8(g->p);
+			fixed_t temp;
+			if (followtic & FZT_SPAWNED)
+			{
+				if (follow)
+					P_RemoveMobj(follow);
+				P_SetTarget(&follow, P_SpawnMobjFromMobj(g->mo, 0, 0, 0, MT_GHOST));
+				P_SetTarget(&follow->tracer, g->mo);
+				follow->tics = -1;
+				temp = READINT16(g->p)<<FRACBITS;
+				follow->height = FixedMul(follow->scale, temp);
+
+				if (followtic & FZT_LINKDRAW)
+					follow->flags2 |= MF2_LINKDRAW;
+
+				if (followtic & FZT_COLORIZED)
+					follow->colorized = true;
+
+				if (followtic & FZT_SKIN)
+					follow->skin = &skins[READUINT8(g->p)];
+			}
+			if (follow)
+			{
+				if (followtic & FZT_SCALE)
+					follow->destscale = READFIXED(g->p);
+				else
+					follow->destscale = g->mo->destscale;
+				if (follow->destscale != follow->scale)
+					P_SetScale(follow, follow->destscale);
+
+				P_UnsetThingPosition(follow);
+				temp = READINT16(g->p)<<8;
+				follow->x = g->mo->x + temp;
+				temp = READINT16(g->p)<<8;
+				follow->y = g->mo->y + temp;
+				temp = READINT16(g->p)<<8;
+				follow->z = g->mo->z + temp;
+				P_SetThingPosition(follow);
+				if (followtic & FZT_SKIN)
+					follow->sprite2 = READUINT8(g->p);
+				else
+					follow->sprite2 = 0;
+				follow->sprite = READUINT16(g->p);
+				follow->frame = (READUINT8(g->p)) | (g->mo->frame & FF_TRANSMASK);
+				follow->angle = g->mo->angle;
+				follow->color = READUINT8(g->p);
+
+				if (!(followtic & FZT_SPAWNED))
+				{
+					if (xziptic & EZT_FLIP)
+					{
+						follow->flags2 ^= MF2_OBJECTFLIP;
+						follow->eflags ^= MFE_VERTICALFLIP;
+					}
+				}
+			}
+		}
+		else if (follow)
+		{
+			P_RemoveMobj(follow);
+			P_SetTarget(&follow, NULL);
+		}
+		// Demo ends after ghost data.
+		if (*g->p == DEMOMARKER)
+		{
+			g->mo->momx = g->mo->momy = g->mo->momz = 0;
+#if 1 // freeze frame (maybe more useful for time attackers)
+			g->mo->colorized = true;
+			if (follow)
+				follow->colorized = true;
+#else // dissapearing act
+			g->mo->fuse = TICRATE;
+			if (follow)
+				follow->fuse = TICRATE;
+#endif
+			if (p)
+				p->next = g->next;
+			else
+				ghosts = g->next;
+			Z_Free(g);
+			continue;
+		}
+		p = g;
+#undef follow
+	}
+}
+
+void G_ReadMetalTic(mobj_t *metal)
+{
+	UINT8 ziptic;
+	UINT8 xziptic = 0;
+
+	if (!metal_p)
+		return;
+
+	if (!metal->health)
+	{
+		G_StopMetalDemo();
+		return;
+	}
+
+	switch (*metal_p)
+	{
+		case METALSNICE:
+			break;
+		case METALDEATH:
+			if (metal->tracer)
+				P_RemoveMobj(metal->tracer);
+			P_KillMobj(metal, NULL, NULL, 0);
+			/* FALLTHRU */
+		case DEMOMARKER:
+		default:
+			// end of demo data stream
+			G_StopMetalDemo();
+			return;
+	}
+	metal_p++;
+
+	ziptic = READUINT8(metal_p);
+
+	// Read changes from the tic
+	if (ziptic & GZT_XYZ)
+	{
+		P_TeleportMove(metal, READFIXED(metal_p), READFIXED(metal_p), READFIXED(metal_p));
+		oldmetal.x = metal->x;
+		oldmetal.y = metal->y;
+		oldmetal.z = metal->z;
+	}
+	else
+	{
+		if (ziptic & GZT_MOMXY)
+		{
+			oldmetal.momx = READINT16(metal_p)<<8;
+			oldmetal.momy = READINT16(metal_p)<<8;
+		}
+		if (ziptic & GZT_MOMZ)
+			oldmetal.momz = READINT16(metal_p)<<8;
+		oldmetal.x += oldmetal.momx;
+		oldmetal.y += oldmetal.momy;
+		oldmetal.z += oldmetal.momz;
+	}
+	if (ziptic & GZT_ANGLE)
+		metal->angle = READUINT8(metal_p)<<24;
+	if (ziptic & GZT_FRAME)
+		oldmetal.frame = READUINT32(metal_p);
+	if (ziptic & GZT_SPR2)
+		oldmetal.sprite2 = READUINT8(metal_p);
+
+	// Set movement, position, and angle
+	// oldmetal contains where you're supposed to be.
+	metal->momx = oldmetal.momx;
+	metal->momy = oldmetal.momy;
+	metal->momz = oldmetal.momz;
+	P_UnsetThingPosition(metal);
+	metal->x = oldmetal.x;
+	metal->y = oldmetal.y;
+	metal->z = oldmetal.z;
+	P_SetThingPosition(metal);
+	metal->frame = oldmetal.frame;
+	metal->sprite2 = oldmetal.sprite2;
+
+	if (ziptic & GZT_EXTRA)
+	{ // But wait, there's more!
+		xziptic = READUINT8(metal_p);
+		if (xziptic & EZT_FLIP)
+		{
+			metal->eflags ^= MFE_VERTICALFLIP;
+			metal->flags2 ^= MF2_OBJECTFLIP;
+		}
+		if (xziptic & EZT_SCALE)
+		{
+			metal->destscale = READFIXED(metal_p);
+			if (metal->destscale != metal->scale)
+				P_SetScale(metal, metal->destscale);
+		}
+		if (xziptic & EZT_THOKMASK)
+		{ // Let's only spawn ONE of these per frame, thanks.
+			mobj_t *mobj;
+			INT32 type = -1;
+			if (metal->skin)
+			{
+				skin_t *skin = (skin_t *)metal->skin;
+				switch (xziptic & EZT_THOKMASK)
+				{
+				case EZT_THOK:
+					type = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
+					break;
+				case EZT_SPIN:
+					type = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
+					break;
+				case EZT_REV:
+					type = skin->revitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
+					break;
+				}
+			}
+			if (type != MT_NULL)
+			{
+				if (type == MT_GHOST)
+				{
+					mobj = P_SpawnGhostMobj(metal); // does a large portion of the work for us
+				}
+				else
+				{
+					mobj = P_SpawnMobjFromMobj(metal, 0, 0, -FixedDiv(FixedMul(metal->info->height, metal->scale) - metal->height,3*FRACUNIT), MT_THOK);
+					mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
+					mobj->frame = states[mobjinfo[type].spawnstate].frame;
+					mobj->angle = metal->angle;
+					mobj->color = metal->color;
+					mobj->skin = metal->skin;
+					P_SetScale(mobj, (mobj->destscale = metal->scale));
+
+					if (type == MT_THOK) // spintrail-specific modification for MT_THOK
+					{
+						mobj->frame = FF_TRANS70;
+						mobj->fuse = mobj->tics;
+					}
+					mobj->tics = -1; // nope.
+				}
+				mobj->floorz = mobj->z;
+				mobj->ceilingz = mobj->z+mobj->height;
+				P_UnsetThingPosition(mobj);
+				mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
+				P_SetThingPosition(mobj);
+				if (!mobj->fuse)
+					mobj->fuse = 8;
+				P_SetTarget(&mobj->target, metal);
+			}
+		}
+		if (xziptic & EZT_SPRITE)
+			metal->sprite = READUINT16(metal_p);
+		if (xziptic & EZT_HEIGHT)
+		{
+			fixed_t temp = READINT16(metal_p)<<FRACBITS;
+			metal->height = FixedMul(temp, metal->scale);
+		}
+	}
+
+#define follow metal->tracer
+		if (ziptic & GZT_FOLLOW)
+		{ // Even more...
+			UINT8 followtic = READUINT8(metal_p);
+			fixed_t temp;
+			if (followtic & FZT_SPAWNED)
+			{
+				if (follow)
+					P_RemoveMobj(follow);
+				P_SetTarget(&follow, P_SpawnMobjFromMobj(metal, 0, 0, 0, MT_GHOST));
+				P_SetTarget(&follow->tracer, metal);
+				follow->tics = -1;
+				temp = READINT16(metal_p)<<FRACBITS;
+				follow->height = FixedMul(follow->scale, temp);
+
+				if (followtic & FZT_LINKDRAW)
+					follow->flags2 |= MF2_LINKDRAW;
+
+				if (followtic & FZT_COLORIZED)
+					follow->colorized = true;
+
+				if (followtic & FZT_SKIN)
+					follow->skin = &skins[READUINT8(metal_p)];
+			}
+			if (follow)
+			{
+				if (followtic & FZT_SCALE)
+					follow->destscale = READFIXED(metal_p);
+				else
+					follow->destscale = metal->destscale;
+				if (follow->destscale != follow->scale)
+					P_SetScale(follow, follow->destscale);
+
+				P_UnsetThingPosition(follow);
+				temp = READINT16(metal_p)<<8;
+				follow->x = metal->x + temp;
+				temp = READINT16(metal_p)<<8;
+				follow->y = metal->y + temp;
+				temp = READINT16(metal_p)<<8;
+				follow->z = metal->z + temp;
+				P_SetThingPosition(follow);
+				if (followtic & FZT_SKIN)
+					follow->sprite2 = READUINT8(metal_p);
+				else
+					follow->sprite2 = 0;
+				follow->sprite = READUINT16(metal_p);
+				follow->frame = READUINT32(metal_p); // NOT & FF_FRAMEMASK here, so 32 bits
+				follow->angle = metal->angle;
+				follow->color = READUINT8(metal_p);
+
+				if (!(followtic & FZT_SPAWNED))
+				{
+					if (xziptic & EZT_FLIP)
+					{
+						follow->flags2 ^= MF2_OBJECTFLIP;
+						follow->eflags ^= MFE_VERTICALFLIP;
+					}
+				}
+			}
+		}
+		else if (follow)
+		{
+			P_RemoveMobj(follow);
+			P_SetTarget(&follow, NULL);
+		}
+#undef follow
+}
+
+void G_WriteMetalTic(mobj_t *metal)
+{
+	UINT8 ziptic = 0;
+	UINT8 *ziptic_p;
+	fixed_t height;
+
+	if (!demo_p) // demo_p will be NULL until the race start linedef executor is activated!
+		return;
+
+	WRITEUINT8(demo_p, METALSNICE);
+	ziptic_p = demo_p++; // the ziptic, written at the end of this function
+
+	#define MAXMOM (0xFFFF<<8)
+
+	// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
+	if (abs(metal->x-oldmetal.x) > MAXMOM
+	|| abs(metal->y-oldmetal.y) > MAXMOM
+	|| abs(metal->z-oldmetal.z) > MAXMOM)
+	{
+		oldmetal.x = metal->x;
+		oldmetal.y = metal->y;
+		oldmetal.z = metal->z;
+		ziptic |= GZT_XYZ;
+		WRITEFIXED(demo_p,oldmetal.x);
+		WRITEFIXED(demo_p,oldmetal.y);
+		WRITEFIXED(demo_p,oldmetal.z);
+	}
+	else
+	{
+		// For moving normally:
+		// Store one full byte of movement, plus one byte of fractional movement.
+		INT16 momx = (INT16)((metal->x-oldmetal.x)>>8);
+		INT16 momy = (INT16)((metal->y-oldmetal.y)>>8);
+		if (momx != oldmetal.momx
+		|| momy != oldmetal.momy)
+		{
+			oldmetal.momx = momx;
+			oldmetal.momy = momy;
+			ziptic |= GZT_MOMXY;
+			WRITEINT16(demo_p,momx);
+			WRITEINT16(demo_p,momy);
+		}
+		momx = (INT16)((metal->z-oldmetal.z)>>8);
+		if (momx != oldmetal.momz)
+		{
+			oldmetal.momz = momx;
+			ziptic |= GZT_MOMZ;
+			WRITEINT16(demo_p,momx);
+		}
+
+		// This SHOULD set oldmetal.x/y/z to match metal->x/y/z
+		// but it keeps the fractional loss of one byte,
+		// so it will hopefully be made up for in future tics.
+		oldmetal.x += oldmetal.momx<<8;
+		oldmetal.y += oldmetal.momy<<8;
+		oldmetal.z += oldmetal.momz<<8;
+	}
+
+	#undef MAXMOM
+
+	// Only store the 8 most relevant bits of angle
+	// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
+	// and it does not affect movement at all anyway.
+	if (metal->player && metal->player->drawangle>>24 != oldmetal.angle)
+	{
+		oldmetal.angle = metal->player->drawangle>>24;
+		ziptic |= GZT_ANGLE;
+		WRITEUINT8(demo_p,oldmetal.angle);
+	}
+
+	// Store the sprite frame.
+	if ((metal->frame & FF_FRAMEMASK) != oldmetal.frame)
+	{
+		oldmetal.frame = metal->frame; // NOT & FF_FRAMEMASK here, so 32 bits
+		ziptic |= GZT_FRAME;
+		WRITEUINT32(demo_p,oldmetal.frame);
+	}
+
+	if (metal->sprite == SPR_PLAY
+	&& metal->sprite2 != oldmetal.sprite2)
+	{
+		oldmetal.sprite2 = metal->sprite2;
+		ziptic |= GZT_SPR2;
+		WRITEUINT8(demo_p,oldmetal.sprite2);
+	}
+
+	// Check for sprite set changes
+	if (metal->sprite != oldmetal.sprite)
+	{
+		oldmetal.sprite = metal->sprite;
+		ghostext.flags |= EZT_SPRITE;
+	}
+
+	if ((height = FixedDiv(metal->height, metal->scale)) != oldmetal.height)
+	{
+		oldmetal.height = height;
+		ghostext.flags |= EZT_HEIGHT;
+	}
+
+	if (ghostext.flags & ~(EZT_COLOR|EZT_HIT)) // these two aren't handled by metal ever
+	{
+		ziptic |= GZT_EXTRA;
+
+		if (ghostext.scale == ghostext.lastscale)
+			ghostext.flags &= ~EZT_SCALE;
+
+		WRITEUINT8(demo_p,ghostext.flags);
+		if (ghostext.flags & EZT_SCALE)
+		{
+			WRITEFIXED(demo_p,ghostext.scale);
+			ghostext.lastscale = ghostext.scale;
+		}
+		if (ghostext.flags & EZT_SPRITE)
+			WRITEUINT16(demo_p,oldmetal.sprite);
+		if (ghostext.flags & EZT_HEIGHT)
+		{
+			height >>= FRACBITS;
+			WRITEINT16(demo_p, height);
+		}
+		ghostext.flags = 0;
+	}
+
+	if (metal->player && metal->player->followmobj && !(metal->player->followmobj->sprite == SPR_NULL || (metal->player->followmobj->flags2 & MF2_DONTDRAW)))
+	{
+		INT16 temp;
+		UINT8 *followtic_p = demo_p++;
+		UINT8 followtic = 0;
+
+		ziptic |= GZT_FOLLOW;
+
+		if (metal->player->followmobj->skin)
+			followtic |= FZT_SKIN;
+
+		if (!(oldmetal.flags2 & MF2_AMBUSH))
+		{
+			followtic |= FZT_SPAWNED;
+			WRITEINT16(demo_p,metal->player->followmobj->info->height>>FRACBITS);
+			if (metal->player->followmobj->flags2 & MF2_LINKDRAW)
+				followtic |= FZT_LINKDRAW;
+			if (metal->player->followmobj->colorized)
+				followtic |= FZT_COLORIZED;
+			if (followtic & FZT_SKIN)
+				WRITEUINT8(demo_p,(UINT8)(((skin_t *)(metal->player->followmobj->skin))-skins));
+			oldmetal.flags2 |= MF2_AMBUSH;
+		}
+
+		if (metal->player->followmobj->scale != metal->scale)
+		{
+			followtic |= FZT_SCALE;
+			WRITEFIXED(demo_p,metal->player->followmobj->scale);
+		}
+
+		temp = (INT16)((metal->player->followmobj->x-metal->x)>>8);
+		WRITEINT16(demo_p,temp);
+		temp = (INT16)((metal->player->followmobj->y-metal->y)>>8);
+		WRITEINT16(demo_p,temp);
+		temp = (INT16)((metal->player->followmobj->z-metal->z)>>8);
+		WRITEINT16(demo_p,temp);
+		if (followtic & FZT_SKIN)
+			WRITEUINT8(demo_p,metal->player->followmobj->sprite2);
+		WRITEUINT16(demo_p,metal->player->followmobj->sprite);
+		WRITEUINT32(demo_p,metal->player->followmobj->frame); // NOT & FF_FRAMEMASK here, so 32 bits
+		WRITEUINT8(demo_p,metal->player->followmobj->color);
+
+		*followtic_p = followtic;
+	}
+	else
+		oldmetal.flags2 &= ~MF2_AMBUSH;
+
+	*ziptic_p = ziptic;
+
+	// attention here for the ticcmd size!
+	// latest demos with mouse aiming byte in ticcmd
+	if (demo_p >= demoend - 32)
+	{
+		G_StopMetalRecording(false); // no more space
+		return;
+	}
+}
+
+//
+// G_RecordDemo
+//
+void G_RecordDemo(const char *name)
+{
+	INT32 maxsize;
+
+	strcpy(demoname, name);
+	strcat(demoname, ".lmp");
+	maxsize = 1024*1024;
+	if (M_CheckParm("-maxdemo") && M_IsNextParm())
+		maxsize = atoi(M_GetNextParm()) * 1024;
+//	if (demobuffer)
+//		free(demobuffer);
+	demo_p = NULL;
+	demobuffer = malloc(maxsize);
+	demoend = demobuffer + maxsize;
+
+	demorecording = true;
+}
+
+void G_RecordMetal(void)
+{
+	INT32 maxsize;
+	maxsize = 1024*1024;
+	if (M_CheckParm("-maxdemo") && M_IsNextParm())
+		maxsize = atoi(M_GetNextParm()) * 1024;
+	demo_p = NULL;
+	demobuffer = malloc(maxsize);
+	demoend = demobuffer + maxsize;
+	metalrecording = true;
+}
+
+void G_BeginRecording(void)
+{
+	UINT8 i;
+	char name[16];
+	player_t *player = &players[consoleplayer];
+
+	if (demo_p)
+		return;
+	memset(name,0,sizeof(name));
+
+	demo_p = demobuffer;
+	demoflags = DF_GHOST|(modeattacking<<DF_ATTACKSHIFT);
+
+	// Setup header.
+	M_Memcpy(demo_p, DEMOHEADER, 12); demo_p += 12;
+	WRITEUINT8(demo_p,VERSION);
+	WRITEUINT8(demo_p,SUBVERSION);
+	WRITEUINT16(demo_p,DEMOVERSION);
+
+	// demo checksum
+	demo_p += 16;
+
+	// game data
+	M_Memcpy(demo_p, "PLAY", 4); demo_p += 4;
+	WRITEINT16(demo_p,gamemap);
+	M_Memcpy(demo_p, mapmd5, 16); demo_p += 16;
+
+	WRITEUINT8(demo_p,demoflags);
+	switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
+	{
+	case ATTACKING_NONE: // 0
+		break;
+	case ATTACKING_RECORD: // 1
+		demotime_p = demo_p;
+		WRITEUINT32(demo_p,UINT32_MAX); // time
+		WRITEUINT32(demo_p,0); // score
+		WRITEUINT16(demo_p,0); // rings
+		break;
+	case ATTACKING_NIGHTS: // 2
+		demotime_p = demo_p;
+		WRITEUINT32(demo_p,UINT32_MAX); // time
+		WRITEUINT32(demo_p,0); // score
+		break;
+	default: // 3
+		break;
+	}
+
+	WRITEUINT32(demo_p,P_GetInitSeed());
+
+	// Name
+	for (i = 0; i < 16 && cv_playername.string[i]; i++)
+		name[i] = cv_playername.string[i];
+	for (; i < 16; i++)
+		name[i] = '\0';
+	M_Memcpy(demo_p,name,16);
+	demo_p += 16;
+
+	// Skin
+	for (i = 0; i < 16 && cv_skin.string[i]; i++)
+		name[i] = cv_skin.string[i];
+	for (; i < 16; i++)
+		name[i] = '\0';
+	M_Memcpy(demo_p,name,16);
+	demo_p += 16;
+
+	// Color
+	for (i = 0; i < 16 && cv_playercolor.string[i]; i++)
+		name[i] = cv_playercolor.string[i];
+	for (; i < 16; i++)
+		name[i] = '\0';
+	M_Memcpy(demo_p,name,16);
+	demo_p += 16;
+
+	// Stats
+	WRITEUINT8(demo_p,player->charability);
+	WRITEUINT8(demo_p,player->charability2);
+	WRITEUINT8(demo_p,player->actionspd>>FRACBITS);
+	WRITEUINT8(demo_p,player->mindash>>FRACBITS);
+	WRITEUINT8(demo_p,player->maxdash>>FRACBITS);
+	WRITEUINT8(demo_p,player->normalspeed>>FRACBITS);
+	WRITEUINT8(demo_p,player->runspeed>>FRACBITS);
+	WRITEUINT8(demo_p,player->thrustfactor);
+	WRITEUINT8(demo_p,player->accelstart);
+	WRITEUINT8(demo_p,player->acceleration);
+	WRITEUINT8(demo_p,player->height>>FRACBITS);
+	WRITEUINT8(demo_p,player->spinheight>>FRACBITS);
+	WRITEUINT8(demo_p,player->camerascale>>FRACBITS);
+	WRITEUINT8(demo_p,player->shieldscale>>FRACBITS);
+
+	// Trying to convert it back to % causes demo desync due to precision loss.
+	// Don't do it.
+	WRITEFIXED(demo_p, player->jumpfactor);
+
+	// And mobjtype_t is best with UINT32 too...
+	WRITEUINT32(demo_p, player->followitem);
+
+	// Save pflag data - see SendWeaponPref()
+	{
+		UINT8 buf = 0;
+		pflags_t pflags = 0;
+		if (cv_flipcam.value)
+		{
+			buf |= 0x01;
+			pflags |= PF_FLIPCAM;
+		}
+		if (cv_analog[0].value)
+		{
+			buf |= 0x02;
+			pflags |= PF_ANALOGMODE;
+		}
+		if (cv_directionchar[0].value)
+		{
+			buf |= 0x04;
+			pflags |= PF_DIRECTIONCHAR;
+		}
+		if (cv_autobrake.value)
+		{
+			buf |= 0x08;
+			pflags |= PF_AUTOBRAKE;
+		}
+		if (cv_usejoystick.value)
+			buf |= 0x10;
+		CV_SetValue(&cv_showinputjoy, !!(cv_usejoystick.value));
+
+		WRITEUINT8(demo_p,buf);
+		player->pflags = pflags;
+	}
+
+	// Save netvar data
+	CV_SaveNetVars(&demo_p);
+
+	memset(&oldcmd,0,sizeof(oldcmd));
+	memset(&oldghost,0,sizeof(oldghost));
+	memset(&ghostext,0,sizeof(ghostext));
+	ghostext.lastcolor = ghostext.color = GHC_NORMAL;
+	ghostext.lastscale = ghostext.scale = FRACUNIT;
+
+	if (player->mo)
+	{
+		oldghost.x = player->mo->x;
+		oldghost.y = player->mo->y;
+		oldghost.z = player->mo->z;
+		oldghost.angle = player->mo->angle>>24;
+
+		// preticker started us gravity flipped
+		if (player->mo->eflags & MFE_VERTICALFLIP)
+			ghostext.flags |= EZT_FLIP;
+	}
+}
+
+void G_BeginMetal(void)
+{
+	mobj_t *mo = players[consoleplayer].mo;
+
+#if 0
+	if (demo_p)
+		return;
+#endif
+
+	demo_p = demobuffer;
+
+	// Write header.
+	M_Memcpy(demo_p, DEMOHEADER, 12); demo_p += 12;
+	WRITEUINT8(demo_p,VERSION);
+	WRITEUINT8(demo_p,SUBVERSION);
+	WRITEUINT16(demo_p,DEMOVERSION);
+
+	// demo checksum
+	demo_p += 16;
+
+	M_Memcpy(demo_p, "METL", 4); demo_p += 4;
+
+	memset(&ghostext,0,sizeof(ghostext));
+	ghostext.lastscale = ghostext.scale = FRACUNIT;
+
+	// Set up our memory.
+	memset(&oldmetal,0,sizeof(oldmetal));
+	oldmetal.x = mo->x;
+	oldmetal.y = mo->y;
+	oldmetal.z = mo->z;
+	oldmetal.angle = mo->angle>>24;
+}
+
+void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings)
+{
+	if (!demorecording || !demotime_p)
+		return;
+	if (demoflags & DF_RECORDATTACK)
+	{
+		WRITEUINT32(demotime_p, ptime);
+		WRITEUINT32(demotime_p, pscore);
+		WRITEUINT16(demotime_p, prings);
+		demotime_p = NULL;
+	}
+	else if (demoflags & DF_NIGHTSATTACK)
+	{
+		WRITEUINT32(demotime_p, ptime);
+		WRITEUINT32(demotime_p, pscore);
+		demotime_p = NULL;
+	}
+}
+
+// Returns bitfield:
+// 1 == new demo has lower time
+// 2 == new demo has higher score
+// 4 == new demo has higher rings
+UINT8 G_CmpDemoTime(char *oldname, char *newname)
+{
+	UINT8 *buffer,*p;
+	UINT8 flags;
+	UINT32 oldtime, newtime, oldscore, newscore;
+	UINT16 oldrings, newrings, oldversion;
+	size_t bufsize ATTRUNUSED;
+	UINT8 c;
+	UINT16 s ATTRUNUSED;
+	UINT8 aflags = 0;
+
+	// load the new file
+	FIL_DefaultExtension(newname, ".lmp");
+	bufsize = FIL_ReadFile(newname, &buffer);
+	I_Assert(bufsize != 0);
+	p = buffer;
+
+	// read demo header
+	I_Assert(!memcmp(p, DEMOHEADER, 12));
+	p += 12; // DEMOHEADER
+	c = READUINT8(p); // VERSION
+	I_Assert(c == VERSION);
+	c = READUINT8(p); // SUBVERSION
+	I_Assert(c == SUBVERSION);
+	s = READUINT16(p);
+	I_Assert(s == DEMOVERSION);
+	p += 16; // demo checksum
+	I_Assert(!memcmp(p, "PLAY", 4));
+	p += 4; // PLAY
+	p += 2; // gamemap
+	p += 16; // map md5
+	flags = READUINT8(p); // demoflags
+
+	aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK);
+	I_Assert(aflags);
+	if (flags & DF_RECORDATTACK)
+	{
+		newtime = READUINT32(p);
+		newscore = READUINT32(p);
+		newrings = READUINT16(p);
+	}
+	else if (flags & DF_NIGHTSATTACK)
+	{
+		newtime = READUINT32(p);
+		newscore = READUINT32(p);
+		newrings = 0;
+	}
+	else // appease compiler
+		return 0;
+
+	Z_Free(buffer);
+
+	// load old file
+	FIL_DefaultExtension(oldname, ".lmp");
+	if (!FIL_ReadFile(oldname, &buffer))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), oldname);
+		return UINT8_MAX;
+	}
+	p = buffer;
+
+	// read demo header
+	if (memcmp(p, DEMOHEADER, 12))
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
+		Z_Free(buffer);
+		return UINT8_MAX;
+	} p += 12; // DEMOHEADER
+	p++; // VERSION
+	p++; // SUBVERSION
+	oldversion = READUINT16(p);
+	switch(oldversion) // demoversion
+	{
+	case DEMOVERSION: // latest always supported
+		break;
+	// too old, cannot support.
+	default:
+		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
+		Z_Free(buffer);
+		return UINT8_MAX;
+	}
+	p += 16; // demo checksum
+	if (memcmp(p, "PLAY", 4))
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
+		Z_Free(buffer);
+		return UINT8_MAX;
+	} p += 4; // "PLAY"
+	if (oldversion <= 0x0008)
+		p++; // gamemap
+	else
+		p += 2; // gamemap
+	p += 16; // mapmd5
+	flags = READUINT8(p);
+	if (!(flags & aflags))
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname);
+		Z_Free(buffer);
+		return UINT8_MAX;
+	}
+	if (flags & DF_RECORDATTACK)
+	{
+		oldtime = READUINT32(p);
+		oldscore = READUINT32(p);
+		oldrings = READUINT16(p);
+	}
+	else if (flags & DF_NIGHTSATTACK)
+	{
+		oldtime = READUINT32(p);
+		oldscore = READUINT32(p);
+		oldrings = 0;
+	}
+	else // appease compiler
+		return UINT8_MAX;
+
+	Z_Free(buffer);
+
+	c = 0;
+	if (newtime < oldtime
+	|| (newtime == oldtime && (newscore > oldscore || newrings > oldrings)))
+		c |= 1; // Better time
+	if (newscore > oldscore
+	|| (newscore == oldscore && newtime < oldtime))
+		c |= 1<<1; // Better score
+	if (newrings > oldrings
+	|| (newrings == oldrings && newtime < oldtime))
+		c |= 1<<2; // Better rings
+	return c;
+}
+
+//
+// G_PlayDemo
+//
+void G_DeferedPlayDemo(const char *name)
+{
+	COM_BufAddText("playdemo \"");
+	COM_BufAddText(name);
+	COM_BufAddText("\"\n");
+}
+
+//
+// Start a demo from a .LMP file or from a wad resource
+//
+void G_DoPlayDemo(char *defdemoname)
+{
+	UINT8 i;
+	lumpnum_t l;
+	char skin[17],color[17],*n,*pdemoname;
+	UINT8 version,subversion,charability,charability2,thrustfactor,accelstart,acceleration;
+	pflags_t pflags;
+	UINT32 randseed, followitem;
+	fixed_t camerascale,shieldscale,actionspd,mindash,maxdash,normalspeed,runspeed,jumpfactor,height,spinheight;
+	char msg[1024];
+
+	skin[16] = '\0';
+	color[16] = '\0';
+
+	n = defdemoname+strlen(defdemoname);
+	while (*n != '/' && *n != '\\' && n != defdemoname)
+		n--;
+	if (n != defdemoname)
+		n++;
+	pdemoname = ZZ_Alloc(strlen(n)+1);
+	strcpy(pdemoname,n);
+
+	// Internal if no extension, external if one exists
+	if (FIL_CheckExtension(defdemoname))
+	{
+		//FIL_DefaultExtension(defdemoname, ".lmp");
+		if (!FIL_ReadFile(defdemoname, &demobuffer))
+		{
+			snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname);
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			gameaction = ga_nothing;
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			return;
+		}
+		demo_p = demobuffer;
+	}
+	// load demo resource from WAD
+	else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
+	{
+		snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
+		CONS_Alert(CONS_ERROR, "%s", msg);
+		gameaction = ga_nothing;
+		M_StartMessage(msg, NULL, MM_NOTHING);
+		return;
+	}
+	else // it's an internal demo
+		demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC);
+
+	// read demo header
+	gameaction = ga_nothing;
+	demoplayback = true;
+	if (memcmp(demo_p, DEMOHEADER, 12))
+	{
+		snprintf(msg, 1024, M_GetText("%s is not a SRB2 replay file.\n"), pdemoname);
+		CONS_Alert(CONS_ERROR, "%s", msg);
+		M_StartMessage(msg, NULL, MM_NOTHING);
+		Z_Free(pdemoname);
+		Z_Free(demobuffer);
+		demoplayback = false;
+		titledemo = false;
+		return;
+	}
+	demo_p += 12; // DEMOHEADER
+
+	version = READUINT8(demo_p);
+	subversion = READUINT8(demo_p);
+	demoversion = READUINT16(demo_p);
+	switch(demoversion)
+	{
+	case DEMOVERSION: // latest always supported
+		break;
+	// too old, cannot support.
+	default:
+		snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
+		CONS_Alert(CONS_ERROR, "%s", msg);
+		M_StartMessage(msg, NULL, MM_NOTHING);
+		Z_Free(pdemoname);
+		Z_Free(demobuffer);
+		demoplayback = false;
+		titledemo = false;
+		return;
+	}
+	demo_p += 16; // demo checksum
+	if (memcmp(demo_p, "PLAY", 4))
+	{
+		snprintf(msg, 1024, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemoname);
+		CONS_Alert(CONS_ERROR, "%s", msg);
+		M_StartMessage(msg, NULL, MM_NOTHING);
+		Z_Free(pdemoname);
+		Z_Free(demobuffer);
+		demoplayback = false;
+		titledemo = false;
+		return;
+	}
+	demo_p += 4; // "PLAY"
+	gamemap = READINT16(demo_p);
+	demo_p += 16; // mapmd5
+
+	demoflags = READUINT8(demo_p);
+	modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
+	CON_ToggleOff();
+
+	hu_demoscore = 0;
+	hu_demotime = UINT32_MAX;
+	hu_demorings = 0;
+
+	switch (modeattacking)
+	{
+	case ATTACKING_NONE: // 0
+		break;
+	case ATTACKING_RECORD: // 1
+		hu_demotime  = READUINT32(demo_p);
+		hu_demoscore = READUINT32(demo_p);
+		hu_demorings = READUINT16(demo_p);
+		break;
+	case ATTACKING_NIGHTS: // 2
+		hu_demotime  = READUINT32(demo_p);
+		hu_demoscore = READUINT32(demo_p);
+		break;
+	default: // 3
+		modeattacking = ATTACKING_NONE;
+		break;
+	}
+
+	// Random seed
+	randseed = READUINT32(demo_p);
+
+	// Player name
+	M_Memcpy(player_names[0],demo_p,16);
+	demo_p += 16;
+
+	// Skin
+	M_Memcpy(skin,demo_p,16);
+	demo_p += 16;
+
+	// Color
+	M_Memcpy(color,demo_p,16);
+	demo_p += 16;
+
+	charability = READUINT8(demo_p);
+	charability2 = READUINT8(demo_p);
+	actionspd = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	mindash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	maxdash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	normalspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	runspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	thrustfactor = READUINT8(demo_p);
+	accelstart = READUINT8(demo_p);
+	acceleration = READUINT8(demo_p);
+	height = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	spinheight = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	camerascale = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	shieldscale = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	jumpfactor = READFIXED(demo_p);
+	followitem = READUINT32(demo_p);
+
+	// pflag data
+	{
+		UINT8 buf = READUINT8(demo_p);
+		pflags = 0;
+		if (buf & 0x01)
+			pflags |= PF_FLIPCAM;
+		if (buf & 0x02)
+			pflags |= PF_ANALOGMODE;
+		if (buf & 0x04)
+			pflags |= PF_DIRECTIONCHAR;
+		if (buf & 0x08)
+			pflags |= PF_AUTOBRAKE;
+		CV_SetValue(&cv_showinputjoy, !!(buf & 0x10));
+	}
+
+	// net var data
+	CV_LoadNetVars(&demo_p);
+
+	// Sigh ... it's an empty demo.
+	if (*demo_p == DEMOMARKER)
+	{
+		snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
+		CONS_Alert(CONS_ERROR, "%s", msg);
+		M_StartMessage(msg, NULL, MM_NOTHING);
+		Z_Free(pdemoname);
+		Z_Free(demobuffer);
+		demoplayback = false;
+		titledemo = false;
+		return;
+	}
+
+	Z_Free(pdemoname);
+
+	memset(&oldcmd,0,sizeof(oldcmd));
+	memset(&oldghost,0,sizeof(oldghost));
+
+	if (VERSION != version || SUBVERSION != subversion)
+		CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n"));
+
+	// didn't start recording right away.
+	demo_start = false;
+
+	// Set skin
+	SetPlayerSkin(0, skin);
+
+	LUAh_MapChange(gamemap);
+	displayplayer = consoleplayer = 0;
+	memset(playeringame,0,sizeof(playeringame));
+	playeringame[0] = true;
+	P_SetRandSeed(randseed);
+	G_InitNew(false, G_BuildMapName(gamemap), true, true, false);
+
+	// Set color
+	for (i = 0; i < MAXSKINCOLORS; i++)
+		if (!stricmp(Color_Names[i],color))
+		{
+			players[0].skincolor = i;
+			break;
+		}
+	CV_StealthSetValue(&cv_playercolor, players[0].skincolor);
+	if (players[0].mo)
+	{
+		players[0].mo->color = players[0].skincolor;
+		oldghost.x = players[0].mo->x;
+		oldghost.y = players[0].mo->y;
+		oldghost.z = players[0].mo->z;
+	}
+
+	// Set saved attribute values
+	// No cheat checking here, because even if they ARE wrong...
+	// it would only break the replay if we clipped them.
+	players[0].camerascale = camerascale;
+	players[0].shieldscale = shieldscale;
+	players[0].charability = charability;
+	players[0].charability2 = charability2;
+	players[0].actionspd = actionspd;
+	players[0].mindash = mindash;
+	players[0].maxdash = maxdash;
+	players[0].normalspeed = normalspeed;
+	players[0].runspeed = runspeed;
+	players[0].thrustfactor = thrustfactor;
+	players[0].accelstart = accelstart;
+	players[0].acceleration = acceleration;
+	players[0].height = height;
+	players[0].spinheight = spinheight;
+	players[0].jumpfactor = jumpfactor;
+	players[0].followitem = followitem;
+	players[0].pflags = pflags;
+
+	demo_start = true;
+}
+
+void G_AddGhost(char *defdemoname)
+{
+	INT32 i;
+	lumpnum_t l;
+	char name[17],skin[17],color[17],*n,*pdemoname,md5[16];
+	demoghost *gh;
+	UINT8 flags;
+	UINT8 *buffer,*p;
+	mapthing_t *mthing;
+	UINT16 count, ghostversion;
+
+	name[16] = '\0';
+	skin[16] = '\0';
+	color[16] = '\0';
+
+	n = defdemoname+strlen(defdemoname);
+	while (*n != '/' && *n != '\\' && n != defdemoname)
+		n--;
+	if (n != defdemoname)
+		n++;
+	pdemoname = ZZ_Alloc(strlen(n)+1);
+	strcpy(pdemoname,n);
+
+	// Internal if no extension, external if one exists
+	if (FIL_CheckExtension(defdemoname))
+	{
+		//FIL_DefaultExtension(defdemoname, ".lmp");
+		if (!FIL_ReadFileTag(defdemoname, &buffer, PU_LEVEL))
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), defdemoname);
+			Z_Free(pdemoname);
+			return;
+		}
+		p = buffer;
+	}
+	// load demo resource from WAD
+	else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
+		Z_Free(pdemoname);
+		return;
+	}
+	else // it's an internal demo
+		buffer = p = W_CacheLumpNum(l, PU_LEVEL);
+
+	// read demo header
+	if (memcmp(p, DEMOHEADER, 12))
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Not a SRB2 replay.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	} p += 12; // DEMOHEADER
+	p++; // VERSION
+	p++; // SUBVERSION
+	ghostversion = READUINT16(p);
+	switch(ghostversion)
+	{
+	case DEMOVERSION: // latest always supported
+		break;
+	// too old, cannot support.
+	default:
+		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	}
+	M_Memcpy(md5, p, 16); p += 16; // demo checksum
+	for (gh = ghosts; gh; gh = gh->next)
+		if (!memcmp(md5, gh->checksum, 16)) // another ghost in the game already has this checksum?
+		{ // Don't add another one, then!
+			CONS_Debug(DBG_SETUP, "Rejecting duplicate ghost %s (MD5 was matched)\n", pdemoname);
+			Z_Free(pdemoname);
+			Z_Free(buffer);
+			return;
+		}
+	if (memcmp(p, "PLAY", 4))
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo format unacceptable.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	} p += 4; // "PLAY"
+	if (ghostversion <= 0x0008)
+		p++; // gamemap
+	else
+		p += 2; // gamemap
+	p += 16; // mapmd5 (possibly check for consistency?)
+	flags = READUINT8(p);
+	if (!(flags & DF_GHOST))
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: No ghost data in this demo.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	}
+	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
+	{
+	case ATTACKING_NONE: // 0
+		break;
+	case ATTACKING_RECORD: // 1
+		p += 10; // demo time, score, and rings
+		break;
+	case ATTACKING_NIGHTS: // 2
+		p += 8; // demo time left, score
+		break;
+	default: // 3
+		break;
+	}
+
+	p += 4; // random seed
+
+	// Player name (TODO: Display this somehow if it doesn't match cv_playername!)
+	M_Memcpy(name, p,16);
+	p += 16;
+
+	// Skin
+	M_Memcpy(skin, p,16);
+	p += 16;
+
+	// Color
+	M_Memcpy(color, p,16);
+	p += 16;
+
+	// Ghosts do not have a player structure to put this in.
+	p++; // charability
+	p++; // charability2
+	p++; // actionspd
+	p++; // mindash
+	p++; // maxdash
+	p++; // normalspeed
+	p++; // runspeed
+	p++; // thrustfactor
+	p++; // accelstart
+	p++; // acceleration
+	p++; // height
+	p++; // spinheight
+	p++; // camerascale
+	p++; // shieldscale
+	p += 4; // jumpfactor
+	p += 4; // followitem
+
+	p++; // pflag data
+
+	// net var data
+	count = READUINT16(p);
+	while (count--)
+	{
+		p += 2;
+		SKIPSTRING(p);
+		p++;
+	}
+
+	if (*p == DEMOMARKER)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	}
+
+	gh = Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL);
+	gh->next = ghosts;
+	gh->buffer = buffer;
+	M_Memcpy(gh->checksum, md5, 16);
+	gh->p = p;
+
+	ghosts = gh;
+
+	gh->version = ghostversion;
+	mthing = playerstarts[0];
+	I_Assert(mthing);
+	{ // A bit more complex than P_SpawnPlayer because ghosts aren't solid and won't just push themselves out of the ceiling.
+		fixed_t z,f,c;
+		fixed_t offset = mthing->z << FRACBITS;
+		gh->mo = P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST);
+		gh->mo->angle = FixedAngle(mthing->angle << FRACBITS);
+		f = gh->mo->floorz;
+		c = gh->mo->ceilingz - mobjinfo[MT_PLAYER].height;
+		if (!!(mthing->options & MTF_AMBUSH) ^ !!(mthing->options & MTF_OBJECTFLIP))
+		{
+			z = c - offset;
+			if (z < f)
+				z = f;
+		}
+		else
+		{
+			z = f + offset;
+			if (z > c)
+				z = c;
+		}
+		gh->mo->z = z;
+	}
+
+	gh->oldmo.x = gh->mo->x;
+	gh->oldmo.y = gh->mo->y;
+	gh->oldmo.z = gh->mo->z;
+
+	// Set skin
+	gh->mo->skin = &skins[0];
+	for (i = 0; i < numskins; i++)
+		if (!stricmp(skins[i].name,skin))
+		{
+			gh->mo->skin = &skins[i];
+			break;
+		}
+	gh->oldmo.skin = gh->mo->skin;
+
+	// Set color
+	gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor;
+	for (i = 0; i < MAXSKINCOLORS; i++)
+		if (!stricmp(Color_Names[i],color))
+		{
+			gh->mo->color = (UINT8)i;
+			break;
+		}
+	gh->oldmo.color = gh->mo->color;
+
+	gh->mo->state = states+S_PLAY_STND;
+	gh->mo->sprite = gh->mo->state->sprite;
+	gh->mo->sprite2 = (gh->mo->state->frame & FF_FRAMEMASK);
+	//gh->mo->frame = tr_trans30<<FF_TRANSSHIFT;
+	gh->mo->flags2 |= MF2_DONTDRAW;
+	gh->fadein = (9-3)*6; // fade from invisible to trans30 over as close to 35 tics as possible
+	gh->mo->tics = -1;
+
+	CONS_Printf(M_GetText("Added ghost %s from %s\n"), name, pdemoname);
+	Z_Free(pdemoname);
+}
+
+// Clean up all ghosts
+void G_FreeGhosts(void)
+{
+	while (ghosts)
+	{
+		demoghost *next = ghosts->next;
+		Z_Free(ghosts);
+		ghosts = next;
+	}
+	ghosts = NULL;
+}
+
+//
+// G_TimeDemo
+// NOTE: name is a full filename for external demos
+//
+static INT32 restorecv_vidwait;
+
+void G_TimeDemo(const char *name)
+{
+	nodrawers = M_CheckParm("-nodraw");
+	noblit = M_CheckParm("-noblit");
+	restorecv_vidwait = cv_vidwait.value;
+	if (cv_vidwait.value)
+		CV_Set(&cv_vidwait, "0");
+	timingdemo = true;
+	singletics = true;
+	framecount = 0;
+	demostarttime = I_GetTime();
+	G_DeferedPlayDemo(name);
+}
+
+void G_DoPlayMetal(void)
+{
+	lumpnum_t l;
+	mobj_t *mo = NULL;
+	thinker_t *th;
+
+	// it's an internal demo
+	if ((l = W_CheckNumForName(va("%sMS",G_BuildMapName(gamemap)))) == LUMPERROR)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("No bot recording for this map.\n"));
+		return;
+	}
+	else
+		metalbuffer = metal_p = W_CacheLumpNum(l, PU_STATIC);
+
+	// find metal sonic
+	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+	{
+		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			continue;
+
+		mo = (mobj_t *)th;
+		if (mo->type != MT_METALSONIC_RACE)
+			continue;
+
+		break;
+	}
+	if (th == &thlist[THINK_MOBJ])
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Failed to find bot entity.\n"));
+		Z_Free(metalbuffer);
+		return;
+	}
+
+	// read demo header
+    metal_p += 12; // DEMOHEADER
+	metal_p++; // VERSION
+	metal_p++; // SUBVERSION
+	metalversion = READUINT16(metal_p);
+	switch(metalversion)
+	{
+	case DEMOVERSION: // latest always supported
+		break;
+	// too old, cannot support.
+	default:
+		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n"));
+		Z_Free(metalbuffer);
+		return;
+	}
+	metal_p += 16; // demo checksum
+	if (memcmp(metal_p, "METL", 4))
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, wasn't recorded in Metal format.\n"));
+		Z_Free(metalbuffer);
+		return;
+	} metal_p += 4; // "METL"
+
+	// read initial tic
+	memset(&oldmetal,0,sizeof(oldmetal));
+	oldmetal.x = mo->x;
+	oldmetal.y = mo->y;
+	oldmetal.z = mo->z;
+	metalplayback = mo;
+}
+
+void G_DoneLevelLoad(void)
+{
+	CONS_Printf(M_GetText("Loaded level in %f sec\n"), (double)(I_GetTime() - demostarttime) / TICRATE);
+	framecount = 0;
+	demostarttime = I_GetTime();
+}
+
+/*
+===================
+=
+= G_CheckDemoStatus
+=
+= Called after a death or level completion to allow demos to be cleaned up
+= Returns true if a new demo loop action will take place
+===================
+*/
+
+// Stops metal sonic's demo. Separate from other functions because metal + replays can coexist
+void G_StopMetalDemo(void)
+{
+
+	// Metal Sonic finishing doesn't end the game, dammit.
+	Z_Free(metalbuffer);
+	metalbuffer = NULL;
+	metalplayback = NULL;
+	metal_p = NULL;
+}
+
+// Stops metal sonic recording.
+ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill)
+{
+	boolean saved = false;
+	if (demo_p)
+	{
+		UINT8 *p = demobuffer+16; // checksum position
+		if (kill)
+			WRITEUINT8(demo_p, METALDEATH); // add the metal death marker
+		else
+			WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
+#ifdef NOMD5
+		{
+			UINT8 i;
+			for (i = 0; i < 16; i++, p++)
+				*p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct.
+		}
+#else
+		md5_buffer((char *)p+16, demo_p - (p+16), (void *)p); // make a checksum of everything after the checksum in the file.
+#endif
+		saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuffer, demo_p - demobuffer); // finally output the file.
+	}
+	free(demobuffer);
+	metalrecording = false;
+	if (saved)
+		I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
+	I_Error("Failed to save demo!");
+}
+
+// reset engine variable set for the demos
+// called from stopdemo command, map command, and g_checkdemoStatus.
+void G_StopDemo(void)
+{
+	Z_Free(demobuffer);
+	demobuffer = NULL;
+	demoplayback = false;
+	titledemo = false;
+	timingdemo = false;
+	singletics = false;
+
+	if (gamestate == GS_INTERMISSION)
+		Y_EndIntermission(); // cleanup
+
+	G_SetGamestate(GS_NULL);
+	wipegamestate = GS_NULL;
+	SV_StopServer();
+	SV_ResetServer();
+}
+
+boolean G_CheckDemoStatus(void)
+{
+	boolean saved;
+
+	G_FreeGhosts();
+
+	// DO NOT end metal sonic demos here
+
+	if (timingdemo)
+	{
+		INT32 demotime;
+		double f1, f2;
+		demotime = I_GetTime() - demostarttime;
+		if (!demotime)
+			return true;
+		G_StopDemo();
+		timingdemo = false;
+		f1 = (double)demotime;
+		f2 = (double)framecount*TICRATE;
+
+		CONS_Printf(M_GetText("timed %u gametics in %d realtics - %u frames\n%f seconds, %f avg fps\n"),
+			leveltime,demotime,(UINT32)framecount,f1/TICRATE,f2/f1);
+
+		// CSV-readable timedemo results, for external parsing
+		if (timedemo_csv)
+		{
+			FILE *f;
+			const char *csvpath = va("%s"PATHSEP"%s", srb2home, "timedemo.csv");
+			const char *header = "id,demoname,seconds,avgfps,leveltime,demotime,framecount,ticrate,rendermode,vidmode,vidwidth,vidheight,procbits\n";
+			const char *rowformat = "\"%s\",\"%s\",%f,%f,%u,%d,%u,%u,%u,%u,%u,%u,%u\n";
+			boolean headerrow = !FIL_FileExists(csvpath);
+			UINT8 procbits = 0;
+
+			// Bitness
+			if (sizeof(void*) == 4)
+				procbits = 32;
+			else if (sizeof(void*) == 8)
+				procbits = 64;
+
+			f = fopen(csvpath, "a+");
+
+			if (f)
+			{
+				if (headerrow)
+					fputs(header, f);
+				fprintf(f, rowformat,
+					timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits);
+				fclose(f);
+				CONS_Printf("Timedemo results saved to '%s'\n", csvpath);
+			}
+			else
+			{
+				// Just print the CSV output to console
+				CON_LogMessage(header);
+				CONS_Printf(rowformat,
+					timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits);
+			}
+		}
+
+		if (restorecv_vidwait != cv_vidwait.value)
+			CV_SetValue(&cv_vidwait, restorecv_vidwait);
+		D_AdvanceDemo();
+		return true;
+	}
+
+	if (demoplayback)
+	{
+		if (singledemo)
+			I_Quit();
+		G_StopDemo();
+
+		if (modeattacking)
+			M_EndModeAttackRun();
+		else
+			D_AdvanceDemo();
+
+		return true;
+	}
+
+	if (demorecording)
+	{
+		UINT8 *p = demobuffer+16; // checksum position
+#ifdef NOMD5
+		UINT8 i;
+		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
+		for (i = 0; i < 16; i++, p++)
+			*p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct.
+#else
+		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
+		md5_buffer((char *)p+16, demo_p - (p+16), p); // make a checksum of everything after the checksum in the file.
+#endif
+		saved = FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer); // finally output the file.
+		free(demobuffer);
+		demorecording = false;
+
+		if (modeattacking != ATTACKING_RECORD)
+		{
+			if (saved)
+				CONS_Printf(M_GetText("Demo %s recorded\n"), demoname);
+			else
+				CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname);
+		}
+		return true;
+	}
+
+	return false;
+}
diff --git a/src/g_demo.h b/src/g_demo.h
new file mode 100644
index 0000000000000000000000000000000000000000..df25042c48030402622a71a611c8a7f2a53649e7
--- /dev/null
+++ b/src/g_demo.h
@@ -0,0 +1,86 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  g_demo.h
+/// \brief Demo recording and playback
+
+#ifndef __G_DEMO__
+#define __G_DEMO__
+
+#include "doomdef.h"
+#include "doomstat.h"
+#include "d_event.h"
+
+// ======================================
+// DEMO playback/recording related stuff.
+// ======================================
+
+// demoplaying back and demo recording
+extern boolean demoplayback, titledemo, demorecording, timingdemo;
+extern tic_t demostarttime;
+
+// Quit after playing a demo from cmdline.
+extern boolean singledemo;
+extern boolean demo_start;
+extern boolean demosynced;
+
+extern mobj_t *metalplayback;
+
+// Only called by startup code.
+void G_RecordDemo(const char *name);
+void G_RecordMetal(void);
+void G_BeginRecording(void);
+void G_BeginMetal(void);
+
+// Only called by shutdown code.
+void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings);
+UINT8 G_CmpDemoTime(char *oldname, char *newname);
+
+typedef enum
+{
+	GHC_NORMAL = 0,
+	GHC_SUPER,
+	GHC_FIREFLOWER,
+	GHC_INVINCIBLE,
+	GHC_NIGHTSSKIN, // not actually a colour
+	GHC_RETURNSKIN // ditto
+} ghostcolor_t;
+
+// Record/playback tics
+void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
+void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
+void G_GhostAddThok(void);
+void G_GhostAddSpin(void);
+void G_GhostAddRev(void);
+void G_GhostAddColor(ghostcolor_t color);
+void G_GhostAddFlip(void);
+void G_GhostAddScale(fixed_t scale);
+void G_GhostAddHit(mobj_t *victim);
+void G_WriteGhostTic(mobj_t *ghost);
+void G_ConsGhostTic(void);
+void G_GhostTicker(void);
+void G_ReadMetalTic(mobj_t *metal);
+void G_WriteMetalTic(mobj_t *metal);
+void G_SaveMetal(UINT8 **buffer);
+void G_LoadMetal(UINT8 **buffer);
+
+void G_DeferedPlayDemo(const char *demo);
+void G_DoPlayDemo(char *defdemoname);
+void G_TimeDemo(const char *name);
+void G_AddGhost(char *defdemoname);
+void G_FreeGhosts(void);
+void G_DoPlayMetal(void);
+void G_DoneLevelLoad(void);
+void G_StopMetalDemo(void);
+ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill);
+void G_StopDemo(void);
+boolean G_CheckDemoStatus(void);
+
+#endif // __G_DEMO__
diff --git a/src/g_game.c b/src/g_game.c
index bf73f6ce24cde224f797a294b36bde86b356f509..6dba6045af80f666a99b48e6d0032f172666dfb8 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -27,6 +27,7 @@
 #include "r_main.h"
 #include "s_sound.h"
 #include "g_game.h"
+#include "g_demo.h"
 #include "m_cheat.h"
 #include "m_misc.h"
 #include "m_menu.h"
@@ -41,11 +42,9 @@
 #include "r_skins.h"
 #include "y_inter.h"
 #include "v_video.h"
-#include "dehacked.h" // get_number (for ghost thok)
 #include "lua_hook.h"
 #include "b_bot.h"
 #include "m_cond.h" // condition sets
-#include "md5.h" // demo checksums
 
 #include "lua_hud.h"
 
@@ -103,11 +102,6 @@ UINT8  numDemos      = 0;
 UINT32 demoDelayTime = 15*TICRATE;
 UINT32 demoIdleTime  = 3*TICRATE;
 
-boolean timingdemo; // if true, exit with report on completion
-boolean nodrawers; // for comparative timing purposes
-boolean noblit; // for comparative timing purposes
-tic_t demostarttime; // for comparative timing purposes
-
 boolean netgame; // only true if packets are broadcast
 boolean multiplayer;
 boolean playeringame[MAXPLAYERS];
@@ -254,57 +248,12 @@ UINT32 timesBeaten;
 UINT32 timesBeatenWithEmeralds;
 UINT32 timesBeatenUltimate;
 
-static char demoname[64];
-boolean demorecording;
-boolean demoplayback;
-boolean titledemo; // Title Screen demo can be cancelled by any key
-static UINT8 *demobuffer = NULL;
-static UINT8 *demo_p, *demotime_p;
-static UINT8 *demoend;
-static UINT8 demoflags;
-static UINT16 demoversion;
-boolean singledemo; // quit after playing a demo from cmdline
-boolean demo_start; // don't start playing demo right away
-boolean demosynced = true; // console warning message
-
-boolean metalrecording; // recording as metal sonic
-mobj_t *metalplayback;
-static UINT8 *metalbuffer = NULL;
-static UINT8 *metal_p;
-static UINT16 metalversion;
-
 typedef struct joystickvector2_s
 {
 	INT32 xaxis;
 	INT32 yaxis;
 } joystickvector2_t;
 
-// extra data stuff (events registered this frame while recording)
-static struct {
-	UINT8 flags; // EZT flags
-
-	// EZT_COLOR
-	UINT8 color, lastcolor;
-
-	// EZT_SCALE
-	fixed_t scale, lastscale;
-
-	// EZT_HIT
-	UINT16 hits;
-	mobj_t **hitlist;
-} ghostext;
-
-// Your naming conventions are stupid and useless.
-// There is no conflict here.
-typedef struct demoghost {
-	UINT8 checksum[16];
-	UINT8 *buffer, *p, color, fadein;
-	UINT16 version;
-	mobj_t oldmo, *mo;
-	struct demoghost *next;
-} demoghost;
-demoghost *ghosts = NULL;
-
 boolean precache = true; // if true, load all graphics at start
 
 INT16 prevmap, nextmap;
@@ -1100,8 +1049,6 @@ static void G_HandleAxisDeadZone(UINT8 splitnum, joystickvector2_t *joystickvect
 	}
 }
 
-
-
 //
 // G_BuildTiccmd
 // Builds a ticcmd from all of the available inputs
@@ -1719,6 +1666,25 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	}
 }
 
+ticcmd_t *G_CopyTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
+{
+	return M_Memcpy(dest, src, n*sizeof(*src));
+}
+
+ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
+{
+	size_t i;
+	for (i = 0; i < n; i++)
+	{
+		dest[i].forwardmove = src[i].forwardmove;
+		dest[i].sidemove = src[i].sidemove;
+		dest[i].angleturn = SHORT(src[i].angleturn);
+		dest[i].aiming = (INT16)SHORT(src[i].aiming);
+		dest[i].buttons = (UINT16)SHORT(src[i].buttons);
+	}
+	return dest;
+}
+
 // User has designated that they want
 // analog ON, so tell the game to stop
 // fudging with it.
@@ -4512,7 +4478,7 @@ void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 pickedchar, b
 
 	if (demoplayback)
 		COM_BufAddText("stopdemo\n");
-	ghosts = NULL;
+	G_FreeGhosts(); // TODO: do we actually need to do this?
 
 	// this leave the actual game if needed
 	SV_StartSinglePlayerServer();
@@ -4921,2430 +4887,6 @@ INT32 G_FindMapByNameOrCode(const char *mapname, char **realmapnamep)
 	return newmapnum;
 }
 
-//
-// DEMO RECORDING
-//
-
-#define DEMOVERSION 0x000c
-#define DEMOHEADER  "\xF0" "SRB2Replay" "\x0F"
-
-#define DF_GHOST        0x01 // This demo contains ghost data too!
-#define DF_RECORDATTACK 0x02 // This demo is from record attack and contains its final completion time, score, and rings!
-#define DF_NIGHTSATTACK 0x04 // This demo is from NiGHTS attack and contains its time left, score, and mares!
-#define DF_ATTACKMASK   0x06 // This demo is from ??? attack and contains ???
-#define DF_ATTACKSHIFT  1
-
-// For demos
-#define ZT_FWD     0x01
-#define ZT_SIDE    0x02
-#define ZT_ANGLE   0x04
-#define ZT_BUTTONS 0x08
-#define ZT_AIMING  0x10
-#define DEMOMARKER 0x80 // demoend
-#define METALDEATH 0x44
-#define METALSNICE 0x69
-
-static ticcmd_t oldcmd;
-
-// For Metal Sonic and time attack ghosts
-#define GZT_XYZ    0x01
-#define GZT_MOMXY  0x02
-#define GZT_MOMZ   0x04
-#define GZT_ANGLE  0x08
-#define GZT_FRAME  0x10 // Animation frame
-#define GZT_SPR2   0x20 // Player animations
-#define GZT_EXTRA  0x40
-#define GZT_FOLLOW 0x80 // Followmobj
-
-// GZT_EXTRA flags
-#define EZT_THOK   0x01 // Spawned a thok object
-#define EZT_SPIN   0x02 // Because one type of thok object apparently wasn't enough
-#define EZT_REV    0x03 // And two types wasn't enough either yet
-#define EZT_THOKMASK 0x03
-#define EZT_COLOR  0x04 // Changed color (Super transformation, Mario fireflowers/invulnerability, etc.)
-#define EZT_FLIP   0x08 // Reversed gravity
-#define EZT_SCALE  0x10 // Changed size
-#define EZT_HIT    0x20 // Damaged a mobj
-#define EZT_SPRITE 0x40 // Changed sprite set completely out of PLAY (NiGHTS, SOCs, whatever)
-#define EZT_HEIGHT 0x80 // Changed height
-
-// GZT_FOLLOW flags
-#define FZT_SPAWNED 0x01 // just been spawned
-#define FZT_SKIN 0x02 // has skin
-#define FZT_LINKDRAW 0x04 // has linkdraw (combine with spawned only)
-#define FZT_COLORIZED 0x08 // colorized (ditto)
-#define FZT_SCALE 0x10 // different scale to object
-// spare FZT slots 0x20 to 0x80
-
-static mobj_t oldmetal, oldghost;
-
-void G_SaveMetal(UINT8 **buffer)
-{
-	I_Assert(buffer != NULL && *buffer != NULL);
-
-	WRITEUINT32(*buffer, metal_p - metalbuffer);
-}
-
-void G_LoadMetal(UINT8 **buffer)
-{
-	I_Assert(buffer != NULL && *buffer != NULL);
-
-	G_DoPlayMetal();
-	metal_p = metalbuffer + READUINT32(*buffer);
-}
-
-ticcmd_t *G_CopyTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
-{
-	return M_Memcpy(dest, src, n*sizeof(*src));
-}
-
-ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
-{
-	size_t i;
-	for (i = 0; i < n; i++)
-	{
-		dest[i].forwardmove = src[i].forwardmove;
-		dest[i].sidemove = src[i].sidemove;
-		dest[i].angleturn = SHORT(src[i].angleturn);
-		dest[i].aiming = (INT16)SHORT(src[i].aiming);
-		dest[i].buttons = (UINT16)SHORT(src[i].buttons);
-	}
-	return dest;
-}
-
-void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
-{
-	UINT8 ziptic;
-	(void)playernum;
-
-	if (!demo_p || !demo_start)
-		return;
-	ziptic = READUINT8(demo_p);
-
-	if (ziptic & ZT_FWD)
-		oldcmd.forwardmove = READSINT8(demo_p);
-	if (ziptic & ZT_SIDE)
-		oldcmd.sidemove = READSINT8(demo_p);
-	if (ziptic & ZT_ANGLE)
-		oldcmd.angleturn = READINT16(demo_p);
-	if (ziptic & ZT_BUTTONS)
-		oldcmd.buttons = (oldcmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) | (READUINT16(demo_p) & ~(BT_CAMLEFT|BT_CAMRIGHT));
-	if (ziptic & ZT_AIMING)
-		oldcmd.aiming = READINT16(demo_p);
-
-	G_CopyTiccmd(cmd, &oldcmd, 1);
-
-	if (!(demoflags & DF_GHOST) && *demo_p == DEMOMARKER)
-	{
-		// end of demo data stream
-		G_CheckDemoStatus();
-		return;
-	}
-}
-
-void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
-{
-	char ziptic = 0;
-	UINT8 *ziptic_p;
-	(void)playernum;
-
-	if (!demo_p)
-		return;
-	ziptic_p = demo_p++; // the ziptic, written at the end of this function
-
-	if (cmd->forwardmove != oldcmd.forwardmove)
-	{
-		WRITEUINT8(demo_p,cmd->forwardmove);
-		oldcmd.forwardmove = cmd->forwardmove;
-		ziptic |= ZT_FWD;
-	}
-
-	if (cmd->sidemove != oldcmd.sidemove)
-	{
-		WRITEUINT8(demo_p,cmd->sidemove);
-		oldcmd.sidemove = cmd->sidemove;
-		ziptic |= ZT_SIDE;
-	}
-
-	if (cmd->angleturn != oldcmd.angleturn)
-	{
-		WRITEINT16(demo_p,cmd->angleturn);
-		oldcmd.angleturn = cmd->angleturn;
-		ziptic |= ZT_ANGLE;
-	}
-
-	if (cmd->buttons != oldcmd.buttons)
-	{
-		WRITEUINT16(demo_p,cmd->buttons);
-		oldcmd.buttons = cmd->buttons;
-		ziptic |= ZT_BUTTONS;
-	}
-
-	if (cmd->aiming != oldcmd.aiming)
-	{
-		WRITEINT16(demo_p,cmd->aiming);
-		oldcmd.aiming = cmd->aiming;
-		ziptic |= ZT_AIMING;
-	}
-
-	*ziptic_p = ziptic;
-
-	// attention here for the ticcmd size!
-	// latest demos with mouse aiming byte in ticcmd
-	if (!(demoflags & DF_GHOST) && ziptic_p > demoend - 9)
-	{
-		G_CheckDemoStatus(); // no more space
-		return;
-	}
-}
-
-void G_GhostAddThok(void)
-{
-	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
-		return;
-	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_THOK;
-}
-
-void G_GhostAddSpin(void)
-{
-	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
-		return;
-	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_SPIN;
-}
-
-void G_GhostAddRev(void)
-{
-	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
-		return;
-	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_REV;
-}
-
-void G_GhostAddFlip(void)
-{
-	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
-		return;
-	ghostext.flags |= EZT_FLIP;
-}
-
-void G_GhostAddColor(ghostcolor_t color)
-{
-	if (!demorecording || !(demoflags & DF_GHOST))
-		return;
-	if (ghostext.lastcolor == (UINT8)color)
-	{
-		ghostext.flags &= ~EZT_COLOR;
-		return;
-	}
-	ghostext.flags |= EZT_COLOR;
-	ghostext.color = (UINT8)color;
-}
-
-void G_GhostAddScale(fixed_t scale)
-{
-	if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST)))
-		return;
-	if (ghostext.lastscale == scale)
-	{
-		ghostext.flags &= ~EZT_SCALE;
-		return;
-	}
-	ghostext.flags |= EZT_SCALE;
-	ghostext.scale = scale;
-}
-
-void G_GhostAddHit(mobj_t *victim)
-{
-	if (!demorecording || !(demoflags & DF_GHOST))
-		return;
-	ghostext.flags |= EZT_HIT;
-	ghostext.hits++;
-	ghostext.hitlist = Z_Realloc(ghostext.hitlist, ghostext.hits * sizeof(mobj_t *), PU_LEVEL, NULL);
-	ghostext.hitlist[ghostext.hits-1] = victim;
-}
-
-void G_WriteGhostTic(mobj_t *ghost)
-{
-	char ziptic = 0;
-	UINT8 *ziptic_p;
-	UINT32 i;
-	fixed_t height;
-
-	if (!demo_p)
-		return;
-	if (!(demoflags & DF_GHOST))
-		return; // No ghost data to write.
-
-	ziptic_p = demo_p++; // the ziptic, written at the end of this function
-
-	#define MAXMOM (0xFFFF<<8)
-
-	// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
-	if (abs(ghost->x-oldghost.x) > MAXMOM
-	|| abs(ghost->y-oldghost.y) > MAXMOM
-	|| abs(ghost->z-oldghost.z) > MAXMOM)
-	{
-		oldghost.x = ghost->x;
-		oldghost.y = ghost->y;
-		oldghost.z = ghost->z;
-		ziptic |= GZT_XYZ;
-		WRITEFIXED(demo_p,oldghost.x);
-		WRITEFIXED(demo_p,oldghost.y);
-		WRITEFIXED(demo_p,oldghost.z);
-	}
-	else
-	{
-		// For moving normally:
-		// Store one full byte of movement, plus one byte of fractional movement.
-		INT16 momx = (INT16)((ghost->x-oldghost.x)>>8);
-		INT16 momy = (INT16)((ghost->y-oldghost.y)>>8);
-		if (momx != oldghost.momx
-		|| momy != oldghost.momy)
-		{
-			oldghost.momx = momx;
-			oldghost.momy = momy;
-			ziptic |= GZT_MOMXY;
-			WRITEINT16(demo_p,momx);
-			WRITEINT16(demo_p,momy);
-		}
-		momx = (INT16)((ghost->z-oldghost.z)>>8);
-		if (momx != oldghost.momz)
-		{
-			oldghost.momz = momx;
-			ziptic |= GZT_MOMZ;
-			WRITEINT16(demo_p,momx);
-		}
-
-		// This SHOULD set oldghost.x/y/z to match ghost->x/y/z
-		// but it keeps the fractional loss of one byte,
-		// so it will hopefully be made up for in future tics.
-		oldghost.x += oldghost.momx<<8;
-		oldghost.y += oldghost.momy<<8;
-		oldghost.z += oldghost.momz<<8;
-	}
-
-	#undef MAXMOM
-
-	// Only store the 8 most relevant bits of angle
-	// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
-	// and it does not affect this mode of movement at all anyway.
-	if (ghost->player && ghost->player->drawangle>>24 != oldghost.angle)
-	{
-		oldghost.angle = ghost->player->drawangle>>24;
-		ziptic |= GZT_ANGLE;
-		WRITEUINT8(demo_p,oldghost.angle);
-	}
-
-	// Store the sprite frame.
-	if ((ghost->frame & FF_FRAMEMASK) != oldghost.frame)
-	{
-		oldghost.frame = (ghost->frame & FF_FRAMEMASK);
-		ziptic |= GZT_FRAME;
-		WRITEUINT8(demo_p,oldghost.frame);
-	}
-
-	if (ghost->sprite == SPR_PLAY
-	&& ghost->sprite2 != oldghost.sprite2)
-	{
-		oldghost.sprite2 = ghost->sprite2;
-		ziptic |= GZT_SPR2;
-		WRITEUINT8(demo_p,oldghost.sprite2);
-	}
-
-	// Check for sprite set changes
-	if (ghost->sprite != oldghost.sprite)
-	{
-		oldghost.sprite = ghost->sprite;
-		ghostext.flags |= EZT_SPRITE;
-	}
-
-	if ((height = FixedDiv(ghost->height, ghost->scale)) != oldghost.height)
-	{
-		oldghost.height = height;
-		ghostext.flags |= EZT_HEIGHT;
-	}
-
-	if (ghostext.flags)
-	{
-		ziptic |= GZT_EXTRA;
-
-		if (ghostext.color == ghostext.lastcolor)
-			ghostext.flags &= ~EZT_COLOR;
-		if (ghostext.scale == ghostext.lastscale)
-			ghostext.flags &= ~EZT_SCALE;
-
-		WRITEUINT8(demo_p,ghostext.flags);
-		if (ghostext.flags & EZT_COLOR)
-		{
-			WRITEUINT8(demo_p,ghostext.color);
-			ghostext.lastcolor = ghostext.color;
-		}
-		if (ghostext.flags & EZT_SCALE)
-		{
-			WRITEFIXED(demo_p,ghostext.scale);
-			ghostext.lastscale = ghostext.scale;
-		}
-		if (ghostext.flags & EZT_HIT)
-		{
-			WRITEUINT16(demo_p,ghostext.hits);
-			for (i = 0; i < ghostext.hits; i++)
-			{
-				mobj_t *mo = ghostext.hitlist[i];
-				//WRITEUINT32(demo_p,UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.)
-				WRITEUINT32(demo_p,mo->type);
-				WRITEUINT16(demo_p,(UINT16)mo->health);
-				WRITEFIXED(demo_p,mo->x);
-				WRITEFIXED(demo_p,mo->y);
-				WRITEFIXED(demo_p,mo->z);
-				WRITEANGLE(demo_p,mo->angle);
-			}
-			Z_Free(ghostext.hitlist);
-			ghostext.hits = 0;
-			ghostext.hitlist = NULL;
-		}
-		if (ghostext.flags & EZT_SPRITE)
-			WRITEUINT16(demo_p,oldghost.sprite);
-		if (ghostext.flags & EZT_HEIGHT)
-		{
-			height >>= FRACBITS;
-			WRITEINT16(demo_p, height);
-		}
-		ghostext.flags = 0;
-	}
-
-	if (ghost->player && ghost->player->followmobj && !(ghost->player->followmobj->sprite == SPR_NULL || (ghost->player->followmobj->flags2 & MF2_DONTDRAW))) // bloats tails runs but what can ya do
-	{
-		INT16 temp;
-		UINT8 *followtic_p = demo_p++;
-		UINT8 followtic = 0;
-
-		ziptic |= GZT_FOLLOW;
-
-		if (ghost->player->followmobj->skin)
-			followtic |= FZT_SKIN;
-
-		if (!(oldghost.flags2 & MF2_AMBUSH))
-		{
-			followtic |= FZT_SPAWNED;
-			WRITEINT16(demo_p,ghost->player->followmobj->info->height>>FRACBITS);
-			if (ghost->player->followmobj->flags2 & MF2_LINKDRAW)
-				followtic |= FZT_LINKDRAW;
-			if (ghost->player->followmobj->colorized)
-				followtic |= FZT_COLORIZED;
-			if (followtic & FZT_SKIN)
-				WRITEUINT8(demo_p,(UINT8)(((skin_t *)(ghost->player->followmobj->skin))-skins));
-			oldghost.flags2 |= MF2_AMBUSH;
-		}
-
-		if (ghost->player->followmobj->scale != ghost->scale)
-		{
-			followtic |= FZT_SCALE;
-			WRITEFIXED(demo_p,ghost->player->followmobj->scale);
-		}
-
-		temp = (INT16)((ghost->player->followmobj->x-ghost->x)>>8);
-		WRITEINT16(demo_p,temp);
-		temp = (INT16)((ghost->player->followmobj->y-ghost->y)>>8);
-		WRITEINT16(demo_p,temp);
-		temp = (INT16)((ghost->player->followmobj->z-ghost->z)>>8);
-		WRITEINT16(demo_p,temp);
-		if (followtic & FZT_SKIN)
-			WRITEUINT8(demo_p,ghost->player->followmobj->sprite2);
-		WRITEUINT16(demo_p,ghost->player->followmobj->sprite);
-		WRITEUINT8(demo_p,(ghost->player->followmobj->frame & FF_FRAMEMASK));
-		WRITEUINT8(demo_p,ghost->player->followmobj->color);
-
-		*followtic_p = followtic;
-	}
-	else
-		oldghost.flags2 &= ~MF2_AMBUSH;
-
-	*ziptic_p = ziptic;
-
-	// attention here for the ticcmd size!
-	// latest demos with mouse aiming byte in ticcmd
-	if (demo_p >= demoend - (13 + 9 + 9))
-	{
-		G_CheckDemoStatus(); // no more space
-		return;
-	}
-}
-
-// Uses ghost data to do consistency checks on your position.
-// This fixes desynchronising demos when fighting eggman.
-void G_ConsGhostTic(void)
-{
-	UINT8 ziptic;
-	UINT16 px,py,pz,gx,gy,gz;
-	mobj_t *testmo;
-
-	if (!demo_p || !demo_start)
-		return;
-	if (!(demoflags & DF_GHOST))
-		return; // No ghost data to use.
-
-	testmo = players[0].mo;
-
-	// Grab ghost data.
-	ziptic = READUINT8(demo_p);
-	if (ziptic & GZT_XYZ)
-	{
-		oldghost.x = READFIXED(demo_p);
-		oldghost.y = READFIXED(demo_p);
-		oldghost.z = READFIXED(demo_p);
-	}
-	else
-	{
-		if (ziptic & GZT_MOMXY)
-		{
-			oldghost.momx = READINT16(demo_p)<<8;
-			oldghost.momy = READINT16(demo_p)<<8;
-		}
-		if (ziptic & GZT_MOMZ)
-			oldghost.momz = READINT16(demo_p)<<8;
-		oldghost.x += oldghost.momx;
-		oldghost.y += oldghost.momy;
-		oldghost.z += oldghost.momz;
-	}
-	if (ziptic & GZT_ANGLE)
-		demo_p++;
-	if (ziptic & GZT_FRAME)
-		demo_p++;
-	if (ziptic & GZT_SPR2)
-		demo_p++;
-
-	if (ziptic & GZT_EXTRA)
-	{ // But wait, there's more!
-		UINT8 xziptic = READUINT8(demo_p);
-		if (xziptic & EZT_COLOR)
-			demo_p++;
-		if (xziptic & EZT_SCALE)
-			demo_p += sizeof(fixed_t);
-		if (xziptic & EZT_HIT)
-		{ // Resync mob damage.
-			UINT16 i, count = READUINT16(demo_p);
-			thinker_t *th;
-			mobj_t *mobj;
-
-			UINT32 type;
-			UINT16 health;
-			fixed_t x;
-			fixed_t y;
-			fixed_t z;
-
-			for (i = 0; i < count; i++)
-			{
-				//demo_p += 4; // reserved.
-				type = READUINT32(demo_p);
-				health = READUINT16(demo_p);
-				x = READFIXED(demo_p);
-				y = READFIXED(demo_p);
-				z = READFIXED(demo_p);
-				demo_p += sizeof(angle_t); // angle, unnecessary for cons.
-
-				mobj = NULL;
-				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
-				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-						continue;
-					mobj = (mobj_t *)th;
-					if (mobj->type == (mobjtype_t)type && mobj->x == x && mobj->y == y && mobj->z == z)
-						break;
-				}
-				if (th != &thlist[THINK_MOBJ] && mobj->health != health) // Wasn't damaged?! This is desync! Fix it!
-				{
-					if (demosynced)
-						CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
-					demosynced = false;
-					P_DamageMobj(mobj, players[0].mo, players[0].mo, 1, 0);
-				}
-			}
-		}
-		if (xziptic & EZT_SPRITE)
-			demo_p += sizeof(UINT16);
-		if (xziptic & EZT_HEIGHT)
-			demo_p += sizeof(INT16);
-	}
-
-	if (ziptic & GZT_FOLLOW)
-	{ // Even more...
-		UINT8 followtic = READUINT8(demo_p);
-		if (followtic & FZT_SPAWNED)
-		{
-			demo_p += sizeof(INT16);
-			if (followtic & FZT_SKIN)
-				demo_p++;
-		}
-		if (followtic & FZT_SCALE)
-			demo_p += sizeof(fixed_t);
-		demo_p += sizeof(INT16);
-		demo_p += sizeof(INT16);
-		demo_p += sizeof(INT16);
-		if (followtic & FZT_SKIN)
-			demo_p++;
-		demo_p += sizeof(UINT16);
-		demo_p++;
-		demo_p++;
-	}
-
-	// Re-synchronise
-	px = testmo->x>>FRACBITS;
-	py = testmo->y>>FRACBITS;
-	pz = testmo->z>>FRACBITS;
-	gx = oldghost.x>>FRACBITS;
-	gy = oldghost.y>>FRACBITS;
-	gz = oldghost.z>>FRACBITS;
-
-	if (px != gx || py != gy || pz != gz)
-	{
-		if (demosynced)
-			CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
-		demosynced = false;
-
-		P_UnsetThingPosition(testmo);
-		testmo->x = oldghost.x;
-		testmo->y = oldghost.y;
-		P_SetThingPosition(testmo);
-		testmo->z = oldghost.z;
-	}
-
-	if (*demo_p == DEMOMARKER)
-	{
-		// end of demo data stream
-		G_CheckDemoStatus();
-		return;
-	}
-}
-
-void G_GhostTicker(void)
-{
-	demoghost *g,*p;
-	for(g = ghosts, p = NULL; g; g = g->next)
-	{
-		// Skip normal demo data.
-		UINT8 ziptic = READUINT8(g->p);
-		UINT8 xziptic = 0;
-		if (ziptic & ZT_FWD)
-			g->p++;
-		if (ziptic & ZT_SIDE)
-			g->p++;
-		if (ziptic & ZT_ANGLE)
-			g->p += 2;
-		if (ziptic & ZT_BUTTONS)
-			g->p += 2;
-		if (ziptic & ZT_AIMING)
-			g->p += 2;
-
-		// Grab ghost data.
-		ziptic = READUINT8(g->p);
-		if (ziptic & GZT_XYZ)
-		{
-			g->oldmo.x = READFIXED(g->p);
-			g->oldmo.y = READFIXED(g->p);
-			g->oldmo.z = READFIXED(g->p);
-		}
-		else
-		{
-			if (ziptic & GZT_MOMXY)
-			{
-				g->oldmo.momx = READINT16(g->p)<<8;
-				g->oldmo.momy = READINT16(g->p)<<8;
-			}
-			if (ziptic & GZT_MOMZ)
-				g->oldmo.momz = READINT16(g->p)<<8;
-			g->oldmo.x += g->oldmo.momx;
-			g->oldmo.y += g->oldmo.momy;
-			g->oldmo.z += g->oldmo.momz;
-		}
-		if (ziptic & GZT_ANGLE)
-			g->mo->angle = READUINT8(g->p)<<24;
-		if (ziptic & GZT_FRAME)
-			g->oldmo.frame = READUINT8(g->p);
-		if (ziptic & GZT_SPR2)
-			g->oldmo.sprite2 = READUINT8(g->p);
-
-		// Update ghost
-		P_UnsetThingPosition(g->mo);
-		g->mo->x = g->oldmo.x;
-		g->mo->y = g->oldmo.y;
-		g->mo->z = g->oldmo.z;
-		P_SetThingPosition(g->mo);
-		g->mo->frame = g->oldmo.frame | tr_trans30<<FF_TRANSSHIFT;
-		if (g->fadein)
-		{
-			g->mo->frame += (((--g->fadein)/6)<<FF_TRANSSHIFT); // this calc never exceeds 9 unless g->fadein is bad, and it's only set once, so...
-			g->mo->flags2 &= ~MF2_DONTDRAW;
-		}
-		g->mo->sprite2 = g->oldmo.sprite2;
-
-		if (ziptic & GZT_EXTRA)
-		{ // But wait, there's more!
-			xziptic = READUINT8(g->p);
-			if (xziptic & EZT_COLOR)
-			{
-				g->color = READUINT8(g->p);
-				switch(g->color)
-				{
-				default:
-				case GHC_RETURNSKIN:
-					g->mo->skin = g->oldmo.skin;
-					/* FALLTHRU */
-				case GHC_NORMAL: // Go back to skin color
-					g->mo->color = g->oldmo.color;
-					break;
-				// Handled below
-				case GHC_SUPER:
-				case GHC_INVINCIBLE:
-					break;
-				case GHC_FIREFLOWER: // Fireflower
-					g->mo->color = SKINCOLOR_WHITE;
-					break;
-				case GHC_NIGHTSSKIN: // not actually a colour
-					g->mo->skin = &skins[DEFAULTNIGHTSSKIN];
-					break;
-				}
-			}
-			if (xziptic & EZT_FLIP)
-				g->mo->eflags ^= MFE_VERTICALFLIP;
-			if (xziptic & EZT_SCALE)
-			{
-				g->mo->destscale = READFIXED(g->p);
-				if (g->mo->destscale != g->mo->scale)
-					P_SetScale(g->mo, g->mo->destscale);
-			}
-			if (xziptic & EZT_THOKMASK)
-			{ // Let's only spawn ONE of these per frame, thanks.
-				mobj_t *mobj;
-				INT32 type = -1;
-				if (g->mo->skin)
-				{
-					skin_t *skin = (skin_t *)g->mo->skin;
-					switch (xziptic & EZT_THOKMASK)
-					{
-					case EZT_THOK:
-						type = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
-						break;
-					case EZT_SPIN:
-						type = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
-						break;
-					case EZT_REV:
-						type = skin->revitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
-						break;
-					}
-				}
-				if (type != MT_NULL)
-				{
-					if (type == MT_GHOST)
-					{
-						mobj = P_SpawnGhostMobj(g->mo); // does a large portion of the work for us
-						mobj->frame = (mobj->frame & ~FF_FRAMEMASK)|tr_trans60<<FF_TRANSSHIFT; // P_SpawnGhostMobj sets trans50, we want trans60
-					}
-					else
-					{
-						mobj = P_SpawnMobjFromMobj(g->mo, 0, 0, -FixedDiv(FixedMul(g->mo->info->height, g->mo->scale) - g->mo->height,3*FRACUNIT), MT_THOK);
-						mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
-						mobj->frame = (states[mobjinfo[type].spawnstate].frame & FF_FRAMEMASK) | tr_trans60<<FF_TRANSSHIFT;
-						mobj->color = g->mo->color;
-						mobj->skin = g->mo->skin;
-						P_SetScale(mobj, (mobj->destscale = g->mo->scale));
-
-						if (type == MT_THOK) // spintrail-specific modification for MT_THOK
-						{
-							mobj->frame = FF_TRANS80;
-							mobj->fuse = mobj->tics;
-						}
-						mobj->tics = -1; // nope.
-					}
-					mobj->floorz = mobj->z;
-					mobj->ceilingz = mobj->z+mobj->height;
-					P_UnsetThingPosition(mobj);
-					mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
-					P_SetThingPosition(mobj);
-					if (!mobj->fuse)
-						mobj->fuse = 8;
-					P_SetTarget(&mobj->target, g->mo);
-				}
-			}
-			if (xziptic & EZT_HIT)
-			{ // Spawn hit poofs for killing things!
-				UINT16 i, count = READUINT16(g->p), health;
-				UINT32 type;
-				fixed_t x,y,z;
-				angle_t angle;
-				mobj_t *poof;
-				for (i = 0; i < count; i++)
-				{
-					//g->p += 4; // reserved
-					type = READUINT32(g->p);
-					health = READUINT16(g->p);
-					x = READFIXED(g->p);
-					y = READFIXED(g->p);
-					z = READFIXED(g->p);
-					angle = READANGLE(g->p);
-					if (!(mobjinfo[type].flags & MF_SHOOTABLE)
-					|| !(mobjinfo[type].flags & (MF_ENEMY|MF_MONITOR))
-					|| health != 0 || i >= 4) // only spawn for the first 4 hits per frame, to prevent ghosts from splode-spamming too bad.
-						continue;
-					poof = P_SpawnMobj(x, y, z, MT_GHOST);
-					poof->angle = angle;
-					poof->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
-					poof->health = 0;
-					P_SetMobjStateNF(poof, S_XPLD1);
-				}
-			}
-			if (xziptic & EZT_SPRITE)
-				g->mo->sprite = READUINT16(g->p);
-			if (xziptic & EZT_HEIGHT)
-			{
-				fixed_t temp = READINT16(g->p)<<FRACBITS;
-				g->mo->height = FixedMul(temp, g->mo->scale);
-			}
-		}
-
-		// Tick ghost colors (Super and Mario Invincibility flashing)
-		switch(g->color)
-		{
-		case GHC_SUPER: // Super (P_DoSuperStuff)
-			if (g->mo->skin)
-			{
-				skin_t *skin = (skin_t *)g->mo->skin;
-				g->mo->color = skin->supercolor;
-			}
-			else
-				g->mo->color = SKINCOLOR_SUPERGOLD1;
-			g->mo->color += abs( ( (signed)( (unsigned)leveltime >> 1 ) % 9) - 4);
-			break;
-		case GHC_INVINCIBLE: // Mario invincibility (P_CheckInvincibilityTimer)
-			g->mo->color = (UINT8)(SKINCOLOR_RUBY + (leveltime % (MAXSKINCOLORS - SKINCOLOR_RUBY))); // Passes through all saturated colours
-			break;
-		default:
-			break;
-		}
-
-#define follow g->mo->tracer
-		if (ziptic & GZT_FOLLOW)
-		{ // Even more...
-			UINT8 followtic = READUINT8(g->p);
-			fixed_t temp;
-			if (followtic & FZT_SPAWNED)
-			{
-				if (follow)
-					P_RemoveMobj(follow);
-				P_SetTarget(&follow, P_SpawnMobjFromMobj(g->mo, 0, 0, 0, MT_GHOST));
-				P_SetTarget(&follow->tracer, g->mo);
-				follow->tics = -1;
-				temp = READINT16(g->p)<<FRACBITS;
-				follow->height = FixedMul(follow->scale, temp);
-
-				if (followtic & FZT_LINKDRAW)
-					follow->flags2 |= MF2_LINKDRAW;
-
-				if (followtic & FZT_COLORIZED)
-					follow->colorized = true;
-
-				if (followtic & FZT_SKIN)
-					follow->skin = &skins[READUINT8(g->p)];
-			}
-			if (follow)
-			{
-				if (followtic & FZT_SCALE)
-					follow->destscale = READFIXED(g->p);
-				else
-					follow->destscale = g->mo->destscale;
-				if (follow->destscale != follow->scale)
-					P_SetScale(follow, follow->destscale);
-
-				P_UnsetThingPosition(follow);
-				temp = READINT16(g->p)<<8;
-				follow->x = g->mo->x + temp;
-				temp = READINT16(g->p)<<8;
-				follow->y = g->mo->y + temp;
-				temp = READINT16(g->p)<<8;
-				follow->z = g->mo->z + temp;
-				P_SetThingPosition(follow);
-				if (followtic & FZT_SKIN)
-					follow->sprite2 = READUINT8(g->p);
-				else
-					follow->sprite2 = 0;
-				follow->sprite = READUINT16(g->p);
-				follow->frame = (READUINT8(g->p)) | (g->mo->frame & FF_TRANSMASK);
-				follow->angle = g->mo->angle;
-				follow->color = READUINT8(g->p);
-
-				if (!(followtic & FZT_SPAWNED))
-				{
-					if (xziptic & EZT_FLIP)
-					{
-						follow->flags2 ^= MF2_OBJECTFLIP;
-						follow->eflags ^= MFE_VERTICALFLIP;
-					}
-				}
-			}
-		}
-		else if (follow)
-		{
-			P_RemoveMobj(follow);
-			P_SetTarget(&follow, NULL);
-		}
-		// Demo ends after ghost data.
-		if (*g->p == DEMOMARKER)
-		{
-			g->mo->momx = g->mo->momy = g->mo->momz = 0;
-#if 1 // freeze frame (maybe more useful for time attackers)
-			g->mo->colorized = true;
-			if (follow)
-				follow->colorized = true;
-#else // dissapearing act
-			g->mo->fuse = TICRATE;
-			if (follow)
-				follow->fuse = TICRATE;
-#endif
-			if (p)
-				p->next = g->next;
-			else
-				ghosts = g->next;
-			Z_Free(g);
-			continue;
-		}
-		p = g;
-#undef follow
-	}
-}
-
-void G_ReadMetalTic(mobj_t *metal)
-{
-	UINT8 ziptic;
-	UINT8 xziptic = 0;
-
-	if (!metal_p)
-		return;
-
-	if (!metal->health)
-	{
-		G_StopMetalDemo();
-		return;
-	}
-
-	switch (*metal_p)
-	{
-		case METALSNICE:
-			break;
-		case METALDEATH:
-			if (metal->tracer)
-				P_RemoveMobj(metal->tracer);
-			P_KillMobj(metal, NULL, NULL, 0);
-			/* FALLTHRU */
-		case DEMOMARKER:
-		default:
-			// end of demo data stream
-			G_StopMetalDemo();
-			return;
-	}
-	metal_p++;
-
-	ziptic = READUINT8(metal_p);
-
-	// Read changes from the tic
-	if (ziptic & GZT_XYZ)
-	{
-		P_TeleportMove(metal, READFIXED(metal_p), READFIXED(metal_p), READFIXED(metal_p));
-		oldmetal.x = metal->x;
-		oldmetal.y = metal->y;
-		oldmetal.z = metal->z;
-	}
-	else
-	{
-		if (ziptic & GZT_MOMXY)
-		{
-			oldmetal.momx = READINT16(metal_p)<<8;
-			oldmetal.momy = READINT16(metal_p)<<8;
-		}
-		if (ziptic & GZT_MOMZ)
-			oldmetal.momz = READINT16(metal_p)<<8;
-		oldmetal.x += oldmetal.momx;
-		oldmetal.y += oldmetal.momy;
-		oldmetal.z += oldmetal.momz;
-	}
-	if (ziptic & GZT_ANGLE)
-		metal->angle = READUINT8(metal_p)<<24;
-	if (ziptic & GZT_FRAME)
-		oldmetal.frame = READUINT32(metal_p);
-	if (ziptic & GZT_SPR2)
-		oldmetal.sprite2 = READUINT8(metal_p);
-
-	// Set movement, position, and angle
-	// oldmetal contains where you're supposed to be.
-	metal->momx = oldmetal.momx;
-	metal->momy = oldmetal.momy;
-	metal->momz = oldmetal.momz;
-	P_UnsetThingPosition(metal);
-	metal->x = oldmetal.x;
-	metal->y = oldmetal.y;
-	metal->z = oldmetal.z;
-	P_SetThingPosition(metal);
-	metal->frame = oldmetal.frame;
-	metal->sprite2 = oldmetal.sprite2;
-
-	if (ziptic & GZT_EXTRA)
-	{ // But wait, there's more!
-		xziptic = READUINT8(metal_p);
-		if (xziptic & EZT_FLIP)
-		{
-			metal->eflags ^= MFE_VERTICALFLIP;
-			metal->flags2 ^= MF2_OBJECTFLIP;
-		}
-		if (xziptic & EZT_SCALE)
-		{
-			metal->destscale = READFIXED(metal_p);
-			if (metal->destscale != metal->scale)
-				P_SetScale(metal, metal->destscale);
-		}
-		if (xziptic & EZT_THOKMASK)
-		{ // Let's only spawn ONE of these per frame, thanks.
-			mobj_t *mobj;
-			INT32 type = -1;
-			if (metal->skin)
-			{
-				skin_t *skin = (skin_t *)metal->skin;
-				switch (xziptic & EZT_THOKMASK)
-				{
-				case EZT_THOK:
-					type = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
-					break;
-				case EZT_SPIN:
-					type = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
-					break;
-				case EZT_REV:
-					type = skin->revitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
-					break;
-				}
-			}
-			if (type != MT_NULL)
-			{
-				if (type == MT_GHOST)
-				{
-					mobj = P_SpawnGhostMobj(metal); // does a large portion of the work for us
-				}
-				else
-				{
-					mobj = P_SpawnMobjFromMobj(metal, 0, 0, -FixedDiv(FixedMul(metal->info->height, metal->scale) - metal->height,3*FRACUNIT), MT_THOK);
-					mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
-					mobj->frame = states[mobjinfo[type].spawnstate].frame;
-					mobj->angle = metal->angle;
-					mobj->color = metal->color;
-					mobj->skin = metal->skin;
-					P_SetScale(mobj, (mobj->destscale = metal->scale));
-
-					if (type == MT_THOK) // spintrail-specific modification for MT_THOK
-					{
-						mobj->frame = FF_TRANS70;
-						mobj->fuse = mobj->tics;
-					}
-					mobj->tics = -1; // nope.
-				}
-				mobj->floorz = mobj->z;
-				mobj->ceilingz = mobj->z+mobj->height;
-				P_UnsetThingPosition(mobj);
-				mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
-				P_SetThingPosition(mobj);
-				if (!mobj->fuse)
-					mobj->fuse = 8;
-				P_SetTarget(&mobj->target, metal);
-			}
-		}
-		if (xziptic & EZT_SPRITE)
-			metal->sprite = READUINT16(metal_p);
-		if (xziptic & EZT_HEIGHT)
-		{
-			fixed_t temp = READINT16(metal_p)<<FRACBITS;
-			metal->height = FixedMul(temp, metal->scale);
-		}
-	}
-
-#define follow metal->tracer
-		if (ziptic & GZT_FOLLOW)
-		{ // Even more...
-			UINT8 followtic = READUINT8(metal_p);
-			fixed_t temp;
-			if (followtic & FZT_SPAWNED)
-			{
-				if (follow)
-					P_RemoveMobj(follow);
-				P_SetTarget(&follow, P_SpawnMobjFromMobj(metal, 0, 0, 0, MT_GHOST));
-				P_SetTarget(&follow->tracer, metal);
-				follow->tics = -1;
-				temp = READINT16(metal_p)<<FRACBITS;
-				follow->height = FixedMul(follow->scale, temp);
-
-				if (followtic & FZT_LINKDRAW)
-					follow->flags2 |= MF2_LINKDRAW;
-
-				if (followtic & FZT_COLORIZED)
-					follow->colorized = true;
-
-				if (followtic & FZT_SKIN)
-					follow->skin = &skins[READUINT8(metal_p)];
-			}
-			if (follow)
-			{
-				if (followtic & FZT_SCALE)
-					follow->destscale = READFIXED(metal_p);
-				else
-					follow->destscale = metal->destscale;
-				if (follow->destscale != follow->scale)
-					P_SetScale(follow, follow->destscale);
-
-				P_UnsetThingPosition(follow);
-				temp = READINT16(metal_p)<<8;
-				follow->x = metal->x + temp;
-				temp = READINT16(metal_p)<<8;
-				follow->y = metal->y + temp;
-				temp = READINT16(metal_p)<<8;
-				follow->z = metal->z + temp;
-				P_SetThingPosition(follow);
-				if (followtic & FZT_SKIN)
-					follow->sprite2 = READUINT8(metal_p);
-				else
-					follow->sprite2 = 0;
-				follow->sprite = READUINT16(metal_p);
-				follow->frame = READUINT32(metal_p); // NOT & FF_FRAMEMASK here, so 32 bits
-				follow->angle = metal->angle;
-				follow->color = READUINT8(metal_p);
-
-				if (!(followtic & FZT_SPAWNED))
-				{
-					if (xziptic & EZT_FLIP)
-					{
-						follow->flags2 ^= MF2_OBJECTFLIP;
-						follow->eflags ^= MFE_VERTICALFLIP;
-					}
-				}
-			}
-		}
-		else if (follow)
-		{
-			P_RemoveMobj(follow);
-			P_SetTarget(&follow, NULL);
-		}
-#undef follow
-}
-
-void G_WriteMetalTic(mobj_t *metal)
-{
-	UINT8 ziptic = 0;
-	UINT8 *ziptic_p;
-	fixed_t height;
-
-	if (!demo_p) // demo_p will be NULL until the race start linedef executor is activated!
-		return;
-
-	WRITEUINT8(demo_p, METALSNICE);
-	ziptic_p = demo_p++; // the ziptic, written at the end of this function
-
-	#define MAXMOM (0xFFFF<<8)
-
-	// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
-	if (abs(metal->x-oldmetal.x) > MAXMOM
-	|| abs(metal->y-oldmetal.y) > MAXMOM
-	|| abs(metal->z-oldmetal.z) > MAXMOM)
-	{
-		oldmetal.x = metal->x;
-		oldmetal.y = metal->y;
-		oldmetal.z = metal->z;
-		ziptic |= GZT_XYZ;
-		WRITEFIXED(demo_p,oldmetal.x);
-		WRITEFIXED(demo_p,oldmetal.y);
-		WRITEFIXED(demo_p,oldmetal.z);
-	}
-	else
-	{
-		// For moving normally:
-		// Store one full byte of movement, plus one byte of fractional movement.
-		INT16 momx = (INT16)((metal->x-oldmetal.x)>>8);
-		INT16 momy = (INT16)((metal->y-oldmetal.y)>>8);
-		if (momx != oldmetal.momx
-		|| momy != oldmetal.momy)
-		{
-			oldmetal.momx = momx;
-			oldmetal.momy = momy;
-			ziptic |= GZT_MOMXY;
-			WRITEINT16(demo_p,momx);
-			WRITEINT16(demo_p,momy);
-		}
-		momx = (INT16)((metal->z-oldmetal.z)>>8);
-		if (momx != oldmetal.momz)
-		{
-			oldmetal.momz = momx;
-			ziptic |= GZT_MOMZ;
-			WRITEINT16(demo_p,momx);
-		}
-
-		// This SHOULD set oldmetal.x/y/z to match metal->x/y/z
-		// but it keeps the fractional loss of one byte,
-		// so it will hopefully be made up for in future tics.
-		oldmetal.x += oldmetal.momx<<8;
-		oldmetal.y += oldmetal.momy<<8;
-		oldmetal.z += oldmetal.momz<<8;
-	}
-
-	#undef MAXMOM
-
-	// Only store the 8 most relevant bits of angle
-	// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
-	// and it does not affect movement at all anyway.
-	if (metal->player && metal->player->drawangle>>24 != oldmetal.angle)
-	{
-		oldmetal.angle = metal->player->drawangle>>24;
-		ziptic |= GZT_ANGLE;
-		WRITEUINT8(demo_p,oldmetal.angle);
-	}
-
-	// Store the sprite frame.
-	if ((metal->frame & FF_FRAMEMASK) != oldmetal.frame)
-	{
-		oldmetal.frame = metal->frame; // NOT & FF_FRAMEMASK here, so 32 bits
-		ziptic |= GZT_FRAME;
-		WRITEUINT32(demo_p,oldmetal.frame);
-	}
-
-	if (metal->sprite == SPR_PLAY
-	&& metal->sprite2 != oldmetal.sprite2)
-	{
-		oldmetal.sprite2 = metal->sprite2;
-		ziptic |= GZT_SPR2;
-		WRITEUINT8(demo_p,oldmetal.sprite2);
-	}
-
-	// Check for sprite set changes
-	if (metal->sprite != oldmetal.sprite)
-	{
-		oldmetal.sprite = metal->sprite;
-		ghostext.flags |= EZT_SPRITE;
-	}
-
-	if ((height = FixedDiv(metal->height, metal->scale)) != oldmetal.height)
-	{
-		oldmetal.height = height;
-		ghostext.flags |= EZT_HEIGHT;
-	}
-
-	if (ghostext.flags & ~(EZT_COLOR|EZT_HIT)) // these two aren't handled by metal ever
-	{
-		ziptic |= GZT_EXTRA;
-
-		if (ghostext.scale == ghostext.lastscale)
-			ghostext.flags &= ~EZT_SCALE;
-
-		WRITEUINT8(demo_p,ghostext.flags);
-		if (ghostext.flags & EZT_SCALE)
-		{
-			WRITEFIXED(demo_p,ghostext.scale);
-			ghostext.lastscale = ghostext.scale;
-		}
-		if (ghostext.flags & EZT_SPRITE)
-			WRITEUINT16(demo_p,oldmetal.sprite);
-		if (ghostext.flags & EZT_HEIGHT)
-		{
-			height >>= FRACBITS;
-			WRITEINT16(demo_p, height);
-		}
-		ghostext.flags = 0;
-	}
-
-	if (metal->player && metal->player->followmobj && !(metal->player->followmobj->sprite == SPR_NULL || (metal->player->followmobj->flags2 & MF2_DONTDRAW)))
-	{
-		INT16 temp;
-		UINT8 *followtic_p = demo_p++;
-		UINT8 followtic = 0;
-
-		ziptic |= GZT_FOLLOW;
-
-		if (metal->player->followmobj->skin)
-			followtic |= FZT_SKIN;
-
-		if (!(oldmetal.flags2 & MF2_AMBUSH))
-		{
-			followtic |= FZT_SPAWNED;
-			WRITEINT16(demo_p,metal->player->followmobj->info->height>>FRACBITS);
-			if (metal->player->followmobj->flags2 & MF2_LINKDRAW)
-				followtic |= FZT_LINKDRAW;
-			if (metal->player->followmobj->colorized)
-				followtic |= FZT_COLORIZED;
-			if (followtic & FZT_SKIN)
-				WRITEUINT8(demo_p,(UINT8)(((skin_t *)(metal->player->followmobj->skin))-skins));
-			oldmetal.flags2 |= MF2_AMBUSH;
-		}
-
-		if (metal->player->followmobj->scale != metal->scale)
-		{
-			followtic |= FZT_SCALE;
-			WRITEFIXED(demo_p,metal->player->followmobj->scale);
-		}
-
-		temp = (INT16)((metal->player->followmobj->x-metal->x)>>8);
-		WRITEINT16(demo_p,temp);
-		temp = (INT16)((metal->player->followmobj->y-metal->y)>>8);
-		WRITEINT16(demo_p,temp);
-		temp = (INT16)((metal->player->followmobj->z-metal->z)>>8);
-		WRITEINT16(demo_p,temp);
-		if (followtic & FZT_SKIN)
-			WRITEUINT8(demo_p,metal->player->followmobj->sprite2);
-		WRITEUINT16(demo_p,metal->player->followmobj->sprite);
-		WRITEUINT32(demo_p,metal->player->followmobj->frame); // NOT & FF_FRAMEMASK here, so 32 bits
-		WRITEUINT8(demo_p,metal->player->followmobj->color);
-
-		*followtic_p = followtic;
-	}
-	else
-		oldmetal.flags2 &= ~MF2_AMBUSH;
-
-	*ziptic_p = ziptic;
-
-	// attention here for the ticcmd size!
-	// latest demos with mouse aiming byte in ticcmd
-	if (demo_p >= demoend - 32)
-	{
-		G_StopMetalRecording(false); // no more space
-		return;
-	}
-}
-
-//
-// G_RecordDemo
-//
-void G_RecordDemo(const char *name)
-{
-	INT32 maxsize;
-
-	strcpy(demoname, name);
-	strcat(demoname, ".lmp");
-	maxsize = 1024*1024;
-	if (M_CheckParm("-maxdemo") && M_IsNextParm())
-		maxsize = atoi(M_GetNextParm()) * 1024;
-//	if (demobuffer)
-//		free(demobuffer);
-	demo_p = NULL;
-	demobuffer = malloc(maxsize);
-	demoend = demobuffer + maxsize;
-
-	demorecording = true;
-}
-
-void G_RecordMetal(void)
-{
-	INT32 maxsize;
-	maxsize = 1024*1024;
-	if (M_CheckParm("-maxdemo") && M_IsNextParm())
-		maxsize = atoi(M_GetNextParm()) * 1024;
-	demo_p = NULL;
-	demobuffer = malloc(maxsize);
-	demoend = demobuffer + maxsize;
-	metalrecording = true;
-}
-
-void G_BeginRecording(void)
-{
-	UINT8 i;
-	char name[16];
-	player_t *player = &players[consoleplayer];
-
-	if (demo_p)
-		return;
-	memset(name,0,sizeof(name));
-
-	demo_p = demobuffer;
-	demoflags = DF_GHOST|(modeattacking<<DF_ATTACKSHIFT);
-
-	// Setup header.
-	M_Memcpy(demo_p, DEMOHEADER, 12); demo_p += 12;
-	WRITEUINT8(demo_p,VERSION);
-	WRITEUINT8(demo_p,SUBVERSION);
-	WRITEUINT16(demo_p,DEMOVERSION);
-
-	// demo checksum
-	demo_p += 16;
-
-	// game data
-	M_Memcpy(demo_p, "PLAY", 4); demo_p += 4;
-	WRITEINT16(demo_p,gamemap);
-	M_Memcpy(demo_p, mapmd5, 16); demo_p += 16;
-
-	WRITEUINT8(demo_p,demoflags);
-	switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
-	{
-	case ATTACKING_NONE: // 0
-		break;
-	case ATTACKING_RECORD: // 1
-		demotime_p = demo_p;
-		WRITEUINT32(demo_p,UINT32_MAX); // time
-		WRITEUINT32(demo_p,0); // score
-		WRITEUINT16(demo_p,0); // rings
-		break;
-	case ATTACKING_NIGHTS: // 2
-		demotime_p = demo_p;
-		WRITEUINT32(demo_p,UINT32_MAX); // time
-		WRITEUINT32(demo_p,0); // score
-		break;
-	default: // 3
-		break;
-	}
-
-	WRITEUINT32(demo_p,P_GetInitSeed());
-
-	// Name
-	for (i = 0; i < 16 && cv_playername.string[i]; i++)
-		name[i] = cv_playername.string[i];
-	for (; i < 16; i++)
-		name[i] = '\0';
-	M_Memcpy(demo_p,name,16);
-	demo_p += 16;
-
-	// Skin
-	for (i = 0; i < 16 && cv_skin.string[i]; i++)
-		name[i] = cv_skin.string[i];
-	for (; i < 16; i++)
-		name[i] = '\0';
-	M_Memcpy(demo_p,name,16);
-	demo_p += 16;
-
-	// Color
-	for (i = 0; i < 16 && cv_playercolor.string[i]; i++)
-		name[i] = cv_playercolor.string[i];
-	for (; i < 16; i++)
-		name[i] = '\0';
-	M_Memcpy(demo_p,name,16);
-	demo_p += 16;
-
-	// Stats
-	WRITEUINT8(demo_p,player->charability);
-	WRITEUINT8(demo_p,player->charability2);
-	WRITEUINT8(demo_p,player->actionspd>>FRACBITS);
-	WRITEUINT8(demo_p,player->mindash>>FRACBITS);
-	WRITEUINT8(demo_p,player->maxdash>>FRACBITS);
-	WRITEUINT8(demo_p,player->normalspeed>>FRACBITS);
-	WRITEUINT8(demo_p,player->runspeed>>FRACBITS);
-	WRITEUINT8(demo_p,player->thrustfactor);
-	WRITEUINT8(demo_p,player->accelstart);
-	WRITEUINT8(demo_p,player->acceleration);
-	WRITEUINT8(demo_p,player->height>>FRACBITS);
-	WRITEUINT8(demo_p,player->spinheight>>FRACBITS);
-	WRITEUINT8(demo_p,player->camerascale>>FRACBITS);
-	WRITEUINT8(demo_p,player->shieldscale>>FRACBITS);
-
-	// Trying to convert it back to % causes demo desync due to precision loss.
-	// Don't do it.
-	WRITEFIXED(demo_p, player->jumpfactor);
-
-	// And mobjtype_t is best with UINT32 too...
-	WRITEUINT32(demo_p, player->followitem);
-
-	// Save pflag data - see SendWeaponPref()
-	{
-		UINT8 buf = 0;
-		pflags_t pflags = 0;
-		if (cv_flipcam.value)
-		{
-			buf |= 0x01;
-			pflags |= PF_FLIPCAM;
-		}
-		if (cv_analog[0].value)
-		{
-			buf |= 0x02;
-			pflags |= PF_ANALOGMODE;
-		}
-		if (cv_directionchar[0].value)
-		{
-			buf |= 0x04;
-			pflags |= PF_DIRECTIONCHAR;
-		}
-		if (cv_autobrake.value)
-		{
-			buf |= 0x08;
-			pflags |= PF_AUTOBRAKE;
-		}
-		if (cv_usejoystick.value)
-			buf |= 0x10;
-		CV_SetValue(&cv_showinputjoy, !!(cv_usejoystick.value));
-
-		WRITEUINT8(demo_p,buf);
-		player->pflags = pflags;
-	}
-
-	// Save netvar data
-	CV_SaveNetVars(&demo_p);
-
-	memset(&oldcmd,0,sizeof(oldcmd));
-	memset(&oldghost,0,sizeof(oldghost));
-	memset(&ghostext,0,sizeof(ghostext));
-	ghostext.lastcolor = ghostext.color = GHC_NORMAL;
-	ghostext.lastscale = ghostext.scale = FRACUNIT;
-
-	if (player->mo)
-	{
-		oldghost.x = player->mo->x;
-		oldghost.y = player->mo->y;
-		oldghost.z = player->mo->z;
-		oldghost.angle = player->mo->angle>>24;
-
-		// preticker started us gravity flipped
-		if (player->mo->eflags & MFE_VERTICALFLIP)
-			ghostext.flags |= EZT_FLIP;
-	}
-}
-
-void G_BeginMetal(void)
-{
-	mobj_t *mo = players[consoleplayer].mo;
-
-#if 0
-	if (demo_p)
-		return;
-#endif
-
-	demo_p = demobuffer;
-
-	// Write header.
-	M_Memcpy(demo_p, DEMOHEADER, 12); demo_p += 12;
-	WRITEUINT8(demo_p,VERSION);
-	WRITEUINT8(demo_p,SUBVERSION);
-	WRITEUINT16(demo_p,DEMOVERSION);
-
-	// demo checksum
-	demo_p += 16;
-
-	M_Memcpy(demo_p, "METL", 4); demo_p += 4;
-
-	memset(&ghostext,0,sizeof(ghostext));
-	ghostext.lastscale = ghostext.scale = FRACUNIT;
-
-	// Set up our memory.
-	memset(&oldmetal,0,sizeof(oldmetal));
-	oldmetal.x = mo->x;
-	oldmetal.y = mo->y;
-	oldmetal.z = mo->z;
-	oldmetal.angle = mo->angle>>24;
-}
-
-void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings)
-{
-	if (!demorecording || !demotime_p)
-		return;
-	if (demoflags & DF_RECORDATTACK)
-	{
-		WRITEUINT32(demotime_p, ptime);
-		WRITEUINT32(demotime_p, pscore);
-		WRITEUINT16(demotime_p, prings);
-		demotime_p = NULL;
-	}
-	else if (demoflags & DF_NIGHTSATTACK)
-	{
-		WRITEUINT32(demotime_p, ptime);
-		WRITEUINT32(demotime_p, pscore);
-		demotime_p = NULL;
-	}
-}
-
-// Returns bitfield:
-// 1 == new demo has lower time
-// 2 == new demo has higher score
-// 4 == new demo has higher rings
-UINT8 G_CmpDemoTime(char *oldname, char *newname)
-{
-	UINT8 *buffer,*p;
-	UINT8 flags;
-	UINT32 oldtime, newtime, oldscore, newscore;
-	UINT16 oldrings, newrings, oldversion;
-	size_t bufsize ATTRUNUSED;
-	UINT8 c;
-	UINT16 s ATTRUNUSED;
-	UINT8 aflags = 0;
-
-	// load the new file
-	FIL_DefaultExtension(newname, ".lmp");
-	bufsize = FIL_ReadFile(newname, &buffer);
-	I_Assert(bufsize != 0);
-	p = buffer;
-
-	// read demo header
-	I_Assert(!memcmp(p, DEMOHEADER, 12));
-	p += 12; // DEMOHEADER
-	c = READUINT8(p); // VERSION
-	I_Assert(c == VERSION);
-	c = READUINT8(p); // SUBVERSION
-	I_Assert(c == SUBVERSION);
-	s = READUINT16(p);
-	I_Assert(s == DEMOVERSION);
-	p += 16; // demo checksum
-	I_Assert(!memcmp(p, "PLAY", 4));
-	p += 4; // PLAY
-	p += 2; // gamemap
-	p += 16; // map md5
-	flags = READUINT8(p); // demoflags
-
-	aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK);
-	I_Assert(aflags);
-	if (flags & DF_RECORDATTACK)
-	{
-		newtime = READUINT32(p);
-		newscore = READUINT32(p);
-		newrings = READUINT16(p);
-	}
-	else if (flags & DF_NIGHTSATTACK)
-	{
-		newtime = READUINT32(p);
-		newscore = READUINT32(p);
-		newrings = 0;
-	}
-	else // appease compiler
-		return 0;
-
-	Z_Free(buffer);
-
-	// load old file
-	FIL_DefaultExtension(oldname, ".lmp");
-	if (!FIL_ReadFile(oldname, &buffer))
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), oldname);
-		return UINT8_MAX;
-	}
-	p = buffer;
-
-	// read demo header
-	if (memcmp(p, DEMOHEADER, 12))
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
-		Z_Free(buffer);
-		return UINT8_MAX;
-	} p += 12; // DEMOHEADER
-	p++; // VERSION
-	p++; // SUBVERSION
-	oldversion = READUINT16(p);
-	switch(oldversion) // demoversion
-	{
-	case DEMOVERSION: // latest always supported
-		break;
-	// too old, cannot support.
-	default:
-		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
-		Z_Free(buffer);
-		return UINT8_MAX;
-	}
-	p += 16; // demo checksum
-	if (memcmp(p, "PLAY", 4))
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
-		Z_Free(buffer);
-		return UINT8_MAX;
-	} p += 4; // "PLAY"
-	if (oldversion <= 0x0008)
-		p++; // gamemap
-	else
-		p += 2; // gamemap
-	p += 16; // mapmd5
-	flags = READUINT8(p);
-	if (!(flags & aflags))
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname);
-		Z_Free(buffer);
-		return UINT8_MAX;
-	}
-	if (flags & DF_RECORDATTACK)
-	{
-		oldtime = READUINT32(p);
-		oldscore = READUINT32(p);
-		oldrings = READUINT16(p);
-	}
-	else if (flags & DF_NIGHTSATTACK)
-	{
-		oldtime = READUINT32(p);
-		oldscore = READUINT32(p);
-		oldrings = 0;
-	}
-	else // appease compiler
-		return UINT8_MAX;
-
-	Z_Free(buffer);
-
-	c = 0;
-	if (newtime < oldtime
-	|| (newtime == oldtime && (newscore > oldscore || newrings > oldrings)))
-		c |= 1; // Better time
-	if (newscore > oldscore
-	|| (newscore == oldscore && newtime < oldtime))
-		c |= 1<<1; // Better score
-	if (newrings > oldrings
-	|| (newrings == oldrings && newtime < oldtime))
-		c |= 1<<2; // Better rings
-	return c;
-}
-
-//
-// G_PlayDemo
-//
-void G_DeferedPlayDemo(const char *name)
-{
-	COM_BufAddText("playdemo \"");
-	COM_BufAddText(name);
-	COM_BufAddText("\"\n");
-}
-
-//
-// Start a demo from a .LMP file or from a wad resource
-//
-void G_DoPlayDemo(char *defdemoname)
-{
-	UINT8 i;
-	lumpnum_t l;
-	char skin[17],color[17],*n,*pdemoname;
-	UINT8 version,subversion,charability,charability2,thrustfactor,accelstart,acceleration;
-	pflags_t pflags;
-	UINT32 randseed, followitem;
-	fixed_t camerascale,shieldscale,actionspd,mindash,maxdash,normalspeed,runspeed,jumpfactor,height,spinheight;
-	char msg[1024];
-
-	skin[16] = '\0';
-	color[16] = '\0';
-
-	n = defdemoname+strlen(defdemoname);
-	while (*n != '/' && *n != '\\' && n != defdemoname)
-		n--;
-	if (n != defdemoname)
-		n++;
-	pdemoname = ZZ_Alloc(strlen(n)+1);
-	strcpy(pdemoname,n);
-
-	// Internal if no extension, external if one exists
-	if (FIL_CheckExtension(defdemoname))
-	{
-		//FIL_DefaultExtension(defdemoname, ".lmp");
-		if (!FIL_ReadFile(defdemoname, &demobuffer))
-		{
-			snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname);
-			CONS_Alert(CONS_ERROR, "%s", msg);
-			gameaction = ga_nothing;
-			M_StartMessage(msg, NULL, MM_NOTHING);
-			return;
-		}
-		demo_p = demobuffer;
-	}
-	// load demo resource from WAD
-	else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
-	{
-		snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		gameaction = ga_nothing;
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		return;
-	}
-	else // it's an internal demo
-		demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC);
-
-	// read demo header
-	gameaction = ga_nothing;
-	demoplayback = true;
-	if (memcmp(demo_p, DEMOHEADER, 12))
-	{
-		snprintf(msg, 1024, M_GetText("%s is not a SRB2 replay file.\n"), pdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		Z_Free(pdemoname);
-		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
-		return;
-	}
-	demo_p += 12; // DEMOHEADER
-
-	version = READUINT8(demo_p);
-	subversion = READUINT8(demo_p);
-	demoversion = READUINT16(demo_p);
-	switch(demoversion)
-	{
-	case DEMOVERSION: // latest always supported
-		break;
-	// too old, cannot support.
-	default:
-		snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		Z_Free(pdemoname);
-		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
-		return;
-	}
-	demo_p += 16; // demo checksum
-	if (memcmp(demo_p, "PLAY", 4))
-	{
-		snprintf(msg, 1024, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		Z_Free(pdemoname);
-		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
-		return;
-	}
-	demo_p += 4; // "PLAY"
-	gamemap = READINT16(demo_p);
-	demo_p += 16; // mapmd5
-
-	demoflags = READUINT8(demo_p);
-	modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
-	CON_ToggleOff();
-
-	hu_demoscore = 0;
-	hu_demotime = UINT32_MAX;
-	hu_demorings = 0;
-
-	switch (modeattacking)
-	{
-	case ATTACKING_NONE: // 0
-		break;
-	case ATTACKING_RECORD: // 1
-		hu_demotime  = READUINT32(demo_p);
-		hu_demoscore = READUINT32(demo_p);
-		hu_demorings = READUINT16(demo_p);
-		break;
-	case ATTACKING_NIGHTS: // 2
-		hu_demotime  = READUINT32(demo_p);
-		hu_demoscore = READUINT32(demo_p);
-		break;
-	default: // 3
-		modeattacking = ATTACKING_NONE;
-		break;
-	}
-
-	// Random seed
-	randseed = READUINT32(demo_p);
-
-	// Player name
-	M_Memcpy(player_names[0],demo_p,16);
-	demo_p += 16;
-
-	// Skin
-	M_Memcpy(skin,demo_p,16);
-	demo_p += 16;
-
-	// Color
-	M_Memcpy(color,demo_p,16);
-	demo_p += 16;
-
-	charability = READUINT8(demo_p);
-	charability2 = READUINT8(demo_p);
-	actionspd = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	mindash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	maxdash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	normalspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	runspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	thrustfactor = READUINT8(demo_p);
-	accelstart = READUINT8(demo_p);
-	acceleration = READUINT8(demo_p);
-	height = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	spinheight = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	camerascale = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	shieldscale = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	jumpfactor = READFIXED(demo_p);
-	followitem = READUINT32(demo_p);
-
-	// pflag data
-	{
-		UINT8 buf = READUINT8(demo_p);
-		pflags = 0;
-		if (buf & 0x01)
-			pflags |= PF_FLIPCAM;
-		if (buf & 0x02)
-			pflags |= PF_ANALOGMODE;
-		if (buf & 0x04)
-			pflags |= PF_DIRECTIONCHAR;
-		if (buf & 0x08)
-			pflags |= PF_AUTOBRAKE;
-		CV_SetValue(&cv_showinputjoy, !!(buf & 0x10));
-	}
-
-	// net var data
-	CV_LoadNetVars(&demo_p);
-
-	// Sigh ... it's an empty demo.
-	if (*demo_p == DEMOMARKER)
-	{
-		snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		Z_Free(pdemoname);
-		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
-		return;
-	}
-
-	Z_Free(pdemoname);
-
-	memset(&oldcmd,0,sizeof(oldcmd));
-	memset(&oldghost,0,sizeof(oldghost));
-
-	if (VERSION != version || SUBVERSION != subversion)
-		CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n"));
-
-	// didn't start recording right away.
-	demo_start = false;
-
-	// Set skin
-	SetPlayerSkin(0, skin);
-
-	LUAh_MapChange(gamemap);
-	displayplayer = consoleplayer = 0;
-	memset(playeringame,0,sizeof(playeringame));
-	playeringame[0] = true;
-	P_SetRandSeed(randseed);
-	G_InitNew(false, G_BuildMapName(gamemap), true, true, false);
-
-	// Set color
-	for (i = 0; i < MAXSKINCOLORS; i++)
-		if (!stricmp(Color_Names[i],color))
-		{
-			players[0].skincolor = i;
-			break;
-		}
-	CV_StealthSetValue(&cv_playercolor, players[0].skincolor);
-	if (players[0].mo)
-	{
-		players[0].mo->color = players[0].skincolor;
-		oldghost.x = players[0].mo->x;
-		oldghost.y = players[0].mo->y;
-		oldghost.z = players[0].mo->z;
-	}
-
-	// Set saved attribute values
-	// No cheat checking here, because even if they ARE wrong...
-	// it would only break the replay if we clipped them.
-	players[0].camerascale = camerascale;
-	players[0].shieldscale = shieldscale;
-	players[0].charability = charability;
-	players[0].charability2 = charability2;
-	players[0].actionspd = actionspd;
-	players[0].mindash = mindash;
-	players[0].maxdash = maxdash;
-	players[0].normalspeed = normalspeed;
-	players[0].runspeed = runspeed;
-	players[0].thrustfactor = thrustfactor;
-	players[0].accelstart = accelstart;
-	players[0].acceleration = acceleration;
-	players[0].height = height;
-	players[0].spinheight = spinheight;
-	players[0].jumpfactor = jumpfactor;
-	players[0].followitem = followitem;
-	players[0].pflags = pflags;
-
-	demo_start = true;
-}
-
-void G_AddGhost(char *defdemoname)
-{
-	INT32 i;
-	lumpnum_t l;
-	char name[17],skin[17],color[17],*n,*pdemoname,md5[16];
-	demoghost *gh;
-	UINT8 flags;
-	UINT8 *buffer,*p;
-	mapthing_t *mthing;
-	UINT16 count, ghostversion;
-
-	name[16] = '\0';
-	skin[16] = '\0';
-	color[16] = '\0';
-
-	n = defdemoname+strlen(defdemoname);
-	while (*n != '/' && *n != '\\' && n != defdemoname)
-		n--;
-	if (n != defdemoname)
-		n++;
-	pdemoname = ZZ_Alloc(strlen(n)+1);
-	strcpy(pdemoname,n);
-
-	// Internal if no extension, external if one exists
-	if (FIL_CheckExtension(defdemoname))
-	{
-		//FIL_DefaultExtension(defdemoname, ".lmp");
-		if (!FIL_ReadFileTag(defdemoname, &buffer, PU_LEVEL))
-		{
-			CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), defdemoname);
-			Z_Free(pdemoname);
-			return;
-		}
-		p = buffer;
-	}
-	// load demo resource from WAD
-	else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
-		Z_Free(pdemoname);
-		return;
-	}
-	else // it's an internal demo
-		buffer = p = W_CacheLumpNum(l, PU_LEVEL);
-
-	// read demo header
-	if (memcmp(p, DEMOHEADER, 12))
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Not a SRB2 replay.\n"), pdemoname);
-		Z_Free(pdemoname);
-		Z_Free(buffer);
-		return;
-	} p += 12; // DEMOHEADER
-	p++; // VERSION
-	p++; // SUBVERSION
-	ghostversion = READUINT16(p);
-	switch(ghostversion)
-	{
-	case DEMOVERSION: // latest always supported
-		break;
-	// too old, cannot support.
-	default:
-		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
-		Z_Free(pdemoname);
-		Z_Free(buffer);
-		return;
-	}
-	M_Memcpy(md5, p, 16); p += 16; // demo checksum
-	for (gh = ghosts; gh; gh = gh->next)
-		if (!memcmp(md5, gh->checksum, 16)) // another ghost in the game already has this checksum?
-		{ // Don't add another one, then!
-			CONS_Debug(DBG_SETUP, "Rejecting duplicate ghost %s (MD5 was matched)\n", pdemoname);
-			Z_Free(pdemoname);
-			Z_Free(buffer);
-			return;
-		}
-	if (memcmp(p, "PLAY", 4))
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo format unacceptable.\n"), pdemoname);
-		Z_Free(pdemoname);
-		Z_Free(buffer);
-		return;
-	} p += 4; // "PLAY"
-	if (ghostversion <= 0x0008)
-		p++; // gamemap
-	else
-		p += 2; // gamemap
-	p += 16; // mapmd5 (possibly check for consistency?)
-	flags = READUINT8(p);
-	if (!(flags & DF_GHOST))
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: No ghost data in this demo.\n"), pdemoname);
-		Z_Free(pdemoname);
-		Z_Free(buffer);
-		return;
-	}
-	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
-	{
-	case ATTACKING_NONE: // 0
-		break;
-	case ATTACKING_RECORD: // 1
-		p += 10; // demo time, score, and rings
-		break;
-	case ATTACKING_NIGHTS: // 2
-		p += 8; // demo time left, score
-		break;
-	default: // 3
-		break;
-	}
-
-	p += 4; // random seed
-
-	// Player name (TODO: Display this somehow if it doesn't match cv_playername!)
-	M_Memcpy(name, p,16);
-	p += 16;
-
-	// Skin
-	M_Memcpy(skin, p,16);
-	p += 16;
-
-	// Color
-	M_Memcpy(color, p,16);
-	p += 16;
-
-	// Ghosts do not have a player structure to put this in.
-	p++; // charability
-	p++; // charability2
-	p++; // actionspd
-	p++; // mindash
-	p++; // maxdash
-	p++; // normalspeed
-	p++; // runspeed
-	p++; // thrustfactor
-	p++; // accelstart
-	p++; // acceleration
-	p++; // height
-	p++; // spinheight
-	p++; // camerascale
-	p++; // shieldscale
-	p += 4; // jumpfactor
-	p += 4; // followitem
-
-	p++; // pflag data
-
-	// net var data
-	count = READUINT16(p);
-	while (count--)
-	{
-		p += 2;
-		SKIPSTRING(p);
-		p++;
-	}
-
-	if (*p == DEMOMARKER)
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname);
-		Z_Free(pdemoname);
-		Z_Free(buffer);
-		return;
-	}
-
-	gh = Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL);
-	gh->next = ghosts;
-	gh->buffer = buffer;
-	M_Memcpy(gh->checksum, md5, 16);
-	gh->p = p;
-
-	ghosts = gh;
-
-	gh->version = ghostversion;
-	mthing = playerstarts[0];
-	I_Assert(mthing);
-	{ // A bit more complex than P_SpawnPlayer because ghosts aren't solid and won't just push themselves out of the ceiling.
-		fixed_t z,f,c;
-		fixed_t offset = mthing->z << FRACBITS;
-		gh->mo = P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST);
-		gh->mo->angle = FixedAngle(mthing->angle << FRACBITS);
-		f = gh->mo->floorz;
-		c = gh->mo->ceilingz - mobjinfo[MT_PLAYER].height;
-		if (!!(mthing->options & MTF_AMBUSH) ^ !!(mthing->options & MTF_OBJECTFLIP))
-		{
-			z = c - offset;
-			if (z < f)
-				z = f;
-		}
-		else
-		{
-			z = f + offset;
-			if (z > c)
-				z = c;
-		}
-		gh->mo->z = z;
-	}
-
-	gh->oldmo.x = gh->mo->x;
-	gh->oldmo.y = gh->mo->y;
-	gh->oldmo.z = gh->mo->z;
-
-	// Set skin
-	gh->mo->skin = &skins[0];
-	for (i = 0; i < numskins; i++)
-		if (!stricmp(skins[i].name,skin))
-		{
-			gh->mo->skin = &skins[i];
-			break;
-		}
-	gh->oldmo.skin = gh->mo->skin;
-
-	// Set color
-	gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor;
-	for (i = 0; i < MAXSKINCOLORS; i++)
-		if (!stricmp(Color_Names[i],color))
-		{
-			gh->mo->color = (UINT8)i;
-			break;
-		}
-	gh->oldmo.color = gh->mo->color;
-
-	gh->mo->state = states+S_PLAY_STND;
-	gh->mo->sprite = gh->mo->state->sprite;
-	gh->mo->sprite2 = (gh->mo->state->frame & FF_FRAMEMASK);
-	//gh->mo->frame = tr_trans30<<FF_TRANSSHIFT;
-	gh->mo->flags2 |= MF2_DONTDRAW;
-	gh->fadein = (9-3)*6; // fade from invisible to trans30 over as close to 35 tics as possible
-	gh->mo->tics = -1;
-
-	CONS_Printf(M_GetText("Added ghost %s from %s\n"), name, pdemoname);
-	Z_Free(pdemoname);
-}
-
-//
-// G_TimeDemo
-// NOTE: name is a full filename for external demos
-//
-static INT32 restorecv_vidwait;
-
-void G_TimeDemo(const char *name)
-{
-	nodrawers = M_CheckParm("-nodraw");
-	noblit = M_CheckParm("-noblit");
-	restorecv_vidwait = cv_vidwait.value;
-	if (cv_vidwait.value)
-		CV_Set(&cv_vidwait, "0");
-	timingdemo = true;
-	singletics = true;
-	framecount = 0;
-	demostarttime = I_GetTime();
-	G_DeferedPlayDemo(name);
-}
-
-void G_DoPlayMetal(void)
-{
-	lumpnum_t l;
-	mobj_t *mo = NULL;
-	thinker_t *th;
-
-	// it's an internal demo
-	if ((l = W_CheckNumForName(va("%sMS",G_BuildMapName(gamemap)))) == LUMPERROR)
-	{
-		CONS_Alert(CONS_WARNING, M_GetText("No bot recording for this map.\n"));
-		return;
-	}
-	else
-		metalbuffer = metal_p = W_CacheLumpNum(l, PU_STATIC);
-
-	// find metal sonic
-	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
-	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-			continue;
-
-		mo = (mobj_t *)th;
-		if (mo->type != MT_METALSONIC_RACE)
-			continue;
-
-		break;
-	}
-	if (th == &thlist[THINK_MOBJ])
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("Failed to find bot entity.\n"));
-		Z_Free(metalbuffer);
-		return;
-	}
-
-	// read demo header
-    metal_p += 12; // DEMOHEADER
-	metal_p++; // VERSION
-	metal_p++; // SUBVERSION
-	metalversion = READUINT16(metal_p);
-	switch(metalversion)
-	{
-	case DEMOVERSION: // latest always supported
-		break;
-	// too old, cannot support.
-	default:
-		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n"));
-		Z_Free(metalbuffer);
-		return;
-	}
-	metal_p += 16; // demo checksum
-	if (memcmp(metal_p, "METL", 4))
-	{
-		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, wasn't recorded in Metal format.\n"));
-		Z_Free(metalbuffer);
-		return;
-	} metal_p += 4; // "METL"
-
-	// read initial tic
-	memset(&oldmetal,0,sizeof(oldmetal));
-	oldmetal.x = mo->x;
-	oldmetal.y = mo->y;
-	oldmetal.z = mo->z;
-	metalplayback = mo;
-}
-
-void G_DoneLevelLoad(void)
-{
-	CONS_Printf(M_GetText("Loaded level in %f sec\n"), (double)(I_GetTime() - demostarttime) / TICRATE);
-	framecount = 0;
-	demostarttime = I_GetTime();
-}
-
-/*
-===================
-=
-= G_CheckDemoStatus
-=
-= Called after a death or level completion to allow demos to be cleaned up
-= Returns true if a new demo loop action will take place
-===================
-*/
-
-// Stops metal sonic's demo. Separate from other functions because metal + replays can coexist
-void G_StopMetalDemo(void)
-{
-
-	// Metal Sonic finishing doesn't end the game, dammit.
-	Z_Free(metalbuffer);
-	metalbuffer = NULL;
-	metalplayback = NULL;
-	metal_p = NULL;
-}
-
-// Stops metal sonic recording.
-ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill)
-{
-	boolean saved = false;
-	if (demo_p)
-	{
-		UINT8 *p = demobuffer+16; // checksum position
-		if (kill)
-			WRITEUINT8(demo_p, METALDEATH); // add the metal death marker
-		else
-			WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
-#ifdef NOMD5
-		{
-			UINT8 i;
-			for (i = 0; i < 16; i++, p++)
-				*p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct.
-		}
-#else
-		md5_buffer((char *)p+16, demo_p - (p+16), (void *)p); // make a checksum of everything after the checksum in the file.
-#endif
-		saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuffer, demo_p - demobuffer); // finally output the file.
-	}
-	free(demobuffer);
-	metalrecording = false;
-	if (saved)
-		I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
-	I_Error("Failed to save demo!");
-}
-
-// reset engine variable set for the demos
-// called from stopdemo command, map command, and g_checkdemoStatus.
-void G_StopDemo(void)
-{
-	Z_Free(demobuffer);
-	demobuffer = NULL;
-	demoplayback = false;
-	titledemo = false;
-	timingdemo = false;
-	singletics = false;
-
-	if (gamestate == GS_INTERMISSION)
-		Y_EndIntermission(); // cleanup
-
-	G_SetGamestate(GS_NULL);
-	wipegamestate = GS_NULL;
-	SV_StopServer();
-	SV_ResetServer();
-}
-
-boolean G_CheckDemoStatus(void)
-{
-	boolean saved;
-
-	while (ghosts)
-	{
-		demoghost *next = ghosts->next;
-		Z_Free(ghosts);
-		ghosts = next;
-	}
-	ghosts = NULL;
-
-
-	// DO NOT end metal sonic demos here
-
-	if (timingdemo)
-	{
-		INT32 demotime;
-		double f1, f2;
-		demotime = I_GetTime() - demostarttime;
-		if (!demotime)
-			return true;
-		G_StopDemo();
-		timingdemo = false;
-		f1 = (double)demotime;
-		f2 = (double)framecount*TICRATE;
-
-		CONS_Printf(M_GetText("timed %u gametics in %d realtics - %u frames\n%f seconds, %f avg fps\n"),
-			leveltime,demotime,(UINT32)framecount,f1/TICRATE,f2/f1);
-
-		// CSV-readable timedemo results, for external parsing
-		if (timedemo_csv)
-		{
-			FILE *f;
-			const char *csvpath = va("%s"PATHSEP"%s", srb2home, "timedemo.csv");
-			const char *header = "id,demoname,seconds,avgfps,leveltime,demotime,framecount,ticrate,rendermode,vidmode,vidwidth,vidheight,procbits\n";
-			const char *rowformat = "\"%s\",\"%s\",%f,%f,%u,%d,%u,%u,%u,%u,%u,%u,%u\n";
-			boolean headerrow = !FIL_FileExists(csvpath);
-			UINT8 procbits = 0;
-
-			// Bitness
-			if (sizeof(void*) == 4)
-				procbits = 32;
-			else if (sizeof(void*) == 8)
-				procbits = 64;
-
-			f = fopen(csvpath, "a+");
-
-			if (f)
-			{
-				if (headerrow)
-					fputs(header, f);
-				fprintf(f, rowformat,
-					timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits);
-				fclose(f);
-				CONS_Printf("Timedemo results saved to '%s'\n", csvpath);
-			}
-			else
-			{
-				// Just print the CSV output to console
-				CON_LogMessage(header);
-				CONS_Printf(rowformat,
-					timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits);
-			}
-		}
-
-		if (restorecv_vidwait != cv_vidwait.value)
-			CV_SetValue(&cv_vidwait, restorecv_vidwait);
-		D_AdvanceDemo();
-		return true;
-	}
-
-	if (demoplayback)
-	{
-		if (singledemo)
-			I_Quit();
-		G_StopDemo();
-
-		if (modeattacking)
-			M_EndModeAttackRun();
-		else
-			D_AdvanceDemo();
-
-		return true;
-	}
-
-	if (demorecording)
-	{
-		UINT8 *p = demobuffer+16; // checksum position
-#ifdef NOMD5
-		UINT8 i;
-		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
-		for (i = 0; i < 16; i++, p++)
-			*p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct.
-#else
-		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
-		md5_buffer((char *)p+16, demo_p - (p+16), p); // make a checksum of everything after the checksum in the file.
-#endif
-		saved = FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer); // finally output the file.
-		free(demobuffer);
-		demorecording = false;
-
-		if (modeattacking != ATTACKING_RECORD)
-		{
-			if (saved)
-				CONS_Printf(M_GetText("Demo %s recorded\n"), demoname);
-			else
-				CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname);
-		}
-		return true;
-	}
-
-	return false;
-}
-
 //
 // G_SetGamestate
 //
diff --git a/src/g_game.h b/src/g_game.h
index 8a2c5b0aedce18feb0a6f4514e465294be65db94..df0c9392e69c87cf3b139e29f97f34d950b28af2 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -17,6 +17,7 @@
 #include "doomdef.h"
 #include "doomstat.h"
 #include "d_event.h"
+#include "g_demo.h"
 
 extern char gamedatafilename[64];
 extern char timeattackfolder[64];
@@ -31,21 +32,6 @@ extern char player_names[MAXPLAYERS][MAXPLAYERNAME+1];
 extern player_t players[MAXPLAYERS];
 extern boolean playeringame[MAXPLAYERS];
 
-// ======================================
-// DEMO playback/recording related stuff.
-// ======================================
-
-// demoplaying back and demo recording
-extern boolean demoplayback, titledemo, demorecording, timingdemo;
-extern tic_t demostarttime;
-
-// Quit after playing a demo from cmdline.
-extern boolean singledemo;
-extern boolean demo_start;
-extern boolean demosynced;
-
-extern mobj_t *metalplayback;
-
 // gametic at level start
 extern tic_t levelstarttic;
 
@@ -173,7 +159,6 @@ void G_DoLoadLevel(boolean resetplayer);
 void G_StartTitleCard(void);
 void G_PreLevelTitleCard(void);
 boolean G_IsTitleCardAvailable(void);
-void G_DeferedPlayDemo(const char *demo);
 
 // Can be called by the startup code or M_Responder, calls P_SetupLevel.
 void G_LoadGame(UINT32 slot, INT16 mapoverride);
@@ -184,54 +169,6 @@ void G_SaveGame(UINT32 slot);
 
 void G_SaveGameOver(UINT32 slot, boolean modifylives);
 
-// Only called by startup code.
-void G_RecordDemo(const char *name);
-void G_RecordMetal(void);
-void G_BeginRecording(void);
-void G_BeginMetal(void);
-
-// Only called by shutdown code.
-void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings);
-UINT8 G_CmpDemoTime(char *oldname, char *newname);
-
-typedef enum
-{
-	GHC_NORMAL = 0,
-	GHC_SUPER,
-	GHC_FIREFLOWER,
-	GHC_INVINCIBLE,
-	GHC_NIGHTSSKIN, // not actually a colour
-	GHC_RETURNSKIN // ditto
-} ghostcolor_t;
-
-// Record/playback tics
-void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
-void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
-void G_GhostAddThok(void);
-void G_GhostAddSpin(void);
-void G_GhostAddRev(void);
-void G_GhostAddColor(ghostcolor_t color);
-void G_GhostAddFlip(void);
-void G_GhostAddScale(fixed_t scale);
-void G_GhostAddHit(mobj_t *victim);
-void G_WriteGhostTic(mobj_t *ghost);
-void G_ConsGhostTic(void);
-void G_GhostTicker(void);
-void G_ReadMetalTic(mobj_t *metal);
-void G_WriteMetalTic(mobj_t *metal);
-void G_SaveMetal(UINT8 **buffer);
-void G_LoadMetal(UINT8 **buffer);
-
-void G_DoPlayDemo(char *defdemoname);
-void G_TimeDemo(const char *name);
-void G_AddGhost(char *defdemoname);
-void G_DoPlayMetal(void);
-void G_DoneLevelLoad(void);
-void G_StopMetalDemo(void);
-ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill);
-void G_StopDemo(void);
-boolean G_CheckDemoStatus(void);
-
 extern UINT32 gametypedefaultrules[NUMGAMETYPES];
 extern UINT32 gametypetol[NUMGAMETYPES];
 extern INT16 gametyperankings[NUMGAMETYPES];
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index 598a635aa47c18b4b79697efdc131e601ab97607..d0133176589e9a03f6681fc393e9d3cf0c19526a 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -291,7 +291,7 @@ void HWR_DrawStretchyFixedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t
 			if (cx >= -0.1f && cx <= 0.1f && SHORT(gpatch->width) == BASEVIDWIDTH && cy >= -0.1f && cy <= 0.1f && SHORT(gpatch->height) == BASEVIDHEIGHT)
 			{
 				// Need to temporarily cache the real patch to get the colour of the top left pixel
-				patch_t *realpatch = W_CacheLumpNumPwad(gpatch->wadnum, gpatch->lumpnum, PU_STATIC);
+				patch_t *realpatch = W_CacheSoftwarePatchNumPwad(gpatch->wadnum, gpatch->lumpnum, PU_STATIC);
 				const column_t *column = (const column_t *)((const UINT8 *)(realpatch) + LONG((realpatch)->columnofs[0]));
 				if (!column->topdelta)
 				{
@@ -450,7 +450,7 @@ void HWR_DrawCroppedPatch(GLPatch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscal
 			if (cx >= -0.1f && cx <= 0.1f && SHORT(gpatch->width) == BASEVIDWIDTH && cy >= -0.1f && cy <= 0.1f && SHORT(gpatch->height) == BASEVIDHEIGHT)
 			{
 				// Need to temporarily cache the real patch to get the colour of the top left pixel
-				patch_t *realpatch = W_CacheLumpNumPwad(gpatch->wadnum, gpatch->lumpnum, PU_STATIC);
+				patch_t *realpatch = W_CacheSoftwarePatchNumPwad(gpatch->wadnum, gpatch->lumpnum, PU_STATIC);
 				const column_t *column = (const column_t *)((const UINT8 *)(realpatch) + LONG((realpatch)->columnofs[0]));
 				if (!column->topdelta)
 				{
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 12c78e9a41a2a87b64ca0c5ffac6c37c6756da97..2355053412b11ea4ef69460bf8f243544581b0ff 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -1879,7 +1879,9 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 		{
 			for (rover = gr_backsector->ffloors; rover; rover = rover->next)
 			{
-				if (!(rover->flags & FF_EXISTS) || !(rover->flags & FF_RENDERSIDES) || (rover->flags & FF_INVERTSIDES))
+				if (!(rover->flags & FF_EXISTS) || !(rover->flags & FF_RENDERSIDES))
+					continue;
+				if (!(rover->flags & FF_ALLSIDES) && rover->flags & FF_INVERTSIDES)
 					continue;
 				if (*rover->topheight < lowcut || *rover->bottomheight > highcut)
 					continue;
@@ -2019,7 +2021,9 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 		{
 			for (rover = gr_frontsector->ffloors; rover; rover = rover->next)
 			{
-				if (!(rover->flags & FF_EXISTS) || !(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_ALLSIDES))
+				if (!(rover->flags & FF_EXISTS) || !(rover->flags & FF_RENDERSIDES))
+					continue;
+				if (!(rover->flags & FF_ALLSIDES || rover->flags & FF_INVERTSIDES))
 					continue;
 				if (*rover->topheight < lowcut || *rover->bottomheight > highcut)
 					continue;
@@ -3404,7 +3408,7 @@ static void HWR_Subsector(size_t num)
 
 			if (centerHeight <= locCeilingHeight &&
 			    centerHeight >= locFloorHeight &&
-			    ((dup_viewz < cullHeight && !(rover->flags & FF_INVERTPLANES)) ||
+			    ((dup_viewz < cullHeight && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))) ||
 			     (dup_viewz > cullHeight && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 			{
 				if (rover->flags & FF_FOG)
@@ -3465,7 +3469,7 @@ static void HWR_Subsector(size_t num)
 
 			if (centerHeight >= locFloorHeight &&
 			    centerHeight <= locCeilingHeight &&
-			    ((dup_viewz > cullHeight && !(rover->flags & FF_INVERTPLANES)) ||
+			    ((dup_viewz > cullHeight && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))) ||
 			     (dup_viewz < cullHeight && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 			{
 				if (rover->flags & FF_FOG)
@@ -3956,7 +3960,10 @@ static void HWR_DrawDropShadow(mobj_t *thing, gr_vissprite_t *spr, fixed_t scale
 	{
 		light = R_GetPlaneLight(thing->subsector->sector, floorz, false); // Always use the light at the top instead of whatever I was doing before
 
-		lightlevel = *thing->subsector->sector->lightlist[light].lightlevel;
+		if (*thing->subsector->sector->lightlist[light].lightlevel > 255)
+			lightlevel = 255;
+		else
+			lightlevel = *thing->subsector->sector->lightlist[light].lightlevel;
 
 		if (*thing->subsector->sector->lightlist[light].extra_colormap)
 			colormap = *thing->subsector->sector->lightlist[light].extra_colormap;
@@ -4157,7 +4164,7 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 		if (h <= temp)
 		{
 			if (!(spr->mobj->frame & FF_FULLBRIGHT))
-				lightlevel = *list[i-1].lightlevel;
+				lightlevel = *list[i-1].lightlevel > 255 ? 255 : *list[i-1].lightlevel;
 			colormap = *list[i-1].extra_colormap;
 			break;
 		}
@@ -4172,7 +4179,7 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 		if (!(list[i].flags & FF_NOSHADE) && (list[i].flags & FF_CUTSPRITES))
 		{
 			if (!(spr->mobj->frame & FF_FULLBRIGHT))
-				lightlevel = *list[i].lightlevel;
+				lightlevel = *list[i].lightlevel > 255 ? 255 : *list[i].lightlevel;
 			colormap = *list[i].extra_colormap;
 		}
 
@@ -4393,7 +4400,7 @@ static void HWR_DrawSprite(gr_vissprite_t *spr)
 		extracolormap_t *colormap = sector->extra_colormap;
 
 		if (!(spr->mobj->frame & FF_FULLBRIGHT))
-			lightlevel = sector->lightlevel;
+			lightlevel = sector->lightlevel > 255 ? 255 : sector->lightlevel;
 
 		if (colormap)
 			Surf.FlatColor.rgba = HWR_Lighting(lightlevel, colormap->rgba, colormap->fadergba, false, false);
@@ -4490,7 +4497,7 @@ static inline void HWR_DrawPrecipitationSprite(gr_vissprite_t *spr)
 			light = R_GetPlaneLight(sector, spr->mobj->z + spr->mobj->height, false); // Always use the light at the top instead of whatever I was doing before
 
 			if (!(spr->mobj->frame & FF_FULLBRIGHT))
-				lightlevel = *sector->lightlist[light].lightlevel;
+				lightlevel = *sector->lightlist[light].lightlevel > 255 ? 255 : *sector->lightlist[light].lightlevel;
 
 			if (*sector->lightlist[light].extra_colormap)
 				colormap = *sector->lightlist[light].extra_colormap;
@@ -4498,7 +4505,7 @@ static inline void HWR_DrawPrecipitationSprite(gr_vissprite_t *spr)
 		else
 		{
 			if (!(spr->mobj->frame & FF_FULLBRIGHT))
-				lightlevel = sector->lightlevel;
+				lightlevel = sector->lightlevel > 255 ? 255 : sector->lightlevel;
 
 			if (sector->extra_colormap)
 				colormap = sector->extra_colormap;
@@ -6319,7 +6326,6 @@ void HWR_Shutdown(void)
 	CONS_Printf("HWR_Shutdown()\n");
 	HWR_FreeExtraSubsectors();
 	HWR_FreePolyPool();
-	HWR_FreeMipmapCache();
 	HWR_FreeTextureCache();
 	HWD.pfnFlushScreenTextures();
 }
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index 5c3cd40a6c02cedacfa55988b94675654a9dd558..2e3af4a4cac7f0e6487ff5e2649aa78b03380906 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -1221,7 +1221,7 @@ boolean HWR_DrawModel(gr_vissprite_t *spr)
 			light = R_GetPlaneLight(sector, spr->mobj->z + spr->mobj->height, false); // Always use the light at the top instead of whatever I was doing before
 
 			if (!(spr->mobj->frame & FF_FULLBRIGHT))
-				lightlevel = *sector->lightlist[light].lightlevel;
+				lightlevel = *sector->lightlist[light].lightlevel > 255 ? 255 : *sector->lightlist[light].lightlevel;
 
 			if (*sector->lightlist[light].extra_colormap)
 				colormap = *sector->lightlist[light].extra_colormap;
@@ -1229,7 +1229,7 @@ boolean HWR_DrawModel(gr_vissprite_t *spr)
 		else
 		{
 			if (!(spr->mobj->frame & FF_FULLBRIGHT))
-				lightlevel = sector->lightlevel;
+				lightlevel = sector->lightlevel > 255 ? 255 : sector->lightlevel;
 
 			if (sector->extra_colormap)
 				colormap = sector->extra_colormap;
diff --git a/src/i_video.h b/src/i_video.h
index bdc10c9c5d2aab8a2eaa8eceff5a1b2db7411cd5..98ed7f38a18822d8038ec0973ef3c680572c2d89 100644
--- a/src/i_video.h
+++ b/src/i_video.h
@@ -36,10 +36,10 @@ typedef enum
 */
 extern rendermode_t rendermode;
 
-/**	\brief hardware renderer loaded
+/**	\brief OpenGL state
 	0 = never loaded, 1 = loaded successfully, -1 = failed loading
 */
-extern INT32 hwrenderloaded;
+extern INT32 vid_opengl_state;
 
 /**	\brief use highcolor modes if true
 */
@@ -49,11 +49,7 @@ extern boolean highcolor;
 */
 void I_StartupGraphics(void);
 
-/**	\brief setup hardware mode
-*/
-void I_StartupHardwareGraphics(void);
-
-/**	\brief restore old video mode
+/**	\brief shutdown video mode
 */
 void I_ShutdownGraphics(void);
 
@@ -97,6 +93,14 @@ INT32 VID_SetMode(INT32 modenum);
 */
 void VID_CheckRenderer(void);
 
+/**	\brief Load OpenGL mode
+*/
+void VID_StartupOpenGL(void);
+
+/**	\brief Checks if OpenGL loaded
+*/
+void VID_CheckGLLoaded(rendermode_t oldrender);
+
 /**	\brief	The VID_GetModeName function
 
 	\param	modenum	video mode number
diff --git a/src/info.h b/src/info.h
index f8a7136740486df8cbed9af6a76bbc8b4996b378..586209ff954905bb3cb1506f9e5adbb4bb27eb5e 100644
--- a/src/info.h
+++ b/src/info.h
@@ -165,6 +165,7 @@ void A_SetTics();
 void A_SetRandomTics();
 void A_ChangeColorRelative();
 void A_ChangeColorAbsolute();
+void A_Dye();
 void A_MoveRelative();
 void A_MoveAbsolute();
 void A_Thrust();
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index e24753aff852307278bdeba2b0109e41b7d1b6bd..28a0e9b988da94a654d22874d4a0689dfe1e05ce 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -223,10 +223,16 @@ static const char *GetUserdataUType(lua_State *L)
 //   or players[0].powers -> "player_t.powers"
 static int lib_userdataType(lua_State *L)
 {
+	int type;
 	lua_settop(L, 1); // pop everything except arg 1 (in case somebody decided to add more)
-	luaL_checktype(L, 1, LUA_TUSERDATA);
-	lua_pushstring(L, GetUserdataUType(L));
-	return 1;
+	type = lua_type(L, 1);
+	if (type == LUA_TLIGHTUSERDATA || type == LUA_TUSERDATA)
+	{
+		lua_pushstring(L, GetUserdataUType(L));
+		return 1;
+	}
+	else
+		return luaL_typerror(L, 1, "userdata");
 }
 
 static int lib_isPlayerAdmin(lua_State *L)
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index a9fbad65f5585631490a8f7647288f0682685922..4fe234deeecc88f6643d04bd0663a070b3fa85be 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -430,22 +430,8 @@ static int lib_cvRegisterVar(lua_State *L)
 
 static int lib_cvFindVar(lua_State *L)
 {
-	consvar_t *cv;
-	if (( cv = CV_FindVar(luaL_checkstring(L,1)) ))
-	{
-		lua_settop(L,1);/* We only want one argument in the stack. */
-		lua_pushlightuserdata(L, cv);/* Now the second value on stack. */
-		luaL_getmetatable(L, META_CVAR);
-		/*
-		The metatable is the last value on the stack, so this
-		applies it to the second value, which is the cvar.
-		*/
-		lua_setmetatable(L,2);
-		lua_pushvalue(L,2);
-		return 1;
-	}
-	else
-		return 0;
+	LUA_PushLightUserdata(L, CV_FindVar(luaL_checkstring(L,1)), META_CVAR);
+	return 1;
 }
 
 // CONS_Printf for a single player
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 244b1bbb8748c16d0322ddd13f3b87838ce2d980..315c35cdf3e3b00c2c96759056a1db201b220a46 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -40,6 +40,7 @@ enum hook {
 	hook_JumpSpinSpecial,
 	hook_BotTiccmd,
 	hook_BotAI,
+	hook_BotRespawn,
 	hook_LinedefExecute,
 	hook_PlayerMsg,
 	hook_HurtMsg,
@@ -91,6 +92,7 @@ boolean LUAh_MobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8
 #define LUAh_JumpSpinSpecial(player) LUAh_PlayerHook(player, hook_JumpSpinSpecial) // Hook for P_DoJumpStuff (Spin button effect (mid-air))
 boolean LUAh_BotTiccmd(player_t *bot, ticcmd_t *cmd); // Hook for B_BuildTiccmd
 boolean LUAh_BotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd); // Hook for B_BuildTailsTiccmd by skin name
+boolean LUAh_BotRespawn(mobj_t *sonic, mobj_t *tails); // Hook for B_CheckRespawn
 boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector); // Hook for linedef executors
 boolean LUAh_PlayerMsg(int source, int target, int flags, char *msg); // Hook for chat messages
 boolean LUAh_HurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype); // Hook for hurt messages
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index fb89e6b026806fbffad28430cbd5f2144e1c5223..c7dfe381b06f577854cded011f227e37c33ca5b1 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -52,6 +52,7 @@ const char *const hookNames[hook_MAX+1] = {
 	"JumpSpinSpecial",
 	"BotTiccmd",
 	"BotAI",
+	"BotRespawn",
 	"LinedefExecute",
 	"PlayerMsg",
 	"HurtMsg",
@@ -80,9 +81,7 @@ struct hook_s
 	UINT16 id;
 	union {
 		mobjtype_t mt;
-		char *skinname;
-		char *musname;
-		char *funcname;
+		char *str;
 	} s;
 	boolean error;
 };
@@ -151,28 +150,16 @@ static int lib_addHook(lua_State *L)
 		break;
 	case hook_BotAI:
 	case hook_ShouldJingleContinue:
-		hook.s.skinname = NULL;
+		hook.s.str = NULL;
 		if (lua_isstring(L, 2))
 		{ // lowercase copy
-			const char *s = lua_tostring(L, 2);
-			char *p = hook.s.skinname = ZZ_Alloc(strlen(s)+1);
-			do {
-				*p = tolower(*s);
-				++p;
-			} while(*(++s));
-			*p = 0;
+			hook.s.str = Z_StrDup(lua_tostring(L, 2));
+			strlwr(hook.s.str);
 		}
 		break;
 	case hook_LinedefExecute: // Linedef executor functions
-		{ // uppercase copy
-			const char *s = luaL_checkstring(L, 2);
-			char *p = hook.s.funcname = ZZ_Alloc(strlen(s)+1);
-			do {
-				*p = toupper(*s);
-				++p;
-			} while(*(++s));
-			*p = 0;
-		}
+		hook.s.str = Z_StrDup(luaL_checkstring(L, 2));
+		strupr(hook.s.str);
 		break;
 	default:
 		break;
@@ -1075,7 +1062,7 @@ boolean LUAh_BotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 	for (hookp = roothook; hookp; hookp = hookp->next)
 	{
 		if (hookp->type != hook_BotAI
-		|| (hookp->s.skinname && strcmp(hookp->s.skinname, ((skin_t*)tails->skin)->name)))
+		|| (hookp->s.str && strcmp(hookp->s.str, ((skin_t*)tails->skin)->name)))
 			continue;
 
 		if (lua_gettop(gL) == 0)
@@ -1125,6 +1112,51 @@ boolean LUAh_BotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 	return hooked;
 }
 
+// Hook for B_CheckRespawn
+boolean LUAh_BotRespawn(mobj_t *sonic, mobj_t *tails)
+{
+	hook_p hookp;
+	UINT8 shouldRespawn = 0; // 0 = default, 1 = force yes, 2 = force no.
+	if (!gL || !(hooksAvailable[hook_BotRespawn/8] & (1<<(hook_BotRespawn%8))))
+		return false;
+
+	lua_settop(gL, 0);
+
+	for (hookp = roothook; hookp; hookp = hookp->next)
+	{
+		if (hookp->type != hook_BotRespawn)
+			continue;
+
+		if (lua_gettop(gL) == 0)
+		{
+			LUA_PushUserdata(gL, sonic, META_MOBJ);
+			LUA_PushUserdata(gL, tails, META_MOBJ);
+		}
+		lua_pushfstring(gL, FMT_HOOKID, hookp->id);
+		lua_gettable(gL, LUA_REGISTRYINDEX);
+		lua_pushvalue(gL, -3);
+		lua_pushvalue(gL, -3);
+		if (lua_pcall(gL, 2, 1, 0)) {
+			if (!hookp->error || cv_debug & DBG_LUA)
+				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
+			lua_pop(gL, 1);
+			hookp->error = true;
+			continue;
+		}
+		if (!lua_isnil(gL, -1))
+		{
+			if (lua_toboolean(gL, -1))
+				shouldRespawn = 1; // Force yes
+			else
+				shouldRespawn = 2; // Force no
+		}
+		lua_pop(gL, 1);
+	}
+
+	lua_settop(gL, 0);
+	return shouldRespawn;
+}
+
 // Hook for linedef executors
 boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 {
@@ -1137,7 +1169,7 @@ boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 
 	for (hookp = linedefexecutorhooks; hookp; hookp = hookp->next)
 	{
-		if (strcmp(hookp->s.funcname, line->stringargs[0]))
+		if (strcmp(hookp->s.str, line->stringargs[0]))
 			continue;
 
 		if (lua_gettop(gL) == 0)
@@ -1675,7 +1707,7 @@ boolean LUAh_ShouldJingleContinue(player_t *player, const char *musname)
 	for (hookp = roothook; hookp; hookp = hookp->next)
 	{
 		if (hookp->type != hook_ShouldJingleContinue
-			|| (hookp->s.musname && strcmp(hookp->s.musname, musname)))
+			|| (hookp->s.str && strcmp(hookp->s.str, musname)))
 			continue;
 
 		if (lua_gettop(gL) == 0)
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 25870d0b1cfced651926f7cbc40eeb4880c001da..b7902ef79a8e580282633a76820b775cfb44622b 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -388,10 +388,7 @@ static int lib_setSpriteInfo(lua_State *L)
 		lua_Integer i = 0;
 		const char *str = NULL;
 		if (lua_isnumber(L, 2))
-		{
 			i = lua_tointeger(L, 2);
-			i++; // shift index in case of missing rotsprite support
-		}
 		else
 			str = luaL_checkstring(L, 2);
 
diff --git a/src/lua_script.c b/src/lua_script.c
index 66222294de89c6339ff992be2251605763548996..fff5b88062eb87ff246ec428611327d69d1a0a38 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -571,6 +571,27 @@ fixed_t LUA_EvalMath(const char *word)
 	return res;
 }
 
+/*
+LUA_PushUserdata but no userdata is created.
+You can't invalidate it therefore.
+*/
+
+void LUA_PushLightUserdata (lua_State *L, void *data, const char *meta)
+{
+	if (data)
+	{
+		lua_pushlightuserdata(L, data);
+		luaL_getmetatable(L, meta);
+		/*
+		The metatable is the last value on the stack, so this
+		applies it to the second value, which is the userdata.
+		*/
+		lua_setmetatable(L, -2);
+	}
+	else
+		lua_pushnil(L);
+}
+
 // Takes a pointer, any pointer, and a metatable name
 // Creates a userdata for that pointer with the given metatable
 // Pushes it to the stack and stores it in the registry.
diff --git a/src/lua_script.h b/src/lua_script.h
index 7bdf7685b39dc3d5daf74a3a110222e71dc43984..3166fdfc7ce03b21fc0f6f4b6a0fd081850faad7 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -44,6 +44,7 @@ void LUA_LoadLump(UINT16 wad, UINT16 lump);
 void LUA_DumpFile(const char *filename);
 #endif
 fixed_t LUA_EvalMath(const char *word);
+void LUA_PushLightUserdata(lua_State *L, void *data, const char *meta);
 void LUA_PushUserdata(lua_State *L, void *data, const char *meta);
 void LUA_InvalidateUserdata(void *data);
 void LUA_InvalidateLevel(void);
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 7fdb9ad0a014e05828c289d67bb0342039208d19..cc54c1aea257d5d0ef8180fe3ef15d25b392afb6 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -38,8 +38,20 @@ typedef INT32 fixed_t;
 /*!
   \brief convert fixed_t into floating number
 */
-#define FIXED_TO_FLOAT(x) (((float)(x)) / ((float)FRACUNIT))
-#define FLOAT_TO_FIXED(f) (fixed_t)((f) * ((float)FRACUNIT))
+
+FUNCMATH FUNCINLINE static ATTRINLINE float FixedToFloat(fixed_t x)
+{
+	return x / (float)FRACUNIT;
+}
+
+FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FloatToFixed(float f)
+{
+	return (fixed_t)(f * FRACUNIT);
+}
+
+// for backwards compat
+#define FIXED_TO_FLOAT(x) FixedToFloat(x) // (((float)(x)) / ((float)FRACUNIT))
+#define FLOAT_TO_FIXED(f) FloatToFixed(f) // (fixed_t)((f) * ((float)FRACUNIT))
 
 
 #if defined (__WATCOMC__) && FRACBITS == 16
diff --git a/src/m_menu.c b/src/m_menu.c
index f8240d40318322b1116a89c59310eaae827c25d4..ea50f4b5b119afabdb3aad99a728902bb4ec42fc 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -522,6 +522,8 @@ static menuitem_t MISC_AddonsMenu[] =
 // ---------------------------------
 static menuitem_t MAPauseMenu[] =
 {
+	{IT_CALL | IT_STRING,    NULL, "Emblem Hints...",      M_EmblemHints,         32},
+
 	{IT_CALL | IT_STRING,    NULL, "Continue",             M_SelectableClearMenus,48},
 	{IT_CALL | IT_STRING,    NULL, "Retry",                M_ModeAttackRetry,     56},
 	{IT_CALL | IT_STRING,    NULL, "Abort",                M_ModeAttackEndGame,   64},
@@ -529,6 +531,7 @@ static menuitem_t MAPauseMenu[] =
 
 typedef enum
 {
+	mapause_hints,
 	mapause_continue,
 	mapause_retry,
 	mapause_abort
@@ -730,9 +733,9 @@ static menuitem_t SR_SoundTestMenu[] =
 
 static menuitem_t SR_EmblemHintMenu[] =
 {
-	{IT_STRING | IT_ARROWS,       NULL, "Page", M_HandleEmblemHints, 10},
-	{IT_STRING|IT_CVAR,         NULL, "Emblem Radar", &cv_itemfinder, 20},
-	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",         &SPauseDef,     30}
+	{IT_STRING | IT_ARROWS,  NULL, "Page",    M_HandleEmblemHints, 10},
+	{IT_STRING|IT_CVAR,      NULL, "Emblem Radar", &cv_itemfinder, 20},
+	{IT_WHITESTRING|IT_CALL, NULL, "Back",         M_GoBack,       30}
 };
 
 // --------------------------------
@@ -2098,7 +2101,7 @@ static void M_VideoOptions(INT32 choice)
 {
 	(void)choice;
 #ifdef HWRENDER
-	if (hwrenderloaded == -1)
+	if (vid_opengl_state == -1)
 	{
 		OP_VideoOptionsMenu[op_video_renderer].status = (IT_TRANSTEXT | IT_PAIR);
 		OP_VideoOptionsMenu[op_video_renderer].patch = "Renderer";
@@ -3634,6 +3637,7 @@ void M_StartControlPanel(void)
 	else if (modeattacking)
 	{
 		currentMenu = &MAPauseDef;
+		MAPauseMenu[mapause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 		itemOn = mapause_continue;
 	}
 	else if (!(netgame || multiplayer)) // Single Player
@@ -7304,6 +7308,7 @@ static void M_EmblemHints(INT32 choice)
 	SR_EmblemHintMenu[0].status = (local > NUMHINTS*2) ? (IT_STRING | IT_ARROWS) : (IT_DISABLED);
 	SR_EmblemHintMenu[1].status = (M_SecretUnlocked(SECRET_ITEMFINDER)) ? (IT_CVAR|IT_STRING) : (IT_SECRET);
 	hintpage = 1;
+	SR_EmblemHintDef.prevMenu = currentMenu;
 	M_SetupNextMenu(&SR_EmblemHintDef);
 	itemOn = 2; // always start on back.
 }
@@ -7984,12 +7989,20 @@ 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 =
-		(M_SecretUnlocked(SECRET_NIGHTSMODE)) ? IT_CALL|IT_STRING : IT_SECRET;
+
+	levellistmode = LLM_RECORDATTACK;
+	if (M_GametypeHasLevels(-1))
+		SP_MainMenu[sprecordattack].status = (M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING : IT_SECRET;
+	else
+		SP_MainMenu[sprecordattack].status = IT_NOTHING|IT_DISABLED;
+
+	levellistmode = LLM_NIGHTSATTACK;
+	if (M_GametypeHasLevels(-1))
+		SP_MainMenu[spnightsmode].status = (M_SecretUnlocked(SECRET_NIGHTSMODE)) ? IT_CALL|IT_STRING : IT_SECRET;
+	else
+		SP_MainMenu[spnightsmode].status = IT_NOTHING|IT_DISABLED;
+
+	SP_MainMenu[sptutorial].status = tutorialmap ? IT_CALL|IT_STRING : IT_NOTHING|IT_DISABLED;
 
 	M_SetupNextMenu(&SP_MainDef);
 }
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 09d33c5370da8c35c8b7ba84342a2c5890f3a481..2341be6d366a7f97f7551959f3437b19f81a7e2c 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -189,6 +189,7 @@ void A_SetTics(mobj_t *actor);
 void A_SetRandomTics(mobj_t *actor);
 void A_ChangeColorRelative(mobj_t *actor);
 void A_ChangeColorAbsolute(mobj_t *actor);
+void A_Dye(mobj_t *actor);
 void A_MoveRelative(mobj_t *actor);
 void A_MoveAbsolute(mobj_t *actor);
 void A_Thrust(mobj_t *actor);
@@ -8773,6 +8774,42 @@ void A_ChangeColorAbsolute(mobj_t *actor)
 		actor->color = (UINT8)locvar2;
 }
 
+// Function: A_Dye
+//
+// Description: Colorizes an object.
+//
+// var1 = if (var1 != 0), dye your target instead of yourself
+// var2 = color value to dye
+//
+void A_Dye(mobj_t *actor)
+{
+	INT32 locvar1 = var1;
+	INT32 locvar2 = var2;
+
+	mobj_t *target = ((locvar1 && actor->target) ? actor->target : actor);
+	UINT8 color = (UINT8)locvar2;
+#ifdef HAVE_BLUA
+	if (LUA_CallAction("A_Dye", actor))
+		return;
+#endif
+	if (color >= MAXTRANSLATIONS)
+		return;
+	
+	if (!color)
+		target->colorized = false;
+	else
+		target->colorized = true;
+		
+	// What if it's a player?
+	if (target->player)
+	{
+		target->player->powers[pw_dye] = color;
+		return;
+	}
+	
+	target->color = color;
+}
+
 // Function: A_MoveRelative
 //
 // Description: Moves an object (wrapper for P_Thrust)
diff --git a/src/p_inter.c b/src/p_inter.c
index 67d197375bfe46f393533d9c1e43b1dbef744166..30e3c31b189ec383284205d442dd874dcf035eb6 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -3150,7 +3150,7 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou
 			return false;
 
 		// In COOP/RACE, you can't hurt other players unless cv_friendlyfire is on
-		if (!(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)) && (G_PlatformGametype()))
+		if (!(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)) && (gametyperules & GTR_FRIENDLY))
 		{
 			if (gametype == GT_COOP && inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK)) // co-op only
 			{
diff --git a/src/p_mobj.c b/src/p_mobj.c
index b27d27a769ca23cb5274ef653d427694021e0650..aaea9d49b9b22128350ec3c7567534caf49432dc 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -9283,8 +9283,11 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 			if (mobj->tracer && mobj->tracer->player && mobj->tracer->health > 0
 				&& P_AproxDistance(P_AproxDistance(mobj->tracer->x - mobj->x, mobj->tracer->y - mobj->y), mobj->tracer->z - mobj->z) <= mobj->radius*16)
 			{
+				var1 = mobj->info->speed;
+				var2 = 1;
+
 				// Home in on the target.
-				P_HomingAttack(mobj, mobj->tracer);
+				A_HomingChase(mobj);
 
 				if (mobj->z < mobj->floorz)
 					mobj->z = mobj->floorz;
diff --git a/src/p_setup.c b/src/p_setup.c
index 7b57b38001e40f9bed08aa387ac6b41d6e138911..4ebd4a87398cab916e132d1f4b768a81293243c7 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -1185,10 +1185,14 @@ static void P_LoadSidedefs(UINT8 *data)
 			case 9: // Mace parameters
 			case 14: // Bustable block parameters
 			case 15: // Fan particle spawner parameters
+			case 334: // Trigger linedef executor: Object dye - Continuous
+			case 335: // Trigger linedef executor: Object dye - Each time
+			case 336: // Trigger linedef executor: Object dye - Once
 			case 425: // Calls P_SetMobjState on calling mobj
 			case 434: // Custom Power
 			case 442: // Calls P_SetMobjState on mobjs of a given type in the tagged sectors
 			case 461: // Spawns an object on the map based on texture offsets
+			case 463: // Colorizes an object
 			{
 				char process[8*3+1];
 				memset(process,0,8*3+1);
diff --git a/src/p_sight.c b/src/p_sight.c
index c9083b99b9f3bc0bd2b7fe4b7ee1573238a3c56f..3d1ee9e601090190739da591c68acbecd0dd06e7 100644
--- a/src/p_sight.c
+++ b/src/p_sight.c
@@ -495,7 +495,7 @@ boolean P_CheckSight(mobj_t *t1, mobj_t *t2)
 			if (rover->flags & FF_SOLID)
 				continue; // shortcut since neither mobj can be inside the 3dfloor
 
-			if (!(rover->flags & FF_INVERTPLANES))
+			if (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))
 			{
 				if (los.sightzstart >= topz1 && t2->z + t2->height < topz2)
 					return false; // blocked by upper outside plane
@@ -504,7 +504,7 @@ boolean P_CheckSight(mobj_t *t1, mobj_t *t2)
 					return false; // blocked by lower outside plane
 			}
 
-			if (rover->flags & FF_INVERTPLANES || rover->flags & FF_BOTHPLANES)
+			if (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES)
 			{
 				if (los.sightzstart < topz1 && t2->z >= topz2)
 					return false; // blocked by upper inside plane
diff --git a/src/p_spec.c b/src/p_spec.c
index ee539bdd629ed8fcf87fe0ee35c8b9f47af1fd04..cf4ab3da374ab87e9109af84b09ff4971d9a97a6 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2034,6 +2034,17 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 			if (!(actor && actor->player && ((stricmp(triggerline->text, skins[actor->player->skin].name) == 0) ^ ((triggerline->flags & ML_NOCLIMB) == ML_NOCLIMB))))
 				return false;
 			break;
+		case 334: // object dye - continuous
+		case 335: // object dye - each time
+		case 336: // object dye - once
+			{
+				INT32 triggercolor = (INT32)sides[triggerline->sidenum[0]].toptexture;
+				UINT8 color = (actor->player ? actor->player->powers[pw_dye] : actor->color);
+				boolean invert = (triggerline->flags & ML_NOCLIMB ? true : false);
+				
+				if (invert ^ (triggercolor != color))
+					return false;
+			}
 		default:
 			break;
 	}
@@ -2167,6 +2178,7 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 	 || specialtype == 328 // Nights lap - Once
 	 || specialtype == 330 // Nights Bonus Time - Once
 	 || specialtype == 333 // Skin - Once
+	 || specialtype == 336 // Dye - Once
 	 || specialtype == 399) // Level Load
 		triggerline->special = 0; // Clear it out
 
@@ -2208,7 +2220,8 @@ void P_LinedefExecute(INT16 tag, mobj_t *actor, sector_t *caller)
 		 || lines[masterline].special == 310 // CTF Red team - Each time
 		 || lines[masterline].special == 312 // CTF Blue team - Each time
 		 || lines[masterline].special == 322 // Trigger on X calls - Each Time
-		 || lines[masterline].special == 332)// Skin - Each time
+		 || lines[masterline].special == 332 // Skin - Each time
+		 || lines[masterline].special == 335)// Dye - Each time
 			continue;
 
 		if (lines[masterline].special < 300
@@ -4037,7 +4050,23 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				}
 			}
 			break;
-
+		
+		case 463: // Dye object
+			{
+				INT32 color = sides[line->sidenum[0]].toptexture;
+				
+				if (mo)
+				{
+					if (color < 0 || color >= MAXTRANSLATIONS)
+						return;
+					
+					var1 = 0;
+					var2 = color;
+					A_Dye(mo);
+				}
+			}
+			break;
+		
 #ifdef POLYOBJECTS
 		case 480: // Polyobj_DoorSlide
 		case 481: // Polyobj_DoorSwing
@@ -7069,7 +7098,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 				break;
 
 			case 202: // Fog
-				ffloorflags = FF_EXISTS|FF_RENDERALL|FF_FOG|FF_BOTHPLANES|FF_INVERTPLANES|FF_ALLSIDES|FF_INVERTSIDES|FF_CUTEXTRA|FF_EXTRA|FF_DOUBLESHADOW|FF_CUTSPRITES;
+				ffloorflags = FF_EXISTS|FF_RENDERALL|FF_FOG|FF_INVERTPLANES|FF_INVERTSIDES|FF_CUTEXTRA|FF_EXTRA|FF_DOUBLESHADOW|FF_CUTSPRITES;
 				sec = sides[*lines[i].sidenum].sector - sectors;
 				// SoM: Because it's fog, check for an extra colormap and set the fog flag...
 				if (sectors[sec].extra_colormap)
@@ -7205,6 +7234,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 			case 310:
 			case 312:
 			case 332:
+			case 335:
 				sec = sides[*lines[i].sidenum].sector - sectors;
 				P_AddEachTimeThinker(&sectors[sec], &lines[i]);
 				break;
@@ -7257,6 +7287,11 @@ void P_SpawnSpecials(boolean fromnetsave)
 			case 331:
 			case 333:
 				break;
+			
+			// Object dye executors
+			case 334:
+			case 336:
+				break;
 
 			case 399: // Linedef execute on map load
 				// This is handled in P_RunLevelLoadExecutors.
diff --git a/src/p_user.c b/src/p_user.c
index c12bc0c5983d9cebf20091a78fb17c890e48d2a9..994eb7007c3ad4056a9f3564565e55e60d5431fc 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -1548,6 +1548,10 @@ boolean P_EvaluateMusicStatus(UINT16 status, const char *musname)
 	int i;
 	boolean result = false;
 
+#ifndef HAVE_BLUA
+	(void)musname;
+#endif
+
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		if (!P_IsLocalPlayer(&players[i]))
@@ -7978,7 +7982,7 @@ static void P_MovePlayer(player_t *player)
 			&& player->mo->state < &states[S_PLAY_NIGHTS_TRANS6]))) // Note the < instead of <=
 		{
 			skin_t *skin = ((skin_t *)(player->mo->skin));
-			if (skin->flags & SF_SUPER)
+			if (( skin->flags & (SF_SUPER|SF_NONIGHTSSUPER) ) == SF_SUPER)
 			{
 				player->mo->color = skin->supercolor
 					+ ((player->nightstime == player->startedtime)
@@ -9327,7 +9331,7 @@ boolean P_HomingAttack(mobj_t *source, mobj_t *enemy) // Home in on your target
 	if (enemy->health <= 0) // dead
 		return false;
 
-	if (!((enemy->flags & (MF_ENEMY|MF_BOSS|MF_MONITOR) && (enemy->flags & MF_SHOOTABLE)) || (enemy->flags & MF_SPRING)) == !(enemy->flags2 & MF2_INVERTAIMABLE)) // allows if it has the flags desired XOR it has the invert aimable flag
+	if (source->player && (!((enemy->flags & (MF_ENEMY|MF_BOSS|MF_MONITOR) && (enemy->flags & MF_SHOOTABLE)) || (enemy->flags & MF_SPRING)) == !(enemy->flags2 & MF2_INVERTAIMABLE))) // allows if it has the flags desired XOR it has the invert aimable flag
 		return false;
 
 	if (enemy->flags2 & MF2_FRET)
@@ -12209,7 +12213,7 @@ void P_PlayerThink(player_t *player)
 		player->powers[pw_nocontrol]--;
 	else
 		player->powers[pw_nocontrol] = 0;
-
+	
 	//pw_super acts as a timer now
 	if (player->powers[pw_super]
 	&& (player->mo->state < &states[S_PLAY_SUPER_TRANS1]
@@ -12854,6 +12858,12 @@ void P_PlayerAfterThink(player_t *player)
 		player->mo->flags |= MF_NOGRAVITY;
 	}
 
+	if (player->powers[pw_dye])
+	{
+		player->mo->colorized = true;
+		player->mo->color = player->powers[pw_dye];
+	}
+
 	if (player->followmobj && (player->spectator || player->mo->health <= 0 || player->followmobj->type != player->followitem))
 	{
 		P_RemoveMobj(player->followmobj);
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 85113be43d1fa150ac665e3f0c29c05e28e244fe..77ab2a82f6b1d4901d7a5526a982f6377a3c3a64 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -955,8 +955,8 @@ static void R_Subsector(size_t num)
 				*rover->bottomheight;
 			if (planecenterz <= ceilingcenterz
 				&& planecenterz >= floorcenterz
-				&& ((viewz < heightcheck && !(rover->flags & FF_INVERTPLANES))
-				|| (viewz > heightcheck && (rover->flags & FF_BOTHPLANES))))
+				&& ((viewz < heightcheck && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES)))
+				|| (viewz > heightcheck && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 			{
 				light = R_GetPlaneLight(frontsector, planecenterz,
 					viewz < heightcheck);
@@ -993,8 +993,8 @@ static void R_Subsector(size_t num)
 				*rover->topheight;
 			if (planecenterz >= floorcenterz
 				&& planecenterz <= ceilingcenterz
-				&& ((viewz > heightcheck && !(rover->flags & FF_INVERTPLANES))
-				|| (viewz < heightcheck && (rover->flags & FF_BOTHPLANES))))
+				&& ((viewz > heightcheck && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES)))
+				|| (viewz < heightcheck && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 			{
 				light = R_GetPlaneLight(frontsector, planecenterz, viewz < heightcheck);
 
diff --git a/src/r_data.c b/src/r_data.c
index 3d80bbda3cdda75b170ec2456853b7f0e31ab9d0..831e75bef64276bae116ee376b31e2e3eda52a3c 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -721,14 +721,12 @@ Rloadflats (INT32 i, INT32 w)
 	}
 	else
 	{
-		texstart = W_CheckNumForNamePwad("F_START", (UINT16)w, 0);
+		texstart = W_CheckNumForMarkerStartPwad("F_START", (UINT16)w, 0);
 		texend = W_CheckNumForNamePwad("F_END", (UINT16)w, texstart);
 	}
 
 	if (!( texstart == INT16_MAX || texend == INT16_MAX ))
 	{
-		texstart++; // Do not count the first marker
-
 		// Work through each lump between the markers in the WAD.
 		for (j = 0; j < (texend - texstart); j++)
 		{
@@ -841,7 +839,7 @@ Rloadtextures (INT32 i, INT32 w)
 	}
 	else
 	{
-		texstart = W_CheckNumForNamePwad(TX_START, (UINT16)w, 0);
+		texstart = W_CheckNumForMarkerStartPwad(TX_START, (UINT16)w, 0);
 		texend = W_CheckNumForNamePwad(TX_END, (UINT16)w, 0);
 		texturesLumpPos = W_CheckNumForNamePwad("TEXTURES", (UINT16)w, 0);
 		if (texturesLumpPos != INT16_MAX)
@@ -850,8 +848,6 @@ Rloadtextures (INT32 i, INT32 w)
 
 	if (!( texstart == INT16_MAX || texend == INT16_MAX ))
 	{
-		texstart++; // Do not count the first marker
-
 		// Work through each lump between the markers in the WAD.
 		for (j = 0; j < (texend - texstart); j++)
 		{
@@ -958,14 +954,12 @@ void R_LoadTextures(void)
 		}
 		else
 		{
-			texstart = W_CheckNumForNamePwad("F_START", (UINT16)w, 0);
+			texstart = W_CheckNumForMarkerStartPwad("F_START", (UINT16)w, 0);
 			texend = W_CheckNumForNamePwad("F_END", (UINT16)w, texstart);
 		}
 
 		if (!( texstart == INT16_MAX || texend == INT16_MAX ))
 		{
-			texstart++; // Do not count the first marker
-
 			// PK3s have subfolders, so we can't just make a simple sum
 			if (wadfiles[w]->type == RET_PK3)
 			{
@@ -998,15 +992,13 @@ void R_LoadTextures(void)
 		}
 		else
 		{
-			texstart = W_CheckNumForNamePwad(TX_START, (UINT16)w, 0);
+			texstart = W_CheckNumForMarkerStartPwad(TX_START, (UINT16)w, 0);
 			texend = W_CheckNumForNamePwad(TX_END, (UINT16)w, 0);
 		}
 
 		if (texstart == INT16_MAX || texend == INT16_MAX)
 			continue;
 
-		texstart++; // Do not count the first marker
-
 		// PK3s have subfolders, so we can't just make a simple sum
 		if (wadfiles[w]->type == RET_PK3)
 		{
@@ -1592,9 +1584,9 @@ lumpnum_t R_GetFlatNumForName(const char *name)
 		switch (wadfiles[i]->type)
 		{
 		case RET_WAD:
-			if ((start = W_CheckNumForNamePwad("F_START", (UINT16)i, 0)) == INT16_MAX)
+			if ((start = W_CheckNumForMarkerStartPwad("F_START", (UINT16)i, 0)) == INT16_MAX)
 			{
-				if ((start = W_CheckNumForNamePwad("FF_START", (UINT16)i, 0)) == INT16_MAX)
+				if ((start = W_CheckNumForMarkerStartPwad("FF_START", (UINT16)i, 0)) == INT16_MAX)
 					continue;
 				else if ((end = W_CheckNumForNamePwad("FF_END", (UINT16)i, start)) == INT16_MAX)
 					continue;
diff --git a/src/r_defs.h b/src/r_defs.h
index ca2af8728dec7064d4e0be9c5ef2085f0b09d7ff..943f54761c3df84c1a0f65209eae85b2ab496629 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -130,11 +130,11 @@ typedef enum
 	FF_CUTEXTRA          = 0x100,      ///< Cuts out hidden translucent pixels.
 	FF_CUTLEVEL          = 0x180,      ///< Cuts out all hidden pixels.
 	FF_CUTSPRITES        = 0x200,      ///< Final step in making 3D water.
-	FF_BOTHPLANES        = 0x400,      ///< Renders both planes all the time.
+	FF_BOTHPLANES        = 0x400,      ///< Render inside and outside planes.
 	FF_EXTRA             = 0x800,      ///< Gets cut by ::FF_CUTEXTRA.
 	FF_TRANSLUCENT       = 0x1000,     ///< See through!
 	FF_FOG               = 0x2000,     ///< Fog "brush."
-	FF_INVERTPLANES      = 0x4000,     ///< Reverse the plane visibility rules.
+	FF_INVERTPLANES      = 0x4000,     ///< Only render inside planes.
 	FF_ALLSIDES          = 0x8000,     ///< Render inside and outside sides.
 	FF_INVERTSIDES       = 0x10000,    ///< Only render inside sides.
 	FF_DOUBLESHADOW      = 0x20000,    ///< Make two lightlist entries to reset light?
diff --git a/src/r_draw.c b/src/r_draw.c
index b983db0aadd44c56c1df4c89b3732f5be0c229c5..0155ec113365735bd1e76ac1bceac4d121875b4f 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -233,11 +233,11 @@ const UINT8 Color_Index[MAXTRANSLATIONS-1][16] = {
 	{0x00, 0xd0, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x44, 0x45, 0x46}, // SKINCOLOR_SUPERORANGE4
 	{0xd0, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x44, 0x45, 0x46, 0x47}, // SKINCOLOR_SUPERORANGE5
 
-	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x50, 0x51, 0x52, 0x53, 0x48}, // SKINCOLOR_SUPERGOLD1
-	{0x00, 0x50, 0x51, 0x52, 0x53, 0x53, 0x48, 0x48, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41}, // SKINCOLOR_SUPERGOLD2
-	{0x51, 0x52, 0x53, 0x53, 0x48, 0x48, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41, 0x42, 0x43}, // SKINCOLOR_SUPERGOLD3
-	{0x53, 0x48, 0x48, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46}, // SKINCOLOR_SUPERGOLD4
-	{0x48, 0x48, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47}, // SKINCOLOR_SUPERGOLD5
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x50, 0x51, 0x52, 0x53, 0x48, 0x48, 0x48}, // SKINCOLOR_SUPERGOLD1
+	{0x00, 0x50, 0x51, 0x52, 0x53, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41, 0x41, 0x41}, // SKINCOLOR_SUPERGOLD2
+	{0x51, 0x52, 0x53, 0x53, 0x48, 0x49, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41, 0x42, 0x43, 0x43, 0x43}, // SKINCOLOR_SUPERGOLD3
+	{0x53, 0x48, 0x48, 0x49, 0x49, 0x49, 0x49, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x46, 0x46}, // SKINCOLOR_SUPERGOLD4
+	{0x48, 0x48, 0x49, 0x49, 0x49, 0x40, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x47, 0x47}, // SKINCOLOR_SUPERGOLD5
 
 	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x58, 0x58, 0xbc, 0xbc, 0xbc}, // SKINCOLOR_SUPERPERIDOT1
 	{0x00, 0x58, 0x58, 0x58, 0xbc, 0xbc, 0xbc, 0xbc, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbe, 0xbe}, // SKINCOLOR_SUPERPERIDOT2
diff --git a/src/r_segs.c b/src/r_segs.c
index fb3e02dbbc2a30de9f252867e8f28ddec41aec33..e777ab2d0b30c286b29370884b7ac8b59460ef5d 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -2163,7 +2163,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 				{
 					if (!(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_EXISTS))
 						continue;
-					if (rover->flags & FF_INVERTSIDES)
+					if (!(rover->flags & FF_ALLSIDES) && rover->flags & FF_INVERTSIDES)
 						continue;
 
 					if (rover->norender == leveltime)
@@ -2218,7 +2218,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 				{
 					if (!(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_EXISTS))
 						continue;
-					if (!(rover->flags & FF_ALLSIDES))
+					if (!(rover->flags & FF_ALLSIDES || rover->flags & FF_INVERTSIDES))
 						continue;
 
 					if (rover->norender == leveltime)
@@ -2273,7 +2273,9 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			{
 				for (rover = backsector->ffloors, i = 0; rover && i < MAXFFLOORS; rover = rover->next)
 				{
-					if (!(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_EXISTS) || rover->flags & FF_INVERTSIDES)
+					if (!(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_EXISTS))
+						continue;
+					if (!(rover->flags & FF_ALLSIDES) && rover->flags & FF_INVERTSIDES)
 						continue;
 					if (rover->norender == leveltime)
 						continue;
@@ -2293,7 +2295,9 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			{
 				for (rover = frontsector->ffloors, i = 0; rover && i < MAXFFLOORS; rover = rover->next)
 				{
-					if (!(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_EXISTS) || !(rover->flags & FF_ALLSIDES))
+					if (!(rover->flags & FF_RENDERSIDES) || !(rover->flags & FF_EXISTS))
+						continue;
+					if (!(rover->flags & FF_ALLSIDES || rover->flags & FF_INVERTSIDES))
 						continue;
 					if (rover->norender == leveltime)
 						continue;
@@ -2618,8 +2622,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 					if ((roverleft>>4 <= worldhigh || roverright>>4 <= worldhighslope) &&
 					    (roverleft>>4 >= worldlow || roverright>>4 >= worldlowslope) &&
-					    ((viewz < planevistest && !(rover->flags & FF_INVERTPLANES)) ||
-					     (viewz > planevistest && (rover->flags & FF_BOTHPLANES))))
+					    ((viewz < planevistest && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))) ||
+					     (viewz > planevistest && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 					{
 						//ffloor[i].slope = *rover->b_slope;
 						ffloor[i].b_pos = roverleft;
@@ -2641,8 +2645,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 					if ((roverleft>>4 <= worldhigh || roverright>>4 <= worldhighslope) &&
 					    (roverleft>>4 >= worldlow || roverright>>4 >= worldlowslope) &&
-					    ((viewz > planevistest && !(rover->flags & FF_INVERTPLANES)) ||
-					     (viewz < planevistest && (rover->flags & FF_BOTHPLANES))))
+					    ((viewz > planevistest && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))) ||
+					     (viewz < planevistest && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 					{
 						//ffloor[i].slope = *rover->t_slope;
 						ffloor[i].b_pos = roverleft;
@@ -2675,8 +2679,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 					if ((roverleft>>4 <= worldhigh || roverright>>4 <= worldhighslope) &&
 					    (roverleft>>4 >= worldlow || roverright>>4 >= worldlowslope) &&
-					    ((viewz < planevistest && !(rover->flags & FF_INVERTPLANES)) ||
-					     (viewz > planevistest && (rover->flags & FF_BOTHPLANES))))
+					    ((viewz < planevistest && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))) ||
+					     (viewz > planevistest && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 					{
 						//ffloor[i].slope = *rover->b_slope;
 						ffloor[i].b_pos = roverleft;
@@ -2698,8 +2702,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 					if ((roverleft>>4 <= worldhigh || roverright>>4 <= worldhighslope) &&
 					    (roverleft>>4 >= worldlow || roverright>>4 >= worldlowslope) &&
-					    ((viewz > planevistest && !(rover->flags & FF_INVERTPLANES)) ||
-					     (viewz < planevistest && (rover->flags & FF_BOTHPLANES))))
+					    ((viewz > planevistest && (rover->flags & FF_BOTHPLANES || !(rover->flags & FF_INVERTPLANES))) ||
+					     (viewz < planevistest && (rover->flags & FF_BOTHPLANES || rover->flags & FF_INVERTPLANES))))
 					{
 						//ffloor[i].slope = *rover->t_slope;
 						ffloor[i].b_pos = roverleft;
diff --git a/src/r_skins.c b/src/r_skins.c
index 2e9548bd7cd0831469b3169f6618d726996ab4c6..caf1fb17299dc0cbad0352df4fa8ec5b71b79ef4 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -504,6 +504,7 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
 	GETFLAG(FASTEDGE)
 	GETFLAG(MULTIABILITY)
 	GETFLAG(NONIGHTSROTATION)
+	GETFLAG(NONIGHTSSUPER)
 #undef GETFLAG
 
 	else // let's check if it's a sound, otherwise error out
diff --git a/src/r_things.c b/src/r_things.c
index fc0469f4cf369b7cbfd0f535bde717dafe92a796..aec4ed9502b11ba885c1d3c01a80cde562e5f0dd 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -428,9 +428,9 @@ void R_AddSpriteDefs(UINT16 wadnum)
 	switch (wadfiles[wadnum]->type)
 	{
 	case RET_WAD:
-		start = W_CheckNumForNamePwad("S_START", wadnum, 0);
+		start = W_CheckNumForMarkerStartPwad("S_START", wadnum, 0);
 		if (start == INT16_MAX)
-			start = W_CheckNumForNamePwad("SS_START", wadnum, 0); //deutex compatib.
+			start = W_CheckNumForMarkerStartPwad("SS_START", wadnum, 0); //deutex compatib.
 
 		end = W_CheckNumForNamePwad("S_END",wadnum,start);
 		if (end == INT16_MAX)
@@ -452,8 +452,6 @@ void R_AddSpriteDefs(UINT16 wadnum)
 
 		start = 0; //let say S_START is lump 0
 	}
-	else
-		start++;   // just after S_START
 
 	if (end == INT16_MAX || start >= end)
 	{
diff --git a/src/s_sound.c b/src/s_sound.c
index 0cc40a9cef5226b0b6ec275d71dac5a29823637f..6952b17f244f5a0113652274cc83d89d3fc89353 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1456,6 +1456,7 @@ musicdef_t soundtestsfx = {
 	0, // with no conditions
 	0,
 	0,
+	0,
 	false,
 	NULL
 };
@@ -1651,6 +1652,8 @@ ReadMusicDefFields (UINT16 wadnum, int line, boolean fields, char *stoken,
 				fixed_t bpmf = FLOAT_TO_FIXED(bpm);
 				if (bpmf > 0)
 					def->bpm = FixedDiv((60*TICRATE)<<FRACBITS, bpmf);
+			} else if (!stricmp(stoken, "loopms")) {
+				def->loop_ms = atoi(textline);
 			} else {
 				CONS_Alert(CONS_WARNING,
 						"MUSICDEF: Invalid field '%s'. (file %s, line %d)\n",
@@ -2262,6 +2265,8 @@ static void S_UnloadMusic(void)
 
 static boolean S_PlayMusic(boolean looping, UINT32 fadeinms)
 {
+	musicdef_t *def;
+
 	if (S_MusicDisabled())
 		return false;
 
@@ -2273,6 +2278,17 @@ static boolean S_PlayMusic(boolean looping, UINT32 fadeinms)
 		return false;
 	}
 
+	/* set loop point from MUSICDEF */
+	for (def = musicdefstart; def; def = def->next)
+	{
+		if (strcasecmp(def->name, music_name) == 0)
+		{
+			if (def->loop_ms)
+				S_SetMusicLoopPoint(def->loop_ms);
+			break;
+		}
+	}
+
 	S_InitMusicVolume(); // switch between digi and sequence volume
 
 	if (S_MusicNotInFocus())
diff --git a/src/s_sound.h b/src/s_sound.h
index d7e0c46ab8dd1fbc683cea7b5ed246fc2ebc6dda..119722af43a23e5910581be41a67d615b38061a4 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -208,6 +208,7 @@ typedef struct musicdef_s
 	INT16 soundtestcond; // +ve for map, -ve for conditionset, 0 for already here
 	tic_t stoppingtics;
 	fixed_t bpm;
+	UINT32 loop_ms;/* override LOOPPOINT/LOOPMS */
 	boolean allowed; // question marks or listenable on sound test?
 	struct musicdef_s *next;
 } musicdef_t;
diff --git a/src/screen.c b/src/screen.c
index 6bdf91ed861cbd2e3ffda335b6dd76004e8cd591..6e5fd54cd5e09046b42e300251ebecff01f4fa47 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -449,7 +449,7 @@ void SCR_ActuallyChangeRenderer(void)
 
 #ifdef HWRENDER
 	// Well, it didn't even load anyway.
-	if ((hwrenderloaded == -1) && (setrenderneeded == render_opengl))
+	if ((vid_opengl_state == -1) && (setrenderneeded == render_opengl))
 	{
 		if (M_CheckParm("-nogl"))
 			CONS_Alert(CONS_ERROR, "OpenGL rendering was disabled!\n");
@@ -474,7 +474,7 @@ void SCR_ChangeRenderer(void)
 	{
 		target_renderer = cv_renderer.value;
 #ifdef HWRENDER
-		if (M_CheckParm("-opengl") && (hwrenderloaded == 1))
+		if (M_CheckParm("-opengl") && (vid_opengl_state == 1))
 			target_renderer = rendermode = render_opengl;
 		else
 #endif
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 7e260f4c01ed5a83960bd8f65bdc1f50fbad5ee6..6335b3028a841997ddb38b92f70164060390fbf5 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -213,6 +213,7 @@
     <ClInclude Include="..\fastcmp.h" />
     <ClInclude Include="..\filesrch.h" />
     <ClInclude Include="..\f_finale.h" />
+    <ClInclude Include="..\g_demo.h" />
     <ClInclude Include="..\g_game.h" />
     <ClInclude Include="..\g_input.h" />
     <ClInclude Include="..\g_state.h" />
@@ -364,6 +365,7 @@
     <ClCompile Include="..\filesrch.c" />
     <ClCompile Include="..\f_finale.c" />
     <ClCompile Include="..\f_wipe.c" />
+    <ClCompile Include="..\g_demo.c" />
     <ClCompile Include="..\g_game.c" />
     <ClCompile Include="..\g_input.c" />
     <ClCompile Include="..\hardware\hw3sound.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index 21820551a4ad1bfcfe85765d7975fa482c646e55..a226e8397286000cc794a0172c03d983270e6d16 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -180,6 +180,9 @@
     <ClInclude Include="..\f_finale.h">
       <Filter>F_Frame</Filter>
     </ClInclude>
+    <ClInclude Include="..\g_demo.h">
+      <Filter>G_Game</Filter>
+    </ClInclude>
     <ClInclude Include="..\g_game.h">
       <Filter>G_Game</Filter>
     </ClInclude>
@@ -600,6 +603,9 @@
     <ClCompile Include="..\f_wipe.c">
       <Filter>F_Frame</Filter>
     </ClCompile>
+    <ClCompile Include="..\g_demo.c">
+      <Filter>G_Game</Filter>
+    </ClCompile>
     <ClCompile Include="..\g_game.c">
       <Filter>G_Game</Filter>
     </ClCompile>
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index c2f492000326bdd6319cd8a2f68a3f81afa9960f..c042d141c10362f1ef643ed844a8e31d6bc2d176 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -104,7 +104,7 @@ static consvar_t cv_stretch = {"stretch", "Off", CV_SAVE|CV_NOSHOWHELP, CV_OnOff
 static consvar_t cv_alwaysgrabmouse = {"alwaysgrabmouse", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 UINT8 graphics_started = 0; // Is used in console.c and screen.c
-INT32 hwrenderloaded = 0;
+INT32 vid_opengl_state = 0;
 
 // To disable fullscreen at startup; is set in VID_PrepareModeList
 boolean allow_fullscreen = false;
@@ -1440,7 +1440,7 @@ static SDL_bool Impl_CreateContext(void)
 {
 	// Renderer-specific stuff
 #ifdef HWRENDER
-	if ((rendermode == render_opengl) && (hwrenderloaded != -1))
+	if ((rendermode == render_opengl) && (vid_opengl_state != -1))
 	{
 		if (!sdlglcontext)
 			sdlglcontext = SDL_GL_CreateContext(window);
@@ -1473,10 +1473,10 @@ static SDL_bool Impl_CreateContext(void)
 	return SDL_TRUE;
 }
 
-#ifdef HWRENDER
-static void VID_CheckGLLoaded(rendermode_t oldrender)
+void VID_CheckGLLoaded(rendermode_t oldrender)
 {
-	if (hwrenderloaded == -1) // Well, it didn't work the first time anyway.
+#ifdef HWRENDER
+	if (vid_opengl_state == -1) // Well, it didn't work the first time anyway.
 	{
 		rendermode = oldrender;
 		if (chosenrendermode == render_opengl) // fallback to software
@@ -1488,40 +1488,66 @@ static void VID_CheckGLLoaded(rendermode_t oldrender)
 			setrenderneeded = 0;
 		}
 	}
-}
 #endif
+}
 
 void VID_CheckRenderer(void)
 {
-	SDL_bool rendererchanged = SDL_FALSE;
+	boolean rendererchanged = false;
+	boolean contextcreated = false;
 	rendermode_t oldrenderer = rendermode;
 
 	if (dedicated)
 		return;
 
-#ifdef HWRENDER
-	if (!graphics_started)
-		VID_CheckGLLoaded(oldrenderer);
-#endif
-
 	if (setrenderneeded)
 	{
 		rendermode = setrenderneeded;
-		rendererchanged = SDL_TRUE;
+		rendererchanged = true;
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl)
 		{
 			VID_CheckGLLoaded(oldrenderer);
+
 			// Initialise OpenGL before calling SDLSetMode!!!
-			if (hwrenderloaded != 1)
-				I_StartupHardwareGraphics();
-			else if (hwrenderloaded == -1)
-				rendererchanged = SDL_FALSE;
+			// This is because SDLSetMode calls OglSdlSurface.
+			if (vid_opengl_state == 0)
+			{
+				VID_StartupOpenGL();
+				// Loaded successfully!
+				if (vid_opengl_state == 1)
+				{
+					// Destroy the current window, if it exists.
+					if (window)
+					{
+						SDL_DestroyWindow(window);
+						window = NULL;
+					}
+
+					// Destroy the current window rendering context, if that also exists.
+					if (renderer)
+					{
+						SDL_DestroyRenderer(renderer);
+						renderer = NULL;
+					}
+
+					// Create a new window.
+					Impl_CreateWindow(USE_FULLSCREEN);
+
+					// From there, the OpenGL context was already created.
+					contextcreated = true;
+				}
+			}
+			else if (vid_opengl_state == -1)
+				rendererchanged = false;
 		}
 #endif
 
-		Impl_CreateContext();
+		if (!contextcreated)
+			Impl_CreateContext();
+
+		setrenderneeded = 0;
 	}
 
 	SDLSetMode(vid.width, vid.height, USE_FULLSCREEN, (rendererchanged ? SDL_FALSE : SDL_TRUE));
@@ -1534,15 +1560,25 @@ void VID_CheckRenderer(void)
 			SDL_FreeSurface(bufSurface);
 			bufSurface = NULL;
 		}
+
+		if (rendererchanged)
+		{
 #ifdef HWRENDER
-		if (hwrenderloaded == 1) // Only if OpenGL ever loaded!
-			HWR_FreeTextureCache();
+			if (vid_opengl_state == 1) // Only if OpenGL ever loaded!
+				HWR_FreeTextureCache();
 #endif
-		SCR_SetDrawFuncs();
+			SCR_SetDrawFuncs();
+		}
 	}
 #ifdef HWRENDER
 	else if (rendermode == render_opengl)
-		R_InitHardwareMode();
+	{
+		if (rendererchanged)
+		{
+			R_InitHardwareMode();
+			V_SetPalette(0);
+		}
+	}
 #else
 	(void)oldrenderer;
 #endif
@@ -1586,7 +1622,7 @@ static SDL_bool Impl_CreateWindow(SDL_bool fullscreen)
 		flags |= SDL_WINDOW_BORDERLESS;
 
 #ifdef HWRENDER
-	if (hwrenderloaded != -1)
+	if (vid_opengl_state == 1)
 		flags |= SDL_WINDOW_OPENGL;
 #endif
 
@@ -1720,11 +1756,12 @@ void I_StartupGraphics(void)
 
 	//SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY>>1,SDL_DEFAULT_REPEAT_INTERVAL<<2);
 	VID_Command_ModeList_f();
+
 #ifdef HWRENDER
 	if (M_CheckParm("-nogl"))
-		hwrenderloaded = -1; // Don't call SDL_GL_LoadLibrary
-	else
-		I_StartupHardwareGraphics();
+		vid_opengl_state = -1; // Don't startup OpenGL
+	else if (chosenrendermode == render_opengl)
+		VID_StartupOpenGL();
 #endif
 
 	// Fury: we do window initialization after GL setup to allow
@@ -1779,12 +1816,13 @@ void I_StartupGraphics(void)
 	graphics_started = true;
 }
 
-void I_StartupHardwareGraphics(void)
+void VID_StartupOpenGL(void)
 {
 #ifdef HWRENDER
 	static boolean glstartup = false;
 	if (!glstartup)
 	{
+		CONS_Printf("VID_StartupOpenGL()...\n");
 		HWD.pfnInit             = hwSym("Init",NULL);
 		HWD.pfnFinishUpdate     = NULL;
 		HWD.pfnDraw2DLine       = hwSym("Draw2DLine",NULL);
@@ -1816,13 +1854,13 @@ void I_StartupHardwareGraphics(void)
 		// check gl renderer lib
 		if (HWD.pfnGetRenderVersion() != VERSION)
 		{
-			CONS_Alert(CONS_ERROR, M_GetText("The version of the renderer doesn't match the version of the executable\nBe sure you have installed SRB2 properly.\n"));
-			hwrenderloaded = -1;
+			CONS_Alert(CONS_ERROR, M_GetText("The version of the renderer doesn't match the version of the executable!\nBe sure you have installed SRB2 properly.\n"));
+			vid_opengl_state = -1;
 		}
 		else
-			hwrenderloaded = HWD.pfnInit(I_Error) ? 1 : -1; // let load the OpenGL library
+			vid_opengl_state = HWD.pfnInit(I_Error) ? 1 : -1; // let load the OpenGL library
 
-		if (hwrenderloaded == -1)
+		if (vid_opengl_state == -1)
 		{
 			rendermode = render_soft;
 			setrenderneeded = 0;
diff --git a/src/st_stuff.c b/src/st_stuff.c
index ee7fc3f2166a148856aee09cb7febdfa655d6237..6e365dc6812b380db3fd8e84db6e944821c1684a 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -1331,6 +1331,16 @@ void ST_drawTitleCard(void)
 	INT32 ttlscroll = FixedInt(lt_scroll);
 	INT32 zzticker;
 	patch_t *actpat, *zigzag, *zztext;
+	UINT8 colornum;
+	const UINT8 *colormap;
+	stplyr = &players[consoleplayer];
+	
+	if (stplyr->skincolor)
+		colornum = stplyr->skincolor;
+	else
+		colornum = cv_playercolor.value;
+	
+	colormap = R_GetTranslationColormap(TC_DEFAULT, colornum, GTC_CACHE);
 
 	if (!G_IsTitleCardAvailable())
 		return;
@@ -1364,16 +1374,16 @@ void ST_drawTitleCard(void)
 	if (!splitscreen || (splitscreen && stplyr == &players[displayplayer]))
 	{
 		zzticker = lt_ticker;
-		V_DrawScaledPatch(FixedInt(lt_zigzag), (-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag);
-		V_DrawScaledPatch(FixedInt(lt_zigzag), (zigzag->height-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag);
-		V_DrawScaledPatch(FixedInt(lt_zigzag), (-zigzag->height+zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext);
-		V_DrawScaledPatch(FixedInt(lt_zigzag), (zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext);
+		V_DrawMappedPatch(FixedInt(lt_zigzag), (-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag, colormap);
+		V_DrawMappedPatch(FixedInt(lt_zigzag), (zigzag->height-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag, colormap);
+		V_DrawMappedPatch(FixedInt(lt_zigzag), (-zigzag->height+zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext, colormap);
+		V_DrawMappedPatch(FixedInt(lt_zigzag), (zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext, colormap);
 	}
 
 	if (actnum)
 	{
 		if (!splitscreen)
-			V_DrawScaledPatch(ttlnumxpos + ttlscroll, 104 - ttlscroll, 0, actpat);
+			V_DrawMappedPatch(ttlnumxpos + ttlscroll, 104 - ttlscroll, 0, actpat, colormap);
 		V_DrawLevelActNum(ttlnumxpos + ttlscroll, 104, V_PERPLAYER, actnum);
 	}
 
diff --git a/src/w_wad.c b/src/w_wad.c
index f7ccc175b87df030d492b1e9b6469ac42a99b070..1008aca8fce83767c0096a7e5296ec5b996e9b0d 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -194,7 +194,6 @@ static inline void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 	if (posStart != INT16_MAX)
 	{
 		posEnd = W_CheckNumForFolderEndPK3("Lua/", wadnum, posStart);
-		posStart++; // first "lump" will be "Lua/" folder itself, so ignore it
 		for (; posStart < posEnd; posStart++)
 			LUA_LoadLump(wadnum, posStart);
 	}
@@ -204,7 +203,6 @@ static inline void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 	{
 		posEnd = W_CheckNumForFolderEndPK3("SOC/", wadnum, posStart);
 
-		posStart++; // first "lump" will be "SOC/" folder itself, so ignore it
 		for(; posStart < posEnd; posStart++)
 		{
 			lumpinfo_t *lump_p = &wadfiles[wadnum]->lumpinfo[posStart];
@@ -525,8 +523,8 @@ typedef struct zlentry_s
 static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 {
     zend_t zend;
-    zentry_t* zentries;
-    zentry_t* zentry;
+    zentry_t zentry;
+    zlentry_t zlentry;
 
 	UINT16 numlumps = *nlmp;
 	lumpinfo_t* lumpinfo;
@@ -554,40 +552,36 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 	numlumps = zend.entries;
 
 	lump_p = lumpinfo = Z_Malloc(numlumps * sizeof (*lumpinfo), PU_STATIC, NULL);
-	zentry = zentries = malloc(numlumps * sizeof (*zentries));
 
 	fseek(handle, zend.cdiroffset, SEEK_SET);
-	for (i = 0; i < numlumps; i++, zentry++, lump_p++)
+	for (i = 0; i < numlumps; i++, lump_p++)
 	{
 		char* fullname;
 		char* trimname;
 		char* dotpos;
 
-		if (fread(zentry, 1, sizeof(zentry_t), handle) < sizeof(zentry_t))
+		if (fread(&zentry, 1, sizeof(zentry_t), handle) < sizeof(zentry_t))
 		{
 			CONS_Alert(CONS_ERROR, "Failed to read central directory (%s)\n", M_FileError(handle));
 			Z_Free(lumpinfo);
-			free(zentries);
 			return NULL;
 		}
-		if (memcmp(zentry->signature, pat_central, 4))
+		if (memcmp(zentry.signature, pat_central, 4))
 		{
 			CONS_Alert(CONS_ERROR, "Central directory is corrupt\n");
 			Z_Free(lumpinfo);
-			free(zentries);
 			return NULL;
 		}
 
-		lump_p->position = zentry->offset + zentry->namelen + zentry->xtralen + sizeof(zlentry_t);
-		lump_p->disksize = zentry->compsize;
-		lump_p->size = zentry->size;
+		lump_p->position = zentry.offset; // NOT ACCURATE YET: we still need to read the local entry to find our true position
+		lump_p->disksize = zentry.compsize;
+		lump_p->size = zentry.size;
 
-		fullname = malloc(zentry->namelen + 1);
-		if (fgets(fullname, zentry->namelen + 1, handle) != fullname)
+		fullname = malloc(zentry.namelen + 1);
+		if (fgets(fullname, zentry.namelen + 1, handle) != fullname)
 		{
 			CONS_Alert(CONS_ERROR, "Unable to read lumpname (%s)\n", M_FileError(handle));
 			Z_Free(lumpinfo);
-			free(zentries);
 			free(fullname);
 			return NULL;
 		}
@@ -604,12 +598,12 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 		memset(lump_p->name, '\0', 9); // Making sure they're initialized to 0. Is it necessary?
 		strncpy(lump_p->name, trimname, min(8, dotpos - trimname));
 
-		lump_p->name2 = Z_Calloc(zentry->namelen + 1, PU_STATIC, NULL);
-		strncpy(lump_p->name2, fullname, zentry->namelen);
+		lump_p->name2 = Z_Calloc(zentry.namelen + 1, PU_STATIC, NULL);
+		strncpy(lump_p->name2, fullname, zentry.namelen);
 
 		free(fullname);
 
-		switch(zentry->compression)
+		switch(zentry.compression)
 		{
 		case 0:
 			lump_p->compression = CM_NOCOMPRESSION;
@@ -627,9 +621,29 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 			lump_p->compression = CM_UNSUPPORTED;
 			break;
 		}
+
+		// skip and ignore comments/extra fields
+		if (fseek(handle, zentry.xtralen + zentry.commlen, SEEK_CUR) != 0)
+		{
+			CONS_Alert(CONS_ERROR, "Central directory is corrupt\n");
+			Z_Free(lumpinfo);
+			return NULL;
+		}
 	}
 
-	free(zentries);
+	// Adjust lump position values properly
+	for (i = 0, lump_p = lumpinfo; i < numlumps; i++, lump_p++)
+	{
+		// skip and ignore comments/extra fields
+		if ((fseek(handle, lump_p->position, SEEK_SET) != 0) || (fread(&zlentry, 1, sizeof(zlentry_t), handle) < sizeof(zlentry_t)))
+		{
+			CONS_Alert(CONS_ERROR, "Local headers for lump %s are corrupt\n", lump_p->name2);
+			Z_Free(lumpinfo);
+			return NULL;
+		}
+
+		lump_p->position += sizeof(zlentry_t) + zlentry.namelen + zlentry.xtralen;
+	}
 
 	*nlmp = numlumps;
 	return lumpinfo;
@@ -914,15 +928,32 @@ UINT16 W_CheckNumForNamePwad(const char *name, UINT16 wad, UINT16 startlump)
 	return INT16_MAX;
 }
 
+UINT16
+W_CheckNumForMarkerStartPwad (const char *name, UINT16 wad, UINT16 startlump)
+{
+	UINT16 marker;
+	marker = W_CheckNumForNamePwad(name, wad, startlump);
+	if (marker != INT16_MAX)
+		marker++; // Do not count the first marker
+	return marker;
+}
+
 // Look for the first lump from a folder.
 UINT16 W_CheckNumForFolderStartPK3(const char *name, UINT16 wad, UINT16 startlump)
 {
+	size_t name_length;
 	INT32 i;
 	lumpinfo_t *lump_p = wadfiles[wad]->lumpinfo + startlump;
+	name_length = strlen(name);
 	for (i = startlump; i < wadfiles[wad]->numlumps; i++, lump_p++)
 	{
-		if (strnicmp(name, lump_p->name2, strlen(name)) == 0)
+		if (strnicmp(name, lump_p->name2, name_length) == 0)
+		{
+			/* SLADE is special and puts a single directory entry. Skip that. */
+			if (strlen(lump_p->name2) == name_length)
+				i++;
 			break;
+		}
 	}
 	return i;
 }
@@ -1025,7 +1056,7 @@ lumpnum_t W_CheckNumForMap(const char *name)
 			else
 				continue;
 			// Now look for the specified map.
-			for (++lumpNum; lumpNum < end; lumpNum++)
+			for (; lumpNum < end; lumpNum++)
 				if (!strnicmp(name, (wadfiles[i]->lumpinfo + lumpNum)->name, 8))
 					return (i<<16) + lumpNum;
 		}
@@ -1505,11 +1536,9 @@ void *W_CacheLumpName(const char *name, INT32 tag)
 // Cache a patch into heap memory, convert the patch format as necessary
 //
 
-void *W_CachePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag)
+void *W_CacheSoftwarePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag)
 {
-#ifdef HWRENDER
-	GLPatch_t *grPatch;
-#endif
+	lumpcache_t *lumpcache = NULL;
 
 	if (needpatchflush)
 		W_FlushCachedPatches();
@@ -1517,44 +1546,65 @@ void *W_CachePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag)
 	if (!TestValidLump(wad, lump))
 		return NULL;
 
-#ifdef HWRENDER
-	// Software-only compile cache the data without conversion
-	if (rendermode == render_soft || rendermode == render_none)
-#endif
+	lumpcache = wadfiles[wad]->patchcache;
+
+	if (!lumpcache[lump])
 	{
-		lumpcache_t *lumpcache = wadfiles[wad]->patchcache;
-		if (!lumpcache[lump])
-		{
-			size_t len = W_LumpLengthPwad(wad, lump);
-			void *ptr, *lumpdata;
+		size_t len = W_LumpLengthPwad(wad, lump);
+		void *ptr, *lumpdata;
 #ifndef NO_PNG_LUMPS
-			void *srcdata = NULL;
+		void *srcdata = NULL;
 #endif
 
-			ptr = Z_Malloc(len, tag, &lumpcache[lump]);
-			lumpdata = Z_Malloc(len, tag, NULL);
+		ptr = Z_Malloc(len, tag, &lumpcache[lump]);
+		lumpdata = Z_Malloc(len, tag, NULL);
 
-			// read the lump in full
-			W_ReadLumpHeaderPwad(wad, lump, lumpdata, 0, 0);
+		// read the lump in full
+		W_ReadLumpHeaderPwad(wad, lump, lumpdata, 0, 0);
 
 #ifndef NO_PNG_LUMPS
-			// lump is a png so convert it
-			if (R_IsLumpPNG((UINT8 *)lumpdata, len))
-			{
-				size_t newlen;
-				srcdata = R_PNGToPatch((UINT8 *)lumpdata, len, &newlen);
-				ptr = Z_Realloc(ptr, newlen, tag, &lumpcache[lump]);
-				M_Memcpy(ptr, srcdata, newlen);
-				Z_Free(srcdata);
-			}
-			else // just copy it into the patch cache
-#endif
-				M_Memcpy(ptr, lumpdata, len);
+		// lump is a png so convert it
+		if (R_IsLumpPNG((UINT8 *)lumpdata, len))
+		{
+			size_t newlen;
+			srcdata = R_PNGToPatch((UINT8 *)lumpdata, len, &newlen);
+			ptr = Z_Realloc(ptr, newlen, tag, &lumpcache[lump]);
+			M_Memcpy(ptr, srcdata, newlen);
+			Z_Free(srcdata);
 		}
-		else
-			Z_ChangeTag(lumpcache[lump], tag);
+		else // just copy it into the patch cache
+#endif
+			M_Memcpy(ptr, lumpdata, len);
+	}
+	else
+		Z_ChangeTag(lumpcache[lump], tag);
 
-		return lumpcache[lump];
+	return lumpcache[lump];
+}
+
+void *W_CacheSoftwarePatchNum(lumpnum_t lumpnum, INT32 tag)
+{
+	return W_CacheSoftwarePatchNumPwad(WADFILENUM(lumpnum),LUMPNUM(lumpnum),tag);
+}
+
+void *W_CachePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag)
+{
+#ifdef HWRENDER
+	GLPatch_t *grPatch;
+#endif
+
+	if (needpatchflush)
+		W_FlushCachedPatches();
+
+	if (!TestValidLump(wad, lump))
+		return NULL;
+
+#ifdef HWRENDER
+	// Software-only compile cache the data without conversion
+	if (rendermode == render_soft || rendermode == render_none)
+#endif
+	{
+		return W_CacheSoftwarePatchNumPwad(wad, lump, tag);
 	}
 #ifdef HWRENDER
 
@@ -1829,6 +1879,10 @@ W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 		}
 
 		free(fullname);
+
+		// skip and ignore comments/extra fields
+		if (fseek(fp, zentry.xtralen + zentry.commlen, SEEK_CUR) != 0)
+			return true;
 	}
 
 	return true;
diff --git a/src/w_wad.h b/src/w_wad.h
index d598d9b391833d32b7d4f528b8b9fe035f9c341e..d4455ba1446e151abe7d33402315dc5c205d93c6 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -156,6 +156,9 @@ const char *W_CheckNameForNum(lumpnum_t lumpnum);
 
 UINT16 W_CheckNumForNamePwad(const char *name, UINT16 wad, UINT16 startlump); // checks only in one pwad
 
+/* Find the first lump after F_START for instance. */
+UINT16 W_CheckNumForMarkerStartPwad(const char *name, UINT16 wad, UINT16 startlump);
+
 UINT16 W_CheckNumForFullNamePK3(const char *name, UINT16 wad, UINT16 startlump);
 UINT16 W_CheckNumForFolderStartPK3(const char *name, UINT16 wad, UINT16 startlump);
 UINT16 W_CheckNumForFolderEndPK3(const char *name, UINT16 wad, UINT16 startlump);
@@ -191,8 +194,15 @@ boolean W_IsPatchCached(lumpnum_t lump, void *ptr);
 void *W_CacheLumpName(const char *name, INT32 tag);
 void *W_CachePatchName(const char *name, INT32 tag);
 
-void *W_CachePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag); // return a patch_t
-void *W_CachePatchNum(lumpnum_t lumpnum, INT32 tag); // return a patch_t
+// Returns either a Software patch, or an OpenGL patch.
+// Performs any necessary conversions from PNG images.
+void *W_CachePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag);
+void *W_CachePatchNum(lumpnum_t lumpnum, INT32 tag);
+
+// Returns a Software patch.
+// Performs any necessary conversions from PNG images.
+void *W_CacheSoftwarePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag);
+void *W_CacheSoftwarePatchNum(lumpnum_t lumpnum, INT32 tag);
 
 void W_UnlockCachedPatch(void *patch);
 void W_FlushCachedPatches(void);
diff --git a/src/win32/win_vid.c b/src/win32/win_vid.c
index d0aab92b3e16a4d13fb4cb57039869a732bfaef1..4e7bab5696d6861350412b47f52b591352da8cde 100644
--- a/src/win32/win_vid.c
+++ b/src/win32/win_vid.c
@@ -239,10 +239,7 @@ void I_StartupGraphics(void)
 	if (!dedicated) graphics_started = true;
 }
 
-void I_StartupHardwareGraphics(void)
-{
-	// oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo yeah oh yeah woo y
-}
+void VID_StartupOpenGL(void){}
 
 // ------------------
 // I_ShutdownGraphics
@@ -951,10 +948,8 @@ INT32 VID_SetMode(INT32 modenum)
 	return 1;
 }
 
-void VID_CheckRenderer(void)
-{
-	// ..............
-}
+void VID_CheckRenderer(void) {}
+void VID_CheckGLLoaded(rendermode_t oldrender) {}
 
 // ========================================================================
 // Free the video buffer of the last video mode,