/*
Copyright (C) 2003-2006 Andrey Nazarov

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 2
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.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

//
// mvd_record.c - local multiview demo recorder
//

#include "server.h"
#include "mvd.h"

cvar_t	*mvd_running;
cvar_t	*mvd_nodelta;
cvar_t	*mvd_wait_clients;
cvar_t	*mvd_cache_megs;
cvar_t	*mvd_cache_count;
cvar_t	*mvd_max_size;
cvar_t	*mvd_max_duration;
cvar_t	*mvd_shownet;
cvar_t	*mvd_debug;
cvar_t	*mvd_noblend;
cvar_t	*mvd_nogun;
cvar_t	*mvd_motd;
cvar_t	*mvd_custom_fov;
cvar_t	*mvd_nextserver;
cvar_t	*mvd_timeout;
cvar_t	*mvd_admin_password;
cvar_t	*mvd_flood_msgs;
cvar_t	*mvd_flood_persecond;
cvar_t	*mvd_flood_waitdelay;
cvar_t	*mvd_autoscores;
cvar_t	*mvd_safecmd;

#define DCS_BYTES	260
#define DCS_DWORDS	(DCS_BYTES/4)

#if( DCS_BYTES != MAX_CONFIGSTRINGS/8 )
#error Invalid DCS_BYTES
#endif

typedef struct {
	qboolean	recording;

	qboolean	paused;
	byte		dirty_configstrings[DCS_BYTES];

	fileHandle_t demofile;
	mvdCacheEntry_t	*cache;

	// multicast and unicast datagrams acclumulated last frame
	sizebuf_t	multicast;
	byte		multicast_buf[MAX_MSGLEN];

	entity_state_t *baselines[SV_BASELINES_CHUNKS];

	deltaFrame_t	frames[MVD_REC_NUM_FRAMES];
	int				frameNum;
} mvdRecorder_t;

static mvdRecorder_t rec;


list_t rec_cache;
static int	rec_sequence;


/*
=============
MVD_RecWrite
=============
*/
static void MVD_RecWrite( void ) {
	int length;

	if( !msg_write.cursize ) {
		return;
	}

	length = LittleLong( msg_write.cursize );
	FS_Write( &length, 4, rec.demofile );
	FS_Write( msg_write.data, msg_write.cursize, rec.demofile );

	SZ_Clear( &msg_write );
}

/*
==================
MVD_PlayerIsActive

Can't just use client->state == cs_spawned
I want to capture bots too!
FIXME: some better way to do this?
Called on game servers only.
==================
*/
qboolean MVD_PlayerIsActive( int clientNum ) {
	edict_t *ent;

	if( sv.state != ss_game ) {
		Com_Error( ERR_DROP, "MVD_PlayerIsActive: called on non-game server" );
	}
	
	ent = EDICT_NUM( clientNum + 1 );
	if( !ent->inuse ) {
		return qfalse;
	}

	if( !ent->client ) {
		return qfalse;
	}

	// HACK: make sure player_state_t is valid
	if( !ent->client->ps.fov ) {
		return qfalse;
	}

	// HACK: if pm_type == PM_FREEZE, assume intermission is running
	// if PMF_NO_PREDICTION is set, they are following someone!
	if( ent->client->ps.pmove.pm_type == PM_FREEZE &&
		!( ent->client->ps.pmove.pm_flags & PMF_NO_PREDICTION ) )
	{
		return qtrue;
	}

	// if set to invisible, skip
	if( ent->svflags & SVF_NOCLIENT ) {
		return qfalse;
	}

	if( ent->s.modelindex || ent->s.effects || ent->s.sound || ent->s.event ) {
		return qtrue;
	}

	return qfalse;

}

/*
==================
MVD_RecFlush

Flushes accumulated multicast datagram,
dumps entire message to disk and
clears the write buffer.
==================
*/
static void MVD_RecFlush( void ) {
	MSG_WriteData( rec.multicast.data, rec.multicast.cursize );
	SZ_Clear( &rec.multicast );
	MVD_RecWrite();
}

/*
==================
MVD_RecBeginFrame
==================
*/
void MVD_RecBeginFrame( void ) {
	int i, j;
	int index, length;

	if( !rec.recording ) {
		return;
	}

	if( sv.state != ss_game ) {
		rec.paused = qfalse;
		return;
	}

	if( mvd_wait_clients->integer == 1 ) {
		for( i = 0; i < sv_maxclients->integer; i++ ) {
			if( MVD_PlayerIsActive( i ) ) {
				break;
			}
		}
	} else if( mvd_wait_clients->integer > 1 ) {
		for( i = 0; i < sv_maxclients->integer; i++ ) {
			if( svs.clientpool[i].state == cs_spawned ) {
				break;
			}
		}
	} else {
		i = 0;
	}

	if( i == sv_maxclients->integer ) {
		if( rec.paused ) {
			return;
		}
		Com_Printf( "MVD recording paused - no active clients.\n" );
		for( i = 0; i < DCS_DWORDS; i++ ) {
			(( uint32 * )rec.dirty_configstrings)[i] = 0;
		}
		rec.paused = qtrue;
		return;
	}

	if( !rec.paused ) {
		return;
	}
	for( i = 0; i < DCS_DWORDS; i++ ) {
		if( (( uint32 * )rec.dirty_configstrings)[i] == 0 ) {
			continue;
		}
		index = i << 5;
		for( j = 0; j < 32; j++, index++ ) {
			if( !Q_IsBitSet( rec.dirty_configstrings, index ) ) {
				continue;
			}
			SZ_WriteByte( &rec.multicast, svc_configstring );
			SZ_WriteShort( &rec.multicast, index );
			length = strlen( sv.configstrings[index] );
			if( length > MAX_QPATH ) {
				length = MAX_QPATH;
			}
			SZ_Write( &rec.multicast, sv.configstrings[index], length );
			SZ_WriteByte( &rec.multicast, 0 );
		}
		
	}

	Com_Printf( "MVD recording unpaused, flushed %d bytes.\n", rec.multicast.cursize );
	// will be subsequently written to disk in MVD_RecEndFrame
		
	rec.paused = qfalse;
	
}

/*
==================
MVD_RecEndFrame

Save everything in the world, including all player states,
delta compress the data.
==================
*/
void MVD_RecEndFrame( void ) {
	edict_t *ent;
	entity_state_t *es;
	playerStateEx_t *ps;
	deltaFrame_t *frame, *oldframe;
	int i, oldFrameNum, delta;
	byte portalbytes[MAX_MAP_AREAS/8];
	int numbytes;
	msgPsFlags_t flags;
	//mvdPlayer_t *player;

	if( !rec.recording ) {
		return;
	}

	if( rec.paused ) {
		return; // don't write any frames on empty server
	}

	if( mvd_nodelta->integer || rec.frameNum < 2 ) {
		oldFrameNum = -1;
		delta = 255;
		oldframe = NULL;
	} else {
		oldFrameNum = rec.frameNum - 1;
		delta = 1;
		oldframe = &rec.frames[oldFrameNum & MVD_REC_FRAMES_MASK];
	}

	frame = &rec.frames[rec.frameNum & MVD_REC_FRAMES_MASK];

	MSG_WriteByte( svc_frame );
	MSG_WriteLong( rec.frameNum );		// frame num
	MSG_WriteByte( delta );		// delta frame num

	numbytes = CM_WritePortalBits( &sv.cm, portalbytes );
	MSG_WriteByte( numbytes );				// num of areabytes
	MSG_WriteData( portalbytes, numbytes );
	
	SV_DeltaBeginFrame( frame );

	for( i = 1; i < ge->num_edicts; i++ ) {
		ent = EDICT_NUM( i );

		if( ent->inuse && !( ent->svflags & SVF_NOCLIENT ) &&
			( ent->s.modelindex || ent->s.effects || ent->s.sound || ent->s.event ) )
		{
			es = SV_DeltaAllocEntity();
			*es = ent->s;
			es->number = i;
		}
	}

	/*if( sv.state == ss_broadcast ) {
		for( i = 0; i < mvd.maxPlayers; i++ ) {
			player = &mvd.players[i];
			if( player->packetNum == mvd.activePacketNum ) {
				ps = SV_DeltaAllocPlayer();
				*ps = player->ps;
			}
		}
	} else */{
		for( i = 0 ; i < sv_maxclients->integer; i++ ) {
			if( MVD_PlayerIsActive( i ) ) {
				ps = SV_DeltaAllocPlayer();

				ent = EDICT_NUM( i + 1 );
				ps->ps = ent->client->ps;
				ps->number = i;
                ps->clientNum = i;
			}
		}
	}

	flags = MSG_PS_IGNORE_PREDICTION|MSG_PS_IGNORE_DELTAANGLES;
	if( mvd_noblend->integer ) {
		flags |= MSG_PS_IGNORE_BLEND;
	}
	if( mvd_nogun->integer ) {
		flags |= MSG_PS_IGNORE_GUNINDEX|MSG_PS_IGNORE_GUNFRAMES;
	}
	SV_EmitPacketPlayers( oldframe, frame, flags );
	SV_EmitPacketEntities( oldframe, frame, CLIENTNUM_ANY, rec.baselines );

	rec.frameNum++;

	MVD_RecFlush();

	if( !rec.cache ) {
		return;
	}

	numbytes = FS_RawTell( rec.demofile );
	rec.cache->size = numbytes;
	rec.cache->numFrames = rec.frameNum;

	if( mvd_max_size->value && numbytes > mvd_max_size->value * 1000 ) {
		Com_Printf( "Stopping MVD autorecord, maximum size reached\n" );
		MVD_RecStop();
		return;
	}

	if( mvd_max_duration->value && rec.frameNum > mvd_max_duration->value * 600 ) {
		Com_Printf( "Stopping MVD autorecord, maximum duration reached\n" );
		MVD_RecStop();
		return;
	}

}

/*
==============
MVD_RecMulticast
==============
*/
void MVD_RecMulticast( const vec3_t origin, multicast_t to ) {
	sizebuf_t *buf;
	int size;

	/* ignore milticasts if paused */
	if( !rec.recording || rec.paused ) {
		return;
	}

	size = ( to << 12 ) | ( msg_write.cursize & 0xFFF );
	
	buf = &rec.multicast;
	SZ_WriteByte( buf, svc_multicast );
	SZ_WriteShort( buf, size );

	/* write exact position for PVS/PHS checks */
	if( to != MULTICAST_ALL && to != MULTICAST_ALL_R ) {
		SZ_WriteExactPos( buf, origin );
	}
	
	SZ_Write( buf, msg_write.data, msg_write.cursize );
}

/*
==============
MVD_RecUnicast

NOTE: clientNum should be already ORed with reliable bit
==============
*/
void MVD_RecUnicast( int clientNum ) {
	sizebuf_t *buf;

	/* FIXME: ignore unicasts if paused */
	if( !rec.recording || rec.paused ) {
		return;
	}

    if( !MVD_PlayerIsActive( clientNum & 0x7F ) ) {
        return;
    }

	buf = &rec.multicast;
	SZ_WriteByte( buf, svc_unicast );
	SZ_WriteByte( buf, clientNum );
	SZ_WriteShort( buf, msg_write.cursize );
	SZ_Write( buf, msg_write.data, msg_write.cursize );
	
}

/*
==============
MVD_RecConfigstring
==============
*/
void MVD_RecConfigstring( int index, const char *string ) {
	sizebuf_t *buf;

	if( !rec.recording ) {
		return;
	}
	if( rec.paused ) {
		Q_SetBit( rec.dirty_configstrings, index );
		return;
	}
	buf = &rec.multicast;
	SZ_WriteByte( buf, svc_configstring );
	SZ_WriteShort( buf, index );
	SZ_WriteString( buf, string );
}

/*
==============
MVD_RecStop

Stops server MVD recording.
==============
*/
void MVD_RecStop( void ) {
	int i, length;

	if( !rec.recording ) {
		return;
	}
    
    // free baselines
    for( i = 0; i < SV_BASELINES_CHUNKS; i++ ) {
        if( rec.baselines[i] ) {
            Z_Free( rec.baselines[i] );
        }
    }

	// FIXME: flush accumulated datagram
	MVD_RecFlush();

	// write demo EOF marker
	length = -1;
	FS_Write( &length, 4, rec.demofile );

	if( rec.cache ) {
		rec.cache->completed = qtrue;
		rec.cache->size = FS_RawTell( rec.demofile );
	}
	
	FS_FCloseFile( rec.demofile );

	memset( &rec, 0, sizeof( rec ) );

}

/*
==============
MVD_Record_f

Begins server MVD recording.
Every entity, every playerinfo and every message will be recorded.
==============
*/
static void MVD_Record_f( void ) {
	char buffer[MAX_QPATH];
	char *name;
	fileHandle_t demofile;
	char *string;
	int i, length;
    edict_t *ent;
    entity_state_t *base, **chunk;

	if( Cmd_Argc() < 2 ) {
		Com_Printf( "Usage: %s [/]<filename>\n", Cmd_Argv( 0 ) );
		return;
	}

	if( sv.state != ss_game ) {
		Com_Printf( "Must be running a game server to record.\n" );
		return;
	}

	if( !sv_mvd_enable->integer ) {
		Com_Printf( "MVD recording disabled on this server.\n" );
		return;
	}

	if( rec.recording ) {
		Com_Printf( "Already recording a local MVD.\n" );
		return;
	}

	//
	// open the demo file
	//
	name = Cmd_Argv( 1 );
	if( name[0] == '/' ) {
		Q_strncpyz( buffer, name + 1, sizeof( buffer ) );
	} else {
		Com_sprintf( buffer, sizeof( buffer ), "demos/%s", name );
		COM_DefaultExtension( buffer, ".mvd2", sizeof( buffer ) );
	}

	FS_FOpenFile( buffer, &demofile, FS_MODE_WRITE );
	if( !demofile ) {
		Com_EPrintf( "Couldn't open %s for writing\n", buffer );
		return;
	}

	rec.demofile = demofile;
	rec.recording = qtrue;
	rec.frameNum = 1;
	
	// setup a buffer for accumulating multicast datagrams
	SZ_Init( &rec.multicast, rec.multicast_buf, sizeof( rec.multicast_buf ) );

	//
	// write a single giant message with all the startup info
	//
	MSG_WriteByte( svc_serverdata );
	MSG_WriteLong( PROTOCOL_VERSION_MVD );
	MSG_WriteLong( svs.spawncount );
	MSG_WriteByte( ATR_DEMO );
	string = Cvar_VariableString( "gamedir" );
	MSG_WriteString( string );
	MSG_WriteShort( CLIENTNUM_NONE );
	MSG_WriteString( sv.configstrings[CS_NAME] );
	MSG_WriteShort( PROTOCOL_VERSION_MVD_MINOR );

	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
		string = sv.configstrings[i];
		if( string[0] ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			length = strlen( string );
			if( length > MAX_QPATH ) {
				length = MAX_QPATH;
			}
			MSG_WriteData( string, length );
			MSG_WriteByte( 0 );
		}
	}
    
	for( i = 1; i < MAX_EDICTS; i++ ) {
		ent = EDICT_NUM( i );

		if( !ent->s.modelindex && !ent->s.sound && !ent->s.effects ) {
			continue;
		}

		ent->s.number = i;

		chunk = &rec.baselines[i >> SV_BASELINES_SHIFT];
		if( *chunk == NULL ) {
			*chunk = SV_Malloc( sizeof( *base ) * SV_BASELINES_PER_CHUNK );
		}

		base = *chunk + ( i & SV_BASELINES_MASK );

		*base = ent->s;
        
		MSG_WriteByte( svc_spawnbaseline );
		MSG_WriteDeltaEntity( NULL, base, MSG_ES_FORCE );
	}

	MSG_WriteByte( svc_stufftext );
	MSG_WriteString( "precache\n" );

	MVD_RecWrite();

	if( !Q_stricmp( Cmd_Argv( 0 ), "mvdautorecord" ) ) {
		mvdCacheEntry_t *entry;
		int length, pathlength;
		const char *path = FS_GetFileFullPath( demofile );

		rec_sequence++;

		length = strlen( buffer ) + 1;
		pathlength = strlen( path ) + 1;
		entry = Z_Malloc( sizeof( *entry ) + length + pathlength );
		entry->filename = ( char * )( entry + 1 );
		entry->fullpath = entry->filename + length;
		strcpy( entry->filename, buffer );
		strcpy( entry->fullpath, path );
		entry->size = 0;
        entry->numFrames = 0;
        entry->completed = qfalse;
		entry->time = time( NULL );
		entry->sequence = rec_sequence;

		List_Append( &rec_cache, entry );
		rec.cache = entry;

		Com_Printf( "Autorecording local MVD to %s\n", buffer );
	} else {
		Com_Printf( "Recording local MVD to %s\n", buffer );
	}
}


/*
==============
MVD_Stop_f

Ends server MVD recording
==============
*/
static void MVD_Stop_f( void ) {
	if( !rec.recording ) {
		Com_Printf( "Not recording a local MVD.\n" );
		return;
	}

	Com_Printf( "Local MVD recording completed.\n" );
	MVD_RecStop();
}

/*
==============
MVD_ListCache_f
==============
*/
void MVD_ListCache( qboolean fromConsole ) {
	mvdCacheEntry_t *entry;
	listElem_t *elem;
	int min, sec;
	int size;

	elem = rec_cache.last;
	if( elem && !(( mvdCacheEntry_t * )elem)->completed && !fromConsole ) {
		elem = elem->prev;
	}
	if( !elem ) {
		Com_Printf( "MVD cache is empty.\n" );
		return;
	}

	Com_Printf( "+-----+-----------+----------+--------------------------------------------+\n" );
	Com_Printf( "| #   |   Size    | Duration | Filename                                   |\n" );
	Com_Printf( "+-----+-----------+----------+--------------------------------------------+\n" );
	for( ; elem; elem = elem->prev ) {
		entry = ( mvdCacheEntry_t * )elem;

		if( !fromConsole && !entry->completed ) {
			continue;
		}

		Com_Printf( "| %-3d | ", entry->sequence );

		size = entry->size;
		if( size < 1000 ) {
			Com_Printf( "%3d bytes ", size );
		} else if( size < 1000000 ) {
			Com_Printf( "%3d.%d KB  ", size / 1000, ( size / 100 ) % 10 );
		} else {
			Com_Printf( "%3d.%d MB  ", size / 1000000, ( size / 100000 ) % 10 );
		}
		sec = entry->numFrames / 10;
		min = sec / 60;
		sec %= 60;
		Com_Printf( "| %02d:%02d    | ", min, sec );
		if( fromConsole ) {
			Com_Printf( "%-40.40s", entry->filename );
		} else {
			Com_Printf( "%-40.40s", COM_SkipPath( entry->filename ) );
		}
		if( !entry->completed ) {
			Com_Printf( " * |\n" );
		} else {
			Com_Printf( "   |\n" );
		}
	}
	Com_Printf( "+-----+-----------+----------+--------------------------------------------+\n" );
}

static void MVD_ListCache_f( void ) {
	MVD_ListCache( qtrue );
}

/*
==============
MVD_ClearCache_f
==============
*/
static void MVD_ClearCache_f( void ) {
	mvdCacheEntry_t *entry;
	listElem_t *elem, *next;
	int count;

	if( !rec_cache.first ) {
		Com_Printf( "MVD cache is empty.\n" );
		return;
	}

	count = 0;
	for( elem = rec_cache.first; elem; elem = next ) {
		next = elem->next;
		entry = ( mvdCacheEntry_t * )elem;

		if( !entry->completed ) {
			Com_Printf( "Stopping MVD autorecord, file requested for removal\n" );
			MVD_RecStop();
		}

		if( remove( entry->fullpath ) ) {
			Com_EPrintf( "Couldn't remove %s\n", entry->filename );
		} else {
			Com_Printf( "Removed %s\n", entry->filename );
		}
		Z_Free( entry );
		count++;
	}

	Com_Printf( "%d files removed\n", count );

	List_Clear( &rec_cache );
	rec_sequence = 0;
}

/*
==============
MVD_FlushCache_f
==============
*/
static void MVD_FlushCache_f( void ) {
	mvdCacheEntry_t *entry;
	listElem_t *elem, *prev;
	int size, count;

	if( !rec_cache.last ) {
		Com_Printf( "MVD cache is empty.\n" );
		return;
	}

	size = count = 0;
	for( elem = rec_cache.last; elem; elem = elem->prev ) {
		entry = ( mvdCacheEntry_t * )elem;

		size += entry->size;
		count++;
		if( mvd_cache_megs->value && size > mvd_cache_megs->value * 1000000 ) {
			break;
		}
		if( mvd_cache_count->integer && count > mvd_cache_count->integer ) {
			break;
		}
	}

	count = 0;
	for( ; elem; elem = prev ) {
		prev = elem->prev;
		List_DeleteElem( elem );
		entry = ( mvdCacheEntry_t * )elem;

		if( !entry->completed ) {
			Com_Printf( "Stopping MVD autorecord, file requested for removal\n" );
			MVD_RecStop();
		}

		if( remove( entry->fullpath ) ) {
			Com_EPrintf( "Couldn't remove %s\n", entry->filename );
		} else {
			Com_Printf( "Removed %s\n", entry->filename );
		}
		Z_Free( entry );
		count++;
	}

	Com_Printf( "%d files removed\n", count );
}

/*
==============
MVD_Register
==============
*/
void MVD_Register( void ) {
	mvd_running = Cvar_Get( "mvd_running", "0", CVAR_ROM );
	mvd_nodelta = Cvar_Get( "mvd_nodelta", "0", 0 );
	mvd_wait_clients = Cvar_Get( "mvd_wait_clients", "1", 0 );
	mvd_cache_megs = Cvar_Get( "mvd_cache_megs", "16", 0 );
	mvd_cache_count = Cvar_Get( "mvd_cache_count", "0", 0 );
	mvd_max_size = Cvar_Get( "mvd_max_size", "0", 0 );
	mvd_max_duration = Cvar_Get( "mvd_max_duration", "0", 0 );
	mvd_shownet = Cvar_Get( "mvd_shownet", "0", 0 );
	mvd_debug = Cvar_Get( "mvd_debug", "0", 0 );
	mvd_buffer_size = Cvar_Get( "mvd_buffer_size", "16", CVAR_LATCH );
	mvd_pause = Cvar_Get( "mvd_pause", "0", 0 );
	mvd_noblend = Cvar_Get( "mvd_noblend", "0", CVAR_LATCH );
	mvd_nogun = Cvar_Get( "mvd_nogun", "1", CVAR_LATCH );
	mvd_motd = Cvar_Get( "mvd_motd", "Hello ${sv_client}!\\n"
            "This is an experimental MVD server!", 0 );
	mvd_custom_fov = Cvar_Get( "mvd_custom_fov", "1", 0 );
	mvd_nextserver = Cvar_Get( "mvd_nextserver", "1", 0 );
	mvd_timeout = Cvar_Get( "mvd_timeout", "120", 0 );
	mvd_admin_password = Cvar_Get( "mvd_admin_password", "", CVAR_PRIVATE );
	mvd_flood_msgs = Cvar_Get( "flood_msgs", "4", 0 );
	mvd_flood_persecond = Cvar_Get( "flood_persecond", "4", 0 ); // FIXME: rename this
	mvd_flood_waitdelay = Cvar_Get( "flood_waitdelay", "10", 0 );
	mvd_autoscores = Cvar_Get( "mvd_autoscores", "", 0 );
	mvd_safecmd = Cvar_Get( "mvd_safecmd", "", 0 );

    /* register some client variables q2admin might expect to be set */
    Cvar_Get( "cl_yawspeed", "140", CVAR_USER_CREATED );
    Cvar_Get( "cl_pitchspeed", "150", CVAR_USER_CREATED );
    Cvar_Get( "cl_anglespeedkey", "1.5", CVAR_USER_CREATED );
    Cvar_Get( "cl_maxfps", "30", CVAR_USER_CREATED );

	Cmd_AddCommandEx( "mvdrecord", MVD_Record_f, MVD_Play_g );
	Cmd_AddCommandEx( "mvdautorecord", MVD_Record_f, MVD_Play_g );
	Cmd_AddCommand( "mvdstop", MVD_Stop_f );
	Cmd_AddCommandEx( "mvdstreamrecord", MVD_StreamedRecord_f, MVD_Play_g );
	Cmd_AddCommand( "mvdstreamstop", MVD_StreamedStop_f );
	Cmd_AddCommand( "mvdlist", MVD_ListCache_f );
	Cmd_AddCommand( "mvdclear", MVD_ClearCache_f );
	Cmd_AddCommand( "mvdflush", MVD_FlushCache_f );
	Cmd_AddCommandEx( "mvdplay", MVD_Play_f, MVD_Play_g );
	Cmd_AddCommand( "mvdconnect", MVD_Connect_f );
	Cmd_AddCommand( "mvdisconnect", MVD_Disconnect_f );
	Cmd_AddCommand( "mvdjump", MVD_Jump_f );
	Cmd_AddCommand( "mvdcmd", MVD_ForwardToServer_f );

#ifndef DEDICATED_ONLY
	if( !dedicated->integer ) {
		Cmd_AddCommand( "+mvdback", MVD_BackwardsDown_f );
		Cmd_AddCommand( "-mvdback", MVD_BackwardsUp_f );
	}
#endif

	SZ_Init( &mvd_buffer.text, mvd_buffer_text, sizeof( mvd_buffer_text ) );
	mvd_buffer.exec = MVD_ExecuteString;

}



