summaryrefslogtreecommitdiff
path: root/source/mvd_client.c
diff options
context:
space:
mode:
Diffstat (limited to 'source/mvd_client.c')
-rw-r--r--source/mvd_client.c1031
1 files changed, 1031 insertions, 0 deletions
diff --git a/source/mvd_client.c b/source/mvd_client.c
new file mode 100644
index 0000000..aa52510
--- /dev/null
+++ b/source/mvd_client.c
@@ -0,0 +1,1031 @@
+/*
+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_client.c
+//
+
+#include "sv_local.h"
+#include "mvd_local.h"
+#include <setjmp.h>
+
+list_t mvd_channels;
+list_t mvd_ready;
+mvd_t mvd_waitingRoom;
+
+jmp_buf mvd_jmpbuf;
+
+cvar_t *mvd_pause;
+cvar_t *mvd_running;
+cvar_t *mvd_shownet;
+cvar_t *mvd_debug;
+cvar_t *mvd_nextserver;
+cvar_t *mvd_timeout;
+cvar_t *mvd_autoscores;
+cvar_t *mvd_safecmd;
+cvar_t *mvd_wait_enter;
+cvar_t *mvd_wait_leave;
+
+// ====================================================================
+
+
+void MVD_Disconnect( mvd_t *mvd ) {
+ if( mvd->demorecording ) {
+ MVD_StreamedStop_f();
+ }
+ if( mvd->stream.state ) {
+ NET_Close( &mvd->stream );
+ }
+ if( mvd->demofile ) {
+ FS_FCloseFile( mvd->demofile );
+ mvd->demofile = 0;
+ }
+
+ mvd->state = MVD_DISCONNECTED;
+}
+
+void MVD_Free( mvd_t *mvd ) {
+ int i;
+
+ for( i = 0; i < SV_BASELINES_CHUNKS; i++ ) {
+ if( mvd->baselines[i] ) {
+ Z_Free( mvd->baselines[i] );
+ }
+ }
+
+#if USE_ZLIB
+ if( mvd->z.state ) {
+ inflateEnd( &mvd->z );
+ }
+ if( mvd->zbuf.data ) {
+ Z_Free( mvd->zbuf.data );
+ }
+#endif
+
+ List_Remove( &mvd->ready );
+ List_Remove( &mvd->entry );
+ Z_Free( mvd );
+}
+
+
+void MVD_Destroy( mvd_t *mvd, const char *fmt, ... ) {
+ va_list argptr;
+ char text[MAXPRINTMSG];
+ udpClient_t *u, *unext;
+ tcpClient_t *t;
+ uint16 length;
+
+ va_start( argptr, fmt );
+ Q_vsnprintf( text, sizeof( text ), fmt, argptr );
+ va_end( argptr );
+
+ Com_Printf( S_COLOR_YELLOW "%s\n", text );
+
+ // cause UDP clients to reconnect
+ LIST_FOR_EACH_SAFE( udpClient_t, u, unext, &mvd->udpClients, entry ) {
+ MVD_SwitchChannel( u, &mvd_waitingRoom );
+ }
+
+ // send EOF to TCP clients and kick them
+ length = 0;
+ LIST_FOR_EACH( tcpClient_t, t, &mvd->tcpClients, mvdEntry ) {
+ SV_HttpWrite( t, &length, 2 );
+ SV_HttpFinish( t );
+ SV_HttpDrop( t, "channel destroyed" );
+ NET_Run( &t->stream );
+ }
+
+ MVD_Disconnect( mvd );
+ MVD_ClearState( mvd );
+ MVD_Free( mvd );
+
+ longjmp( mvd_jmpbuf, -1 );
+}
+
+void MVD_Drop( mvd_t *mvd, const char *fmt, ... ) {
+ va_list argptr;
+ char text[MAXPRINTMSG];
+
+ va_start( argptr, fmt );
+ Q_vsnprintf( text, sizeof( text ), fmt, argptr );
+ va_end( argptr );
+
+ Com_Printf( S_COLOR_YELLOW "%s\n", text );
+
+ if( mvd->state < MVD_WAITING ) {
+ MVD_Disconnect( mvd );
+ MVD_ClearState( mvd );
+ MVD_Free( mvd );
+ } else {
+ MVD_Disconnect( mvd );
+ }
+
+ longjmp( mvd_jmpbuf, -1 );
+}
+
+void MVD_DPrintf( const char *fmt, ... ) {
+ va_list argptr;
+ char text[MAXPRINTMSG];
+
+ if( !mvd_debug->integer ) {
+ return;
+ }
+
+ va_start( argptr, fmt );
+ Q_vsnprintf( text, sizeof( text ), fmt, argptr );
+ va_end( argptr );
+
+ Com_Printf( S_COLOR_BLUE "%s", text );
+}
+
+void MVD_HttpPrintf( mvd_t *mvd, const char *fmt, ... ) {
+ char buffer[MAX_STRING_CHARS];
+ va_list argptr;
+ int length;
+
+ va_start( argptr, fmt );
+ length = Q_vsnprintf( buffer, sizeof( buffer ), fmt, argptr );
+ va_end( argptr );
+
+ if( !FIFO_Write( &mvd->stream.send, buffer, length ) ) {
+ MVD_Drop( mvd, "overflowed" );
+ }
+}
+
+void MVD_ClearState( mvd_t *mvd ) {
+ mvdConfigstring_t *cs, *nextcs;
+ mvdPlayer_t *player;
+ int i;
+
+ for( i = 0; i < MAX_CLIENTS; i++ ) {
+ player = &mvd->players[i];
+ if( player->layout ) {
+ Z_Free( player->layout );
+ player->layout = NULL;
+ }
+ for( cs = player->configstrings; cs; cs = nextcs ) {
+ nextcs = cs->next;
+ Z_Free( cs );
+ }
+ player->configstrings = NULL;
+ }
+
+ for( i = 0; i < SV_BASELINES_CHUNKS; i++ ) {
+ if( mvd->baselines[i] ) {
+ memset( mvd->baselines[i], 0, sizeof( entityStateEx_t ) *
+ SV_BASELINES_PER_CHUNK );
+ }
+ }
+
+ CM_FreeMap( &mvd->cm );
+
+ memset( mvd->configstrings, 0, sizeof( mvd->configstrings ) );
+
+ mvd->framenum = 0;
+ memset( mvd->frames, 0, sizeof( mvd->frames ) );
+}
+
+static void MVD_EmitGamestate( mvd_t *mvd ) {
+ char *string;
+ int i, j;
+ entityStateEx_t *base, *es;
+ player_state_t *ps;
+ int length;
+ uint16 *patch;
+ mvdFrame_t *frame;
+ int flags;
+
+ patch = SZ_GetSpace( &msg_write, 2 );
+
+ // send the serverdata
+ MSG_WriteByte( mvd_serverdata );
+ MSG_WriteLong( PROTOCOL_VERSION_MVD );
+ MSG_WriteShort( PROTOCOL_VERSION_MVD_MINOR );
+ MSG_WriteLong( mvd->servercount );
+ MSG_WriteString( mvd->gamedir );
+ MSG_WriteShort( mvd->clientNum );
+
+ // send configstrings
+ for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
+ string = mvd->configstrings[i];
+ if( !string[0] ) {
+ continue;
+ }
+ length = strlen( string );
+ if( length > MAX_QPATH ) {
+ length = MAX_QPATH;
+ }
+
+ MSG_WriteShort( i );
+ MSG_WriteData( string, length );
+ MSG_WriteByte( 0 );
+ }
+ MSG_WriteShort( MAX_CONFIGSTRINGS );
+
+ // send baselines
+ for( i = 0; i < SV_BASELINES_CHUNKS; i++ ) {
+ base = mvd->baselines[i];
+ if( !base ) {
+ continue;
+ }
+ for( j = 0; j < SV_BASELINES_PER_CHUNK; j++ ) {
+ if( base->s.number ) {
+ MSG_WriteDeltaEntity( NULL, &base->s, MSG_ES_FORCE );
+ }
+ base++;
+ }
+ }
+ MSG_WriteShort( 0 );
+
+ // send uncompressed frame
+ frame = &mvd->frames[mvd->framenum & MVD_UPDATE_MASK];
+ MSG_WriteByte( mvd_frame_nodelta );
+ MSG_WriteLong( frame->serverFrame );
+ MSG_WriteByte( 0 );
+
+ flags = MSG_PS_FORCE;
+ if( sv_mvd_noblend->integer ) {
+ flags |= MSG_PS_IGNORE_BLEND;
+ }
+ if( sv_mvd_nogun->integer ) {
+ flags |= MSG_PS_IGNORE_GUNINDEX|MSG_PS_IGNORE_GUNFRAMES;
+ }
+ for( i = 0; i < frame->numPlayers; i++ ) {
+ j = ( frame->firstPlayer + i ) & MVD_PLAYERS_MASK;
+ ps = &mvd->playerStates[j];
+
+ MSG_WriteDeltaPlayerstate_Packet( NULL, ps, flags );
+ }
+ MSG_WriteByte( CLIENTNUM_NONE );
+
+ for( i = 0; i < frame->numEntities; i++ ) {
+ j = ( frame->firstEntity + i ) & MVD_ENTITIES_MASK;
+ es = &mvd->entityStates[j];
+
+ flags = MSG_ES_FORCE|MSG_ES_NEWENTITY;
+ if( es->s.number <= mvd->maxclients ) {
+ flags |= MSG_ES_FIRSTPERSON;
+ }
+ base = mvd->baselines[es->s.number >> SV_BASELINES_SHIFT];
+ if( base ) {
+ base += es->s.number & SV_BASELINES_MASK;
+ }
+ MSG_WriteDeltaEntity( &base->s, &es->s, flags );
+ }
+ MSG_WriteShort( 0 );
+
+ *patch = LittleShort( msg_write.cursize - 2 );
+}
+
+void MVD_SendGamestate( tcpClient_t *client ) {
+ MVD_EmitGamestate( client->mvd );
+
+ Com_DPrintf( "Sent gamestate to MVD client %s\n",
+ NET_AdrToString( &client->stream.address ) );
+ client->state = cs_spawned;
+
+ SV_HttpWrite( client, msg_write.data, msg_write.cursize );
+ SZ_Clear( &msg_write );
+}
+
+void MVD_RejectStream( const char *error, const char *reason ) {
+ char buffer[MAX_STRING_CHARS];
+ mvd_t *mvd;
+ int number, count;
+
+ SV_HttpPrintf(
+ "HTTP/1.0 %s\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache\r\n"
+ "\r\n", error );
+
+ SV_HttpHeader( error );
+ SV_HttpPrintf(
+ "<h1>%s</h1><p>%s</p><table border=\"1\"><tr>"
+ "<th>Index</th><th>Name</th><th>Map</th><th>Clients</th></tr>",
+ error, reason );
+
+ number = 0;
+ LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, entry ) {
+ SV_HttpPrintf(
+ "<tr><td><a href=\"http://%s/mvdstream/%d\">%d</a></td>",
+ http_host, number, number );
+
+ Q_EscapeMarkup( buffer, mvd->name, sizeof( buffer ) );
+ SV_HttpPrintf( "<td>%s</td>", buffer );
+
+ Q_EscapeMarkup( buffer, mvd->mapname, sizeof( buffer ) );
+ count = List_Count( &mvd->udpClients );
+ SV_HttpPrintf( "<td>%s</td><td>%d</td></tr>", buffer, count );
+
+ number++;
+ }
+ SV_HttpPrintf( "</table>" );
+
+ SV_HttpFooter();
+
+ SV_HttpDrop( http_client, error );
+}
+
+void MVD_GetStream( const char *uri ) {
+ mvd_t *mvd;
+ int index;
+ uint32 magic;
+
+ if( *uri == '/' ) {
+ uri++;
+ }
+
+ if( !*uri ) {
+ MVD_RejectStream( "300 Multiple Choices",
+ "There are several MVD channels available. "
+ "Please select an appropriate stream." );
+ return;
+ }
+
+ index = atoi( uri );
+
+ mvd = LIST_INDEX( mvd_t, index, &mvd_ready, ready );
+ if( !mvd ) {
+ MVD_RejectStream( "404 Not Found",
+ "Requested MVD stream was not found on this server." );
+ return;
+ }
+
+ SV_HttpPrintf( "HTTP/1.0 200 OK\r\n" );
+
+ if( http_client->method == HTTP_METHOD_HEAD ) {
+ SV_HttpPrintf( "\r\n" );
+ SV_HttpDrop( http_client, "200 OK " );
+ return;
+ }
+
+ List_Append( &mvd->tcpClients, &http_client->mvdEntry );
+ http_client->mvd = mvd;
+
+ SV_HttpPrintf(
+#if USE_ZLIB
+ "Content-Encoding: deflate\r\n"
+#endif
+ "Content-Type: application/octet-stream\r\n"
+ "Content-Disposition: attachment; filename=\"stream.mvd2\"\r\n"
+ "\r\n" );
+
+#if USE_ZLIB
+ deflateInit( &http_client->z, Z_DEFAULT_COMPRESSION );
+#endif
+
+ magic = MVD_MAGIC;
+ SV_HttpWrite( http_client, &magic, 4 );
+
+ MVD_SendGamestate( http_client );
+}
+
+
+void MVD_ChangeLevel( mvd_t *mvd ) {
+ udpClient_t *u;
+
+ if( sv.state != ss_broadcast ) {
+ MVD_Spawn_f();
+ return;
+ }
+
+ // cause all UDP clients to reconnect
+ MSG_WriteByte( svc_stufftext );
+ MSG_WriteString( "changing; reconnect\n" );
+
+ LIST_FOR_EACH( udpClient_t, u, &mvd->udpClients, entry ) {
+ SV_ClientReset( u->cl );
+ SV_ClientAddMessage( u->cl, MSG_RELIABLE );
+ }
+
+ SV_SendAsyncPackets();
+
+ SZ_Clear( &msg_write );
+}
+
+#ifndef DEDICATED_ONLY
+/* called by the client code */
+qboolean MVD_GetDemoPercent( int *percent, int *bufferPercent ) {
+#if 0
+ int delta;
+
+ if( !mvd.demoplayback ) {
+ return qfalse;
+ }
+
+ delta = mvd.serverPacketNum - mvd.timelines[0].framenum;
+ *bufferPercent = 100 - delta * 100 / ( mvd.frameBackup - 1 );
+ *percent = mvd.demofilePercent;
+
+ return qtrue;
+#else
+ return qfalse;
+#endif
+}
+#endif
+
+static void MVD_ReadDemo( mvd_t *mvd ) {
+ byte *data;
+ int length, read, total = 0;
+
+ do {
+ data = FIFO_Reserve( &mvd->stream.recv, &length );
+ if( !length ) {
+ return;
+ }
+ read = FS_Read( data, length, mvd->demofile );
+ FIFO_Commit( &mvd->stream.recv, read );
+ total += read;
+ } while( read );
+
+ if( !total ) {
+ MVD_Drop( mvd, "End of MVD file reached" );
+ }
+}
+
+static qboolean MVD_ParseResponse( mvd_t *mvd ) {
+ char key[MAX_TOKEN_CHARS];
+ char *p, *token;
+ const char *line;
+ byte *b, *data;
+ int length;
+
+ while( 1 ) {
+ data = FIFO_Peek( &mvd->stream.recv, &length );
+ if( !length ) {
+ break;
+ }
+ if( ( b = memchr( data, '\n', length ) ) != NULL ) {
+ length = b - data + 1;
+ }
+ if( mvd->responseLength + length > MAX_NET_STRING - 1 ) {
+ MVD_Drop( mvd, "Response line exceeded maximum length" );
+ }
+
+ memcpy( mvd->response + mvd->responseLength, data, length );
+ mvd->responseLength += length;
+ mvd->response[mvd->responseLength] = 0;
+
+ FIFO_Decommit( &mvd->stream.recv, length );
+
+ if( !b ) {
+ continue;
+ }
+
+ line = mvd->response;
+ mvd->responseLength = 0;
+
+ if( !mvd->statusCode ) {
+ // parse version
+ token = COM_SimpleParse( &line );
+ if( !token[0] ) {
+ continue; // empty line?
+ }
+ if( strncmp( token, "HTTP/", 5 ) ) {
+ MVD_Drop( mvd, "Malformed HTTP version" );
+ }
+
+ // parse status code
+ token = COM_SimpleParse( &line );
+ mvd->statusCode = atoi( token );
+ if( !mvd->statusCode ) {
+ MVD_Drop( mvd, "Malformed HTTP status code" );
+ }
+
+ // parse reason phrase
+ if( line ) {
+ while( *line && *line <= ' ' ) {
+ line++;
+ }
+ Q_ClearStr( mvd->statusText, line, MAX_QPATH );
+ }
+ } else {
+ // parse header fields
+ token = COM_SimpleParse( &line );
+ if( !token[0] ) {
+ return qtrue; // end of header
+ }
+ strcpy( key, token );
+ p = strchr( key, ':' );
+ if( !p ) {
+ MVD_Drop( mvd, "Malformed HTTP header field" );
+ }
+ *p = 0;
+ Q_strlwr( key );
+
+ token = COM_SimpleParse( &line );
+ if( !strcmp( key, "content-type" ) ) {
+ } else if( !strcmp( key, "content-encoding" ) ) {
+#if USE_ZLIB
+ if( !Q_stricmp( token, "deflate" ) ||
+ !Q_stricmp( token, "gzip" ) ||
+ !Q_stricmp( token, "x-gzip" ) )
+ {
+ if( inflateInit2( &mvd->z, 47 ) != Z_OK ) {
+ MVD_Drop( mvd, "inflateInit2() failed: %s", mvd->z.msg );
+ }
+ mvd->zbuf.data = MVD_Malloc( MAX_MSGLEN * 2 );
+ mvd->zbuf.size = MAX_MSGLEN * 2;
+ } else
+#endif
+ {
+ MVD_Drop( mvd, "Unsupported content encoding: %s", token );
+ }
+ } else if( !strcmp( key, "content-length" ) ) {
+ mvd->contentLength = atoi( token );
+ } else if( !strcmp( key, "transfer-encoding" ) ) {
+ } else if( !strcmp( key, "location" ) ) {
+ }
+ }
+ }
+
+ return qfalse;
+}
+
+static inline float MVD_BufferPercent( mvd_t *mvd ) {
+ int usage = FIFO_Usage( &mvd->stream.recv );
+ int size = mvd->stream.recv.size;
+
+#ifdef USE_ZLIB
+ usage += FIFO_Usage( &mvd->zbuf );
+ size += mvd->zbuf.size;
+#endif
+
+ if( !size ) {
+ return 0;
+ }
+ return usage * 100.0f / size;
+}
+
+void MVD_Frame( void ) {
+ mvd_t *mvd, *next;
+ neterr_t ret;
+ float usage;
+
+ LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_channels, entry ) {
+ if( mvd->state <= MVD_DEAD || mvd->state >= MVD_DISCONNECTED ) {
+ continue;
+ }
+
+ if( setjmp( mvd_jmpbuf ) ) {
+ continue;
+ }
+
+ if( mvd->demoplayback ) {
+ MVD_ReadDemo( mvd );
+ if( mvd->state == MVD_PREPARING ) {
+ MVD_Parse( mvd );
+ }
+ continue;
+ }
+
+ // process network stream
+ ret = NET_Run( &mvd->stream );
+ switch( ret ) {
+ case NET_AGAIN:
+ continue;
+ case NET_ERROR:
+ MVD_Drop( mvd, "%s to %s", NET_ErrorString(),
+ NET_AdrToString( &mvd->stream.address ) );
+ case NET_CLOSED:
+ MVD_Drop( mvd, "Connection closed" );
+ case NET_OK:
+ break;
+ }
+
+ // run MVD state machine
+ switch( mvd->state ) {
+ case MVD_CONNECTING:
+ Com_Printf( "Connected, awaiting response...\n" );
+ mvd->state = MVD_CONNECTED;
+ // fall through
+ case MVD_CONNECTED:
+ if( !MVD_ParseResponse( mvd ) ) {
+ continue;
+ }
+ if( mvd->statusCode != 200 ) {
+ MVD_Drop( mvd, "HTTP request failed: %d %s",
+ mvd->statusCode, mvd->statusText );
+ }
+ Com_Printf( "Got response, awaiting gamestate...\n" );
+ mvd->state = MVD_CHECKING;
+ // fall through
+ case MVD_CHECKING:
+ case MVD_PREPARING:
+ MVD_Parse( mvd );
+ if( mvd->state > MVD_PREPARING ) {
+ // fall through
+ default:
+ usage = MVD_BufferPercent( mvd );
+ if( mvd->state == MVD_WAITING ) {
+ if( usage >= mvd_wait_leave->value ) {
+ Com_Printf( "Reading data...\n" );
+ mvd->state = MVD_READING;
+ }
+ } else {
+ if( mvd_wait_leave->value > mvd_wait_enter->value &&
+ usage < mvd_wait_enter->value )
+ {
+ Com_Printf( "Buffering data...\n" );
+ mvd->state = MVD_WAITING;
+ }
+ }
+ }
+ }
+ }
+}
+
+
+/*
+====================================================================
+
+OPERATOR COMMANDS
+
+====================================================================
+*/
+
+mvd_t *MVD_SetChannel( int arg ) {
+ char *s = Cmd_Argv( arg );
+ mvd_t *mvd;
+
+ if( !*s ) {
+ if( List_Count( &mvd_channels ) == 1 ) {
+ return LIST_FIRST( mvd_t, &mvd_channels, entry );
+ }
+ Com_Printf( "Please specify a channel\n" );
+ return NULL;
+ }
+
+ mvd = LIST_INDEX( mvd_t, atoi( s ), &mvd_channels, entry );
+ if( mvd ) {
+ return mvd;
+ }
+
+ Com_Printf( "No such channel: %s\n", s );
+ return NULL;
+}
+
+void MVD_Spawn_f( void ) {
+ SV_InitGame( qtrue );
+
+ /* set serverinfo variables */
+ Cvar_Set( "mapname", "nomap" );
+ Cvar_SetInteger( "sv_running", ss_broadcast );
+ Cvar_SetInteger( "sv_paused", 0 );
+ Cvar_SetInteger( "timedemo", 0 );
+
+ sv.spawncount = ( rand() | ( rand() << 16 ) ) ^ Sys_Realtime();
+ sv.spawncount &= 0x7FFFFFFF;
+
+ sv.state = ss_broadcast;
+}
+
+void MVD_ListChannels_f( void ) {
+ mvd_t *mvd;
+ int number, usage;
+
+ if( LIST_EMPTY( &mvd_channels ) ) {
+ Com_Printf( "No active channels.\n" );
+ return;
+ }
+
+ Com_Printf( "num name map state buf address \n"
+ "--- ---------------- -------- ----- --- --------------\n" );
+
+ number = 0;
+ LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) {
+ Com_Printf( "%3d %-16.16s %-8.8s", number,
+ mvd->name, mvd->mapname );
+ switch( mvd->state ) {
+ case MVD_DEAD:
+ Com_Printf( " DEAD " );
+ break;
+ case MVD_CONNECTING:
+ case MVD_CONNECTED:
+ Com_Printf( " CNCT " );
+ break;
+ case MVD_CHECKING:
+ case MVD_PREPARING:
+ Com_Printf( " PREP " );
+ break;
+ case MVD_WAITING:
+ Com_Printf( " WAIT " );
+ break;
+ case MVD_READING:
+ Com_Printf( " READ " );
+ break;
+ case MVD_DISCONNECTED:
+ Com_Printf( " DISC " );
+ break;
+ }
+ usage = MVD_BufferPercent( mvd );
+ Com_Printf( " %3d ", usage );
+ if( mvd->demoplayback ) {
+ Com_Printf( "%s", mvd->demopath );
+ } else {
+ Com_Printf( "%s", NET_AdrToString( &mvd->stream.address ) );
+ }
+ Com_Printf( "\n" );
+ number++;
+ }
+}
+
+void MVD_StreamedStop_f( void ) {
+ mvd_t *mvd;
+ uint16 msglen;
+
+ mvd = MVD_SetChannel( 1 );
+ if( !mvd ) {
+ Com_Printf( "Usage: %s [channel]\n", Cmd_Argv( 0 ) );
+ return;
+ }
+
+ if( !mvd->demorecording ) {
+ Com_Printf( "Not recording on channel %s.\n", mvd->name );
+ return;
+ }
+
+ msglen = 0;
+ FS_Write( &msglen, 2, mvd->demofile );
+ FS_FCloseFile( mvd->demofile );
+
+ mvd->demofile = 0;
+ mvd->demorecording = qfalse;
+
+ Com_Printf( "Stopped recording on channel %s.\n", mvd->name );
+}
+
+void MVD_StreamedRecord_f( void ) {
+ char buffer[MAX_QPATH];
+ char *name;
+ fileHandle_t f;
+ mvd_t *mvd;
+ uint32 magic;
+
+ if( Cmd_Argc() < 2 || ( mvd = MVD_SetChannel( 2 ) ) == NULL ) {
+ Com_Printf( "Usage: %s [/]<filename> [channel]\n", Cmd_Argv( 0 ) );
+ return;
+ }
+
+ if( mvd->demorecording ) {
+ Com_Printf( "Already recording on channel %s.\n", mvd->name );
+ return;
+ }
+
+ if( mvd->state < MVD_WAITING ) {
+ Com_Printf( "Channel %s is not ready for recording.\n", mvd->name );
+ 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, &f, FS_MODE_WRITE );
+ if( !f ) {
+ Com_EPrintf( "Couldn't open %s for writing\n", buffer );
+ return;
+ }
+
+ Com_Printf( "Recording on channel %s into %s\n", mvd->name, buffer );
+
+ mvd->demofile = f;
+ mvd->demorecording = qtrue;
+
+ MVD_EmitGamestate( mvd );
+
+ magic = MVD_MAGIC;
+ FS_Write( &magic, 4, f );
+ FS_Write( msg_write.data, msg_write.cursize, f );
+
+ SZ_Clear( &msg_write );
+}
+
+
+/*
+==============
+MVD_Connect_f
+
+[http://]host[:port][/resource]
+==============
+*/
+void MVD_Connect_f( void ) {
+ netadr_t adr;
+ netstream_t stream;
+ char buffer[MAX_STRING_CHARS];
+ char resource[MAX_STRING_CHARS];
+ char credentials[MAX_STRING_CHARS];
+ char *host, *p;
+ mvd_t *mvd;
+ uint16 port;
+
+ if ( Cmd_Argc() < 2 ) {
+ Com_Printf( "Usage: %s <[http://][user:pass@]server[:port][/resource]>", Cmd_Argv( 0 ) );
+ return;
+ }
+
+ Cmd_ArgvBuffer( 1, buffer, sizeof( buffer ) );
+
+ host = buffer;
+ if( !strncmp( host, "http://", 7 ) ) {
+ host += 7;
+ }
+ p = strchr( host, '@' );
+ if( p ) {
+ *p = 0;
+ strcpy( credentials, host );
+ host = p + 1;
+ } else {
+ credentials[0] = 0;
+ }
+ p = strchr( host, '/' );
+ if( p ) {
+ *p = 0;
+ strcpy( resource, p + 1 );
+ port = BigShort( 80 );
+ } else {
+ Com_sprintf( resource, sizeof( resource ),
+ "mvdstream/%s", Cmd_Argv( 2 ) );
+ port = BigShort( PORT_SERVER );
+ }
+ if( !NET_StringToAdr( host, &adr ) ) {
+ Com_Printf( "Bad server address: %s\n", host );
+ return;
+ }
+
+ if( !adr.port ) {
+ adr.port = port;
+ }
+
+ if( NET_Connect( &adr, &stream ) == NET_ERROR ) {
+ Com_Printf( "%s to %s\n", NET_ErrorString(),
+ NET_AdrToString( &adr ) );
+ return;
+ }
+
+ Z_TagReserve( sizeof( *mvd ) + MAX_MSGLEN * 2 + 256, TAG_MVD );
+
+ mvd = Z_ReservedAllocz( sizeof( *mvd ) );
+ strcpy( mvd->name, "unnamed" );
+ mvd->state = MVD_CONNECTING;
+ mvd->stream = stream;
+ mvd->stream.recv.data = Z_ReservedAlloc( MAX_MSGLEN * 2 );
+ mvd->stream.recv.size = MAX_MSGLEN * 2;
+ mvd->stream.send.data = Z_ReservedAlloc( 256 );
+ mvd->stream.send.size = 256;
+ List_Init( &mvd->udpClients );
+ List_Init( &mvd->tcpClients );
+ List_Init( &mvd->ready );
+ List_Append( &mvd_channels, &mvd->entry );
+
+ Com_Printf( "Connecting to %s...\n", NET_AdrToString( &adr ) );
+
+ MVD_HttpPrintf( mvd,
+ "GET /%s HTTP/1.0\r\n"
+ "Host: %s\r\n"
+ "User-Agent: " APPLICATION "/" VERSION "\r\n"
+#ifdef USE_ZLIB
+ "Accept-Encoding: gzip, deflate\r\n"
+#endif
+ "Accept: application/*\r\n",
+ resource, host );
+ if( credentials[0] ) {
+ Q_Encode64( buffer, credentials, sizeof( buffer ) );
+ MVD_HttpPrintf( mvd, "Authorization: Basic %s\r\n", buffer );
+ }
+ MVD_HttpPrintf( mvd, "\r\n" );
+}
+
+static void MVD_Disconnect_f( void ) {
+ mvd_t *mvd;
+
+ mvd = MVD_SetChannel( 1 );
+ if( !mvd ) {
+ Com_Printf( "Usage: %s [channel]\n", Cmd_Argv( 0 ) );
+ return;
+ }
+
+// MVD_Drop( mvd, "Disconnected from console" );
+}
+
+const char *MVD_Play_g( const char *partial, int state ) {
+ return Com_FileNameGeneratorByFilter( "demos", "*.mvd2;*.mvd2.gz",
+ partial, qfalse, state );
+}
+
+void MVD_Play_f( void ) {
+ char *name;
+ char buffer[MAX_QPATH];
+ fileHandle_t f;
+ int length;
+ uint32 magic = 0;
+ mvd_t *mvd;
+
+ if( Cmd_Argc() < 2 ) {
+ Com_Printf( "Usage: %s [/]<filename>\n", Cmd_Argv( 0 ) );
+ return;
+ }
+
+ 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, &f, FS_MODE_READ );
+ if( !f ) {
+ Com_Printf( "Couldn't open '%s'\n", buffer );
+ return;
+ }
+
+ FS_Read( &magic, 4, f );
+ if( magic != MVD_MAGIC ) {
+ Com_Printf( "'%s' is not a MVD2 file\n", buffer );
+ FS_FCloseFile( f );
+ return;
+ }
+
+ Z_TagReserve( sizeof( *mvd ) + MAX_MSGLEN * 2, TAG_MVD );
+
+ mvd = Z_ReservedAllocz( sizeof( *mvd ) );
+ mvd->state = MVD_PREPARING;
+ mvd->demoplayback = qtrue;
+ mvd->demofile = f;
+ mvd->stream.recv.data = Z_ReservedAlloc( MAX_MSGLEN * 2 );
+ mvd->stream.recv.size = MAX_MSGLEN * 2;
+ List_Init( &mvd->udpClients );
+ List_Init( &mvd->tcpClients );
+ List_Init( &mvd->ready );
+ List_Append( &mvd_channels, &mvd->entry );
+
+ if( dedicated->integer && !sv_nextserver->string[0] ) {
+ Cvar_Set( "nextserver", va( "mvdplay /%s", buffer ) );
+ }
+
+ length = FS_GetFileLengthNoCache( mvd->demofile );
+ mvd->demofileFrameOffset = FS_Tell( mvd->demofile );
+ mvd->demofileSize = length - mvd->demofileFrameOffset;
+ strcpy( mvd->demopath, buffer );
+}
+
+static const cmdreg_t c_mvd[] = {
+ { "mvdplay", MVD_Play_f, MVD_Play_g },
+ { "mvdconnect", MVD_Connect_f },
+ { "mvdisconnect", MVD_Disconnect_f },
+ { "mvdspawn", MVD_Spawn_f },
+ { "mvdchannels", MVD_ListChannels_f },
+
+ { NULL }
+};
+
+
+/*
+==============
+MVD_Register
+==============
+*/
+void MVD_Register( void ) {
+ mvd_shownet = Cvar_Get( "mvd_shownet", "0", 0 );
+ mvd_debug = Cvar_Get( "mvd_debug", "0", 0 );
+ mvd_pause = Cvar_Get( "mvd_pause", "0", 0 );
+ mvd_nextserver = Cvar_Get( "mvd_nextserver", "1", 0 );
+ mvd_timeout = Cvar_Get( "mvd_timeout", "120", 0 );
+ mvd_autoscores = Cvar_Get( "mvd_autoscores", "", 0 );
+ mvd_safecmd = Cvar_Get( "mvd_safecmd", "", 0 );
+ mvd_wait_enter = Cvar_Get( "mvd_wait_enter", "0.5", 0 );
+ mvd_wait_leave = Cvar_Get( "mvd_wait_leave", "2", 0 );
+// mvd_drop_enter = Cvar_Get( "mvd_drop_enter", "95", 0 );
+// mvd_drop_leave = Cvar_Get( "mvd_drop_leave", "90", 0 );
+
+ Cmd_Register( c_mvd );
+
+ List_Init( &mvd_channels );
+ List_Init( &mvd_ready );
+}
+