// 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_time.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 "p_saveg.h" // savebuffer_t
#include "g_party.h"

// SRB2Kart
#include "d_netfil.h" // nameonly

#include "lua_script.h" // LUA_ArchiveDemo and LUA_UnArchiveDemo
#include "lua_libs.h" // gL (Lua state)

#include "k_kart.h"
#include "k_battle.h"
#include "k_bot.h"
#include "k_color.h"
#include "k_follower.h"
#include "k_grandprix.h"
#include "g_party.h"

static CV_PossibleValue_t recordmultiplayerdemos_cons_t[] = {{0, "Disabled"}, {1, "Manual Save"}, {2, "Auto Save"}, {0, NULL}};
consvar_t cv_recordmultiplayerdemos = CVAR_INIT ("netdemo_record", "Manual Save", CV_SAVE, recordmultiplayerdemos_cons_t, NULL);

static CV_PossibleValue_t netdemosyncquality_cons_t[] = {{1, "MIN"}, {35, "MAX"}, {0, NULL}};
consvar_t cv_netdemosyncquality = CVAR_INIT ("netdemo_syncquality", "1", CV_SAVE, netdemosyncquality_cons_t, NULL);

consvar_t cv_netdemosize = CVAR_INIT ("netdemo_size", "6", CV_SAVE, CV_Natural, NULL);

boolean nodrawers; // for comparative timing purposes
boolean noblit; // for comparative timing purposes
tic_t demostarttime; // for comparative timing purposes

static char demoname[MAX_WADPATH];
static savebuffer_t demobuf = {0};
static UINT8 *demotime_p, *demoinfo_p;
static UINT8 demoflags;
boolean demosynced = true; // console warning message

struct demovars_s demo;

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_KART
	INT32 kartitem, kartamount, kartbumpers;

	UINT8 desyncframes; // Don't try to resync unless we've been off for two frames, to monkeypatch a few trouble spots

	// EZT_HIT
	UINT16 hits;
	mobj_t **hitlist;
} ghostext[MAXPLAYERS];

// Your naming conventions are stupid and useless.
// There is no conflict here.
demoghost *ghosts = NULL;

//
// DEMO RECORDING
//

#define DEMOVERSION 0x0008
#define DEMOHEADER  "\xF0" "KartReplay" "\x0F"

#define DF_GHOST        0x01 // This demo contains ghost data too!
#define DF_TIMEATTACK   0x02 // This demo is from Time Attack and contains its final completion time & best lap!
#define DF_ITEMBREAKER 0x04 // This demo is from Item Breaker and contains its final completion time!
#define DF_ATTACKMASK   0x06 // This demo is from ??? attack and contains ???

// 0x08 free

#define DF_NONETMP		0x10 // multiplayer but not netgame

#define DF_LUAVARS		0x20 // this demo contains extra lua vars

#define DF_ATTACKSHIFT  1
#define DF_ENCORE       0x40
#define DF_MULTIPLAYER  0x80 // This demo was recorded in multiplayer mode!
#define DF_GRANDPRIX    0x0100

#define DEMO_SPECTATOR	0x01
#define DEMO_KICKSTART	0x02
#define DEMO_SHRINKME	0x04
#define DEMO_BOT		0x08

// For demos
#define ZT_FWD		0x0001
#define ZT_SIDE		0x0002
#define ZT_TURNING	0x0004
#define ZT_ANGLE	0x0008
#define ZT_THROWDIR	0x0010
#define ZT_BUTTONS	0x0020
#define ZT_AIMING	0x0040
#define ZT_LATENCY	0x0080
#define ZT_FLAGS	0x0100
#define ZT_BOT		0x8000
// Ziptics are UINT16 now, go nuts

#define ZT_BOT_TURN			0x0001
#define ZT_BOT_ITEM			0x0002
#define ZT_BOT_RESPAWN		0x0004

#define DEMOMARKER 0x80 // demobuf.end

UINT8 demo_extradata[MAXPLAYERS];
UINT8 demo_writerng; // 0=no, 1=yes, 2=yes but on a timeout
static ticcmd_t oldcmd[MAXPLAYERS];

#define METALDEATH 0x44
#define METALSNICE 0x69

#define DW_END        0xFF // End of extradata block
#define DW_RNG        0xFE // Check RNG seed!

#define DW_EXTRASTUFF 0xFE // Numbers below this are reserved for writing player slot data

// Below consts are only used for demo extrainfo sections
#define DW_STANDING 0x00

// 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_COLOR  0x001 // Changed color (Super transformation, Mario fireflowers/invulnerability, etc.)
#define EZT_FLIP   0x002 // Reversed gravity
#define EZT_SCALE  0x004 // Changed size
#define EZT_HIT    0x008 // Damaged a mobj
#define EZT_SPRITE 0x010 // Changed sprite set completely out of PLAY (NiGHTS, SOCs, whatever)
#define EZT_KART   0x020 // SRB2Kart: Changed current held item/quantity and bumpers for battle

// 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[MAXPLAYERS];

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);
}

// Finds a skin with the closest stats if the expected skin doesn't exist.
static INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight)
{
	INT32 i, closest_skin = 0;
	UINT8 closest_stats = UINT8_MAX, stat_diff;

	for (i = 0; i < numskins; i++)
	{
		stat_diff = abs(skins[i].kartspeed - kartspeed) + abs(skins[i].kartweight - kartweight);
		if (stat_diff < closest_stats)
		{
			closest_stats = stat_diff;
			closest_skin = i;
		}
	}

	return closest_skin;
}

static void FindClosestSkinForStats(UINT32 p, UINT8 kartspeed, UINT8 kartweight)
{
	INT32 closest_skin = GetSkinNumClosestToStats(kartspeed, kartweight);

	//CONS_Printf("Using %s instead...\n", skins[closest_skin].name);
	SetPlayerSkinByNum(p, closest_skin);
}

void G_ReadDemoExtraData(void)
{
	INT32 p, extradata, i;
	char name[17];

	if (leveltime > starttime)
	{
		rewind_t *rewind = CL_SaveRewindPoint(demobuf.p - demobuf.buffer);
		if (rewind)
		{
			memcpy(rewind->oldcmd, oldcmd, sizeof (oldcmd));
			memcpy(rewind->oldghost, oldghost, sizeof (oldghost));
		}
	}

	memset(name, '\0', 17);

	p = READUINT8(demobuf.p);

	while (p < DW_EXTRASTUFF)
	{
		extradata = READUINT8(demobuf.p);

		if (extradata & DXD_JOINDATA)
		{
			if (!playeringame[p])
			{
				G_AddPlayer(p, p);
			}

			players[p].bot = !!(READUINT8(demobuf.p));
			if (players[p].bot)
			{
				players[p].botvars.difficulty = READUINT8(demobuf.p);
				players[p].botvars.diffincrease = READUINT8(demobuf.p); // needed to avoid having to duplicate logic
				players[p].botvars.rival = (boolean)READUINT8(demobuf.p);
			}
		}
		if (extradata & DXD_PLAYSTATE)
		{
			i = READUINT8(demobuf.p);

			switch (i) {
			case DXD_PST_PLAYING:
				if (players[p].spectator == true)
				{
					if (players[p].bot)
					{
						players[p].spectator = false;
					}
					else
					{
						players[p].pflags |= PF_WANTSTOJOIN;
					}
				}
				//CONS_Printf("player %s is despectating on tic %d\n", player_names[p], leveltime);
				break;

			case DXD_PST_SPECTATING:
				if (players[p].spectator)
				{
					players[p].pflags &= ~PF_WANTSTOJOIN;
				}
				else
				{
					if (players[p].mo)
					{
						P_DamageMobj(players[p].mo, NULL, NULL, 1, DMG_SPECTATOR);
					}
					P_SetPlayerSpectator(p);
				}

				break;

			case DXD_PST_LEFT:
				CL_RemovePlayer(p, 0);
				break;
			}

			G_ResetViews();

			// maybe these are necessary?
			K_CheckBumpers();
			P_CheckRacers();
		}
		if (extradata & DXD_NAME)
		{
			// Name
			M_Memcpy(player_names[p],demobuf.p,16);
			demobuf.p += 16;
		}
		if (extradata & DXD_SKIN)
		{
			UINT8 kartspeed, kartweight;

			// Skin
			M_Memcpy(name, demobuf.p, 16);
			demobuf.p += 16;
			SetPlayerSkin(p, name);

			kartspeed = READUINT8(demobuf.p);
			kartweight = READUINT8(demobuf.p);

			if (stricmp(skins[players[p].skin].name, name) != 0)
				FindClosestSkinForStats(p, kartspeed, kartweight);

			players[p].kartspeed = kartspeed;
			players[p].kartweight = kartweight;
		}
		if (extradata & DXD_COLOR)
		{
			// Color
			M_Memcpy(name, demobuf.p, 16);
			demobuf.p += 16;
			for (i = 0; i < numskincolors; i++)
				if (!stricmp(skincolors[i].name, name))				// SRB2kart
				{
					players[p].skincolor = i;
					if (players[p].mo)
						players[p].mo->color = i;
					break;
				}
		}
		if (extradata & DXD_FOLLOWER)
		{
			// Set our follower
			M_Memcpy(name, demobuf.p, 16);
			demobuf.p += 16;
			K_SetFollowerByName(p, name);

			// Follower's color
			M_Memcpy(name, demobuf.p, 16);
			demobuf.p += 16;
			for (i = 0; i < numskincolors +2; i++)	// +2 because of Match and Opposite
			{
					if (!stricmp(Followercolor_cons_t[i].strvalue, name))
					{
							players[p].followercolor = i;
							break;
					}
			}
		}
		if (extradata & DXD_RESPAWN)
		{
			if (players[p].mo)
			{
				// Is this how this should work..?
				P_DamageMobj(players[p].mo, NULL, NULL, 1, DMG_INSTAKILL);
			}
		}
		if (extradata & DXD_WEAPONPREF)
		{
			WeaponPref_Parse(&demobuf.p, p);

			//CONS_Printf("weaponpref is %d for player %d\n", i, p);
		}

		p = READUINT8(demobuf.p);
	}

	while (p != DW_END)
	{
		UINT32 rng;

		switch (p)
		{
		case DW_RNG:
			rng = READUINT32(demobuf.p);
			if (P_GetRandSeed() != rng)
			{
				P_SetRandSeed(rng);

				if (demosynced)
					CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (RNG)!\n"));
				demosynced = false;
			}
		}

		p = READUINT8(demobuf.p);
	}

	if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
	{
		// end of demo data stream
		G_CheckDemoStatus();
		return;
	}
}

void G_WriteDemoExtraData(void)
{
	INT32 i;
	char name[16];

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (demo_extradata[i])
		{
			WRITEUINT8(demobuf.p, i);
			WRITEUINT8(demobuf.p, demo_extradata[i]);

			if (demo_extradata[i] & DXD_JOINDATA)
			{
				WRITEUINT8(demobuf.p, (UINT8)players[i].bot);
				if (players[i].bot)
				{
					WRITEUINT8(demobuf.p, players[i].botvars.difficulty);
					WRITEUINT8(demobuf.p, players[i].botvars.diffincrease); // needed to avoid having to duplicate logic
					WRITEUINT8(demobuf.p, (UINT8)players[i].botvars.rival);
				}
			}
			if (demo_extradata[i] & DXD_PLAYSTATE)
			{
				UINT8 pst = DXD_PST_PLAYING;

				demo_writerng = 1;

				if (!playeringame[i])
				{
					pst = DXD_PST_LEFT;
				}
				else if (
					players[i].spectator &&
					!(players[i].pflags & PF_WANTSTOJOIN) // <= fuck you specifically
				)
				{
					pst = DXD_PST_SPECTATING;
				}

				WRITEUINT8(demobuf.p, pst);
			}
			if (demo_extradata[i] & DXD_NAME)
			{
				// Name
				memset(name, 0, 16);
				strncpy(name, player_names[i], 16);
				M_Memcpy(demobuf.p,name,16);
				demobuf.p += 16;
			}
			if (demo_extradata[i] & DXD_SKIN)
			{
				// Skin
				memset(name, 0, 16);
				strncpy(name, skins[players[i].skin].name, 16);
				M_Memcpy(demobuf.p,name,16);
				demobuf.p += 16;

				WRITEUINT8(demobuf.p, skins[players[i].skin].kartspeed);
				WRITEUINT8(demobuf.p, skins[players[i].skin].kartweight);

			}
			if (demo_extradata[i] & DXD_COLOR)
			{
				// Color
				memset(name, 0, 16);
				strncpy(name, skincolors[players[i].skincolor].name, 16);
				M_Memcpy(demobuf.p,name,16);
				demobuf.p += 16;
			}
			if (demo_extradata[i] & DXD_FOLLOWER)
			{
				// write follower
				memset(name, 0, 16);
				if (players[i].followerskin == -1)
					strncpy(name, "None", 16);
				else
					strncpy(name, followers[players[i].followerskin].skinname, 16);
				M_Memcpy(demobuf.p, name, 16);
				demobuf.p += 16;

				// write follower color
				memset(name, 0, 16);
				strncpy(name, Followercolor_cons_t[(UINT16)(players[i].followercolor+2)].strvalue, 16);	// Not KartColor_Names because followercolor has extra values such as "Match"
				M_Memcpy(demobuf.p,name,16);
				demobuf.p += 16;

			}
			//if (demo_extradata[i] & DXD_RESPAWN) has no extra data
			if (demo_extradata[i] & DXD_WEAPONPREF)
			{
				WeaponPref_Save(&demobuf.p, i);
			}
		}

		demo_extradata[i] = 0;
	}

	// May not be necessary, but might as well play it safe...
	if ((leveltime & 255) == 128)
		demo_writerng = 1;

	{
		static UINT8 timeout = 0;

		if (timeout) timeout--;

		if (demo_writerng == 1 || (demo_writerng == 2 && timeout == 0))
		{
			demo_writerng = 0;
			timeout = 16;
			WRITEUINT8(demobuf.p, DW_RNG);
			WRITEUINT32(demobuf.p, P_GetRandSeed());
		}
	}

	WRITEUINT8(demobuf.p, DW_END);
}

void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
{
	UINT16 ziptic;

	if (!demobuf.p || !demo.deferstart)
		return;

	ziptic = READUINT16(demobuf.p);

	if (ziptic & ZT_FWD)
		oldcmd[playernum].forwardmove = READSINT8(demobuf.p);
	if (ziptic & ZT_SIDE)
		oldcmd[playernum].sidemove = READSINT8(demobuf.p);
	if (ziptic & ZT_TURNING)
		oldcmd[playernum].turning = READINT16(demobuf.p);
	if (ziptic & ZT_ANGLE)
		oldcmd[playernum].angle = READINT16(demobuf.p);
	if (ziptic & ZT_THROWDIR)
		oldcmd[playernum].throwdir = READINT16(demobuf.p);
	if (ziptic & ZT_BUTTONS)
		oldcmd[playernum].buttons = READUINT16(demobuf.p);
	if (ziptic & ZT_AIMING)
		oldcmd[playernum].aiming = READINT16(demobuf.p);
	if (ziptic & ZT_LATENCY)
		oldcmd[playernum].latency = READUINT8(demobuf.p);
	if (ziptic & ZT_FLAGS)
		oldcmd[playernum].flags = READUINT8(demobuf.p);

	if (ziptic & ZT_BOT)
	{
		UINT16 botziptic = READUINT16(demobuf.p);

		if (botziptic & ZT_BOT_TURN)
			oldcmd[playernum].bot.turnconfirm = READSINT8(demobuf.p);
		if (botziptic & ZT_BOT_ITEM)
			oldcmd[playernum].bot.itemconfirm = READSINT8(demobuf.p);
		if (botziptic & ZT_BOT_RESPAWN)
			oldcmd[playernum].bot.respawnconfirm = READSINT8(demobuf.p);
	}

	G_CopyTiccmd(cmd, &oldcmd[playernum], 1);

	if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
	{
		// end of demo data stream
		G_CheckDemoStatus();
		return;
	}
}

void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
{
	UINT16 ziptic = 0;
	UINT8 *ziptic_p;
	//(void)playernum;

	if (!demobuf.p)
		return;

	ziptic_p = demobuf.p; // the ziptic, written at the end of this function
	demobuf.p += 2;

	if (cmd->forwardmove != oldcmd[playernum].forwardmove)
	{
		WRITESINT8(demobuf.p,cmd->forwardmove);
		oldcmd[playernum].forwardmove = cmd->forwardmove;
		ziptic |= ZT_FWD;
	}

	if (cmd->sidemove != oldcmd[playernum].sidemove)
	{
		WRITESINT8(demobuf.p,cmd->sidemove);
		oldcmd[playernum].sidemove = cmd->sidemove;
		ziptic |= ZT_SIDE;
	}

	if (cmd->turning != oldcmd[playernum].turning)
	{
		WRITEINT16(demobuf.p,cmd->turning);
		oldcmd[playernum].turning = cmd->turning;
		ziptic |= ZT_TURNING;
	}

	if (cmd->angle != oldcmd[playernum].angle)
	{
		WRITEINT16(demobuf.p,cmd->angle);
		oldcmd[playernum].angle = cmd->angle;
		ziptic |= ZT_ANGLE;
	}

	if (cmd->throwdir != oldcmd[playernum].throwdir)
	{
		WRITEINT16(demobuf.p,cmd->throwdir);
		oldcmd[playernum].throwdir = cmd->throwdir;
		ziptic |= ZT_THROWDIR;
	}

	if (cmd->buttons != oldcmd[playernum].buttons)
	{
		WRITEUINT16(demobuf.p,cmd->buttons);
		oldcmd[playernum].buttons = cmd->buttons;
		ziptic |= ZT_BUTTONS;
	}

	if (cmd->aiming != oldcmd[playernum].aiming)
	{
		WRITEINT16(demobuf.p,cmd->aiming);
		oldcmd[playernum].aiming = cmd->aiming;
		ziptic |= ZT_AIMING;
	}

	if (cmd->latency != oldcmd[playernum].latency)
	{
		WRITEUINT8(demobuf.p,cmd->latency);
		oldcmd[playernum].latency = cmd->latency;
		ziptic |= ZT_LATENCY;
	}

	if (cmd->flags != oldcmd[playernum].flags)
	{
		WRITEUINT8(demobuf.p,cmd->flags);
		oldcmd[playernum].flags = cmd->flags;
		ziptic |= ZT_FLAGS;
	}

	if (cmd->flags & TICCMD_BOT)
	{
		ziptic |= ZT_BOT;
	}

	WRITEUINT16(ziptic_p, ziptic);

	if (ziptic & ZT_BOT)
	{
		UINT16 botziptic = 0;
		UINT8 *botziptic_p;

		botziptic_p = demobuf.p; // the ziptic, written at the end of this function
		demobuf.p += 2;

		if (cmd->bot.turnconfirm != oldcmd[playernum].bot.turnconfirm)
		{
			WRITESINT8(demobuf.p, cmd->bot.turnconfirm);
			oldcmd[playernum].bot.turnconfirm = cmd->bot.turnconfirm;
			botziptic |= ZT_BOT_TURN;
		}

		if (cmd->bot.itemconfirm != oldcmd[playernum].bot.itemconfirm)
		{
			WRITESINT8(demobuf.p, cmd->bot.itemconfirm);
			oldcmd[playernum].bot.itemconfirm = cmd->bot.itemconfirm;
			botziptic |= ZT_BOT_ITEM;
		}

		if (cmd->bot.respawnconfirm != oldcmd[playernum].bot.respawnconfirm)
		{
			WRITESINT8(demobuf.p, cmd->bot.respawnconfirm);
			oldcmd[playernum].bot.respawnconfirm = cmd->bot.respawnconfirm;
			botziptic |= ZT_BOT_RESPAWN;
		}

		WRITEUINT16(botziptic_p, botziptic);
	}

	// attention here for the ticcmd size!
	// latest demos with mouse aiming byte in ticcmd
	if (!(demoflags & DF_GHOST) && ziptic_p > demobuf.end - 9)
	{
		G_CheckDemoStatus(); // no more space
		return;
	}
}

void G_GhostAddFlip(INT32 playernum)
{
	if (!metalrecording && (!demo.recording || !(demoflags & DF_GHOST)))
		return;
	ghostext[playernum].flags |= EZT_FLIP;
}

void G_GhostAddColor(INT32 playernum, ghostcolor_t color)
{
	if (!demo.recording || !(demoflags & DF_GHOST))
		return;
	if (ghostext[playernum].lastcolor == (UINT16)color)
	{
		ghostext[playernum].flags &= ~EZT_COLOR;
		return;
	}
	ghostext[playernum].flags |= EZT_COLOR;
	ghostext[playernum].color = (UINT16)color;
}

void G_GhostAddScale(INT32 playernum, fixed_t scale)
{
	if (!metalrecording && (!demo.recording || !(demoflags & DF_GHOST)))
		return;
	if (ghostext[playernum].lastscale == scale)
	{
		ghostext[playernum].flags &= ~EZT_SCALE;
		return;
	}
	ghostext[playernum].flags |= EZT_SCALE;
	ghostext[playernum].scale = scale;
}

void G_GhostAddHit(INT32 playernum, mobj_t *victim)
{
	if (!demo.recording || !(demoflags & DF_GHOST))
		return;
	ghostext[playernum].flags |= EZT_HIT;
	ghostext[playernum].hits++;
	ghostext[playernum].hitlist = Z_Realloc(ghostext[playernum].hitlist, ghostext[playernum].hits * sizeof(mobj_t *), PU_LEVEL, &ghostext[playernum].hitlist);
	P_SetTarget(ghostext[playernum].hitlist + (ghostext[playernum].hits-1), victim);
}

void G_WriteAllGhostTics(void)
{
	boolean toobig = false;
	INT32 i, counter = leveltime;
	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || players[i].spectator)
			continue;

		if (!players[i].mo)
			continue;

		counter++;

		if (multiplayer && ((counter % cv_netdemosyncquality.value) != 0)) // Only write 1 in this many ghost datas per tic to cut down on multiplayer replay size.
			continue;

		WRITEUINT8(demobuf.p, i);
		G_WriteGhostTic(players[i].mo, i);

		// attention here for the ticcmd size!
		// latest demos with mouse aiming byte in ticcmd
		if (demobuf.p >= demobuf.end - (13 + 9 + 9))
		{
			toobig = true;
			break;
		}
	}
	WRITEUINT8(demobuf.p, 0xFF);

	if (toobig)
	{
		G_CheckDemoStatus(); // no more space
		return;
	}
}

void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
{
	char ziptic = 0;
	UINT8 *ziptic_p;
	UINT32 i;

	if (!demobuf.p)
		return;
	if (!(demoflags & DF_GHOST))
		return; // No ghost data to write.

	ziptic_p = demobuf.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[playernum].x) > MAXMOM
	|| abs(ghost->y-oldghost[playernum].y) > MAXMOM
	|| abs(ghost->z-oldghost[playernum].z) > MAXMOM
	|| ((UINT8)(leveltime & 255) > 0 && (UINT8)(leveltime & 255) <= (UINT8)cv_netdemosyncquality.value)) // Hack to enable slightly nicer resyncing
	{
		oldghost[playernum].x = ghost->x;
		oldghost[playernum].y = ghost->y;
		oldghost[playernum].z = ghost->z;
		ziptic |= GZT_XYZ;
		WRITEFIXED(demobuf.p,oldghost[playernum].x);
		WRITEFIXED(demobuf.p,oldghost[playernum].y);
		WRITEFIXED(demobuf.p,oldghost[playernum].z);
	}
	else
	{
		// For moving normally:
		fixed_t momx = ghost->x-oldghost[playernum].x;
		fixed_t momy = ghost->y-oldghost[playernum].y;

		if (momx != oldghost[playernum].momx
		|| momy != oldghost[playernum].momy)
		{
			oldghost[playernum].momx = momx;
			oldghost[playernum].momy = momy;
			ziptic |= GZT_MOMXY;
			WRITEFIXED(demobuf.p,momx);
			WRITEFIXED(demobuf.p,momy);
		}

		momx = ghost->z-oldghost[playernum].z;
		if (momx != oldghost[playernum].momz)
		{
			oldghost[playernum].momz = momx;
			ziptic |= GZT_MOMZ;
			WRITEFIXED(demobuf.p,momx);
		}

		// This SHOULD set oldghost.x/y/z to match ghost->x/y/z
		oldghost[playernum].x += oldghost[playernum].momx;
		oldghost[playernum].y += oldghost[playernum].momy;
		oldghost[playernum].z +=oldghost[playernum].momz;
	}

#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[playernum].angle)
	{
		oldghost[playernum].angle = ghost->player->drawangle>>24;
		ziptic |= GZT_ANGLE;
		WRITEUINT8(demobuf.p,oldghost[playernum].angle);
	}

	// Store the sprite frame.
	if ((ghost->frame & FF_FRAMEMASK) != oldghost[playernum].frame)
	{
		oldghost[playernum].frame = (ghost->frame & FF_FRAMEMASK);
		ziptic |= GZT_FRAME;
		WRITEUINT8(demobuf.p,oldghost[playernum].frame);
	}

	if (ghost->sprite == SPR_PLAY
	&& ghost->sprite2 != oldghost[playernum].sprite2)
	{
		oldghost[playernum].sprite2 = ghost->sprite2;
		ziptic |= GZT_SPR2;
		WRITEUINT8(demobuf.p,oldghost[playernum].sprite2);
	}

	// Check for sprite set changes
	if (ghost->sprite != oldghost[playernum].sprite)
	{
		oldghost[playernum].sprite = ghost->sprite;
		ghostext[playernum].flags |= EZT_SPRITE;
	}

	if (ghost->player && (
			ghostext[playernum].kartitem != ghost->player->itemtype ||
			ghostext[playernum].kartamount != ghost->player->itemamount ||
			ghostext[playernum].kartbumpers != ghost->player->bumper
		))
	{
		ghostext[playernum].flags |= EZT_KART;
		ghostext[playernum].kartitem = ghost->player->itemtype;
		ghostext[playernum].kartamount = ghost->player->itemamount;
		ghostext[playernum].kartbumpers = ghost->player->bumper;
	}

	if (ghostext[playernum].flags)
	{
		ziptic |= GZT_EXTRA;

		if (ghostext[playernum].color == ghostext[playernum].lastcolor)
			ghostext[playernum].flags &= ~EZT_COLOR;
		if (ghostext[playernum].scale == ghostext[playernum].lastscale)
			ghostext[playernum].flags &= ~EZT_SCALE;

		WRITEUINT8(demobuf.p,ghostext[playernum].flags);
		if (ghostext[playernum].flags & EZT_COLOR)
		{
			WRITEUINT16(demobuf.p,ghostext[playernum].color);
			ghostext[playernum].lastcolor = ghostext[playernum].color;
		}
		if (ghostext[playernum].flags & EZT_SCALE)
		{
			WRITEFIXED(demobuf.p,ghostext[playernum].scale);
			ghostext[playernum].lastscale = ghostext[playernum].scale;
		}
		if (ghostext[playernum].flags & EZT_HIT)
		{
			WRITEUINT16(demobuf.p,ghostext[playernum].hits);
			for (i = 0; i < ghostext[playernum].hits; i++)
			{
				mobj_t *mo = ghostext[playernum].hitlist[i];
				//WRITEUINT32(demobuf.p,UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.)
				WRITEUINT32(demobuf.p,mo->type);
				WRITEUINT16(demobuf.p,(UINT16)mo->health);
				WRITEFIXED(demobuf.p,mo->x);
				WRITEFIXED(demobuf.p,mo->y);
				WRITEFIXED(demobuf.p,mo->z);
				WRITEANGLE(demobuf.p,mo->angle);
				P_SetTarget(ghostext[playernum].hitlist+i, NULL);
			}
			ghostext[playernum].hits = 0;
		}
		if (ghostext[playernum].flags & EZT_SPRITE)
			WRITEUINT16(demobuf.p,oldghost[playernum].sprite);
		if (ghostext[playernum].flags & EZT_KART)
		{
			WRITEINT32(demobuf.p, ghostext[playernum].kartitem);
			WRITEINT32(demobuf.p, ghostext[playernum].kartamount);
			WRITEINT32(demobuf.p, ghostext[playernum].kartbumpers);
		}

		ghostext[playernum].flags = 0;
	}

	if (ghost->player && ghost->player->followmobj&& !(ghost->player->followmobj->sprite == SPR_NULL || (ghost->player->followmobj->renderflags & RF_DONTDRAW) == RF_DONTDRAW)) // bloats tails runs but what can ya do
	{
		fixed_t temp;
		UINT8 *followtic_p = demobuf.p++;
		UINT8 followtic = 0;

		ziptic |= GZT_FOLLOW;

		if (ghost->player->followmobj->skin)
			followtic |= FZT_SKIN;

		if (!(oldghost[playernum].flags2 & MF2_AMBUSH))
		{
			followtic |= FZT_SPAWNED;
			WRITEINT16(demobuf.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)
				WRITEUINT16(demobuf.p,(UINT16)(((skin_t *)(ghost->player->followmobj->skin))-skins));
			oldghost[playernum].flags2 |= MF2_AMBUSH;
		}

		if (ghost->player->followmobj->scale != ghost->scale)
		{
			followtic |= FZT_SCALE;
			WRITEFIXED(demobuf.p,ghost->player->followmobj->scale);
		}

		temp = ghost->player->followmobj->x-ghost->x;
		WRITEFIXED(demobuf.p,temp);
		temp = ghost->player->followmobj->y-ghost->y;
		WRITEFIXED(demobuf.p,temp);
		temp = ghost->player->followmobj->z-ghost->z;
		WRITEFIXED(demobuf.p,temp);
		if (followtic & FZT_SKIN)
			WRITEUINT8(demobuf.p,ghost->player->followmobj->sprite2);
		WRITEUINT16(demobuf.p,ghost->player->followmobj->sprite);
		WRITEUINT8(demobuf.p,(ghost->player->followmobj->frame & FF_FRAMEMASK));
		WRITEUINT16(demobuf.p,ghost->player->followmobj->color);

		*followtic_p = followtic;
	}
	else
		oldghost[playernum].flags2 &= ~MF2_AMBUSH;

	*ziptic_p = ziptic;
}

void G_ConsAllGhostTics(void)
{
	UINT8 p;

	if (!demobuf.p || !demo.deferstart)
		return;

	p = READUINT8(demobuf.p);

	while (p != 0xFF)
	{
		G_ConsGhostTic(p);
		p = READUINT8(demobuf.p);
	}

	if (*demobuf.p == DEMOMARKER)
	{
		// end of demo data stream
		G_CheckDemoStatus();
		return;
	}
}

// Uses ghost data to do consistency checks on your position.
// This fixes desynchronising demos when fighting eggman.
void G_ConsGhostTic(INT32 playernum)
{
	UINT8 ziptic;
	INT32 px,py,pz,gx,gy,gz;
	mobj_t *testmo;
	UINT32 syncleeway;

	if (!(demoflags & DF_GHOST))
		return; // No ghost data to use.

	testmo = players[playernum].mo;

	// Grab ghost data.
	ziptic = READUINT8(demobuf.p);
	if (ziptic & GZT_XYZ)
	{
		oldghost[playernum].x = READFIXED(demobuf.p);
		oldghost[playernum].y = READFIXED(demobuf.p);
		oldghost[playernum].z = READFIXED(demobuf.p);
		syncleeway = 0;
	}
	else
	{
		if (ziptic & GZT_MOMXY)
		{
			oldghost[playernum].momx = READFIXED(demobuf.p);
			oldghost[playernum].momy = READFIXED(demobuf.p);
		}
		if (ziptic & GZT_MOMZ)
			oldghost[playernum].momz = READFIXED(demobuf.p);
		oldghost[playernum].x += oldghost[playernum].momx;
		oldghost[playernum].y += oldghost[playernum].momy;
		oldghost[playernum].z += oldghost[playernum].momz;
		syncleeway = FRACUNIT;
	}
	if (ziptic & GZT_ANGLE)
		demobuf.p++;
	if (ziptic & GZT_FRAME)
		demobuf.p++;
	if (ziptic & GZT_SPR2)
		demobuf.p++;

	if (ziptic & GZT_EXTRA)
	{ // But wait, there's more!
		UINT8 xziptic = READUINT8(demobuf.p);
		if (xziptic & EZT_COLOR)
			demobuf.p += sizeof(UINT16);
		if (xziptic & EZT_SCALE)
			demobuf.p += sizeof(fixed_t);
		if (xziptic & EZT_HIT)
		{ // Resync mob damage.
			UINT16 i, count = READUINT16(demobuf.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++)
			{
				//demobuf.p += 4; // reserved.
				type = READUINT32(demobuf.p);
				health = READUINT16(demobuf.p);
				x = READFIXED(demobuf.p);
				y = READFIXED(demobuf.p);
				z = READFIXED(demobuf.p);
				demobuf.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 (health)!\n"));
					demosynced = false;
					P_DamageMobj(mobj, players[0].mo, players[0].mo, 1, DMG_NORMAL);
				}
			}
		}
		if (xziptic & EZT_SPRITE)
			demobuf.p += sizeof(UINT16);
		if (xziptic & EZT_KART)
		{
			ghostext[playernum].kartitem = READINT32(demobuf.p);
			ghostext[playernum].kartamount = READINT32(demobuf.p);
			ghostext[playernum].kartbumpers = READINT32(demobuf.p);
		}
	}

	if (ziptic & GZT_FOLLOW)
	{ // Even more...
		UINT8 followtic = READUINT8(demobuf.p);
		if (followtic & FZT_SPAWNED)
		{
			demobuf.p += sizeof(INT16);
			if (followtic & FZT_SKIN)
				demobuf.p += sizeof(UINT16);
		}
		if (followtic & FZT_SCALE)
			demobuf.p += sizeof(fixed_t);
		// momx, momy and momz
		demobuf.p += sizeof(fixed_t) * 3;
		if (followtic & FZT_SKIN)
			demobuf.p += sizeof(UINT16);
		demobuf.p += sizeof(UINT16);
		demobuf.p++;
		demobuf.p += sizeof(UINT16);
	}

	if (testmo)
	{
		// Re-synchronise
		px = testmo->x;
		py = testmo->y;
		pz = testmo->z;
		gx = oldghost[playernum].x;
		gy = oldghost[playernum].y;
		gz = oldghost[playernum].z;

		if (abs(px-gx) > syncleeway || abs(py-gy) > syncleeway || abs(pz-gz) > syncleeway)
		{
			ghostext[playernum].desyncframes++;

			if (ghostext[playernum].desyncframes >= 2)
			{
				if (demosynced)
					CONS_Alert(CONS_WARNING, "Demo playback has desynced (player %s)!\n", player_names[playernum]);
				demosynced = false;

				P_UnsetThingPosition(testmo);
				testmo->x = oldghost[playernum].x;
				testmo->y = oldghost[playernum].y;
				P_SetThingPosition(testmo);
				testmo->z = oldghost[playernum].z;

				if (abs(testmo->z - testmo->floorz) < 4*FRACUNIT)
					testmo->z = testmo->floorz; // Sync players to the ground when they're likely supposed to be there...

				ghostext[playernum].desyncframes = 2;
			}
		}
		else
			ghostext[playernum].desyncframes = 0;

		if (players[playernum].itemtype != ghostext[playernum].kartitem
			|| players[playernum].itemamount != ghostext[playernum].kartamount
			|| players[playernum].bumper != ghostext[playernum].kartbumpers)
		{
			if (demosynced)
				CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (item/bumpers)!\n"));
			demosynced = false;

			players[playernum].itemtype = ghostext[playernum].kartitem;
			players[playernum].itemamount = ghostext[playernum].kartamount;
			players[playernum].bumper = ghostext[playernum].kartbumpers;
		}
	}

	if (*demobuf.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.
		UINT16 ziptic = READUINT8(g->p);
		UINT8 xziptic = 0;

		if (g->done)
		{
			continue;
		}

		while (ziptic != DW_END) // Get rid of extradata stuff
		{
			if (ziptic < MAXPLAYERS)
			{
#ifdef DEVELOP
				UINT8 playerid = ziptic;
#endif
				// We want to skip *any* player extradata because some demos have extradata for bogus players,
				// but if there is tic data later for those players *then* we'll consider it invalid.

				ziptic = READUINT8(g->p);
				if (ziptic & DXD_JOINDATA)
				{
					if (READUINT8(g->p) != 0)
						I_Error("Ghost is not a record attack ghost (bot JOINDATA)");
				}
				if (ziptic & DXD_PLAYSTATE)
				{
					UINT8 playstate = READUINT8(g->p);
					if (playstate != DXD_PST_PLAYING)
					{
						#ifdef DEVELOP
						CONS_Alert(CONS_WARNING, "Ghost demo has non-playing playstate for player %d\n", playerid + 1);
						#endif
						;
					}
				}
				if (ziptic & DXD_NAME)
					g->p += 16; // yea
				if (ziptic & DXD_SKIN)
					g->p += 16; // We _could_ read this info, but it shouldn't change anything in record attack...
				if (ziptic & DXD_COLOR)
					g->p += 16; // Same tbh
				if (ziptic & DXD_FOLLOWER)
					g->p += 32; // ok (32 because there's both the skin and the colour)
				if (ziptic & DXD_WEAPONPREF)
					g->p++; // ditto
			}
			else if (ziptic == DW_RNG)
				g->p += 4; // RNG seed
			else
			{
				I_Error("Ghost is not a record attack ghost DXD (ziptic = %u)", ziptic); //@TODO lmao don't blow up like this
			}

			ziptic = READUINT8(g->p);
		}

		ziptic = READUINT16(g->p);

		if (ziptic & ZT_FWD)
			g->p++;
		if (ziptic & ZT_SIDE)
			g->p++;
		if (ziptic & ZT_TURNING)
			g->p += 2;
		if (ziptic & ZT_ANGLE)
			g->p += 2;
		if (ziptic & ZT_THROWDIR)
			g->p += 2;
		if (ziptic & ZT_BUTTONS)
			g->p += 2;
		if (ziptic & ZT_AIMING)
			g->p += 2;
		if (ziptic & ZT_LATENCY)
			g->p++;
		if (ziptic & ZT_FLAGS)
			g->p++;
		if (ziptic & ZT_BOT)
		{
			UINT16 botziptic = READUINT16(g->p);
			if (botziptic & ZT_BOT_TURN)
				g->p++;
			if (botziptic & ZT_BOT_ITEM)
				g->p++;
		}

		// Grab ghost data.
		ziptic = READUINT8(g->p);

		if (ziptic == 0xFF)
			goto skippedghosttic; // Didn't write ghost info this frame
		else if (ziptic != 0)
			I_Error("Ghost is not a record attack ghost ZIPTIC"); //@TODO lmao don't blow up like this
		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 = READFIXED(g->p);
				g->oldmo.momy = READFIXED(g->p);
			}
			if (ziptic & GZT_MOMZ)
				g->oldmo.momz = READFIXED(g->p);
			g->oldmo.x += g->oldmo.momx;
			g->oldmo.y += g->oldmo.momy;
			g->oldmo.z += g->oldmo.momz;
		}
		if (ziptic & GZT_ANGLE)
			g->oldmo.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->angle = g->oldmo.angle;
		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->renderflags &= ~RF_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 = READUINT16(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;
				}
			}
			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_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_KART)
				g->p += 12; // kartitem, kartamount, kartbumpers
		}

#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[READUINT16(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 = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p);
				follow->x = g->mo->x + temp;
				temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p);
				follow->y = g->mo->y + temp;
				temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p);
				follow->z = g->mo->z + temp;
				P_SetThingPosition(follow);
				if (followtic & FZT_SKIN)
					follow->sprite2 = READUINT16(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 = READUINT16(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);
		}

skippedghosttic:
		// 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_SUPER1;
			g->mo->color += abs( ( (signed)( (unsigned)leveltime >> 1 ) % 9) - 4);
			break;
		case GHC_INVINCIBLE: // Mario invincibility (P_CheckInvincibilityTimer)
			g->mo->color = K_RainbowColor(leveltime); // Passes through all saturated colours
			break;
		default:
			break;
		}

		if (READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here.
			I_Error("Ghost is not a record attack ghost GHOSTEND"); //@TODO lmao don't blow up like this

		// 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
			g->done = true;
			if (p)
			{
				p->next = g->next;
			}
			continue;
		}

		p = g;
#undef follow
	}
}

// Demo rewinding functions
typedef struct rewindinfo_s {
	tic_t leveltime;

	struct {
		boolean ingame;
		player_t player;
		mobj_t mobj;
	} playerinfo[MAXPLAYERS];

	struct rewindinfo_s *prev;
} rewindinfo_t;

static tic_t currentrewindnum;
static rewindinfo_t *rewindhead = NULL; // Reverse chronological order

void G_InitDemoRewind(void)
{
	CL_ClearRewinds();

	while (rewindhead)
	{
		rewindinfo_t *p = rewindhead->prev;
		Z_Free(rewindhead);
		rewindhead = p;
	}

	currentrewindnum = 0;
}

void G_StoreRewindInfo(void)
{
	static UINT8 timetolog = 8;
	rewindinfo_t *info;
	size_t i;

	if (timetolog-- > 0)
		return;
	timetolog = 8;

	info = Z_Calloc(sizeof(rewindinfo_t), PU_STATIC, NULL);

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || players[i].spectator)
		{
			info->playerinfo[i].ingame = false;
			continue;
		}

		info->playerinfo[i].ingame = true;
		memcpy(&info->playerinfo[i].player, &players[i], sizeof(player_t));
		if (players[i].mo)
			memcpy(&info->playerinfo[i].mobj, players[i].mo, sizeof(mobj_t));
	}

	info->leveltime = leveltime;
	info->prev = rewindhead;
	rewindhead = info;
}

void G_PreviewRewind(tic_t previewtime)
{
	SINT8 i;
	//size_t j;
	fixed_t tweenvalue = 0;
	rewindinfo_t *info = rewindhead, *next_info = rewindhead;

	if (!info)
		return;

	while (info->leveltime > previewtime && info->prev)
	{
		next_info = info;
		info = info->prev;
	}
	if (info != next_info)
		tweenvalue = FixedDiv(previewtime - info->leveltime, next_info->leveltime - info->leveltime);


	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || players[i].spectator)
		{
			if (info->playerinfo[i].player.mo)
			{
				//@TODO spawn temp object to act as a player display
			}

			continue;
		}

		if (!info->playerinfo[i].ingame || !info->playerinfo[i].player.mo)
		{
			if (players[i].mo)
				players[i].mo->renderflags |= RF_DONTDRAW;

			continue;
		}

		if (!players[i].mo)
			continue; //@TODO spawn temp object to act as a player display

		players[i].mo->renderflags &= ~RF_DONTDRAW;

		P_UnsetThingPosition(players[i].mo);
#define TWEEN(pr) info->playerinfo[i].mobj.pr + FixedMul((INT32) (next_info->playerinfo[i].mobj.pr - info->playerinfo[i].mobj.pr), tweenvalue)
		players[i].mo->x = TWEEN(x);
		players[i].mo->y = TWEEN(y);
		players[i].mo->z = TWEEN(z);
		players[i].mo->angle = TWEEN(angle);
#undef TWEEN
		P_SetThingPosition(players[i].mo);

		players[i].drawangle = info->playerinfo[i].player.drawangle + FixedMul((INT32) (next_info->playerinfo[i].player.drawangle - info->playerinfo[i].player.drawangle), tweenvalue);

		players[i].mo->sprite = info->playerinfo[i].mobj.sprite;
		players[i].mo->sprite2 = info->playerinfo[i].mobj.sprite2;
		players[i].mo->frame = info->playerinfo[i].mobj.frame;

		players[i].realtime = info->playerinfo[i].player.realtime;
		// Genuinely CANNOT be fucked. I can redo lua and I can redo netsaves but I draw the line at this abysmal hack.
		/*for (j = 0; j < NUMKARTSTUFF; j++)
			players[i].kartstuff[j] = info->playerinfo[i].player.kartstuff[j];*/
	}

	for (i = splitscreen; i >= 0; i--)
		P_ResetCamera(&players[displayplayers[i]], &camera[i]);
}

void G_ConfirmRewind(tic_t rewindtime)
{
	SINT8 i;
	tic_t j;
	boolean oldsounddisabled = sound_disabled;

	INT32 olddp1 = displayplayers[0], olddp2 = displayplayers[1], olddp3 = displayplayers[2], olddp4 = displayplayers[3];
	UINT8 oldss = splitscreen;

	CV_StealthSetValue(&cv_renderview, 0);

	if (rewindtime <= starttime)
	{
		demo.rewinding = true; // this doesn't APPEAR to cause any misery, and it allows us to prevent running all the wipes again
		G_DoPlayDemo(NULL); // Restart the current demo
	}
	else
	{
		rewind_t *rewind;
		sound_disabled = true; // Prevent sound spam
		demo.rewinding = true;

		rewind = CL_RewindToTime(rewindtime);

		if (rewind)
		{
			demobuf.p = demobuf.buffer + rewind->demopos;
			memcpy(oldcmd, rewind->oldcmd, sizeof (oldcmd));
			memcpy(oldghost, rewind->oldghost, sizeof (oldghost));
			paused = false;
		}
		else
		{
			demo.rewinding = true;
			G_DoPlayDemo(NULL); // Restart the current demo
		}
	}

	for (j = 0; j < rewindtime && leveltime < rewindtime; j++)
	{
		G_Ticker((j % NEWTICRATERATIO) == 0);
	}

	demo.rewinding = false;
	sound_disabled = oldsounddisabled; // Re-enable SFX

	wipegamestate = gamestate; // No fading back in!

	COM_BufInsertText("renderview on\n");

	splitscreen = oldss;
	displayplayers[0] = olddp1;
	displayplayers[1] = olddp2;
	displayplayers[2] = olddp3;
	displayplayers[3] = olddp4;
	R_ExecuteSetViewSize();
	G_ResetViews();

	for (i = splitscreen; i >= 0; i--)
		P_ResetCamera(&players[displayplayers[i]], &camera[i]);
}

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, DMG_NORMAL);
			/* 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)
	{
		// make sure the values are read in the right order
		oldmetal.x = READFIXED(metal_p);
		oldmetal.y = READFIXED(metal_p);
		oldmetal.z = READFIXED(metal_p);
		P_MoveOrigin(metal, oldmetal.x, oldmetal.y, oldmetal.z);
		oldmetal.x = metal->x;
		oldmetal.y = metal->y;
		oldmetal.z = metal->z;
	}
	else
	{
		if (ziptic & GZT_MOMXY)
		{
			oldmetal.momx = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
			oldmetal.momy = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
		}
		if (ziptic & GZT_MOMZ)
			oldmetal.momz = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
		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_SPRITE)
			metal->sprite = READUINT16(metal_p);
	}

#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 = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
				follow->x = metal->x + temp;
				temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
				follow->y = metal->y + temp;
				temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
				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 = READUINT16(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;

	if (!demobuf.p) // demobuf.p will be NULL until the race start linedef executor is activated!
		return;

	WRITEUINT8(demobuf.p, METALSNICE);
	ziptic_p = demobuf.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(demobuf.p,oldmetal.x);
		WRITEFIXED(demobuf.p,oldmetal.y);
		WRITEFIXED(demobuf.p,oldmetal.z);
	}
	else
	{
		// For moving normally:
		// Store movement as a fixed value
		fixed_t momx = metal->x-oldmetal.x;
		fixed_t momy = metal->y-oldmetal.y;
		if (momx != oldmetal.momx
		|| momy != oldmetal.momy)
		{
			oldmetal.momx = momx;
			oldmetal.momy = momy;
			ziptic |= GZT_MOMXY;
			WRITEFIXED(demobuf.p,momx);
			WRITEFIXED(demobuf.p,momy);
		}
		momx = metal->z-oldmetal.z;
		if (momx != oldmetal.momz)
		{
			oldmetal.momz = momx;
			ziptic |= GZT_MOMZ;
			WRITEFIXED(demobuf.p,momx);
		}

		// This SHOULD set oldmetal.x/y/z to match metal->x/y/z
		oldmetal.x += oldmetal.momx;
		oldmetal.y += oldmetal.momy;
		oldmetal.z += oldmetal.momz;
	}

	#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(demobuf.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(demobuf.p,oldmetal.frame);
	}

	if (metal->sprite == SPR_PLAY
	&& metal->sprite2 != oldmetal.sprite2)
	{
		oldmetal.sprite2 = metal->sprite2;
		ziptic |= GZT_SPR2;
		WRITEUINT8(demobuf.p,oldmetal.sprite2);
	}

	// Check for sprite set changes
	if (metal->sprite != oldmetal.sprite)
	{
		oldmetal.sprite = metal->sprite;
		ghostext[0].flags |= EZT_SPRITE;
	}

	if (ghostext[0].flags & ~(EZT_COLOR|EZT_HIT)) // these two aren't handled by metal ever
	{
		ziptic |= GZT_EXTRA;

		if (ghostext[0].scale == ghostext[0].lastscale)
			ghostext[0].flags &= ~EZT_SCALE;

		WRITEUINT8(demobuf.p,ghostext[0].flags);
		if (ghostext[0].flags & EZT_SCALE)
		{
			WRITEFIXED(demobuf.p,ghostext[0].scale);
			ghostext[0].lastscale = ghostext[0].scale;
		}
		if (ghostext[0].flags & EZT_SPRITE)
			WRITEUINT16(demobuf.p,oldmetal.sprite);
		ghostext[0].flags = 0;
	}

	if (metal->player && metal->player->followmobj && !(metal->player->followmobj->sprite == SPR_NULL || (metal->player->followmobj->renderflags & RF_DONTDRAW) == RF_DONTDRAW))
	{
		fixed_t temp;
		UINT8 *followtic_p = demobuf.p++;
		UINT8 followtic = 0;

		ziptic |= GZT_FOLLOW;

		if (metal->player->followmobj->skin)
			followtic |= FZT_SKIN;

		if (!(oldmetal.flags2 & MF2_AMBUSH))
		{
			followtic |= FZT_SPAWNED;
			WRITEINT16(demobuf.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(demobuf.p,(UINT8)(((skin_t *)(metal->player->followmobj->skin))-skins));
			oldmetal.flags2 |= MF2_AMBUSH;
		}

		if (metal->player->followmobj->scale != metal->scale)
		{
			followtic |= FZT_SCALE;
			WRITEFIXED(demobuf.p,metal->player->followmobj->scale);
		}

		temp = metal->player->followmobj->x-metal->x;
		WRITEFIXED(demobuf.p,temp);
		temp = metal->player->followmobj->y-metal->y;
		WRITEFIXED(demobuf.p,temp);
		temp = metal->player->followmobj->z-metal->z;
		WRITEFIXED(demobuf.p,temp);
		if (followtic & FZT_SKIN)
			WRITEUINT8(demobuf.p,metal->player->followmobj->sprite2);
		WRITEUINT16(demobuf.p,metal->player->followmobj->sprite);
		WRITEUINT32(demobuf.p,metal->player->followmobj->frame); // NOT & FF_FRAMEMASK here, so 32 bits
		WRITEUINT16(demobuf.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 (demobuf.p >= demobuf.end - 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 * cv_netdemosize.value;

	P_SaveBufferAlloc(&demobuf, maxsize);
	demobuf.p = NULL;

	demo.recording = true;
	demo.buffer = &demobuf;

	/* FIXME: This whole file is in a wretched state. Take a
	look at G_WriteAllGhostTics and G_WriteDemoTiccmd, they
	write a lot of data. It's not realistic to refactor that
	code in order to know exactly HOW MANY bytes it can write
	out. So here's the deal. Reserve a decent block of memory
	at the end of the buffer and never use it. Those bastard
	functions will check if they overran the buffer, but it
	should be safe enough because they'll think there's less
	memory than there actually is and stop early. */
	const size_t deadspace = 1024;
	I_Assert(demobuf.size > deadspace);
	demobuf.size -= deadspace;
	demobuf.end -= deadspace;
}

void G_RecordMetal(void)
{
	INT32 maxsize;
	maxsize = 1024*1024;
	if (M_CheckParm("-maxdemo") && M_IsNextParm())
		maxsize = atoi(M_GetNextParm()) * 1024;

	P_SaveBufferAlloc(&demobuf, maxsize);
	demobuf.p = NULL;

	metalrecording = true;
}

void G_BeginRecording(void)
{
	UINT8 i, p;
	char name[MAXCOLORNAME+1];
	player_t *player = &players[consoleplayer];

	char *filename;
	UINT8 totalfiles;
	UINT8 *m;

	if (demobuf.p)
		return;
	memset(name,0,sizeof(name));

	demobuf.p = demobuf.buffer;
	demoflags = DF_GHOST|(multiplayer ? DF_MULTIPLAYER : (modeattacking<<DF_ATTACKSHIFT));

	if (multiplayer && !netgame)
		demoflags |= DF_NONETMP;

	if (encoremode)
		demoflags |= DF_ENCORE;

	if (multiplayer)
		demoflags |= DF_LUAVARS;

	// Setup header.
	M_Memcpy(demobuf.p, DEMOHEADER, 12); demobuf.p += 12;
	WRITEUINT8(demobuf.p,VERSION);
	WRITEUINT8(demobuf.p,SUBVERSION);
	WRITEUINT16(demobuf.p,DEMOVERSION);

	// Full replay title
	demobuf.p += 64;
	snprintf(demo.titlename, 64, "%s - %s", G_BuildMapTitle(gamemap), modeattacking ? "Record Attack" : connectedservername);

	// demo checksum
	demobuf.p += sizeof(UINT64);

	// game data
	M_Memcpy(demobuf.p, "PLAY", 4); demobuf.p += 4;
	WRITESTRINGN(demobuf.p, mapheaderinfo[gamemap-1]->lumpname, MAXMAPLUMPNAME);
	WRITEUINT64(demobuf.p, maphash);

	WRITEUINT8(demobuf.p, demoflags);
	WRITEUINT8(demobuf.p, gametype & 0xFF);
	WRITEUINT8(demobuf.p, numlaps);

	// file list
	m = demobuf.p;/* file count */
	demobuf.p += 1;

	totalfiles = 0;
	for (i = NUMMAINWADS-1; ++i < numwadfiles; )
		if (wadfiles[i]->important)
	{
		nameonly(( filename = va("%s", wadfiles[i]->filename) ));
		WRITESTRINGL(demobuf.p, filename, MAX_WADPATH);
		WRITEUINT64(demobuf.p, wadfiles[i]->hash);
		WRITEUINT8(demobuf.p, !!wadfiles[i]->compatmode);

		totalfiles++;
	}

	WRITEUINT8(m, totalfiles);

	switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
	{
		case ATTACKING_NONE: // 0
			break;
		case ATTACKING_TIME: // 1
			demotime_p = demobuf.p;
			WRITEUINT32(demobuf.p,UINT32_MAX); // time
			WRITEUINT32(demobuf.p,UINT32_MAX); // lap
			break;
		case ATTACKING_ITEMBREAK: // 2
			demotime_p = demobuf.p;
			WRITEUINT32(demobuf.p,UINT32_MAX); // time
			break;
		default: // 3
			break;
	}

	WRITEUINT32(demobuf.p,P_GetInitSeed());

	// Reserved for extrainfo location from start of file
	demoinfo_p = demobuf.p;
	WRITEUINT32(demobuf.p, 0);

	// Save netvar data
	CV_SaveDemoVars(&demobuf.p);

	if ((demoflags & DF_GRANDPRIX))
	{
		WRITEUINT8(demobuf.p, grandprixinfo.gamespeed);
		WRITEUINT8(demobuf.p, grandprixinfo.masterbots == true);
		WRITEUINT8(demobuf.p, grandprixinfo.eventmode);
	}

	// Save "mapmusrng" used for altmusic selection
	WRITEUINT8(demobuf.p, mapmusrng);

	// Now store some info for each in-game player

	// Lat' 12/05/19: Do note that for the first game you load, everything that gets saved here is total garbage;
	// The name will always be Player <n>, the skin sonic, the color None and the follower 0. This is only correct on subsequent games.
	// In the case of said first game, the skin and the likes are updated with Got_NameAndColor, which are then saved in extradata for the demo with DXD_SKIN in r_things.c for instance.


	for (p = 0; p < MAXPLAYERS; p++) {
		if (playeringame[p]) {
			player = &players[p];
			WRITEUINT8(demobuf.p, p);

			i = 0;
			if (player->spectator)
				i |= DEMO_SPECTATOR;
			if (player->pflags & PF_KICKSTARTACCEL)
				i |= DEMO_KICKSTART;
			if (player->pflags & PF_SHRINKME)
				i |= DEMO_SHRINKME;
			if (player->bot == true)
				i |= DEMO_BOT;
			WRITEUINT8(demobuf.p, i);

			if (i & DEMO_BOT)
			{
				WRITEUINT8(demobuf.p, player->botvars.difficulty);
				WRITEUINT8(demobuf.p, player->botvars.diffincrease); // needed to avoid having to duplicate logic
				WRITEUINT8(demobuf.p, (UINT8)player->botvars.rival);
			}

			// Name
			memset(name, 0, 16);
			strncpy(name, player_names[p], 16);
			M_Memcpy(demobuf.p,name,16);
			demobuf.p += 16;

			// Skin
			memset(name, 0, 16);
			strncpy(name, skins[player->skin].name, 16);
			M_Memcpy(demobuf.p,name,16);
			demobuf.p += 16;

			// Color
			memset(name, 0, 16);
			strncpy(name, skincolors[player->skincolor].name, 16);
			M_Memcpy(demobuf.p,name,16);
			demobuf.p += 16;

			// Save follower's skin name
			// PS: We must check for 'follower' to determine if the followerskin is valid. It's going to be 0 if we don't have a follower, but 0 is also absolutely a valid follower!
			// Doesn't really matter if the follower mobj is valid so long as it exists in a way or another.

			memset(name, 0, 16);
			if (player->follower)
				strncpy(name, followers[player->followerskin].skinname, 16);
			else
				strncpy(name, "None", 16);	// Say we don't have one, then.

			M_Memcpy(demobuf.p,name,16);
			demobuf.p += 16;

			// Save follower's colour
			memset(name, 0, 16);
			strncpy(name, Followercolor_cons_t[(UINT16)(player->followercolor+2)].strvalue, 16);	// Not KartColor_Names because followercolor has extra values such as "Match"
			M_Memcpy(demobuf.p, name, 16);
			demobuf.p += 16;

			// Score, since Kart uses this to determine where you start on the map
			WRITEUINT32(demobuf.p, player->score);

			// Power Levels
			WRITEUINT16(demobuf.p, clientpowerlevels[p][gametype == GT_BATTLE ? PWRLV_BATTLE : PWRLV_RACE]);

			// Kart speed and weight
			WRITEUINT8(demobuf.p, skins[player->skin].kartspeed);
			WRITEUINT8(demobuf.p, skins[player->skin].kartweight);

			// And mobjtype_t is best with UINT32 too...
			WRITEUINT32(demobuf.p, player->followitem);
		}
	}

	WRITEUINT8(demobuf.p, 0xFF); // Denote the end of the player listing

	// player lua vars, always saved even if empty
	if (demoflags & DF_LUAVARS)
		LUA_Archive(&demobuf, false);

	memset(&oldcmd,0,sizeof(oldcmd));
	memset(&oldghost,0,sizeof(oldghost));
	memset(&ghostext,0,sizeof(ghostext));

	for (i = 0; i < MAXPLAYERS; i++)
	{
		ghostext[i].lastcolor = ghostext[i].color = GHC_NORMAL;
		ghostext[i].lastscale = ghostext[i].scale = FRACUNIT;

		if (players[i].mo)
		{
			oldghost[i].x = players[i].mo->x;
			oldghost[i].y = players[i].mo->y;
			oldghost[i].z = players[i].mo->z;
			oldghost[i].angle = players[i].mo->angle;

			// preticker started us gravity flipped
			if (players[i].mo->eflags & MFE_VERTICALFLIP)
				ghostext[i].flags |= EZT_FLIP;
		}
	}
}

void G_BeginMetal(void)
{
	mobj_t *mo = players[consoleplayer].mo;

#if 0
	if (demobuf.p)
		return;
#endif

	demobuf.p =  demobuf.buffer;

	// Write header.
	M_Memcpy(demobuf.p, DEMOHEADER, 12); demobuf.p += 12;
	WRITEUINT8(demobuf.p,VERSION);
	WRITEUINT8(demobuf.p,SUBVERSION);
	WRITEUINT16(demobuf.p,DEMOVERSION);

	// demo checksum
	demobuf.p += sizeof(UINT64);

	M_Memcpy(demobuf.p, "METL", 4); demobuf.p += 4;

	memset(&ghostext,0,sizeof(ghostext));
	ghostext[0].lastscale = ghostext[0].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_WriteStanding(UINT8 ranking, char *name, INT32 skinnum, UINT16 color, UINT32 val)
{
	char temp[16];

	if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
	{
		WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker
		*(UINT32 *)demoinfo_p = demobuf.p -  demobuf.buffer;
	}

	WRITEUINT8(demobuf.p, DW_STANDING);
	WRITEUINT8(demobuf.p, ranking);

	// Name
	memset(temp, 0, 16);
	strncpy(temp, name, 16);
	M_Memcpy(demobuf.p,temp,16);
	demobuf.p += 16;

	// Skin
	memset(temp, 0, 16);
	strncpy(temp, skins[skinnum].name, 16);
	M_Memcpy(demobuf.p,temp,16);
	demobuf.p += 16;

	// Color
	memset(temp, 0, 16);
	strncpy(temp, skincolors[color].name, 16);
	M_Memcpy(demobuf.p,temp,16);
	demobuf.p += 16;

	// Score/time/whatever
	WRITEUINT32(demobuf.p, val);
}

void G_SetDemoTime(UINT32 ptime, UINT32 plap)
{
	if (!demo.recording || !demotime_p)
		return;
	if (demoflags & DF_TIMEATTACK)
	{
		WRITEUINT32(demotime_p, ptime);
		WRITEUINT32(demotime_p, plap);
		demotime_p = NULL;
	}
	else if (demoflags & DF_ITEMBREAKER)
	{
		WRITEUINT32(demotime_p, ptime);
		(void)plap;
		demotime_p = NULL;
	}
}

static void G_LoadDemoExtraFiles(UINT8 **pp)
{
	UINT8 totalfiles;
	char filename[MAX_WADPATH];
	UINT64 filehash;
	filestatus_t ncs;
	boolean toomany = false;
	boolean alreadyloaded;
	UINT8 i, j;
	boolean compatmode;

	totalfiles = READUINT8((*pp));
	for (i = 0; i < totalfiles; ++i)
	{
		if (toomany)
			SKIPSTRING((*pp));
		else
		{
			strlcpy(filename, (char *)(*pp), sizeof filename);
			SKIPSTRING((*pp));
		}
		filehash = READUINT64((*pp));
		compatmode = READUINT8((*pp));

		if (!toomany)
		{
			alreadyloaded = false;

			for (j = 0; j < numwadfiles; ++j)
			{
				if (filehash == wadfiles[j]->hash)
				{
					alreadyloaded = true;
					break;
				}
			}

			if (alreadyloaded)
				continue;

			if (numwadfiles >= MAX_WADFILES)
				toomany = true;
			else
				ncs = findfile(filename, filehash, false);

			if (toomany)
			{
				CONS_Alert(CONS_WARNING, M_GetText("Too many files loaded to add anymore for demo playback\n"));
				if (!CON_Ready())
					M_StartMessage(M_GetText("There are too many files loaded to add this demo's addons.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
			}
			else if (ncs != FS_FOUND)
			{
				if (ncs == FS_NOTFOUND)
					CONS_Alert(CONS_NOTICE, M_GetText("You do not have a copy of %s\n"), filename);
				else if (ncs == FS_BADHASH)
					CONS_Alert(CONS_NOTICE, M_GetText("Checksum mismatch on %s\n"), filename);
				else
					CONS_Alert(CONS_NOTICE, M_GetText("Unknown error finding file %s\n"), filename);

				if (!CON_Ready())
					M_StartMessage(M_GetText("There were errors trying to add this demo's addons. Check the console for more information.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
			}
			else
			{
				P_PartialAddWadFile(filename, compatmode ? WC_ON : WC_OFF);
			}
		}
	}

	if (P_PartialAddGetStage() >= 0)
		P_MultiSetupWadFiles(true); // in case any partial adds were done
}

static void G_SkipDemoExtraFiles(UINT8 **pp)
{
	UINT8 totalfiles;
	UINT8 i;

	totalfiles = READUINT8((*pp));
	for (i = 0; i < totalfiles; ++i)
	{
		SKIPSTRING((*pp));// file name
		(*pp) += sizeof(UINT64); // hash
		(*pp) += 1; // compatmode
	}
}

// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
// Enabling quick prevents filesystem checks to see if needed files are available to load.
static UINT8 G_CheckDemoExtraFiles(UINT8 **pp, boolean quick)
{
	UINT8 totalfiles, filesloaded, nmusfilecount;
	char filename[MAX_WADPATH];
	UINT64 filehash;
	boolean toomany = false;
	boolean alreadyloaded;
	UINT8 i, j;
	UINT8 error = 0;

	totalfiles = READUINT8((*pp));
	filesloaded = 0;
	for (i = 0; i < totalfiles; ++i)
	{
		if (toomany)
			SKIPSTRING((*pp));
		else
		{
			strlcpy(filename, (char *)(*pp), sizeof filename);
			SKIPSTRING((*pp));
		}
		filehash = READUINT64((*pp));
		(void)READUINT8((*pp)); // compatmode

		if (!toomany)
		{
			alreadyloaded = false;
			nmusfilecount = 0;

			for (j = 0; j < numwadfiles; ++j)
			{
				if (wadfiles[j]->important && j >= NUMMAINWADS)
					nmusfilecount++;
				else
					continue;

				if (filehash == wadfiles[j]->hash)
				{
					alreadyloaded = true;

					if (i != nmusfilecount-1 && error < DFILE_ERROR_OUTOFORDER)
						error |= DFILE_ERROR_OUTOFORDER;

					break;
				}
			}

			if (alreadyloaded)
			{
				filesloaded++;
				continue;
			}

			if (numwadfiles >= MAX_WADFILES)
				error = DFILE_ERROR_CANNOTLOAD;
			else if (!quick && findfile(filename, filehash, false) != FS_FOUND)
				error = DFILE_ERROR_CANNOTLOAD;
			else if (error < DFILE_ERROR_INCOMPLETEOUTOFORDER)
				error |= DFILE_ERROR_NOTLOADED;
		} else
			error = DFILE_ERROR_CANNOTLOAD;
	}

	// Get final file count
	nmusfilecount = 0;

	for (j = 0; j < numwadfiles; ++j)
		if (wadfiles[j]->important && j >= NUMMAINWADS)
			nmusfilecount++;

	if (!error && filesloaded < nmusfilecount)
		error = DFILE_ERROR_EXTRAFILES;

	return error;
}

// 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, oldlap, newlap;
	UINT16 oldversion;
	size_t bufsize ATTRUNUSED;
	UINT8 c;
	UINT16 s ATTRUNUSED;
	UINT8 aflags = 0;
	boolean uselaps = false;

	// 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 += 64; // full demo title
	p += sizeof(UINT64); // demo checksum
	I_Assert(!memcmp(p, "PLAY", 4));
	p += 4; // PLAY
	SKIPSTRING(p); // gamemap
	p += sizeof(UINT64); // map hash
	flags = READUINT8(p); // demoflags
	p++; // gametype
	p++; // numlaps
	G_SkipDemoExtraFiles(&p);

	aflags = flags & (DF_TIMEATTACK|DF_ITEMBREAKER);
	I_Assert(aflags);

	if (flags & DF_TIMEATTACK)
		uselaps = true; // get around uninitalized error

	newtime = READUINT32(p);
	if (uselaps)
		newlap = READUINT32(p);
	else
		newlap = UINT32_MAX;

	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 += 64; // full demo title
	p += sizeof(UINT64); // 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"
	SKIPSTRING(p); // gamemap
	p += sizeof(UINT64); // maphash
	flags = READUINT8(p);
	p++; // gametype
	p++; // numlaps
	G_SkipDemoExtraFiles(&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;
	}

	oldtime = READUINT32(p);
	if (uselaps)
		oldlap = READUINT32(p);
	else
		oldlap = 0;

	Z_Free(buffer);

	c = 0;

	if (uselaps)
	{
		if (newtime < oldtime
		|| (newtime == oldtime && (newlap < oldlap)))
			c |= 1; // Better time
		if (newlap < oldlap
		|| (newlap == oldlap && newtime < oldtime))
			c |= 1<<1; // Better lap time
	}
	else
	{
		if (newtime < oldtime)
			c |= 1; // Better time
	}

	return c;
}

void G_LoadDemoInfo(menudemo_t *pdemo)
{
	UINT8 *infobuffer, *info_p, *extrainfo_p;
	UINT8 version, subversion, pdemoflags;
	UINT16 pdemoversion, count;
	char mapname[MAXMAPLUMPNAME];

	if (!FIL_ReadFile(pdemo->filepath, &infobuffer))
	{
		CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), pdemo->filepath);
		pdemo->type = MD_INVALID;
		sprintf(pdemo->title, "INVALID REPLAY");

		return;
	}

	info_p = infobuffer;

	if (memcmp(info_p, DEMOHEADER, 12))
	{
		CONS_Alert(CONS_ERROR, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemo->filepath);
		pdemo->type = MD_INVALID;
		sprintf(pdemo->title, "INVALID REPLAY");
		Z_Free(infobuffer);
		return;
	}

	pdemo->type = MD_LOADED;

	info_p += 12; // DEMOHEADER

	version = READUINT8(info_p);
	subversion = READUINT8(info_p);
	pdemoversion = READUINT16(info_p);

	switch(pdemoversion)
	{
	case DEMOVERSION: // latest always supported
		// demo title
		M_Memcpy(pdemo->title, info_p, 64);
		info_p += 64;

		break;
	// too old, cannot support.
	default:
		CONS_Alert(CONS_ERROR, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemo->filepath);
		pdemo->type = MD_INVALID;
		sprintf(pdemo->title, "INVALID REPLAY");
		Z_Free(infobuffer);
		return;
	}

	if (version != VERSION || subversion != SUBVERSION)
		pdemo->type = MD_OUTDATED;

	info_p += sizeof(UINT64); // demo checksum
	if (memcmp(info_p, "PLAY", 4))
	{
		CONS_Alert(CONS_ERROR, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemo->filepath);
		pdemo->type = MD_INVALID;
		sprintf(pdemo->title, "INVALID REPLAY");
		Z_Free(infobuffer);
		return;
	}
	info_p += 4; // "PLAY"
	READSTRINGN(info_p, mapname, sizeof(mapname));
	pdemo->map = G_MapNumber(mapname);
	info_p += sizeof(UINT64); // maphash

	pdemoflags = READUINT8(info_p);

	// temp?
	if (!(pdemoflags & DF_MULTIPLAYER))
	{
		CONS_Alert(CONS_ERROR, M_GetText("%s is not a multiplayer replay and can't be listed on this menu fully yet.\n"), pdemo->filepath);
		Z_Free(infobuffer);
		return;
	}

	pdemo->gametype = READUINT8(info_p);
	pdemo->numlaps = READUINT8(info_p);

	pdemo->addonstatus = G_CheckDemoExtraFiles(&info_p, true);
	info_p += 4; // RNG seed

	extrainfo_p = infobuffer + READUINT32(info_p); // The extra UINT32 read is for a blank 4 bytes?

	// Pared down version of CV_LoadNetVars to find the kart speed
	pdemo->kartspeed = KARTSPEED_NORMAL; // Default to normal speed
	count = READUINT16(info_p);
	while (count--)
	{
		UINT16 netid;
		char *svalue;

		netid = READUINT16(info_p);
		svalue = (char *)info_p;
		SKIPSTRING(info_p);
		info_p++; // stealth

		if (netid == cv_kartspeed.netid)
		{
			UINT8 j;
			for (j = 0; kartspeed_cons_t[j].strvalue; j++)
				if (!stricmp(kartspeed_cons_t[j].strvalue, svalue))
					pdemo->kartspeed = kartspeed_cons_t[j].value;
		}
	}

	if (pdemoflags & DF_ENCORE)
		pdemo->kartspeed |= DF_ENCORE;

	// Read standings!
	count = 0;

	while (READUINT8(extrainfo_p) == DW_STANDING) // Assume standings are always first in the extrainfo
	{
		INT32 i;
		char temp[16];

		pdemo->standings[count].ranking = READUINT8(extrainfo_p);

		// Name
		M_Memcpy(pdemo->standings[count].name, extrainfo_p, 16);
		extrainfo_p += 16;

		// Skin
		M_Memcpy(temp,extrainfo_p,16);
		extrainfo_p += 16;
		pdemo->standings[count].skin = UINT8_MAX;
		for (i = 0; i < numskins; i++)
			if (stricmp(skins[i].name, temp) == 0)
			{
				pdemo->standings[count].skin = i;
				break;
			}

		// Color
		M_Memcpy(temp,extrainfo_p,16);
		extrainfo_p += 16;
		for (i = 0; i < numskincolors; i++)
			if (!stricmp(skincolors[i].name,temp))				// SRB2kart
			{
				pdemo->standings[count].color = i;
				break;
			}

		// Score/time/whatever
		pdemo->standings[count].timeorscore = READUINT32(extrainfo_p);

		count++;

		if (count >= MAXPLAYERS)
			break; //@TODO still cycle through the rest of these if extra demo data is ever used
	}

	// I think that's everything we need?
	Z_Free(infobuffer);
}

//
// G_PlayDemo
//
void G_DeferedPlayDemo(const char *name)
{
	COM_BufAddText("playdemo \"");
	COM_BufAddText(name);
	COM_BufAddText("\" -addfiles\n");
}

//
// Start a demo from a .LMP file or from a wad resource
//

#define SKIPERRORS

void G_DoPlayDemo(char *defdemoname)
{
	UINT8 i, p;
	lumpnum_t l;
	char skin[17],color[MAXCOLORNAME+1],follower[17],mapname[MAXMAPLUMPNAME],*n,*pdemoname;
	UINT8 version,subversion;
	UINT32 randseed;
	char msg[1024];

	boolean spectator, bot;
	UINT8 slots[MAXPLAYERS], kartspeed[MAXPLAYERS], kartweight[MAXPLAYERS], numslots = 0;

#if defined(SKIPERRORS) && !defined(DEVELOP)
	boolean skiperrors = false;
#endif

	G_InitDemoRewind();

	skin[16] = '\0';
	follower[16] = '\0';
	color[MAXCOLORNAME] = '\0';

	// No demo name means we're restarting the current demo
	if (defdemoname == NULL)
	{
		demobuf.p = demobuf.buffer;
		pdemoname = ZZ_Alloc(1); // Easier than adding checks for this everywhere it's freed
	}
	else
	{
		n = defdemoname+strlen(defdemoname);
		while (*n != '/' && *n != '\\' && n != defdemoname)
			n--;
		if (n != defdemoname)
			n++;
		pdemoname = ZZ_Alloc(strlen(n)+1);
		strcpy(pdemoname,n);

		M_SetPlaybackMenuPointer();

		// Internal if no extension, external if one exists
		if (FIL_CheckExtension(defdemoname))
		{
			//FIL_DefaultExtension(defdemoname, ".lmp");
			if (P_SaveBufferFromFile(&demobuf, defdemoname) == false)
			{
				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;
			}
		}
		// load demo resource from WAD
		else
		{
			if (n == defdemoname)
			{
				// Raw lump.
				if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
				{
					snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
					CONS_Alert(CONS_ERROR, "%s", msg);
					Z_Free(pdemoname);
					gameaction = ga_nothing;
					M_StartMessage(msg, NULL, MM_NOTHING);
					return;
				}

				P_SaveBufferFromLump(&demobuf, l);
			}
			else
			{
				// vres GHOST_%u
				virtres_t *vRes;
				virtlump_t *vLump;
				UINT16 mapnum;
				size_t step = 0;

				step = 0;
				while (defdemoname+step < n-1)
				{
					mapname[step] = defdemoname[step];
					step++;
				}
				mapname[step] = '\0';

				mapnum = G_MapNumber(mapname);
				if (mapnum >= nummapheaders || mapheaderinfo[mapnum]->lumpnum == LUMPERROR)
				{
					snprintf(msg, 1024, M_GetText("Failed to read lump '%s (couldn't find map %s)'.\n"), defdemoname, mapname);
					CONS_Alert(CONS_ERROR, "%s", msg);
					Z_Free(pdemoname);
					gameaction = ga_nothing;
					M_StartMessage(msg, NULL, MM_NOTHING);
					return;
				}

				vRes = vres_GetMap(mapheaderinfo[mapnum]->lumpnum);
				vLump = vres_Find(vRes, pdemoname);

				if (vLump == NULL)
				{
					snprintf(msg, 1024, M_GetText("Failed to read lump '%s (couldn't find lump %s in %s)'.\n"), defdemoname, pdemoname, mapname);
					CONS_Alert(CONS_ERROR, "%s", msg);
					Z_Free(pdemoname);
					gameaction = ga_nothing;
					M_StartMessage(msg, NULL, MM_NOTHING);
					return;
				}

				P_SaveBufferAlloc(&demobuf, vLump->size);
				memcpy(demobuf.buffer, vLump->data, vLump->size);

				vres_Free(vRes);
			}
#if defined(SKIPERRORS) && !defined(DEVELOP)
			skiperrors = true; // RR: Don't print warnings for staff ghosts, since they'll inevitably happen when we make bugfixes/changes...
#endif
		}
	}

	// read demo header
	gameaction = ga_nothing;
	demo.playback = true;
	demo.buffer = &demobuf;
	if (memcmp(demobuf.p, DEMOHEADER, 12))
	{
		snprintf(msg, 1024, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemoname);
		CONS_Alert(CONS_ERROR, "%s", msg);
		Z_Free(pdemoname);
		P_SaveBufferFree(&demobuf);
		demo.playback = false;
		demo.title = false;
		M_StartMessage(msg, NULL, MM_NOTHING);
		return;
	}
	demobuf.p += 12; // DEMOHEADER

	version = READUINT8(demobuf.p);
	subversion = READUINT8(demobuf.p);
	demo.version = READUINT16(demobuf.p);
	switch(demo.version)
	{
	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);
		Z_Free(pdemoname);
		P_SaveBufferFree(&demobuf);
		demo.playback = false;
		demo.title = false;
		M_StartMessage(msg, NULL, MM_NOTHING);
		return;
	}

	// demo title
	M_Memcpy(demo.titlename, demobuf.p, 64);
	demobuf.p += 64;

	demobuf.p += sizeof(UINT64); // demo checksum

	if (memcmp(demobuf.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);
		Z_Free(pdemoname);
		P_SaveBufferFree(&demobuf);
		demo.playback = false;
		demo.title = false;
		M_StartMessage(msg, NULL, MM_NOTHING);
		return;
	}

	demobuf.p += 4; // "PLAY"
	READSTRINGN(demobuf.p, mapname, sizeof(mapname)); // gamemap
	gamemap = G_MapNumber(mapname)+1;
	demobuf.p += sizeof(UINT64); // maphash

	demoflags = READUINT8(demobuf.p);
	gametype = READUINT8(demobuf.p);
	G_SetGametype(gametype);
	numlaps = READUINT8(demobuf.p);

	if (demo.title) // Titledemos should always play and ought to always be compatible with whatever wadlist is running.
		G_SkipDemoExtraFiles(&demobuf.p);
	else if (demo.loadfiles)
		G_LoadDemoExtraFiles(&demobuf.p);
	else if (demo.ignorefiles)
		G_SkipDemoExtraFiles(&demobuf.p);
	else
	{
		UINT8 error = G_CheckDemoExtraFiles(&demobuf.p, false);

		if (error)
		{
			switch (error)
			{
			case DFILE_ERROR_NOTLOADED:
				snprintf(msg, 1024,
					M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"),
				pdemoname);
				break;

			case DFILE_ERROR_OUTOFORDER:
				snprintf(msg, 1024,
					M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
				pdemoname);
				break;

			case DFILE_ERROR_INCOMPLETEOUTOFORDER:
				snprintf(msg, 1024,
					M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"),
				pdemoname);
				break;

			case DFILE_ERROR_CANNOTLOAD:
				snprintf(msg, 1024,
					M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
				pdemoname);
				break;

			case DFILE_ERROR_EXTRAFILES:
				snprintf(msg, 1024,
					M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
				pdemoname);
				break;
			}

			CONS_Alert(CONS_ERROR, "%s", msg);
			Z_Free(pdemoname);
			P_SaveBufferFree(&demobuf);
			demo.playback = false;
			demo.title = false;
			if (!CON_Ready()) // In the console they'll just see the notice there! No point pulling them out.
				M_StartMessage(msg, NULL, MM_NOTHING);
			return;
		}
	}

	modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
	multiplayer = !!(demoflags & DF_MULTIPLAYER);
	demo.netgame = (multiplayer && !(demoflags & DF_NONETMP));
	CON_ToggleOff();

	hu_demotime = UINT32_MAX;
	hu_demolap = UINT32_MAX;

	switch (modeattacking)
	{
		case ATTACKING_NONE: // 0
			break;
		case ATTACKING_TIME: // 1
			hu_demotime = READUINT32(demobuf.p);
			hu_demolap = READUINT32(demobuf.p);
			break;
		case ATTACKING_ITEMBREAK: // 2
			hu_demotime = READUINT32(demobuf.p);
			break;
		default: // 3
			modeattacking = ATTACKING_NONE;
			break;
	}

	// Random seed
	randseed = READUINT32(demobuf.p);

	demobuf.p += 4; // Extrainfo location

	// ...*map* not loaded?
	if (!gamemap || (gamemap > nummapheaders) || !mapheaderinfo[gamemap-1] || mapheaderinfo[gamemap-1]->lumpnum == LUMPERROR)
	{
		snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname);
		CONS_Alert(CONS_ERROR, "%s", msg);
		Z_Free(pdemoname);
		P_SaveBufferFree(&demobuf);
		demo.playback = false;
		demo.title = false;
		M_StartMessage(msg, NULL, MM_NOTHING);
		return;
	}

	// net var data
	CV_LoadDemoVars(&demobuf.p);

	memset(&grandprixinfo, 0, sizeof grandprixinfo);
	if ((demoflags & DF_GRANDPRIX))
	{
		grandprixinfo.gp = true;
		grandprixinfo.gamespeed = READUINT8(demobuf.p);
		grandprixinfo.masterbots = READUINT8(demobuf.p) != 0;
		grandprixinfo.eventmode = READUINT8(demobuf.p);
	}

	// Load "mapmusrng" used for altmusic selection
	mapmusrng = READUINT8(demobuf.p);

	// Sigh ... it's an empty demo.
	if (*demobuf.p == DEMOMARKER)
	{
		snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
		CONS_Alert(CONS_ERROR, "%s", msg);
		Z_Free(pdemoname);
		P_SaveBufferFree(&demobuf);
		demo.playback = false;
		demo.title = false;
		M_StartMessage(msg, NULL, MM_NOTHING);
		return;
	}

	Z_Free(pdemoname);

	memset(&oldcmd,0,sizeof(oldcmd));
	memset(&oldghost,0,sizeof(oldghost));
	memset(&ghostext,0,sizeof(ghostext));

#if defined(SKIPERRORS) && !defined(DEVELOP)
	if ((VERSION != version || SUBVERSION != subversion) && !skiperrors)
#else
	if (VERSION != version || SUBVERSION != subversion)
#endif
		CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n"));

	// console warning messages
#if defined(SKIPERRORS) && !defined(DEVELOP)
	demosynced = (!skiperrors);
#else
	demosynced = true;
#endif

	// didn't start recording right away.
	demo.deferstart = false;

	consoleplayer = 0;
	memset(playeringame,0,sizeof(playeringame));
	memset(displayplayers,0,sizeof(displayplayers));
	memset(camera,0,sizeof(camera)); // reset freecam

	// Load players that were in-game when the map started
	p = READUINT8(demobuf.p);

	while (p != 0xFF)
	{
		UINT8 flags = READUINT8(demobuf.p);

		spectator = !!(flags & DEMO_SPECTATOR);
		bot = !!(flags & DEMO_BOT);

		if ((spectator || bot))
		{
			if (modeattacking)
			{
				snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with %s, and is thus invalid.\n"), pdemoname, (bot ? "bots" : "spectators"));
				CONS_Alert(CONS_ERROR, "%s", msg);
				Z_Free(pdemoname);
				P_SaveBufferFree(&demobuf);
				demo.playback = false;
				demo.title = false;
				M_StartMessage(msg, NULL, MM_NOTHING);
				return;
			}
		}

		slots[numslots] = p;
		numslots++;

		if (modeattacking && numslots > 1)
		{
			snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with multiple players, and is thus invalid.\n"), pdemoname);
			CONS_Alert(CONS_ERROR, "%s", msg);
			Z_Free(pdemoname);
			P_SaveBufferFree(&demobuf);
			demo.playback = false;
			demo.title = false;
			M_StartMessage(msg, NULL, MM_NOTHING);
			return;
		}

		if (!playeringame[displayplayers[0]] || players[displayplayers[0]].spectator)
			displayplayers[0] = consoleplayer = serverplayer = p;

		G_AddPlayer(p, p);
		players[p].spectator = spectator;

		if (flags & DEMO_KICKSTART)
			players[p].pflags |= PF_KICKSTARTACCEL;
		else
			players[p].pflags &= ~PF_KICKSTARTACCEL;

		if (flags & DEMO_SHRINKME)
			players[p].pflags |= PF_SHRINKME;
		else
			players[p].pflags &= ~PF_SHRINKME;

		K_UpdateShrinkCheat(&players[p]);

		if ((players[p].bot = bot) == true)
		{
			players[p].botvars.difficulty = READUINT8(demobuf.p);
			players[p].botvars.diffincrease = READUINT8(demobuf.p); // needed to avoid having to duplicate logic
			players[p].botvars.rival = (boolean)READUINT8(demobuf.p);
		}

		// Name
		M_Memcpy(player_names[p],demobuf.p,16);
		demobuf.p += 16;

		/*if (players[p].spectator)
		{
			CONS_Printf("player %s is spectator at start\n", player_names[p]);
		}*/

		// Skin
		M_Memcpy(skin,demobuf.p,16);
		demobuf.p += 16;
		SetPlayerSkin(p, skin);

		// Color
		M_Memcpy(color,demobuf.p,16);
		demobuf.p += 16;
		for (i = 0; i < numskincolors; i++)
			if (!stricmp(skincolors[i].name,color))				// SRB2kart
			{
				players[p].skincolor = i;
				break;
			}

		// Follower
		M_Memcpy(follower, demobuf.p, 16);
		demobuf.p += 16;
		K_SetFollowerByName(p, follower);

		// Follower colour
		M_Memcpy(color, demobuf.p, 16);
		demobuf.p += 16;
		for (i = 0; i < numskincolors +2; i++)	// +2 because of Match and Opposite
		{
				if (!stricmp(Followercolor_cons_t[i].strvalue, color))
				{
						players[p].followercolor = i;
						break;
				}
		}

		// Score, since Kart uses this to determine where you start on the map
		players[p].score = READUINT32(demobuf.p);

		// Power Levels
		clientpowerlevels[p][gametype == GT_BATTLE ? PWRLV_BATTLE : PWRLV_RACE] = READUINT16(demobuf.p);

		// Kart stats, temporarily
		kartspeed[p] = READUINT8(demobuf.p);
		kartweight[p] = READUINT8(demobuf.p);

		if (stricmp(skins[players[p].skin].name, skin) != 0)
			FindClosestSkinForStats(p, kartspeed[p], kartweight[p]);

		// Followitem
		players[p].followitem = READUINT32(demobuf.p);

		// Look for the next player
		p = READUINT8(demobuf.p);
	}

	// end of player read (the 0xFF marker)
	// so this is where we are to read our lua variables (if possible!)
	if (demoflags & DF_LUAVARS)	// again, used for compability, lua shit will be saved to replays regardless of if it's even been loaded
	{
		if (!gL) // No Lua state! ...I guess we'll just start one...
			LUA_ClearState();

		// No modeattacking check, DF_LUAVARS won't be present here.
		LUA_UnArchive(&demobuf, false);
	}

	splitscreen = 0;

	if (demo.title)
	{
		splitscreen = M_RandomKey(6)-1;
		splitscreen = min(min(3, numslots-1), splitscreen); // Bias toward 1p and 4p views

		for (p = 0; p <= splitscreen; p++)
			G_ResetView(p+1, slots[M_RandomKey(numslots)], false);
	}

	R_ExecuteSetViewSize();

	P_SetRandSeed(randseed);
	G_InitNew(demoflags & DF_ENCORE, gamemap, true, true, false); // Doesn't matter whether you reset or not here, given changes to resetplayer.

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (players[i].mo)
		{
			players[i].mo->color = players[i].skincolor;
			oldghost[i].x = players[i].mo->x;
			oldghost[i].y = players[i].mo->y;
			oldghost[i].z = players[i].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[i].kartspeed = kartspeed[i];
		players[i].kartweight = kartweight[i];
	}

	demo.deferstart = true;
}

void G_AddGhost(char *defdemoname)
{
	INT32 i;
	lumpnum_t l;
	char name[17],skin[17],color[MAXCOLORNAME+1],*n,*pdemoname;
	UINT64 demohash;
	demoghost *gh;
	UINT8 flags;
	UINT8 *buffer,*p;
	mapthing_t *mthing;
	UINT16 count, ghostversion;
	skin_t *ghskin = &skins[0];
	UINT8 kartspeed = UINT8_MAX, kartweight = UINT8_MAX;

	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;
	}

	p += 64; // title
	demohash = READUINT64(p); // demo checksum

	for (gh = ghosts; gh; gh = gh->next)
		if (demohash == gh->checksum) // another ghost in the game already has this checksum?
		{ // Don't add another one, then!
			CONS_Debug(DBG_SETUP, "Rejecting duplicate ghost %s (hash 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"


	SKIPSTRING(p); // gamemap
	p += sizeof(UINT64); // maphash (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;
	}

	if (flags & DF_LUAVARS) // can't be arsed to add support for grinding away ported lua material
	{
		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Replay data contains luavars, cannot continue.\n"), pdemoname);
		Z_Free(pdemoname);
		Z_Free(buffer);
		return;
	}

	p++; // gametype
	p++; // numlaps
	G_SkipDemoExtraFiles(&p); // Don't wanna modify the file list for ghosts.

	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
	{
		case ATTACKING_NONE: // 0
			break;
		case ATTACKING_TIME: // 1
			p += 8; // demo time, lap
			break;
		case ATTACKING_ITEMBREAK: // 2
			p += 4; // demo time
			break;
		default: // 3
			break;
	}

	p += 4; // random seed
	p += 4; // Extra data location reference

	// net var data
	count = READUINT16(p);
	while (count--)
	{
		SKIPSTRING(p);
		SKIPSTRING(p);
		p++;
	}

	if ((flags & DF_GRANDPRIX))
	{
		p += 3;
	}

	// Skip mapmusrng
	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;
	}

	p++; // player number - doesn't really need to be checked, TODO maybe support adding multiple players' ghosts at once

	// any invalidating flags?
	i = READUINT8(p);
	if ((i & (DEMO_SPECTATOR|DEMO_BOT)) != 0)
	{
		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot (spectator/bot)\n"), defdemoname);
		Z_Free(pdemoname);
		Z_Free(buffer);
		return;
	}

	// 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;

	// Follower data was here, skip it, we don't care about it for ghosts.
	p += 32;	// followerskin (16) + followercolor (16)

	p += 4; // score
	p += 2; // powerlevel

	kartspeed = READUINT8(p);
	kartweight = READUINT8(p);

	p += 4; // followitem (maybe change later)

	if (READUINT8(p) != 0xFF)
	{
		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot. (Bad terminator)\n"), pdemoname);
		Z_Free(pdemoname);
		Z_Free(buffer);
		return;
	}

	for (i = 0; i < numskins; i++)
		if (!stricmp(skins[i].name,skin))
		{
			ghskin = &skins[i];
			break;
		}

	if (i == numskins)
	{
		if (kartspeed != UINT8_MAX && kartweight != UINT8_MAX)
			ghskin = &skins[GetSkinNumClosestToStats(kartspeed, kartweight)];

		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Invalid character. Falling back to %s.\n"), pdemoname, ghskin->name);
	}


	gh = Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL);
	gh->next = ghosts;
	gh->buffer = buffer;
	gh->checksum = demohash;
	gh->p = p;

	ghosts = gh;

	gh->version = ghostversion;
	mthing = playerstarts[0] ? playerstarts[0] : deathmatchstarts[0]; // todo not correct but out of scope
	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->args[0]) ^ !!(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;

	gh->mo->state = states + S_KART_STILL;
	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->renderflags |= RF_DONTDRAW;
	gh->fadein = (9-3)*6; // fade from invisible to trans30 over as close to 35 tics as possible
	gh->mo->tics = -1;

	// Set skin
	gh->mo->skin = gh->oldmo.skin = ghskin;

	// Set color
	gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor;
	for (i = 0; i < numskincolors; i++)
		if (!stricmp(skincolors[i].name,color))
		{
			gh->mo->color = (UINT16)i;
			break;
		}
	gh->oldmo.color = gh->mo->color;

	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;
}

// A simplified version of G_AddGhost...
void G_UpdateStaffGhostName(lumpnum_t l)
{
	UINT8 *buffer,*p;
	UINT16 ghostversion;
	UINT8 flags;

	buffer = p = W_CacheLumpNum(l, PU_CACHE);

	// read demo header
	if (memcmp(p, DEMOHEADER, 12))
	{
		goto fail;
	}

	p += 12; // DEMOHEADER
	p++; // VERSION
	p++; // SUBVERSION

	ghostversion = READUINT16(p);
	switch(ghostversion)
	{
		case DEMOVERSION: // latest always supported
			break;

		// too old, cannot support.
		default:
			goto fail;
	}

	p += 64; // full demo title
	p += sizeof(UINT64); // demo checksum

	if (memcmp(p, "PLAY", 4))
	{
		goto fail;
	}

	p += 4; // "PLAY"
	SKIPSTRING(p); // gamemap
	p += sizeof(UINT64); // maphash (possibly check for consistency?)

	flags = READUINT8(p);
	if (!(flags & DF_GHOST))
	{
		goto fail; // we don't NEED to do it here, but whatever
	}

	p++; // Gametype
	p++; // numlaps
	G_SkipDemoExtraFiles(&p);

	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
	{
		case ATTACKING_NONE: // 0
			break;
		case ATTACKING_TIME: // 1
			p += 8; // demo time, lap
			break;
		case ATTACKING_ITEMBREAK: // 2
			p += 4; // demo time
			break;
		default: // 3
			break;
	}

	p += 4; // random seed
	p += 4; // Extrainfo location marker

	// Ehhhh don't need ghostversion here (?) so I'll reuse the var here
	ghostversion = READUINT16(p);
	while (ghostversion--)
	{
		p += 2;
		SKIPSTRING(p);
		p++; // stealth
	}

	if ((flags & DF_GRANDPRIX))
	{
		p += 3;
	}

	// Assert first player is in and then read name
	if (READUINT8(p) != 0)
		goto fail;
	if (READUINT8(p) & (DEMO_SPECTATOR|DEMO_BOT))
		goto fail;
	M_Memcpy(dummystaffname, p,16);
	dummystaffname[16] = '\0';

	// Ok, no longer any reason to care, bye
fail:
	Z_Free(buffer);
	return;
}

//
// 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");
	demo.timing = 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
	// TODO: Use map header to determine lump name
	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 += sizeof(UINT64); // 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
===================
*/

// Writes the demo's checksum.
static void WriteDemoChecksum(void)
{
	UINT8 *p = demobuf.buffer+16; // checksum position
	WRITEUINT64(p, HASH64(p+16, demobuf.p - (p+16)));
}

// 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 (demobuf.p)
	{
		WRITEUINT8(demobuf.p, (kill) ? METALDEATH : DEMOMARKER); // add the demo end (or metal death) marker
		WriteDemoChecksum();
		saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuf.buffer, demobuf.p - demobuf.buffer); // finally output the file.
	}
	P_SaveBufferFree(&demobuf);
	metalrecording = false;
	if (saved)
		I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
	I_Error("Failed to save demo!");
}

// Stops timing a demo.
static void G_StopTimingDemo(void)
{
	INT32 demotime;
	double f1, f2;
	demotime = I_GetTime() - demostarttime;
	if (!demotime)
		return;
	G_StopDemo();
	demo.timing = 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);
	if (timedemo_quit)
		COM_ImmedExecute("quit");
	else
		D_StartTitle();
}

// reset engine variable set for the demos
// called from stopdemo command, map command, and g_checkdemoStatus.
void G_StopDemo(void)
{
	P_SaveBufferFree(&demobuf);
	demo.playback = false;
	if (demo.title)
		modeattacking = false;
	demo.title = false;
	demo.timing = false;
	singletics = false;

	{
		UINT8 i;
		for (i = 0; i < MAXSPLITSCREENPLAYERS; ++i)
		{
			camera[i].freecam = false;
		}
	}

	if (gamestate == GS_INTERMISSION)
		Y_EndIntermission(); // cleanup

	if (gamestate == GS_VOTING)
		Y_EndVote();

	G_SetGamestate(GS_NULL);
	wipegamestate = GS_NULL;
	SV_StopServer();
	SV_ResetServer();
}

boolean G_CheckDemoStatus(void)
{
	G_FreeGhosts();

	// DO NOT end metal sonic demos here

	if (demo.timing)
	{
		G_StopTimingDemo();
		return true;
	}

	if (demo.playback)
	{
		if (demo.quitafterplaying)
			I_Quit();

		if (multiplayer && !demo.title)
			G_FinishExitLevel();
		else
		{
			G_StopDemo();

			if (timedemo_quit)
				COM_ImmedExecute("quit");
			else if (modeattacking)
				MR_ModeAttackEndGame(0);
			else
				D_StartTitle();
		}

		return true;
	}

	if (demo.recording && (modeattacking || demo.savemode != DSM_NOTSAVING))
	{
		G_SaveDemo();
		return true;
	}

	if (demo.recording)
		P_SaveBufferFree(&demobuf);
	demo.recording = false;
	return false;
}

void G_SaveDemo(void)
{
	UINT8 *p = demobuf.buffer+16; // after version
	UINT32 length;

	// Ensure extrainfo pointer is always available, even if no info is present.
	if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
	{
		WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker
		*(UINT32 *)demoinfo_p = demobuf.p - demobuf.buffer;
	}
	WRITEUINT8(demobuf.p, DW_END); // Mark end of demo extra data.

	M_Memcpy(p, demo.titlename, 64); // Write demo title here
	p += 64;

	if (multiplayer)
	{
		// Change the demo's name to be a slug of the title
		char demo_slug[128];
		char *writepoint;
		size_t i, strindex = 0;
		boolean dash = true;

		for (i = 0; demo.titlename[i] && i < 127; i++)
		{
			if ((demo.titlename[i] >= 'a' && demo.titlename[i] <= 'z') ||
				(demo.titlename[i] >= '0' && demo.titlename[i] <= '9'))
			{
				demo_slug[strindex] = demo.titlename[i];
				strindex++;
				dash = false;
			}
			else if (demo.titlename[i] >= 'A' && demo.titlename[i] <= 'Z')
			{
				demo_slug[strindex] = demo.titlename[i] + 'a' - 'A';
				strindex++;
				dash = false;
			}
			else if (!dash)
			{
				demo_slug[strindex] = '-';
				strindex++;
				dash = true;
			}
		}

		demo_slug[strindex] = 0;
		if (dash) demo_slug[strindex-1] = 0;

		writepoint = strstr(strrchr(demoname, *PATHSEP), "-") + 1;
		demo_slug[128 - (writepoint - demoname) - 4] = 0;
		sprintf(writepoint, "%s.lmp", demo_slug);
	}

	length = *(UINT32 *)demoinfo_p;
	WRITEUINT32(demoinfo_p, length);

	// Doesn't seem like I can use WriteDemoChecksum here, correct me if I'm wrong -Sal

	// Make a checksum of everything after the checksum in the file up to the end of the standard data. Extrainfo is freely modifiable.
	WRITEUINT64(p, HASH64(p+16, (demobuf.buffer + length) - (p+16)));

	if (FIL_WriteFile(demoname, demobuf.buffer, demobuf.p - demobuf.buffer)) // finally output the file.
		demo.savemode = DSM_SAVED;
	P_SaveBufferFree(&demobuf);
	demo.recording = false;

	if (!modeattacking)
	{
		if (demo.savemode == DSM_SAVED)
			CONS_Printf(M_GetText("Demo %s recorded\n"), demoname);
		else
			CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname);
	}
}

boolean G_DemoTitleResponder(event_t *ev)
{
	size_t len;
	INT32 ch;

	if (ev->type != ev_keydown)
		return false;

	ch = (INT32)ev->data1;

	// Only ESC and non-keyboard keys abort connection
	if (ch == KEY_ESCAPE)
	{
		demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING;
		return true;
	}

	if (ch == KEY_ENTER || ch >= NUMKEYS)
	{
		demo.savemode = DSM_WILLSAVE;
		return true;
	}

	if ((ch >= HU_FONTSTART && ch <= HU_FONTEND && fontv[HU_FONT].font[ch-HU_FONTSTART])
	  || ch == ' ') // Allow spaces, of course
	{
		len = strlen(demo.titlename);
		if (len < 64)
		{
			demo.titlename[len+1] = 0;
			demo.titlename[len] = CON_ShiftChar(ch);
		}
	}
	else if (ch == KEY_BACKSPACE)
	{
		if (shiftdown)
			memset(demo.titlename, 0, sizeof(demo.titlename));
		else
		{
			len = strlen(demo.titlename);

			if (len > 0)
				demo.titlename[len-1] = 0;
		}
	}

	return true;
}

void G_SyncDemoParty(INT32 rem, INT32 newsplitscreen)
{
	int r_splitscreen_copy = r_splitscreen;
	INT32 displayplayers_copy[MAXSPLITSCREENPLAYERS];
	memcpy(displayplayers_copy, displayplayers, sizeof displayplayers);

	// If we switch away from someone's view, that player
	// should be removed from the party.
	// However, it is valid to have the player on multiple
	// viewports.

	// Remove this player
	G_LeaveParty(rem);

	// And reset the rest of the party
	for (int i = 0; i <= r_splitscreen_copy; ++i)
		G_LeaveParty(displayplayers_copy[i]);

	// Restore the party, without the removed player, and
	// with the order matching displayplayers
	for (int i = 0; i <= newsplitscreen; ++i)
		G_JoinParty(consoleplayer, displayplayers_copy[i]);

	// memcpy displayplayers back to preserve duplicates
	// (G_JoinParty will not create duplicates itself)
	r_splitscreen = newsplitscreen;
	memcpy(displayplayers, displayplayers_copy, sizeof displayplayers);
	R_ExecuteSetViewSize();
}
