/*
cl_demo.c - demo record & playback
Copyright (C) 2007 Uncle Mike

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
*/

#include "common.h"
#include "client.h"
#include "net_encode.h"

#define dem_unknown		0	// unknown command
#define dem_norewind	1	// startup message
#define dem_read		2	// it's a normal network packet
#define dem_jumptime	3	// move the demostart time value forward by this amount
#define dem_userdata	4	// userdata from the client.dll
#define dem_usercmd		5	// read usercmd_t
#define dem_stop		6	// end of time
#define dem_lastcmd		dem_stop

#define DEMO_STARTUP	0	// this lump contains startup info needed to spawn into the server
#define DEMO_NORMAL		1	// this lump contains playback info of messages, etc., needed during playback.

// Demo flags
#define FDEMO_TITLE		0x01	// Show title
#define FDEMO_PLAY		0x04	// Playing cd track
#define FDEMO_FADE_IN_SLOW	0x08	// Fade in (slow)
#define FDEMO_FADE_IN_FAST	0x10	// Fade in (fast)
#define FDEMO_FADE_OUT_SLOW	0x20	// Fade out (slow)
#define FDEMO_FADE_OUT_FAST	0x40	// Fade out (fast)

#define IDEMOHEADER		(('M'<<24)+('E'<<16)+('D'<<8)+'I') // little-endian "IDEM"
#define DEMO_PROTOCOL	3

const char *demo_cmd[dem_lastcmd+1] =
{
	"dem_unknown",
	"dem_norewind",
	"dem_read",
	"dem_jumptime",
	"dem_userdata",
	"dem_usercmd",
	"dem_stop",
};

#pragma pack( push, 1 )
typedef struct
{
	int		id;		// should be IDEM
	int		dem_protocol;	// should be DEMO_PROTOCOL
	int		net_protocol;	// should be PROTOCOL_VERSION
	double		host_fps;		// fps for demo playing
	char		mapname[64];	// name of map
	char		comment[64];	// comment for demo
	char		gamedir[64];	// name of game directory (FS_Gamedir())
	int		directory_offset;	// offset of Entry Directory.
} demoheader_t;
#pragma pack( pop )

typedef struct
{
	int		entrytype;	// DEMO_STARTUP or DEMO_NORMAL
	float		playback_time;	// time of track
	int		playback_frames;	// # of frames in track
	int		offset;		// file offset of track data
	int		length;		// length of track
	int		flags;		// FX-flags
	char		description[64];	// entry description
} demoentry_t;

typedef struct
{
	demoentry_t	*entries;		// track entry info
	int		numentries;	// number of tracks
} demodirectory_t;

// add angles
typedef struct
{
	float		starttime;
	vec3_t		viewangles;
} demoangle_t;

// private demo states
struct
{
	demoheader_t	header;
	demoentry_t	*entry;
	demodirectory_t	directory;
	int		framecount;
	float		starttime;
	float		realstarttime;
	float		timestamp;
	float		lasttime;
	int		entryIndex;

	// interpolation stuff
	demoangle_t	cmds[ANGLE_BACKUP];
	int		angle_position;
} demo;

/*
====================
CL_StartupDemoHeader

spooling demo header in case
we record a demo on this level
====================
*/
void CL_StartupDemoHeader( void )
{
	if( cls.demoheader )
	{
		FS_Close( cls.demoheader );
	}

	// Note: this is replacing tmpfile()
	cls.demoheader = FS_Open( "demoheader.tmp", "w+b", true );

	if( !cls.demoheader )
	{
		Con_DPrintf( S_ERROR "couldn't open temporary header file.\n" );
		return;
	}

	Con_Printf( "Spooling demo header.\n" );
}

/*
====================
CL_CloseDemoHeader

close demoheader file on engine shutdown
====================
*/
void CL_CloseDemoHeader( void )
{
	if( !cls.demoheader )
		return;

	FS_Close( cls.demoheader );
}

/*
====================
CL_GetDemoRecordClock

write time while demo is recording
====================
*/
float CL_GetDemoRecordClock( void )
{
	return cl.mtime[0];
}

/*
====================
CL_GetDemoPlaybackClock

overwrite host.realtime
====================
*/
float CL_GetDemoPlaybackClock( void )
{
	return host.realtime + host.frametime;
}

/*
====================
CL_GetDemoFramerate

overwrite host.frametime
====================
*/
double CL_GetDemoFramerate( void )
{
	if( cls.timedemo )
		return 0.0;
	return bound( MIN_FPS, demo.header.host_fps, MAX_FPS );
}

/*
====================
CL_WriteDemoCmdHeader

Writes the demo command header and time-delta
====================
*/
void CL_WriteDemoCmdHeader( byte cmd, file_t *file )
{
	float	dt;

	Assert( cmd >= 1 && cmd <= dem_lastcmd );
	if( !file ) return;

	// command
	FS_Write( file, &cmd, sizeof( byte ));

	// time offset
	dt = (float)(CL_GetDemoRecordClock() - demo.starttime);
	FS_Write( file, &dt, sizeof( float ));
}

/*
====================
CL_WriteDemoJumpTime

Update level time on a next level
====================
*/
void CL_WriteDemoJumpTime( void )
{
	if( cls.demowaiting || !cls.demofile )
		return;

	demo.starttime = CL_GetDemoRecordClock(); // setup the demo starttime

	// demo playback should read this as an incoming message.
	// write the client's realtime value out so we can synchronize the reads.
	CL_WriteDemoCmdHeader( dem_jumptime, cls.demofile );
}

/*
====================
CL_WriteDemoUserCmd

Writes the current user cmd
====================
*/
void CL_WriteDemoUserCmd( int cmdnumber )
{
	sizebuf_t	buf;
	word	bytes;
	byte	data[1024];

	if( !cls.demorecording || !cls.demofile )
		return;

	CL_WriteDemoCmdHeader( dem_usercmd, cls.demofile );

	FS_Write( cls.demofile, &cls.netchan.outgoing_sequence, sizeof( int ));
	FS_Write( cls.demofile, &cmdnumber, sizeof( int ));

	// write usercmd_t
	MSG_Init( &buf, "UserCmd", data, sizeof( data ));
	CL_WriteUsercmd( &buf, -1, cmdnumber );	// always no delta

	bytes = MSG_GetNumBytesWritten( &buf );

	FS_Write( cls.demofile, &bytes, sizeof( word ));
	FS_Write( cls.demofile, data, bytes );
}

/*
====================
CL_WriteDemoSequence

Save state of cls.netchan sequences
so that we can play the demo correctly.
====================
*/
void CL_WriteDemoSequence( file_t *file )
{
	Assert( file != NULL );

	FS_Write( file, &cls.netchan.incoming_sequence, sizeof( int ));
	FS_Write( file, &cls.netchan.incoming_acknowledged, sizeof( int ));
	FS_Write( file, &cls.netchan.incoming_reliable_acknowledged, sizeof( int ));
	FS_Write( file, &cls.netchan.incoming_reliable_sequence, sizeof( int ));
	FS_Write( file, &cls.netchan.outgoing_sequence, sizeof( int ));
	FS_Write( file, &cls.netchan.reliable_sequence, sizeof( int ));
	FS_Write( file, &cls.netchan.last_reliable_sequence, sizeof( int ));
}

/*
====================
CL_WriteDemoMessage

Dumps the current net message, prefixed by the length
====================
*/
void CL_WriteDemoMessage( qboolean startup, int start, sizebuf_t *msg )
{
	file_t	*file = startup ? cls.demoheader : cls.demofile;
	int	swlen;
	byte	c;

	if( !file ) return;

	// past the start but not recording a demo.
	if( !startup && !cls.demorecording )
		return;

	swlen = MSG_GetNumBytesWritten( msg ) - start;
	if( swlen <= 0 ) return;

	if( !startup ) demo.framecount++;

	// demo playback should read this as an incoming message.
	c = (cls.state != ca_active) ? dem_norewind : dem_read;

	CL_WriteDemoCmdHeader( c, file );
	CL_WriteDemoSequence( file );

	// write the length out.
	FS_Write( file, &swlen, sizeof( int ));

	// output the buffer. Skip the network packet stuff.
	FS_Write( file, MSG_GetData( msg ) + start, swlen );
}

/*
====================
CL_WriteDemoUserMessage

Dumps the user message (demoaction)
====================
*/
void CL_WriteDemoUserMessage( const byte *buffer, size_t size )
{
	if( !cls.demorecording || cls.demowaiting )
		return;

	if( !cls.demofile || !buffer || size <= 0 )
		return;

	CL_WriteDemoCmdHeader( dem_userdata, cls.demofile );

	// write the length out.
	FS_Write( cls.demofile, &size, sizeof( int ));

	// output the buffer.
	FS_Write( cls.demofile, buffer, size );
}

/*
====================
CL_WriteDemoHeader

Write demo header
====================
*/
void CL_WriteDemoHeader( const char *name )
{
	int	copysize;
	int	savepos;
	int	curpos;

	Con_Printf( "recording to %s.\n", name );
	cls.demofile = FS_Open( name, "wb", false );
	cls.demotime = 0.0;

	if( !cls.demofile )
	{
		Con_Printf( S_ERROR "couldn't open %s.\n", name );
		return;
	}

	cls.demorecording = true;
	cls.demowaiting = true;	// don't start saving messages until a non-delta compressed message is received

	memset( &demo.header, 0, sizeof( demo.header ));

	demo.header.id = IDEMOHEADER;
	demo.header.dem_protocol = DEMO_PROTOCOL;
	demo.header.net_protocol = cls.legacymode ? PROTOCOL_LEGACY_VERSION : PROTOCOL_VERSION;
	demo.header.host_fps = bound( MIN_FPS, host_maxfps->value, MAX_FPS );
	Q_strncpy( demo.header.mapname, clgame.mapname, sizeof( demo.header.mapname ));
	Q_strncpy( demo.header.comment, clgame.maptitle, sizeof( demo.header.comment ));
	Q_strncpy( demo.header.gamedir, FS_Gamedir(), sizeof( demo.header.gamedir ));

	// write header
	FS_Write( cls.demofile, &demo.header, sizeof( demo.header ));

	demo.directory.numentries = 2;
	demo.directory.entries = Mem_Calloc( cls.mempool, sizeof( demoentry_t ) * demo.directory.numentries );

	// DIRECTORY ENTRY # 0
	demo.entry = &demo.directory.entries[0];	// only one here.
	demo.entry->entrytype = DEMO_STARTUP;
	demo.entry->playback_time = 0.0f;		// startup takes 0 time.
	demo.entry->offset = FS_Tell( cls.demofile );	// position for this chunk.

	// finish off the startup info.
	CL_WriteDemoCmdHeader( dem_stop, cls.demoheader );

	// now copy the stuff we cached from the server.
	copysize = savepos = FS_Tell( cls.demoheader );

	FS_Seek( cls.demoheader, 0, SEEK_SET );

	FS_FileCopy( cls.demofile, cls.demoheader, copysize );

	// jump back to end, in case we record another demo for this session.
	FS_Seek( cls.demoheader, savepos, SEEK_SET );

	demo.starttime = CL_GetDemoRecordClock();	// setup the demo starttime
	demo.realstarttime = demo.starttime;
	demo.framecount = 0;
	cls.td_startframe = host.framecount;
	cls.td_lastframe = -1;			// get a new message this frame

	// now move on to entry # 1, the first data chunk.
	curpos = FS_Tell( cls.demofile );
	demo.entry->length = curpos - demo.entry->offset;

	// now we are writing the first real lump.
	demo.entry = &demo.directory.entries[1]; // first real data lump
	demo.entry->entrytype = DEMO_NORMAL;
	demo.entry->playback_time = 0.0f; // startup takes 0 time.

	demo.entry->offset = FS_Tell( cls.demofile );

	// demo playback should read this as an incoming message.
	// write the client's realtime value out so we can synchronize the reads.
	CL_WriteDemoCmdHeader( dem_jumptime, cls.demofile );

	if( clgame.hInstance ) clgame.dllFuncs.pfnReset();

	Cbuf_InsertText( "fullupdate\n" );
	Cbuf_Execute();
}

/*
=================
CL_StopRecord

finish recording demo
=================
*/
void CL_StopRecord( void )
{
	int	i, curpos;
	float	stoptime;
	int	frames;

	if( !cls.demorecording ) return;

	// demo playback should read this as an incoming message.
	CL_WriteDemoCmdHeader( dem_stop, cls.demofile );

	stoptime = CL_GetDemoRecordClock();
	if( clgame.hInstance ) clgame.dllFuncs.pfnReset();

	curpos = FS_Tell( cls.demofile );
	demo.entry->length = curpos - demo.entry->offset;
	demo.entry->playback_time = stoptime - demo.realstarttime;
	demo.entry->playback_frames = demo.framecount;

	//  Now write out the directory and free it and touch up the demo header.
	FS_Write( cls.demofile, &demo.directory.numentries, sizeof( int ));

	for( i = 0; i < demo.directory.numentries; i++ )
		FS_Write( cls.demofile, &demo.directory.entries[i], sizeof( demoentry_t ));

	Mem_Free( demo.directory.entries );
	demo.directory.numentries = 0;

	demo.header.directory_offset = curpos;
	FS_Seek( cls.demofile, 0, SEEK_SET );
	FS_Write( cls.demofile, &demo.header, sizeof( demo.header ));

	FS_Close( cls.demofile );
	cls.demofile = NULL;
	cls.demorecording = false;
	cls.demoname[0] = '\0';
	cls.td_lastframe = host.framecount;
	gameui.globals->demoname[0] = '\0';
	demo.header.host_fps = 0.0;

	frames = cls.td_lastframe - cls.td_startframe;
	Con_Printf( "Completed demo\nRecording time: %02d:%02d, frames %i\n", (int)(cls.demotime / 60.0f), (int)fmod(cls.demotime, 60.0f), frames );
	cls.demotime = 0.0;
}

/*
=================
CL_DrawDemoRecording
=================
*/
void CL_DrawDemoRecording( void )
{
	char	string[64];
	rgba_t	color = { 255, 255, 255, 255 };
	int	pos;
	int	len;

	if(!( host_developer.value && cls.demorecording ))
		return;

	pos = FS_Tell( cls.demofile );
	Q_snprintf( string, sizeof( string ), "^1RECORDING:^7 %s: %s time: %02d:%02d", cls.demoname,
		Q_memprint( pos ), (int)(cls.demotime / 60.0f ), (int)fmod( cls.demotime, 60.0f ));

	Con_DrawStringLen( string, &len, NULL );
	Con_DrawString(( refState.width - len ) >> 1, refState.height >> 4, string, color );
}

/*
=======================================================================

CLIENT SIDE DEMO PLAYBACK

=======================================================================
*/
/*
=================
CL_ReadDemoCmdHeader

read the demo command
=================
*/
qboolean CL_ReadDemoCmdHeader( byte *cmd, float *dt )
{
	// read the command
	// HACKHACK: skip NOPs
	do
	{
		FS_Read( cls.demofile, cmd, sizeof( byte ));
	} while( *cmd == dem_unknown );

	if( *cmd > dem_lastcmd )
	{
		Con_Printf( S_ERROR "Demo cmd %d > %d, file offset = %d\n", *cmd, dem_lastcmd, (int)FS_Tell( cls.demofile ));
		CL_DemoCompleted();
		return false;
	}

	// read the timestamp
	FS_Read( cls.demofile, dt, sizeof( float ));

	return true;
}

/*
=================
CL_ReadDemoUserCmd

read the demo usercmd for predicting
and smooth movement during playback the demo
=================
*/
void CL_ReadDemoUserCmd( qboolean discard )
{
	byte	data[1024];
	int	cmdnumber;
	int	outgoing_sequence;
	runcmd_t	*pcmd;
	word	bytes;

	FS_Read( cls.demofile, &outgoing_sequence, sizeof( int ));
	FS_Read( cls.demofile, &cmdnumber, sizeof( int ));
	FS_Read( cls.demofile, &bytes, sizeof( short ));
	FS_Read( cls.demofile, data, bytes );

	if( !discard )
	{
		usercmd_t		nullcmd;
		sizebuf_t		buf;
		demoangle_t	*a;

		memset( &nullcmd, 0, sizeof( nullcmd ));
		MSG_Init( &buf, "UserCmd", data, sizeof( data ));

		pcmd = &cl.commands[cmdnumber & CL_UPDATE_MASK];
		pcmd->processedfuncs = false;
		pcmd->senttime = 0.0f;
		pcmd->receivedtime = 0.1f;
		pcmd->frame_lerp = 0.1f;
		pcmd->heldback = false;
		pcmd->sendsize = 1;

		// always delta'ing from null
		cl.cmd = &pcmd->cmd;

		MSG_ReadDeltaUsercmd( &buf, &nullcmd, cl.cmd );

		// make sure what interp info contain angles from different frames
		// or lerping will stop working
		if( demo.lasttime != demo.timestamp )
		{
			// select entry into circular buffer
			demo.angle_position = (demo.angle_position + 1) & ANGLE_MASK;
			a = &demo.cmds[demo.angle_position];

			// record update
			a->starttime = demo.timestamp;
			VectorCopy( cl.cmd->viewangles, a->viewangles );
			demo.lasttime = demo.timestamp;
		}

		// NOTE: we need to have the current outgoing sequence correct
		// so we can do prediction correctly during playback
		cls.netchan.outgoing_sequence = outgoing_sequence;
	}
}

/*
=================
CL_ReadDemoSequence

read netchan sequences
=================
*/
void CL_ReadDemoSequence( qboolean discard )
{
	int	incoming_sequence;
	int	incoming_acknowledged;
	int	incoming_reliable_acknowledged;
	int	incoming_reliable_sequence;
	int	outgoing_sequence;
	int	reliable_sequence;
	int	last_reliable_sequence;

	FS_Read( cls.demofile, &incoming_sequence, sizeof( int ));
	FS_Read( cls.demofile, &incoming_acknowledged, sizeof( int ));
	FS_Read( cls.demofile, &incoming_reliable_acknowledged, sizeof( int ));
	FS_Read( cls.demofile, &incoming_reliable_sequence, sizeof( int ));
	FS_Read( cls.demofile, &outgoing_sequence, sizeof( int ));
	FS_Read( cls.demofile, &reliable_sequence, sizeof( int ));
	FS_Read( cls.demofile, &last_reliable_sequence, sizeof( int ));

	if( discard ) return;

	cls.netchan.incoming_sequence	= incoming_sequence;
	cls.netchan.incoming_acknowledged = incoming_acknowledged;
	cls.netchan.incoming_reliable_acknowledged = incoming_reliable_acknowledged;
	cls.netchan.incoming_reliable_sequence = incoming_reliable_sequence;
	cls.netchan.outgoing_sequence	= outgoing_sequence;
	cls.netchan.reliable_sequence	= reliable_sequence;
	cls.netchan.last_reliable_sequence = last_reliable_sequence;
}

/*
=================
CL_DemoStartPlayback
=================
*/
void CL_DemoStartPlayback( int mode )
{
	if( cls.changedemo )
	{
		S_StopAllSounds( true );
		SCR_BeginLoadingPlaque( false );

		CL_ClearState ();
		CL_InitEdicts (); // re-arrange edicts
	}
	else
	{
		// NOTE: at this point demo is still valid
		CL_Disconnect();
		Host_ShutdownServer();

		Con_FastClose();
		UI_SetActiveMenu( false );
	}

	cls.demoplayback = mode;
	cls.state = ca_connected;
	cl.background = (cls.demonum != -1) ? true : false;
	cls.spectator = false;
	cls.signon = 0;

	demo.starttime = CL_GetDemoPlaybackClock(); // for determining whether to read another message

	Netchan_Setup( NS_CLIENT, &cls.netchan, net_from, Cvar_VariableInteger( "net_qport" ), NULL, CL_GetFragmentSize );

	memset( demo.cmds, 0, sizeof( demo.cmds ));
	demo.angle_position = 1;
	demo.framecount = 0;
	cls.lastoutgoingcommand = -1;
 	cls.nextcmdtime = host.realtime;
	cl.last_command_ack = -1;
}

/*
=================
CL_DemoAborted
=================
*/
void CL_DemoAborted( void )
{
	if( cls.demofile )
		FS_Close( cls.demofile );
	cls.demoplayback = false;
	cls.changedemo = false;
	cls.timedemo = false;
	demo.framecount = 0;
	cls.demofile = NULL;
	cls.demonum = -1;

	Cvar_SetValue( "v_dark", 0.0f );
}

/*
=================
CL_DemoCompleted
=================
*/
void CL_DemoCompleted( void )
{
	if( cls.demonum != -1 )
		cls.changedemo = true;

	CL_StopPlayback();

	if( !CL_NextDemo() && !cls.changedemo )
		UI_SetActiveMenu( true );

	Cvar_SetValue( "v_dark", 0.0f );
}

/*
=================
CL_DemoMoveToNextSection

returns true on success, false on failure
g-cont. probably captain obvious mode is ON
=================
*/
qboolean CL_DemoMoveToNextSection( void )
{
	if( ++demo.entryIndex >= demo.directory.numentries )
	{
		// done
		CL_DemoCompleted();
		return false;
	}

	// switch to next section, we got a dem_stop
	demo.entry = &demo.directory.entries[demo.entryIndex];

	// ready to continue reading, reset clock.
	FS_Seek( cls.demofile, demo.entry->offset, SEEK_SET );

	// time is now relative to this chunk's clock.
	demo.starttime = CL_GetDemoPlaybackClock();
	demo.framecount = 0;

	return true;
}

qboolean CL_ReadRawNetworkData( byte *buffer, size_t *length )
{
	int	msglen = 0;

	Assert( buffer != NULL );
	Assert( length != NULL );

	*length = 0; // assume we fail
	FS_Read( cls.demofile, &msglen, sizeof( int ));

	if( msglen < 0 )
	{
		Con_Reportf( S_ERROR "Demo message length < 0\n" );
		CL_DemoCompleted();
		return false;
	}

	if( msglen > MAX_INIT_MSG )
	{
		Con_Reportf( S_ERROR "Demo message %i > %i\n", msglen, MAX_INIT_MSG );
		CL_DemoCompleted();
		return false;
	}

	if( msglen > 0 )
	{
		if( FS_Read( cls.demofile, buffer, msglen ) != msglen )
		{
			Con_Reportf( S_ERROR "Error reading demo message data\n" );
			CL_DemoCompleted();
			return false;
		}
	}

	cls.netchan.last_received = host.realtime;
	cls.netchan.total_received += msglen;
	*length = msglen;

	if( cls.state != ca_active )
		Cbuf_Execute();

	return true;
}

/*
=================
CL_DemoReadMessageQuake

reads demo data and write it to client
=================
*/
qboolean CL_DemoReadMessageQuake( byte *buffer, size_t *length )
{
	vec3_t		viewangles;
	int		msglen = 0;
	demoangle_t	*a;

	*length = 0; // assume we fail

	// decide if it is time to grab the next message
	if( cls.signon == SIGNONS )	// allways grab until fully connected
	{
		if( cls.timedemo )
		{
			if( host.framecount == cls.td_lastframe )
				return false; // already read this frame's message

			cls.td_lastframe = host.framecount;

			// if this is the second frame, grab the real td_starttime
			// so the bogus time on the first frame doesn't count
			if( host.framecount == cls.td_startframe + 1 )
				cls.td_starttime = host.realtime;
		}
		else if( cl.time <= cl.mtime[0] )
		{
			// don't need another message yet
			return false;
		}
	}

	// get the next message
	FS_Read( cls.demofile, &msglen, sizeof( int ));
	FS_Read( cls.demofile, &viewangles[0], sizeof( float ));
	FS_Read( cls.demofile, &viewangles[1], sizeof( float ));
	FS_Read( cls.demofile, &viewangles[2], sizeof( float ));
	cls.netchan.incoming_sequence++;
	demo.timestamp = cl.mtime[0];
	cl.skip_interp = false;

	// make sure what interp info contain angles from different frames
	// or lerping will stop working
	if( demo.lasttime != demo.timestamp )
	{
		// select entry into circular buffer
		demo.angle_position = (demo.angle_position + 1) & ANGLE_MASK;
		a = &demo.cmds[demo.angle_position];

		// record update
		a->starttime = demo.timestamp;
		VectorCopy( viewangles, a->viewangles );
		demo.lasttime = demo.timestamp;
	}

	if( msglen < 0 )
	{
		Con_Reportf( S_ERROR "Demo message length < 0\n" );
		CL_DemoCompleted();
		return false;
	}

	if( msglen > MAX_INIT_MSG )
	{
		Con_Reportf( S_ERROR "Demo message %i > %i\n", msglen, MAX_INIT_MSG );
		CL_DemoCompleted();
		return false;
	}

	if( msglen > 0 )
	{
		if( FS_Read( cls.demofile, buffer, msglen ) != msglen )
		{
			Con_Reportf( S_ERROR "Error reading demo message data\n" );
			CL_DemoCompleted();
			return false;
		}
	}

	cls.netchan.last_received = host.realtime;
	cls.netchan.total_received += msglen;
	*length = msglen;

	if( cls.state != ca_active )
		Cbuf_Execute();
	return true;
}

/*
=================
CL_DemoReadMessage

reads demo data and write it to client
=================
*/
qboolean CL_DemoReadMessage( byte *buffer, size_t *length )
{
	size_t		curpos = 0, lastpos = 0;
	float		fElapsedTime = 0.0f;
	qboolean		swallowmessages = true;
	static int	tdlastdemoframe = 0;
	byte		*userbuf = NULL;
	size_t		size = 0;
	byte		cmd;

	if( !cls.demofile )
	{
		CL_DemoCompleted();
		return false;
	}

	if(( !cl.background && ( cl.paused || cls.key_dest != key_game )) || cls.key_dest == key_console )
	{
		demo.starttime += host.frametime;
		return false; // paused
	}

	if( cls.demoplayback == DEMO_QUAKE1 )
		return CL_DemoReadMessageQuake( buffer, length );

	do
	{
		qboolean	bSkipMessage = false;

		if( !cls.demofile ) break;
		curpos = FS_Tell( cls.demofile );

		if( !CL_ReadDemoCmdHeader( &cmd, &demo.timestamp ))
			return false;

		fElapsedTime = CL_GetDemoPlaybackClock() - demo.starttime;
		if( !cls.timedemo ) bSkipMessage = ((demo.timestamp - cl_serverframetime()) >= fElapsedTime) ? true : false;
		if( cls.changelevel ) demo.framecount = 1;

		// changelevel issues
		if( demo.framecount <= 2 && ( fElapsedTime - demo.timestamp ) > host.frametime )
			demo.starttime = CL_GetDemoPlaybackClock();

		// not ready for a message yet, put it back on the file.
		if( cmd != dem_norewind && cmd != dem_stop && bSkipMessage )
		{
			// never skip first message
			if( demo.framecount != 0 )
			{
				FS_Seek( cls.demofile, curpos, SEEK_SET );
				return false; // not time yet.
			}
		}

		// we already have the usercmd_t for this frame
		// don't read next usercmd_t so predicting will work properly
		if( cmd == dem_usercmd && lastpos != 0 && demo.framecount != 0 )
		{
			FS_Seek( cls.demofile, lastpos, SEEK_SET );
			return false; // not time yet.
		}

		// COMMAND HANDLERS
		switch( cmd )
		{
		case dem_jumptime:
			demo.starttime = CL_GetDemoPlaybackClock();
			return false; // time is changed, skip frame
		case dem_stop:
			CL_DemoMoveToNextSection();
			return false; // header is ended, skip frame
		case dem_userdata:
			FS_Read( cls.demofile, &size, sizeof( int ));
			userbuf = Mem_Malloc( cls.mempool, size );
			FS_Read( cls.demofile, userbuf, size );

			if( clgame.hInstance )
				clgame.dllFuncs.pfnDemo_ReadBuffer( size, userbuf );
			Mem_Free( userbuf );
			userbuf = NULL;
			break;
		case dem_usercmd:
			CL_ReadDemoUserCmd( false );
			lastpos = FS_Tell( cls.demofile );
			break;
		default:
			swallowmessages = false;
			break;
		}
	} while( swallowmessages );

	// If we are playing back a timedemo, and we've already passed on a
	//  frame update for this host_frame tag, then we'll just skip this message.
	if( cls.timedemo && ( tdlastdemoframe == host.framecount ))
	{
		FS_Seek( cls.demofile, FS_Tell ( cls.demofile ) - 5, SEEK_SET );
		return false;
	}

	tdlastdemoframe = host.framecount;

	if( !cls.demofile )
		return false;

	// if not on "LOADING" section, check a few things
	if( demo.entryIndex )
	{
		// We are now on the second frame of a new section,
		// if so, reset start time (unless in a timedemo)
		if( demo.framecount == 1 && !cls.timedemo )
		{
			// cheat by moving the relative start time forward.
			demo.starttime = CL_GetDemoPlaybackClock();
		}
	}

	demo.framecount++;
	CL_ReadDemoSequence( false );

	return CL_ReadRawNetworkData( buffer, length );
}

void CL_DemoFindInterpolatedViewAngles( float t, float *frac, demoangle_t **prev, demoangle_t **next )
{
	int	i, i0, i1, imod;
	float	at;

	if( cls.timedemo ) return;

	imod = demo.angle_position - 1;
	i0 = (imod + 1) & ANGLE_MASK;
	i1 = (imod + 0) & ANGLE_MASK;

	if( demo.cmds[i0].starttime >= t )
	{
		for( i = 0; i < ANGLE_BACKUP - 2; i++ )
		{
			at = demo.cmds[imod & ANGLE_MASK].starttime;
			if( at == 0.0f ) break;

			if( at < t )
			{
				i0 = (imod + 1) & ANGLE_MASK;
				i1 = (imod + 0) & ANGLE_MASK;
				break;
			}
			imod--;
		}
	}

	*next = &demo.cmds[i0];
	*prev = &demo.cmds[i1];

	// avoid division by zero (probably this should never happens)
	if((*prev)->starttime == (*next)->starttime )
	{
		*prev = *next;
		*frac = 0.0f;
		return;
	}

	// time spans the two entries
	*frac = ( t - (*prev)->starttime ) / ((*next)->starttime - (*prev)->starttime );
	*frac = bound( 0.0f, *frac, 1.0f );
}

/*
==============
CL_DemoInterpolateAngles

We can predict or inpolate player movement with standed client code
but viewangles interpolate here
==============
*/
void CL_DemoInterpolateAngles( void )
{
	demoangle_t	*prev = NULL, *next = NULL;
	float		frac = 0.0f;
	float		curtime;

	if( cls.demoplayback == DEMO_QUAKE1 )
	{
		// manually select next & prev states
		next = &demo.cmds[(demo.angle_position - 0) & ANGLE_MASK];
		prev = &demo.cmds[(demo.angle_position - 1) & ANGLE_MASK];
		if( cl.skip_interp ) *prev = *next; // camera was teleported
		frac = cl.lerpFrac;
	}
	else
	{
		curtime = (CL_GetDemoPlaybackClock() - demo.starttime) - host.frametime;
		if( curtime > demo.timestamp )
			curtime = demo.timestamp; // don't run too far

		CL_DemoFindInterpolatedViewAngles( curtime, &frac, &prev, &next );
	}

	if( prev && next )
	{
		vec4_t	q, q1, q2;

		AngleQuaternion( next->viewangles, q1, false );
		AngleQuaternion( prev->viewangles, q2, false );
		QuaternionSlerp( q2, q1, frac, q );
		QuaternionAngle( q, cl.viewangles );
	}
	else if( cl.cmd != NULL )
		VectorCopy( cl.cmd->viewangles, cl.viewangles );
}

/*
==============
CL_FinishTimeDemo

show stats
==============
*/
void CL_FinishTimeDemo( void )
{
	int	frames;
	double	time;

	cls.timedemo = false;

	// the first frame didn't count
	frames = (host.framecount - cls.td_startframe) - 1;
	time = host.realtime - cls.td_starttime;
	if( !time ) time = 1.0;

	Con_Printf( "%i frames %5.3f seconds %5.3f fps\n", frames, time, frames / time );
}

/*
==============
CL_StopPlayback

Called when a demo file runs out, or the user starts a game
==============
*/
void CL_StopPlayback( void )
{
	if( !cls.demoplayback ) return;

	// release demofile
	FS_Close( cls.demofile );
	cls.demoplayback = false;
	demo.framecount = 0;
	cls.demofile = NULL;

	cls.olddemonum = Q_max( -1, cls.demonum - 1 );
	if( demo.directory.entries != NULL )
		Mem_Free( demo.directory.entries );
	cls.td_lastframe = host.framecount;
	demo.directory.numentries = 0;
	demo.directory.entries = NULL;
	demo.header.host_fps = 0.0;
	demo.entry = NULL;

	cls.demoname[0] = '\0';	// clear demoname too
	gameui.globals->demoname[0] = '\0';

	if( cls.timedemo )
		CL_FinishTimeDemo();

	if( cls.changedemo )
	{
		S_StopAllSounds( true );
		S_StopBackgroundTrack();
	}
	else
	{
		// let game known about demo state
		Cvar_FullSet( "cl_background", "0", FCVAR_READ_ONLY );
		cls.state = ca_disconnected;
		memset( &cls.serveradr, 0, sizeof( cls.serveradr ) );
		cls.set_lastdemo = false;
		S_StopBackgroundTrack();
		cls.connect_time = 0;
		cls.demonum = -1;
		cls.signon = 0;

		// and finally clear the state
		CL_ClearState ();
	}
}

/*
==================
CL_GetDemoComment
==================
*/
int GAME_EXPORT CL_GetDemoComment( const char *demoname, char *comment )
{
	file_t		*demfile;
	demoheader_t	demohdr;
	demodirectory_t	directory;
	demoentry_t	entry;
	float		playtime = 0.0f;
	int		i;

	if( !comment ) return false;

	demfile = FS_Open( demoname, "rb", false );
	if( !demfile )
	{
		comment[0] = '\0';
		return false;
	}

	// read in the m_DemoHeader
	FS_Read( demfile, &demohdr, sizeof( demoheader_t ));

	if( demohdr.id != IDEMOHEADER )
	{
		FS_Close( demfile );
		Q_strncpy( comment, "<corrupted>", MAX_STRING );
		return false;
	}

	if(( demohdr.net_protocol != PROTOCOL_VERSION &&
		demohdr.net_protocol != PROTOCOL_LEGACY_VERSION ) ||
		demohdr.dem_protocol != DEMO_PROTOCOL )
	{
		FS_Close( demfile );
		Q_strncpy( comment, "<invalid protocol>", MAX_STRING );
		return false;
	}

	// now read in the directory structure.
	FS_Seek( demfile, demohdr.directory_offset, SEEK_SET );
	FS_Read( demfile, &directory.numentries, sizeof( int ));

	if( directory.numentries < 1 || directory.numentries > 1024 )
	{
		FS_Close( demfile );
		Q_strncpy( comment, "<corrupted>", MAX_STRING );
		return false;
	}

	for( i = 0; i < directory.numentries; i++ )
	{
		FS_Read( demfile, &entry, sizeof( demoentry_t ));
		playtime += entry.playback_time;
	}

	// split comment to sections
	Q_strncpy( comment, demohdr.mapname, CS_SIZE );
	Q_strncpy( comment + CS_SIZE, demohdr.comment, CS_SIZE );
	Q_snprintf( comment + CS_SIZE * 2, CS_TIME, "%g sec", playtime );

	// all done
	FS_Close( demfile );

	return true;
}

/*
==================
CL_NextDemo

Called when a demo finishes
==================
*/
qboolean CL_NextDemo( void )
{
	char	str[MAX_QPATH];

	if( cls.demonum == -1 )
		return false; // don't play demos
	S_StopAllSounds( true );

	if( !cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS )
	{
		cls.demonum = 0;
		if( !cls.demos[cls.demonum][0] )
		{
			Con_Printf( "no demos listed with startdemos\n" );
			cls.demonum = -1;
			return false;
		}
	}

	Q_snprintf( str, MAX_STRING, "playdemo %s\n", cls.demos[cls.demonum] );
	Cbuf_InsertText( str );
	cls.demonum++;

	return true;
}

/*
==================
CL_CheckStartupDemos

queue demos loop after movie playing
==================
*/
void CL_CheckStartupDemos( void )
{
	if( !cls.demos_pending )
		return; // no demos in loop

	if( cls.movienum != -1 )
		return; // wait until movies finished

	if( GameState->nextstate != STATE_RUNFRAME || cls.demoplayback )
	{
		// commandline override
		cls.demos_pending = false;
		cls.demonum = -1;
		return;
	}

	// run demos loop in background mode
	Cvar_SetValue( "v_dark", 1.0f );
	cls.demos_pending = false;
	cls.demonum = 0;
	CL_NextDemo ();
}

/*
==================
CL_DemoGetName
==================
*/
static void CL_DemoGetName( int lastnum, char *filename )
{
	if( lastnum < 0 || lastnum > 9999 )
	{
		// bound
		Q_strcpy( filename, "demo9999" );
		return;
	}

	Q_sprintf( filename, "demo%04d", lastnum );
}

/*
====================
CL_Record_f

record <demoname>
Begins recording a demo from the current position
====================
*/
void CL_Record_f( void )
{
	string		demoname, demopath;
	const char	*name;
	int		n;

	if( Cmd_Argc() == 1 )
	{
		name = "new";
	}
	else if( Cmd_Argc() == 2 )
	{
		name = Cmd_Argv( 1 );
	}
	else
	{
		Con_Printf( S_USAGE "record <demoname>\n" );
		return;
	}

	if( cls.demorecording )
	{
		Con_Printf( "Already recording.\n");
		return;
	}

	if( cls.demoplayback )
	{
		Con_Printf( "Can't record during demo playback.\n");
		return;
	}

	if( !cls.demoheader || cls.state != ca_active )
	{
		Con_Printf( "You must be in a level to record.\n");
		return;
	}

	if( !Q_stricmp( name, "new" ))
	{
		// scan for a free filename
		for( n = 0; n < 10000; n++ )
		{
			CL_DemoGetName( n, demoname );
			if( !FS_FileExists( va( "%s.dem", demoname ), true ))
				break;
		}

		if( n == 10000 )
		{
			Con_Printf( S_ERROR "no free slots for demo recording\n" );
			return;
		}
	}
	else Q_strncpy( demoname, name, sizeof( demoname ));

	// open the demo file
	Q_sprintf( demopath, "%s.dem", demoname );

	// make sure that old demo is removed
	if( FS_FileExists( demopath, false ))
		FS_Delete( demopath );

	Q_strncpy( cls.demoname, demoname, sizeof( cls.demoname ));
	Q_strncpy( gameui.globals->demoname, demoname, sizeof( gameui.globals->demoname ));

	CL_WriteDemoHeader( demopath );
}

/*
====================
CL_PlayDemo_f

playdemo <demoname>
====================
*/
void CL_PlayDemo_f( void )
{
	char	filename[MAX_QPATH];
	char	demoname[MAX_QPATH];
	int	i, ident;

	if( Cmd_Argc() < 2 )
	{
		Con_Printf( S_USAGE "%s <demoname>\n", Cmd_Argv( 0 ));
		return;
	}

	if( cls.demoplayback )
	{
		CL_StopPlayback();
	}

	if( cls.demorecording )
	{
		Con_Printf( "Can't playback during demo record.\n");
		return;
	}

	Q_strncpy( demoname, Cmd_Argv( 1 ), sizeof( demoname ));
	COM_StripExtension( demoname );
	Q_snprintf( filename, sizeof( filename ), "%s.dem", demoname );

	// hidden parameter
	if( Cmd_Argc() > 2 )
		cls.set_lastdemo = Q_atoi( Cmd_Argv( 2 ));

	// member last demo
	if( cls.set_lastdemo )
		Cvar_Set( "lastdemo", demoname );

	if( !FS_FileExists( filename, true ))
	{
		Con_Printf( S_ERROR "couldn't open %s\n", filename );
		CL_DemoAborted();
		return;
	}

	cls.demofile = FS_Open( filename, "rb", true );
	Q_strncpy( cls.demoname, demoname, sizeof( cls.demoname ));
	Q_strncpy( gameui.globals->demoname, demoname, sizeof( gameui.globals->demoname ));

	FS_Read( cls.demofile, &ident, sizeof( int ));
	FS_Seek( cls.demofile, 0, SEEK_SET ); // rewind back to start
	cls.forcetrack = 0;

	// check for quake demos
	if( ident != IDEMOHEADER )
	{
		int	c, neg = false;

		demo.header.host_fps = host_maxfps->value;

		while(( c = FS_Getc( cls.demofile )) != '\n' )
		{
			if( c == '-' ) neg = true;
			else cls.forcetrack = cls.forcetrack * 10 + (c - '0');
		}

		if( neg ) cls.forcetrack = -cls.forcetrack;
		CL_DemoStartPlayback( DEMO_QUAKE1 );
		return; // quake demo is started
	}

	// read in the demo header
	FS_Read( cls.demofile, &demo.header, sizeof( demoheader_t ));

	if( demo.header.id != IDEMOHEADER )
	{
		Con_Printf( S_ERROR "%s is not a demo file\n", demoname );
		CL_DemoAborted();
		return;
	}

	if( demo.header.dem_protocol != DEMO_PROTOCOL )
	{
		Con_Printf( S_ERROR "playdemo: demo protocol outdated (%i should be %i)\n", demo.header.dem_protocol, DEMO_PROTOCOL );
		CL_DemoAborted();
		return;
	}

	if( demo.header.net_protocol != PROTOCOL_VERSION &&
		demo.header.net_protocol != PROTOCOL_LEGACY_VERSION )
	{
		Con_Printf( S_ERROR "playdemo: net protocol outdated (%i should be %i)\n", demo.header.net_protocol, PROTOCOL_VERSION );
		CL_DemoAborted();
		return;
	}

	// now read in the directory structure.
	FS_Seek( cls.demofile, demo.header.directory_offset, SEEK_SET );
	FS_Read( cls.demofile, &demo.directory.numentries, sizeof( int ));

	if( demo.directory.numentries < 1 || demo.directory.numentries > 1024 )
	{
		Con_Printf( S_ERROR "demo had bogus # of directory entries: %i\n", demo.directory.numentries );
		CL_DemoAborted();
		return;
	}

	// allocate demo entries
	demo.directory.entries = Mem_Malloc( cls.mempool, sizeof( demoentry_t ) * demo.directory.numentries );

	for( i = 0; i < demo.directory.numentries; i++ )
	{
		FS_Read( cls.demofile, &demo.directory.entries[i], sizeof( demoentry_t ));
	}

	demo.entryIndex = 0;
	demo.entry = &demo.directory.entries[demo.entryIndex];

	FS_Seek( cls.demofile, demo.entry->offset, SEEK_SET );

	CL_DemoStartPlayback( DEMO_XASH3D );

	// g-cont. is this need?
	Q_strncpy( cls.servername, demoname, sizeof( cls.servername ));
	cls.legacymode = demo.header.net_protocol == PROTOCOL_LEGACY_VERSION;

	// begin a playback demo
}

/*
====================
CL_TimeDemo_f

timedemo <demoname>
====================
*/
void CL_TimeDemo_f( void )
{
	CL_PlayDemo_f ();

	// cls.td_starttime will be grabbed at the second frame of the demo, so
	// all the loading time doesn't get counted
	cls.timedemo = true;
	cls.td_starttime = host.realtime;
	cls.td_startframe = host.framecount;
	cls.td_lastframe = -1;		// get a new message this frame
}

/*
==================
CL_StartDemos_f
==================
*/
void CL_StartDemos_f( void )
{
	int	i, c;

	if( cls.key_dest != key_menu )
	{
		Con_Printf( "'startdemos' is not valid from the console\n" );
		return;
	}

	c = Cmd_Argc() - 1;
	if( c > MAX_DEMOS )
	{
		Con_DPrintf( S_WARN "Host_StartDemos: max %i demos in demoloop\n", MAX_DEMOS );
		c = MAX_DEMOS;
	}

	Con_Printf( "%i demo%s in loop\n", c, (c > 1) ? "s" : "" );

	for( i = 1; i < c + 1; i++ )
		Q_strncpy( cls.demos[i-1], Cmd_Argv( i ), sizeof( cls.demos[0] ));
	cls.demos_pending = true;
}

/*
==================
CL_Demos_f

Return to looping demos
==================
*/
void CL_Demos_f( void )
{
	if( cls.key_dest != key_menu )
	{
		Con_Printf( "'demos' is not valid from the console\n" );
		return;
	}

	// demos loop are not running
	if( cls.olddemonum == -1 )
		return;

	cls.demonum = cls.olddemonum;

	// run demos loop in background mode
	if( !SV_Active() && !cls.demoplayback )
		CL_NextDemo ();
}


/*
====================
CL_Stop_f

stop any client activity
====================
*/
void CL_Stop_f( void )
{
	// stop all
	CL_StopRecord();
	CL_StopPlayback();
	SCR_StopCinematic();

	// stop background track that was runned from the console
	if( !SV_Active( ))
	{
		S_StopBackgroundTrack();
	}
}