diff options
Diffstat (limited to 'source/sv_mvd.c')
-rw-r--r-- | source/sv_mvd.c | 1296 |
1 files changed, 974 insertions, 322 deletions
diff --git a/source/sv_mvd.c b/source/sv_mvd.c index 28f6a68..f4d5d42 100644 --- a/source/sv_mvd.c +++ b/source/sv_mvd.c @@ -1,5 +1,5 @@ /* -Copyright (C) 2003-2006 Andrey Nazarov +Copyright (C) 2003-2008 Andrey Nazarov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -19,20 +19,46 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // -// sv_mvd.c - MVD server and local recorder +// sv_mvd.c - GTV server and local MVD recorder // #include "sv_local.h" +#include "gtv.h" -cvar_t *sv_mvd_enable; -cvar_t *sv_mvd_auth; -cvar_t *sv_mvd_noblend; -cvar_t *sv_mvd_nogun; -cvar_t *sv_mvd_nomsgs; +#define FOR_EACH_GTV( client ) \ + LIST_FOR_EACH( gtv_client_t, client, >v_client_list, entry ) + +#define FOR_EACH_ACTIVE_GTV( client ) \ + LIST_FOR_EACH( gtv_client_t, client, >v_active_list, active ) + +typedef struct { + list_t entry; + list_t active; + clstate_t state; + netstream_t stream; +#if USE_ZLIB + z_stream z; +#endif + unsigned msglen; + unsigned lastmessage; + + unsigned flags; + unsigned maxbuf; + unsigned bufcount; + + byte buffer[256]; + byte *data; + + char name[MAX_CLIENT_NAME]; + char version[MAX_QPATH]; +} gtv_client_t; typedef struct { + qboolean active; client_t *dummy; unsigned layout_time; + unsigned clients_active; + unsigned players_active; // reliable data, may not be discarded sizebuf_t message; @@ -48,29 +74,47 @@ typedef struct { fileHandle_t recording; int numlevels; // stop after that many levels int numframes; // stop after that many frames + + // TCP client pool + gtv_client_t *clients; // [sv_mvd_maxclients] } mvd_server_t; static mvd_server_t mvd; -// TCP client list -static LIST_DECL( mvd_client_list ); - -static cvar_t *sv_mvd_max_size; -static cvar_t *sv_mvd_max_duration; -static cvar_t *sv_mvd_max_levels; -static cvar_t *sv_mvd_begincmd; -static cvar_t *sv_mvd_scorecmd; -static cvar_t *sv_mvd_autorecord; +// TCP client lists +static LIST_DECL( gtv_client_list ); +static LIST_DECL( gtv_active_list ); + +static LIST_DECL( gtv_host_list ); + +static cvar_t *sv_mvd_enable; +static cvar_t *sv_mvd_maxclients; +static cvar_t *sv_mvd_auth; +static cvar_t *sv_mvd_noblend; +static cvar_t *sv_mvd_nogun; +static cvar_t *sv_mvd_nomsgs; +static cvar_t *sv_mvd_maxsize; +static cvar_t *sv_mvd_maxtime; +static cvar_t *sv_mvd_maxmaps; +static cvar_t *sv_mvd_begincmd; +static cvar_t *sv_mvd_scorecmd; +static cvar_t *sv_mvd_autorecord; +static cvar_t *sv_mvd_capture_flags; + +static qboolean mvd_enable( void ); +static void mvd_disable( void ); +static void mvd_drop( gtv_serverop_t op ); + +static void write_stream( gtv_client_t *client, void *data, size_t len ); +static void write_message( gtv_client_t *client, gtv_serverop_t op ); #if USE_ZLIB -static cvar_t *sv_mvd_encoding; +static void flush_stream( gtv_client_t *client, int flush ); #endif -static cvar_t *sv_mvd_capture_flags; - -static qboolean init_mvd( void ); static void rec_stop( void ); static qboolean rec_allowed( void ); static void rec_start( fileHandle_t demofile ); +static void rec_write( void ); /* @@ -138,7 +182,7 @@ static void dummy_record_f( void ) { return; } - if( !init_mvd() ) { + if( !mvd_enable() ) { FS_FCloseFile( demofile ); return; } @@ -252,7 +296,7 @@ static void dummy_spawn( void ) { mvd.dummy->state = cs_spawned; } -static client_t *find_slot( void ) { +static client_t *dummy_find_slot( void ) { client_t *c; int i, j; @@ -284,7 +328,7 @@ static qboolean dummy_create( void ) { int number; // find a free client slot - newcl = find_slot(); + newcl = dummy_find_slot(); if( !newcl ) { Com_EPrintf( "No slot for dummy MVD client\n" ); return qfalse; @@ -357,22 +401,6 @@ static void dummy_run( void ) { } } -static void dummy_drop( const char *reason ) { - tcpClient_t *client; - - rec_stop(); - - LIST_FOR_EACH( tcpClient_t, client, &mvd_client_list, mvdEntry ) { - SV_HttpDrop( client, reason ); - } - - SV_RemoveClient( mvd.dummy ); - mvd.dummy = NULL; - - SZ_Clear( &mvd.datagram ); - SZ_Clear( &mvd.message ); -} - /* ============================================================================== @@ -531,12 +559,9 @@ static void emit_gamestate( void ) { player_state_t *ps; entity_state_t *es; size_t length; - uint8_t *patch; int flags, extra, portalbytes; byte portalbits[MAX_MAP_AREAS/8]; - patch = SZ_GetSpace( &msg_write, 2 ); - // send the serverdata MSG_WriteByte( mvd_serverdata ); MSG_WriteLong( PROTOCOL_VERSION_MVD ); @@ -601,10 +626,6 @@ static void emit_gamestate( void ) { es->number = j; } MSG_WriteShort( 0 ); - - length = msg_write.cursize - 2; - patch[0] = length & 255; - patch[1] = ( length >> 8 ) & 255; } @@ -735,92 +756,177 @@ static void emit_frame( void ) { MSG_WriteShort( 0 ); // end of packetentities } -// if dummy is not yet connected, create and spawn it -static qboolean init_mvd( void ) { +#define MVD_CLIENTS_TIMEOUT (15*60*1000) +#define MVD_PLAYERS_TIMEOUT (5*60*1000) + +/* +================== +SV_MvdBeginFrame +================== +*/ +void SV_MvdBeginFrame( void ) { + gtv_client_t *client; + int i; + + // do nothing if not active if( !mvd.dummy ) { - if( !dummy_create() ) { - return qfalse; + return; + } + + // disconnect MVD dummy if no MVD clients are active for 15 minutes + if( !sv_mvd_autorecord->integer ) { + if( mvd.recording || !LIST_EMPTY( >v_active_list ) ) { + mvd.clients_active = svs.realtime; + } else if( svs.realtime - mvd.clients_active > MVD_CLIENTS_TIMEOUT ) { + Com_Printf( "No MVD clients active for 15 minutes, " + "disconnecting dummy client.\n" ); + SV_DropClient( mvd.dummy, NULL ); + return; } - dummy_spawn(); + } + + // suspend all MVD streams if no players are active for 5 minutes + for( i = 0; i < sv_maxclients->integer; i++ ) { + edict_t * ent = EDICT_NUM( i + 1 ); + if( ent != mvd.dummy->edict && player_is_active( ent ) ) { + mvd.players_active = svs.realtime; + break; + } + } + + if( svs.realtime - mvd.players_active > MVD_PLAYERS_TIMEOUT ) { + if( mvd.active ) { + FOR_EACH_ACTIVE_GTV( client ) { + // send stream stop marker + write_message( client, GTS_STREAM_STOP ); +#if USE_ZLIB + flush_stream( client, Z_SYNC_FLUSH ); +#endif + } + Com_Printf( "No players active for 5 minutes, " + "suspending MVD streams.\n" ); + mvd.active = qfalse; + } + } else if( !mvd.active ) { + // build and emit gamestate build_gamestate(); + emit_gamestate(); + + FOR_EACH_ACTIVE_GTV( client ) { + // send stream start marker + write_message( client, GTS_STREAM_START ); + + // send gamestate + write_message( client, GTS_STREAM_DATA ); +#if USE_ZLIB + flush_stream( client, Z_SYNC_FLUSH ); +#endif + } + + // write it to demofile + if( mvd.recording ) { + rec_write(); + } + + // clear gamestate + SZ_Clear( &msg_write ); + + SZ_Clear( &mvd.datagram ); + SZ_Clear( &mvd.message ); + + Com_Printf( "Resuming MVD streams.\n" ); + mvd.active = qtrue; } - return qtrue; } /* ================== -SV_MvdRunFrame +SV_MvdEndFrame ================== */ -void SV_MvdRunFrame( void ) { - tcpClient_t *client; - uint16_t length; +void SV_MvdEndFrame( void ) { + gtv_client_t *client; + size_t total; + byte header[3]; + // do nothing if not active if( !mvd.dummy ) { return; } dummy_run(); + if( !mvd.active ) { + return; + } + + // if reliable message overflowed, kick all clients if( mvd.message.overflowed ) { - // if reliable message overflowed, kick all clients - Com_EPrintf( "Reliable MVD message overflowed\n" ); - dummy_drop( "overflowed" ); - goto clear; + Com_EPrintf( "Reliable MVD message overflowed!\n" ); + SV_DropClient( mvd.dummy, NULL ); + return; } if( mvd.datagram.overflowed ) { - Com_WPrintf( "Unreliable MVD datagram overflowed\n" ); + Com_WPrintf( "Unreliable MVD datagram overflowed.\n" ); SZ_Clear( &mvd.datagram ); } - // emit a single delta update + // emit a delta update common to all clients emit_frame(); - // check if frame fits - // FIXME: dumping frame should not be allowed in current design - if( mvd.message.cursize + msg_write.cursize > MAX_MSGLEN ) { - Com_WPrintf( "Dumping MVD frame: %"PRIz" bytes\n", msg_write.cursize ); + // if reliable message and frame update don't fit, kick all clients + if( mvd.message.cursize + msg_write.cursize >= MAX_MSGLEN ) { + Com_EPrintf( "MVD frame overflowed!\n" ); SZ_Clear( &msg_write ); + SV_DropClient( mvd.dummy, NULL ); + return; } // check if unreliable datagram fits - if( mvd.message.cursize + msg_write.cursize + mvd.datagram.cursize > MAX_MSGLEN ) { - Com_WPrintf( "Dumping unreliable MVD datagram: %"PRIz" bytes\n", mvd.datagram.cursize ); + if( mvd.message.cursize + msg_write.cursize + mvd.datagram.cursize >= MAX_MSGLEN ) { + Com_WPrintf( "Dumping unreliable MVD datagram.\n" ); SZ_Clear( &mvd.datagram ); } - length = LittleShort( mvd.message.cursize + msg_write.cursize + mvd.datagram.cursize ); + // build message header + total = mvd.message.cursize + msg_write.cursize + mvd.datagram.cursize + 1; + header[0] = total & 255; + header[1] = ( total >> 8 ) & 255; + header[2] = GTS_STREAM_DATA; // send frame to clients - LIST_FOR_EACH( tcpClient_t, client, &mvd_client_list, mvdEntry ) { - if( client->state == cs_spawned ) { - SV_HttpWrite( client, &length, 2 ); - SV_HttpWrite( client, mvd.message.data, mvd.message.cursize ); - SV_HttpWrite( client, msg_write.data, msg_write.cursize ); - SV_HttpWrite( client, mvd.datagram.data, mvd.datagram.cursize ); + FOR_EACH_ACTIVE_GTV( client ) { + write_stream( client, header, sizeof( header ) ); + write_stream( client, mvd.message.data, mvd.message.cursize ); + write_stream( client, msg_write.data, msg_write.cursize ); + write_stream( client, mvd.datagram.data, mvd.datagram.cursize ); #if USE_ZLIB - client->noflush++; -#endif + if( ++client->bufcount > client->maxbuf ) { + flush_stream( client, Z_SYNC_FLUSH ); } +#endif } // write frame to demofile if( mvd.recording ) { - FS_Write( &length, 2, mvd.recording ); + uint16_t len; + + len = LittleShort( total - 1 ); + FS_Write( &len, 2, mvd.recording ); FS_Write( mvd.message.data, mvd.message.cursize, mvd.recording ); FS_Write( msg_write.data, msg_write.cursize, mvd.recording ); FS_Write( mvd.datagram.data, mvd.datagram.cursize, mvd.recording ); - if( sv_mvd_max_size->value > 0 ) { + if( sv_mvd_maxsize->value > 0 ) { int numbytes = FS_RawTell( mvd.recording ); - if( numbytes > sv_mvd_max_size->value * 1000 ) { + if( numbytes > sv_mvd_maxsize->value * 1000 ) { Com_Printf( "Stopping MVD recording, maximum size reached.\n" ); rec_stop(); } - } else if( sv_mvd_max_duration->value > 0 && - ++mvd.numframes > sv_mvd_max_duration->value * 600 ) + } else if( sv_mvd_maxtime->value > 0 && + ++mvd.numframes > sv_mvd_maxtime->value * 600 ) { Com_Printf( "Stopping MVD recording, maximum duration reached.\n" ); rec_stop(); @@ -829,228 +935,10 @@ void SV_MvdRunFrame( void ) { SZ_Clear( &msg_write ); -clear: - SZ_Clear( &mvd.datagram ); - SZ_Clear( &mvd.message ); -} - -/* -============================================================================== - -CLIENT HANDLING - -============================================================================== -*/ - -static void SV_MvdClientNew( tcpClient_t *client ) { - Com_DPrintf( "Sending gamestate to MVD client %s\n", - NET_AdrToString( &client->stream.address ) ); - client->state = cs_spawned; - - emit_gamestate(); - -#if USE_ZLIB - client->noflush = 99999; -#endif - - SV_HttpWrite( client, msg_write.data, msg_write.cursize ); - SZ_Clear( &msg_write ); -} - -void SV_MvdInitStream( void ) { - uint32_t magic; -#if USE_ZLIB - int bits; -#endif - - SV_HttpPrintf( "HTTP/1.0 200 OK\r\n" ); - -#if USE_ZLIB - switch( sv_mvd_encoding->integer ) { - case 1: - SV_HttpPrintf( "Content-Encoding: gzip\r\n" ); - bits = 31; - break; - case 2: - SV_HttpPrintf( "Content-Encoding: deflate\r\n" ); - bits = 15; - break; - default: - bits = 0; - break; - } -#endif - - SV_HttpPrintf( - "Content-Type: application/octet-stream\r\n" - "Content-Disposition: attachment; filename=\"stream.mvd2\"\r\n" - "\r\n" ); - -#if USE_ZLIB - if( bits ) { - deflateInit2( &http_client->z, Z_DEFAULT_COMPRESSION, - Z_DEFLATED, bits, 8, Z_DEFAULT_STRATEGY ); - } -#endif - - magic = MVD_MAGIC; - SV_HttpWrite( http_client, &magic, 4 ); -} - -void SV_MvdGetStream( const char *uri ) { - if( http_client->method == HTTP_METHOD_HEAD ) { - SV_HttpPrintf( "HTTP/1.0 200 OK\r\n\r\n" ); - SV_HttpDrop( http_client, "200 OK " ); - return; - } - - if( !init_mvd() ) { - SV_HttpReject( "503 Service Unavailable", - "Unable to create dummy MVD client. Server is full." ); - return; - } - - List_Append( &mvd_client_list, &http_client->mvdEntry ); - - SV_MvdInitStream(); - - SV_MvdClientNew( http_client ); -} - -/* -============================================================================== - -SERVER HOOKS - -These hooks are called by server code when some event occurs. - -============================================================================== -*/ - -/* -================== -SV_MvdMapChanged - -Server has just changed the map, spawn the MVD dummy and go! -================== -*/ -void SV_MvdMapChanged( void ) { - tcpClient_t *client; - - if( !sv_mvd_enable->integer ) { - return; // do noting if disabled - } - - if( !mvd.dummy ) { - if( !sv_mvd_autorecord->integer ) { - return; // not listening for autorecord command - } - if( !dummy_create() ) { - return; - } - Com_Printf( "Spawning MVD dummy for auto-recording\n" ); - } - SZ_Clear( &mvd.datagram ); SZ_Clear( &mvd.message ); - - dummy_spawn(); - build_gamestate(); - emit_gamestate(); - - // send gamestate to all MVD clients - LIST_FOR_EACH( tcpClient_t, client, &mvd_client_list, mvdEntry ) { - SV_HttpWrite( client, msg_write.data, msg_write.cursize ); - } - - if( mvd.recording ) { - int maxlevels = sv_mvd_max_levels->integer; - - // check if it is time to stop recording - if( maxlevels > 0 && ++mvd.numlevels >= maxlevels ) { - Com_Printf( "Stopping MVD recording, " - "maximum number of level changes reached.\n" ); - rec_stop(); - } else { - FS_Write( msg_write.data, msg_write.cursize, mvd.recording ); - } - } - - SZ_Clear( &msg_write ); -} - -/* -================== -SV_MvdClientDropped - -Server has just dropped a client, check if that was our MVD dummy client. -================== -*/ -void SV_MvdClientDropped( client_t *client, const char *reason ) { - if( client == mvd.dummy ) { - dummy_drop( reason ); - } -} - -/* -================== -SV_MvdInit - -Server is initializing, prepare MVD server for this game. -================== -*/ -void SV_MvdInit( void ) { - if( !sv_mvd_enable->integer ) { - return; // do noting if disabled - } - - // allocate buffers - Z_TagReserve( sizeof( player_state_t ) * sv_maxclients->integer + - sizeof( entity_state_t ) * MAX_EDICTS + MAX_MSGLEN * 2, TAG_SERVER ); - SZ_Init( &mvd.message, Z_ReservedAlloc( MAX_MSGLEN ), MAX_MSGLEN ); - SZ_Init( &mvd.datagram, Z_ReservedAlloc( MAX_MSGLEN ), MAX_MSGLEN ); - mvd.players = Z_ReservedAlloc( sizeof( player_state_t ) * sv_maxclients->integer ); - mvd.entities = Z_ReservedAlloc( sizeof( entity_state_t ) * MAX_EDICTS ); - - // reserve the slot for dummy MVD client - if( !sv_reserved_slots->integer ) { - Cvar_Set( "sv_reserved_slots", "1" ); - } } -/* -================== -SV_MvdShutdown - -Server is shutting down, clean everything up. -================== -*/ -void SV_MvdShutdown( void ) { - tcpClient_t *t; - uint16_t length; - - // stop recording - rec_stop(); - - // send EOF to MVD clients - // NOTE: not clearing mvd_client_list as clients will be - // properly dropped by SV_FinalMessage just after - length = 0; - LIST_FOR_EACH( tcpClient_t, t, &mvd_client_list, mvdEntry ) { - SV_HttpWrite( t, &length, 2 ); - SV_HttpFinish( t ); - } - - // remove MVD dummy - if( mvd.dummy ) { - SV_RemoveClient( mvd.dummy ); - } - - // free static data - Z_Free( mvd.message.data ); - - memset( &mvd, 0, sizeof( mvd ) ); -} /* @@ -1079,7 +967,7 @@ void SV_MvdMulticast( int leafnum, multicast_t to ) { int bits; // do nothing if not active - if( !mvd.dummy ) { + if( !mvd.active ) { return; } @@ -1111,7 +999,7 @@ void SV_MvdUnicast( edict_t *ent, int clientNum, qboolean reliable ) { int bits; // do nothing if not active - if( !mvd.dummy ) { + if( !mvd.active ) { return; } @@ -1167,7 +1055,7 @@ SV_MvdConfigstring ============== */ void SV_MvdConfigstring( int index, const char *string ) { - if( mvd.dummy ) { + if( mvd.active ) { SZ_WriteByte( &mvd.message, mvd_configstring ); SZ_WriteShort( &mvd.message, index ); SZ_WriteString( &mvd.message, string ); @@ -1180,7 +1068,7 @@ SV_MvdBroadcastPrint ============== */ void SV_MvdBroadcastPrint( int level, const char *string ) { - if( mvd.dummy ) { + if( mvd.active ) { SZ_WriteByte( &mvd.message, mvd_print ); SZ_WriteByte( &mvd.message, level ); SZ_WriteString( &mvd.message, string ); @@ -1200,7 +1088,7 @@ void SV_MvdStartSound( int entnum, int channel, int flags, { int extrabits, sendchan; - if( !mvd.dummy ) { + if( !mvd.active ) { return; } @@ -1232,11 +1120,759 @@ void SV_MvdStartSound( int entnum, int channel, int flags, /* ============================================================================== +TCP CLIENTS HANDLING + +============================================================================== +*/ + + +static void remove_client( gtv_client_t *client ) { + NET_Close( &client->stream ); + List_Remove( &client->entry ); + if( client->data ) { + Z_Free( client->data ); + client->data = NULL; + } + client->state = cs_free; +} + +#if USE_ZLIB +static void flush_stream( gtv_client_t *client, int flush ) { + fifo_t *fifo = &client->stream.send; + z_streamp z = &client->z; + byte *data; + size_t len; + int ret; + + if( !z->state ) { + return; + } + + z->next_in = NULL; + z->avail_in = 0; + + do { + data = FIFO_Reserve( fifo, &len ); + if( !len ) { + // FIXME: this is not an error when flushing + return; + } + + z->next_out = data; + z->avail_out = ( uInt )len; + + ret = deflate( z, flush ); + + len -= z->avail_out; + if( len ) { + FIFO_Commit( fifo, len ); + client->bufcount = 0; + } + } while( ret == Z_OK ); +} +#endif + +static void drop_client( gtv_client_t *client, const char *error ) { + if( client->state <= cs_zombie ) { + return; + } + if( error ) { + // notify console + Com_Printf( "TCP client %s[%s] dropped: %s\n", client->name, + NET_AdrToString( &client->stream.address ), error ); + + } + +#if USE_ZLIB + if( client->z.state ) { + // finish zlib stream + flush_stream( client, Z_FINISH ); + deflateEnd( &client->z ); + } +#endif + + List_Remove( &client->active ); + client->state = cs_zombie; + client->lastmessage = svs.realtime; +} + + +static void write_stream( gtv_client_t *client, void *data, size_t len ) { + fifo_t *fifo = &client->stream.send; + + if( client->state <= cs_zombie ) { + return; + } + + if( !len ) { + return; + } + +#if USE_ZLIB + if( client->z.state ) { + z_streamp z = &client->z; + + z->next_in = data; + z->avail_in = ( uInt )len; + + do { + data = FIFO_Reserve( fifo, &len ); + if( !len ) { + drop_client( client, "overflowed" ); + return; + } + + z->next_out = data; + z->avail_out = ( uInt )len; + + if( deflate( z, Z_NO_FLUSH ) != Z_OK ) { + drop_client( client, "deflate() failed" ); + return; + } + + len -= z->avail_out; + if( len ) { + FIFO_Commit( fifo, len ); + client->bufcount = 0; + } + } while( z->avail_in ); + } else +#endif + + if( !FIFO_TryWrite( fifo, data, len ) ) { + drop_client( client, "overflowed" ); + } +} + +static void write_message( gtv_client_t *client, gtv_serverop_t op ) { + byte header[3]; + size_t len = msg_write.cursize + 1; + + header[0] = len & 255; + header[1] = ( len >> 8 ) & 255; + header[2] = op; + write_stream( client, header, sizeof( header ) ); + + write_stream( client, msg_write.data, msg_write.cursize ); +} + +static void parse_hello( gtv_client_t *client ) { + int protocol, flags, maxbuf; + byte *data; + + if( client->state >= cs_primed ) { + write_message( client, GTS_BADREQUEST ); + drop_client( client, "duplicated hello message" ); + return; + } + + protocol = MSG_ReadWord(); + if( protocol != GTV_PROTOCOL_VERSION ) { + write_message( client, GTS_BADREQUEST ); + drop_client( client, "bad protocol" ); + return; + } + + flags = MSG_ReadLong(); + maxbuf = MSG_ReadShort(); + MSG_ReadLong(); + MSG_ReadString( client->name, sizeof( client->name ) ); + MSG_ReadString( client->version, sizeof( client->version ) ); + + if( !SV_MatchAddress( >v_host_list, &client->stream.address ) ) { + write_message( client, GTS_NOACCESS ); + drop_client( client, "not authorized" ); + return; + } + + data = SV_Malloc( MAX_MSGLEN * 2 ); + + // TODO: expand recv buffer + client->stream.send.data = data; + client->stream.send.size = MAX_MSGLEN * 2; + client->data = data; + client->flags = flags; + client->maxbuf = maxbuf; + client->state = cs_primed; + + // send hello + MSG_WriteLong( flags ); + write_message( client, GTS_HELLO ); + SZ_Clear( &msg_write ); + +#if USE_ZLIB + // the rest of the stream will be deflated + if( flags & GTF_DEFLATE ) { + client->z.zalloc = SV_Zalloc; + client->z.zfree = SV_Zfree; + if( deflateInit( &client->z, Z_DEFAULT_COMPRESSION ) != Z_OK ) { + drop_client( client, "deflateInit failed" ); + return; + } + } +#endif + + mvd.clients_active = svs.realtime; + + Com_Printf( "Accepted MVD client %s[%s]\n", client->name, + NET_AdrToString( &client->stream.address ) ); +} + +static void parse_ping( gtv_client_t *client ) { + if( client->state < cs_primed ) { + return; + } + + // send ping reply + write_message( client, GTS_PONG ); +#if USE_ZLIB + flush_stream( client, Z_SYNC_FLUSH ); +#endif +} + +static void parse_stream_start( gtv_client_t *client ) { + if( client->state < cs_primed ) { + return; + } + + client->state = cs_spawned; + + List_Append( >v_active_list, &client->active ); + + if( !mvd_enable() ) { + write_message( client, GTS_ERROR ); + drop_client( client, "couldn't create MVD dummy" ); + return; + } + + // do nothing if not active + if( !mvd.active ) { + return; + } + + // send stream start marker + write_message( client, GTS_STREAM_START ); + + // send gamestate + emit_gamestate(); + write_message( client, GTS_STREAM_DATA ); +#if USE_ZLIB + flush_stream( client, Z_SYNC_FLUSH ); +#endif + SZ_Clear( &msg_write ); +} + +static void parse_stream_stop( gtv_client_t *client ) { + if( client->state < cs_spawned ) { + return; + } + + client->state = cs_primed; + + List_Delete( &client->active ); + + // do nothing if not active + if( !mvd.active ) { + return; + } + + // send stream stop marker + write_message( client, GTS_STREAM_STOP ); +#if USE_ZLIB + flush_stream( client, Z_SYNC_FLUSH ); +#endif +} + +static qboolean parse_message( gtv_client_t *client ) { + uint32_t magic; + uint16_t msglen; + int cmd; + + if( client->state <= cs_zombie ) { + return qfalse; + } + + // check magic + if( client->state < cs_connected ) { + if( !FIFO_TryRead( &client->stream.recv, &magic, 4 ) ) { + return qfalse; + } + if( magic != MVD_MAGIC ) { + drop_client( client, "not a MVD/GTV stream" ); + return qfalse; + } + client->state = cs_connected; + + // send it back + write_stream( client, &magic, 4 ); + } + + // parse msglen + if( !client->msglen ) { + if( !FIFO_TryRead( &client->stream.recv, &msglen, 2 ) ) { + return qfalse; + } + msglen = LittleShort( msglen ); + if( !msglen ) { + drop_client( client, "end of stream" ); + return qfalse; + } + if( msglen > 128 ) { + drop_client( client, "oversize message" ); + return qfalse; + } + client->msglen = msglen; + } + + // read this message + if( !FIFO_ReadMessage( &client->stream.recv, client->msglen ) ) { + return qfalse; + } + + client->msglen = 0; + + cmd = MSG_ReadByte(); + switch( cmd ) { + case GTC_HELLO: + parse_hello( client ); + break; + case GTC_PING: + parse_ping( client ); + break; + case GTC_STREAM_START: + parse_stream_start( client ); + break; + case GTC_STREAM_STOP: + parse_stream_stop( client ); + break; + default: + drop_client( client, "unknown command byte" ); + return qfalse; + } + + if( msg_read.readcount > msg_read.cursize ) { + drop_client( client, "read past end of message" ); + return qfalse; + } + + client->lastmessage = svs.realtime; // don't timeout + return qtrue; +} + +static gtv_client_t *find_slot( void ) { + gtv_client_t *client; + int i; + + for( i = 0; i < sv_mvd_maxclients->integer; i++ ) { + client = &mvd.clients[i]; + if( !client->state ) { + return client; + } + } + return NULL; +} + +static void accept_client( netstream_t *stream ) { + gtv_client_t *client; + netstream_t *s; + + // limit number of connections from single IP + if( sv_iplimit->integer > 0 ) { + int count = 0; + + FOR_EACH_GTV( client ) { + if( NET_IsEqualBaseAdr( &client->stream.address, &stream->address ) ) { + count++; + } + } + if( count >= sv_iplimit->integer ) { + Com_Printf( "TCP client [%s] rejected: too many connections\n", + NET_AdrToString( &stream->address ) ); + NET_Close( stream ); + return; + } + } + + // find a free client slot + client = find_slot(); + if( !client ) { + Com_Printf( "TCP client [%s] rejected: no free slots\n", + NET_AdrToString( &stream->address ) ); + NET_Close( stream ); + return; + } + + memset( client, 0, sizeof( *client ) ); + + s = &client->stream; + s->recv.data = client->buffer; + s->recv.size = 128; + s->send.data = client->buffer + 128; + s->send.size = 128; + s->socket = stream->socket; + s->address = stream->address; + s->state = stream->state; + + client->lastmessage = svs.realtime; + client->state = cs_assigned; + List_SeqAdd( >v_client_list, &client->entry ); + List_Init( &client->active ); + + Com_DPrintf( "TCP client [%s] accepted\n", + NET_AdrToString( &stream->address ) ); +} + +void SV_MvdRunClients( void ) { + gtv_client_t *client; + neterr_t ret; + netstream_t stream; + unsigned zombie_time = 1000 * sv_zombietime->value; + unsigned drop_time = 1000 * sv_timeout->value; + unsigned ghost_time = 1000 * sv_ghostime->value; + unsigned delta; + + // accept new connections + ret = NET_Accept( &net_from, &stream ); + if( ret == NET_ERROR ) { + Com_DPrintf( "%s from %s, ignored\n", NET_ErrorString(), + NET_AdrToString( &net_from ) ); + } else if( ret == NET_OK ) { + accept_client( &stream ); + } + + // run existing connections + FOR_EACH_GTV( client ) { + // check timeouts + delta = svs.realtime - client->lastmessage; + switch( client->state ) { + case cs_zombie: + if( delta > zombie_time || !FIFO_Usage( &client->stream.send ) ) { + remove_client( client ); + continue; + } + break; + case cs_assigned: + case cs_connected: + if( delta > ghost_time || delta > drop_time ) { + drop_client( client, "request timed out" ); + remove_client( client ); + continue; + } + break; + default: + if( delta > drop_time ) { + drop_client( client, "connection timed out" ); + remove_client( client ); + continue; + } + break; + } + + // run network stream + ret = NET_RunStream( &client->stream ); + switch( ret ) { + case NET_AGAIN: + break; + case NET_OK: + // parse the message + while( parse_message( client ) ) + ; + break; + case NET_CLOSED: + drop_client( client, "EOF from client" ); + remove_client( client ); + break; + case NET_ERROR: + drop_client( client, "connection reset by peer" ); + remove_client( client ); + break; + } + } +} + +static void dump_clients( void ) { + gtv_client_t *client; + int count; + + Com_Printf( +"num name buf lastmsg address state\n" +"--- ---------------- --- ------- --------------------- -----\n" ); + count = 0; + FOR_EACH_GTV( client ) { + Com_Printf( "%3d %-16.16s %3"PRIz" %7u %-21s ", + count, client->name, FIFO_Usage( &client->stream.send ), + svs.realtime - client->lastmessage, + NET_AdrToString( &client->stream.address ) ); + + switch( client->state ) { + case cs_zombie: + Com_Printf( "ZMBI " ); + break; + case cs_assigned: + Com_Printf( "ASGN " ); + break; + case cs_connected: + Com_Printf( "CNCT " ); + break; + case cs_primed: + Com_Printf( "PRIM " ); + break; + default: + Com_Printf( "SEND " ); + break; + } + Com_Printf( "\n" ); + + count++; + } +} + +static void dump_versions( void ) { + gtv_client_t *client; + int count; + + Com_Printf( +"num name version\n" +"--- ---------------- -----------------------------------------\n" ); + + FOR_EACH_GTV( client ) { + count = 0; + Com_Printf( "%3i %-16.16s %-40.40s\n", + count, client->name, client->version ); + count++; + } +} + +void SV_MvdStatus_f( void ) { + if( LIST_EMPTY( >v_client_list ) ) { + Com_Printf( "No TCP clients.\n" ); + } else { + if( Cmd_Argc() > 1 ) { + dump_versions(); + } else { + dump_clients(); + } + } + Com_Printf( "\n" ); +} + +static void mvd_disable( void ) { + // remove MVD dummy + if( mvd.dummy ) { + SV_RemoveClient( mvd.dummy ); + mvd.dummy = NULL; + } + + SZ_Clear( &mvd.datagram ); + SZ_Clear( &mvd.message ); + + mvd.active = qfalse; +} + +// something bad happened, remove all clients +static void mvd_drop( gtv_serverop_t op ) { + gtv_client_t *client; + + // stop recording + rec_stop(); + + // drop GTV clients + FOR_EACH_GTV( client ) { + switch( client->state ) { + case cs_spawned: + if( mvd.active ) { + write_message( client, GTS_STREAM_STOP ); + } + // fall through + case cs_primed: + write_message( client, op ); + drop_client( client, NULL ); + NET_RunStream( &client->stream ); + NET_RunStream( &client->stream ); + remove_client( client ); + break; + default: + drop_client( client, NULL ); + remove_client( client ); + break; + } + } + + mvd_disable(); +} + +// if dummy is not yet connected, create and spawn it +static qboolean mvd_enable( void ) { + if( !mvd.dummy ) { + if( !dummy_create() ) { + return qfalse; + } + dummy_spawn(); + build_gamestate(); + mvd.clients_active = mvd.players_active = svs.realtime; + mvd.active = qtrue; + } + return qtrue; +} + + +/* +============================================================================== + +SERVER HOOKS + +These hooks are called by server code when some event occurs. + +============================================================================== +*/ + +/* +================== +SV_MvdMapChanged + +Server has just changed the map, spawn the MVD dummy and go! +================== +*/ +void SV_MvdMapChanged( void ) { + gtv_client_t *client; + + if( !sv_mvd_enable->integer ) { + return; // do noting if disabled + } + + if( !mvd.dummy ) { + if( !sv_mvd_autorecord->integer ) { + return; // not listening for autorecord command + } + if( !dummy_create() ) { + return; + } + Com_Printf( "Spawning MVD dummy for auto-recording\n" ); + } + + dummy_spawn(); + + if( mvd.active ) { + // build and emit gamestate + build_gamestate(); + emit_gamestate(); + + // send gamestate to all MVD clients + FOR_EACH_ACTIVE_GTV( client ) { + write_message( client, GTS_STREAM_DATA ); + } + } + + if( mvd.recording ) { + int maxlevels = sv_mvd_maxmaps->integer; + + // check if it is time to stop recording + if( maxlevels > 0 && ++mvd.numlevels >= maxlevels ) { + Com_Printf( "Stopping MVD recording, " + "maximum number of level changes reached.\n" ); + rec_stop(); + } else if( mvd.active ) { + // write gamestate to demofile + rec_write(); + } + } + + // clear gamestate + SZ_Clear( &msg_write ); + + SZ_Clear( &mvd.datagram ); + SZ_Clear( &mvd.message ); +} + +/* +================== +SV_MvdClientDropped + +Server has just dropped a client, check if that was our MVD dummy client. +================== +*/ +void SV_MvdClientDropped( client_t *client, const char *reason ) { + if( client == mvd.dummy ) { + mvd_drop( GTS_ERROR ); + } +} + +/* +================== +SV_MvdInit + +Server is initializing, prepare MVD server for this game. +================== +*/ +void SV_MvdInit( void ) { + if( !sv_mvd_enable->integer ) { + return; // do noting if disabled + } + + // allocate buffers + Z_TagReserve( sizeof( player_state_t ) * sv_maxclients->integer + + sizeof( entity_state_t ) * MAX_EDICTS + MAX_MSGLEN * 2, TAG_SERVER ); + SZ_Init( &mvd.message, Z_ReservedAlloc( MAX_MSGLEN ), MAX_MSGLEN ); + SZ_Init( &mvd.datagram, Z_ReservedAlloc( MAX_MSGLEN ), MAX_MSGLEN ); + mvd.players = Z_ReservedAlloc( sizeof( player_state_t ) * sv_maxclients->integer ); + mvd.entities = Z_ReservedAlloc( sizeof( entity_state_t ) * MAX_EDICTS ); + + // reserve the slot for dummy MVD client + if( !sv_reserved_slots->integer ) { + Cvar_Set( "sv_reserved_slots", "1" ); + } + + Cvar_ClampInteger( sv_mvd_maxclients, 1, 256 ); + + // open server TCP socket + if( sv_mvd_enable->integer > 1 ) { + if( NET_Listen( qtrue ) == NET_OK ) { + mvd.clients = SV_Mallocz( sizeof( gtv_client_t ) * sv_mvd_maxclients->integer ); + } else { + Com_EPrintf( "%s while opening server TCP port.\n", NET_ErrorString() ); + Cvar_Set( "sv_mvd_enable", "1" ); + } + } +} + +/* +================== +SV_MvdShutdown + +Server is shutting down, clean everything up. +================== +*/ +void SV_MvdShutdown( killtype_t type ) { + // drop all clients + mvd_drop( type == KILL_RESTART ? GTS_RECONNECT : GTS_DISCONNECT ); + + // free static data + Z_Free( mvd.message.data ); + Z_Free( mvd.clients ); + + // close server TCP socket + NET_Listen( qfalse ); + + memset( &mvd, 0, sizeof( mvd ) ); +} + + +/* +============================================================================== + LOCAL MVD RECORDER ============================================================================== */ +static void rec_write( void ) { + uint16_t msglen; + + msglen = LittleShort( msg_write.cursize ); + FS_Write( &msglen, 2, mvd.recording ); + FS_Write( msg_write.data, msg_write.cursize, mvd.recording ); +} + /* ============== rec_stop @@ -1245,15 +1881,15 @@ Stops server local MVD recording. ============== */ static void rec_stop( void ) { - uint16_t length; + uint16_t msglen; if( !mvd.recording ) { return; } // write demo EOF marker - length = 0; - FS_Write( &length, 2, mvd.recording ); + msglen = 0; + FS_Write( &msglen, 2, mvd.recording ); FS_FCloseFile( mvd.recording ); mvd.recording = 0; @@ -1277,12 +1913,13 @@ static void rec_start( fileHandle_t demofile ) { mvd.recording = demofile; mvd.numlevels = 0; mvd.numframes = 0; + mvd.clients_active = svs.realtime; magic = MVD_MAGIC; FS_Write( &magic, 4, demofile ); emit_gamestate(); - FS_Write( msg_write.data, msg_write.cursize, demofile ); + rec_write(); SZ_Clear( &msg_write ); } @@ -1293,14 +1930,13 @@ const cmd_option_t o_mvdrecord[] = { { NULL } }; -extern void MVD_StreamedStop_f( void ); -extern void MVD_StreamedRecord_f( void ); -extern void MVD_File_g( genctx_t *ctx ); - static void SV_MvdRecord_c( genctx_t *ctx, int argnum ) { +#if USE_MVD_CLIENT + // TODO if( argnum == 1 ) { MVD_File_g( ctx ); } +#endif } /* @@ -1319,9 +1955,12 @@ static void SV_MvdRecord_f( void ) { size_t len; if( sv.state != ss_game ) { +#if USE_MVD_CLIENT if( sv.state == ss_broadcast ) { MVD_StreamedRecord_f(); - } else { + } else +#endif + { Com_Printf( "No server running.\n" ); } return; @@ -1366,7 +2005,7 @@ static void SV_MvdRecord_f( void ) { return; } - if( !init_mvd() ) { + if( !mvd_enable() ) { FS_FCloseFile( demofile ); return; } @@ -1389,10 +2028,12 @@ Ends server MVD recording ============== */ static void SV_MvdStop_f( void ) { +#if USE_MVD_CLIENT if( sv.state == ss_broadcast ) { MVD_StreamedStop_f(); return; } +#endif if( !mvd.recording ) { Com_Printf( "Not recording a local MVD.\n" ); return; @@ -1411,20 +2052,34 @@ static void SV_MvdStuff_f( void ) { } } +static void SV_AddGtvHost_f( void ) { + SV_AddMatch_f( >v_host_list ); +} +static void SV_DelGtvHost_f( void ) { + SV_DelMatch_f( >v_host_list ); +} +static void SV_ListGtvHosts_f( void ) { + SV_ListMatches_f( >v_host_list ); +} + static const cmdreg_t c_svmvd[] = { { "mvdrecord", SV_MvdRecord_f, SV_MvdRecord_c }, { "mvdstop", SV_MvdStop_f }, { "mvdstuff", SV_MvdStuff_f }, + { "addgtvhost", SV_AddGtvHost_f }, + { "delgtvhost", SV_DelGtvHost_f }, + { "listgtvhosts", SV_ListGtvHosts_f }, { NULL } }; void SV_MvdRegister( void ) { sv_mvd_enable = Cvar_Get( "sv_mvd_enable", "0", CVAR_LATCH ); + sv_mvd_maxclients = Cvar_Get( "sv_mvd_maxclients", "8", CVAR_LATCH ); sv_mvd_auth = Cvar_Get( "sv_mvd_auth", "", CVAR_PRIVATE ); - sv_mvd_max_size = Cvar_Get( "sv_mvd_max_size", "0", 0 ); - sv_mvd_max_duration = Cvar_Get( "sv_mvd_max_duration", "0", 0 ); - sv_mvd_max_levels = Cvar_Get( "sv_mvd_max_levels", "1", 0 ); + sv_mvd_maxsize = Cvar_Get( "sv_mvd_maxsize", "0", 0 ); + sv_mvd_maxtime = Cvar_Get( "sv_mvd_maxtime", "0", 0 ); + sv_mvd_maxmaps = Cvar_Get( "sv_mvd_maxmaps", "1", 0 ); sv_mvd_noblend = Cvar_Get( "sv_mvd_noblend", "0", CVAR_LATCH ); sv_mvd_nogun = Cvar_Get( "sv_mvd_nogun", "1", CVAR_LATCH ); sv_mvd_nomsgs = Cvar_Get( "sv_mvd_nomsgs", "1", CVAR_LATCH ); @@ -1432,10 +2087,7 @@ void SV_MvdRegister( void ) { "wait 50; putaway; wait 10; help;", 0 ); sv_mvd_scorecmd = Cvar_Get( "sv_mvd_scorecmd", "putaway; wait 10; help;", 0 ); - sv_mvd_autorecord = Cvar_Get( "sv_mvd_autorecord", "0", 0 ); -#if USE_ZLIB - sv_mvd_encoding = Cvar_Get( "sv_mvd_encoding", "1", 0 ); -#endif + sv_mvd_autorecord = Cvar_Get( "sv_mvd_autorecord", "0", CVAR_LATCH ); sv_mvd_capture_flags = Cvar_Get( "sv_mvd_capture_flags", "5", 0 ); dummy_buffer.text = dummy_buffer_text; |