diff options
Diffstat (limited to 'source/mvd_client.c')
-rw-r--r-- | source/mvd_client.c | 1847 |
1 files changed, 1058 insertions, 789 deletions
diff --git a/source/mvd_client.c b/source/mvd_client.c index bd3c794..a4e3370 100644 --- a/source/mvd_client.c +++ b/source/mvd_client.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,15 +19,61 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // -// mvd_client.c +// mvd_client.c -- MVD/GTV client // #include "sv_local.h" #include "mvd_local.h" +#include "gtv.h" #include <setjmp.h> +#define GTV_DEFAULT_BACKOFF (5*1000) +#define GTV_MAXIMUM_BACKOFF (5*3600*1000) + +typedef enum { + GTV_DISCONNECTED, // disconnected + GTV_CONNECTING, // connect() in progress + GTV_PREPARING, // waiting for server hello + GTV_CONNECTED, // keeping connection alive + GTV_ACTIVE, // actively running MVD stream + GTV_OVERFLOWED // recovering from delay buffer overflow +} gtv_state_t; + +typedef struct gtv_s { + list_t entry; + + int id; + char name[MAX_MVD_NAME]; + gtv_state_t state; + mvd_t *mvd; + + // connection variables + netstream_t stream; + char address[MAX_QPATH]; + byte *data; + size_t msglen; +#if USE_ZLIB + qboolean z_act; // true when actively inflating + z_stream z_str; + fifo_t z_buf; +#endif + unsigned last_rcvd; + unsigned last_sent; + void (*drop)( struct gtv_s * ); + void (*destroy)( struct gtv_s * ); + void (*run)( struct gtv_s * ); + unsigned retry_time; + unsigned retry_backoff; + + // demo related variables + fileHandle_t demoplayback; + int demoloop; + string_entry_t *demohead, *demoentry; +} gtv_t; + +static LIST_DECL( mvd_gtvs ); + LIST_DECL( mvd_channels ); -LIST_DECL( mvd_ready ); LIST_DECL( mvd_active ); mvd_t mvd_waitingRoom; @@ -36,52 +82,31 @@ int mvd_chanid; jmp_buf mvd_jmpbuf; -cvar_t *mvd_running; -cvar_t *mvd_shownet; -cvar_t *mvd_debug; -cvar_t *mvd_timeout; -cvar_t *mvd_wait_delay; -cvar_t *mvd_wait_percent; -cvar_t *mvd_chase_msgs; +cvar_t *mvd_shownet; +cvar_t *mvd_timeout; +cvar_t *mvd_wait_delay; +cvar_t *mvd_wait_percent; +cvar_t *mvd_chase_msgs; // ==================================================================== -void MVD_Disconnect( mvd_t *mvd ) { - if( mvd->demoplayback ) { - FS_FCloseFile( mvd->demoplayback ); - mvd->demoplayback = 0; - } else { - if( Com_IsDedicated() ) { - MVD_BroadcastPrintf( mvd, PRINT_HIGH, 0, - "[MVD] Disconnected from the game server!\n" ); - } - NET_Close( &mvd->stream ); - } - - mvd->state = MVD_DISCONNECTED; -} - -static void MVD_FreePlayer( mvd_player_t *player ) { - mvd_cs_t *cs, *next; - - for( cs = player->configstrings; cs; cs = next ) { - next = cs->next; - Z_Free( cs ); - } -} - void MVD_Free( mvd_t *mvd ) { int i; -#if USE_ZLIB - if( mvd->z.state ) { - inflateEnd( &mvd->z ); + // destroy any existing connection + if( mvd->gtv ) { + mvd->gtv->destroy( mvd->gtv ); } - if( mvd->zbuf.data ) { - Z_Free( mvd->zbuf.data ); + + // stop demo recording + if( mvd->demorecording ) { + uint16_t msglen = 0; + FS_Write( &msglen, 2, mvd->demorecording ); + FS_FCloseFile( mvd->demorecording ); + mvd->demorecording = 0; } -#endif + for( i = 0; i < mvd->maxclients; i++ ) { MVD_FreePlayer( &mvd->players[i] ); } @@ -89,16 +114,15 @@ void MVD_Free( mvd_t *mvd ) { CM_FreeMap( &mvd->cm ); + Z_Free( mvd->delay.data ); + List_Remove( &mvd->active ); - List_Remove( &mvd->ready ); List_Remove( &mvd->entry ); Z_Free( mvd ); } void MVD_Destroy( mvd_t *mvd ) { - udpClient_t *u, *unext; - tcpClient_t *t; - uint16_t length; + mvd_client_t *client, *next; // update channel menus if( !LIST_EMPTY( &mvd->active ) ) { @@ -106,42 +130,14 @@ void MVD_Destroy( mvd_t *mvd ) { } // 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 ); - } - - // stop demo recording - if( mvd->demorecording ) { - uint16_t msglen = 0; - FS_Write( &msglen, 2, mvd->demorecording ); - FS_FCloseFile( mvd->demorecording ); - mvd->demorecording = 0; + LIST_FOR_EACH_SAFE( mvd_client_t, client, next, &mvd->clients, entry ) { + MVD_SwitchChannel( client, &mvd_waitingRoom ); } - // disconnect if still connected - MVD_Disconnect( mvd ); - // free all channel data MVD_Free( mvd ); } -void MVD_Drop( mvd_t *mvd ) { - if( mvd->state < MVD_WAITING ) { - MVD_Destroy( mvd ); - } else { - MVD_Disconnect( mvd ); - } -} - void MVD_Destroyf( mvd_t *mvd, const char *fmt, ... ) { va_list argptr; char text[MAXPRINTMSG]; @@ -157,7 +153,54 @@ void MVD_Destroyf( mvd_t *mvd, const char *fmt, ... ) { longjmp( mvd_jmpbuf, -1 ); } -void MVD_Dropf( mvd_t *mvd, const char *fmt, ... ) { +mvd_t *MVD_SetChannel( int arg ) { + char *s = Cmd_Argv( arg ); + mvd_t *mvd; + int id; + + if( LIST_EMPTY( &mvd_channels ) ) { + Com_Printf( "No active channels.\n" ); + return NULL; + } + + if( !*s ) { + if( List_Count( &mvd_channels ) == 1 ) { + return LIST_FIRST( mvd_t, &mvd_channels, entry ); + } + Com_Printf( "Please specify an exact channel ID.\n" ); + return NULL; + } + + if( COM_IsUint( s ) ) { + id = atoi( s ); + LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { + if( mvd->id == id ) { + return mvd; + } + } + } else { + LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { + if( !strcmp( mvd->name, s ) ) { + return mvd; + } + } + } + + Com_Printf( "No such channel ID: %s\n", s ); + return NULL; +} + + +/* +==================================================================== + +COMMON GTV STUFF + +==================================================================== +*/ + +static void q_noreturn q_printf( 2, 3 ) +gtv_dropf( gtv_t *gtv, const char *fmt, ... ) { va_list argptr; char text[MAXPRINTMSG]; @@ -165,660 +208,908 @@ void MVD_Dropf( mvd_t *mvd, const char *fmt, ... ) { Q_vsnprintf( text, sizeof( text ), fmt, argptr ); va_end( argptr ); - Com_Printf( "[%s] %s\n", mvd->name, text ); + Com_Printf( "[%s] %s\n", gtv->name, text ); - MVD_Drop( mvd ); + gtv->drop( gtv ); longjmp( mvd_jmpbuf, -1 ); } -void MVD_DPrintf( const char *fmt, ... ) { +static void q_noreturn q_printf( 2, 3 ) +gtv_destroyf( gtv_t *gtv, 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 ); + Com_Printf( "[%s] %s\n", gtv->name, text ); + + gtv->destroy( gtv ); + + longjmp( mvd_jmpbuf, -1 ); } -static void MVD_HttpPrintf( mvd_t *mvd, const char *fmt, ... ) { - char buffer[MAX_STRING_CHARS]; - va_list argptr; - size_t len; +static mvd_t *create_channel( gtv_t *gtv ) { + mvd_t *mvd; - va_start( argptr, fmt ); - len = Q_vsnprintf( buffer, sizeof( buffer ), fmt, argptr ); - va_end( argptr ); + mvd = MVD_Mallocz( sizeof( *mvd ) ); + mvd->gtv = gtv; + mvd->id = gtv->id; + Q_strlcpy( mvd->name, gtv->name, sizeof( mvd->name ) ); + mvd->pool.edicts = mvd->edicts; + mvd->pool.edict_size = sizeof( edict_t ); + mvd->pool.max_edicts = MAX_EDICTS; + mvd->pm_type = PM_SPECTATOR; + mvd->min_packets = mvd_wait_delay->value * 10; + List_Init( &mvd->clients ); + List_Init( &mvd->entry ); + List_Init( &mvd->active ); + + return mvd; +} - if( len >= sizeof( buffer ) || FIFO_Write( &mvd->stream.send, buffer, len ) != len ) { - MVD_Dropf( mvd, "%s: overflow", __func__ ); +static gtv_t *gtv_set_conn( int arg ) { + char *s = Cmd_Argv( arg ); + gtv_t *gtv; + int id; + + if( LIST_EMPTY( &mvd_gtvs ) ) { + Com_Printf( "No GTV connections.\n" ); + return NULL; } + + if( !*s ) { + if( List_Count( &mvd_gtvs ) == 1 ) { + return LIST_FIRST( gtv_t, &mvd_gtvs, entry ); + } + Com_Printf( "Please specify an exact connection ID.\n" ); + return NULL; + } + + if( COM_IsUint( s ) ) { + id = atoi( s ); + LIST_FOR_EACH( gtv_t, gtv, &mvd_gtvs, entry ) { + if( gtv->id == id ) { + return gtv; + } + } + } else { + LIST_FOR_EACH( gtv_t, gtv, &mvd_gtvs, entry ) { + if( !strcmp( gtv->name, s ) ) { + return gtv; + } + } + } + + Com_Printf( "No such connection ID: %s\n", s ); + return NULL; } -void MVD_ClearState( mvd_t *mvd ) { - mvd_player_t *player; - int i; +/* +============== +MVD_Frame - // clear all entities, don't trust num_edicts as it is possible - // to miscount removed entities - memset( mvd->edicts, 0, sizeof( mvd->edicts ) ); - mvd->pool.num_edicts = 0; +Called from main server loop. +============== +*/ +int MVD_Frame( void ) { + gtv_t *gtv, *next; + int connections = 0; - // clear all players - for( i = 0; i < mvd->maxclients; i++ ) { - player = &mvd->players[i]; - MVD_FreePlayer( player ); - memset( player, 0, sizeof( *player ) ); + // run all GTV connections (but not demos) + LIST_FOR_EACH_SAFE( gtv_t, gtv, next, &mvd_gtvs, entry ) { + if( setjmp( mvd_jmpbuf ) ) { + continue; + } + + gtv->run( gtv ); + + connections++; } - mvd->numplayers = 0; - // free current map - CM_FreeMap( &mvd->cm ); + return connections; +} + + +/* +==================================================================== + +DEMO PLAYER + +==================================================================== +*/ + +static void demo_play_next( gtv_t *gtv, string_entry_t *entry ); - if( mvd->intermission ) { - // save oldscores - //strcpy( mvd->oldscores, mvd->layout ); +static qboolean demo_read_message( fileHandle_t f ) { + size_t ret; + uint16_t msglen; + + ret = FS_Read( &msglen, 2, f ); + if( ret != 2 ) { + return qfalse; + } + if( !msglen ) { + return qfalse; + } + msglen = LittleShort( msglen ); + if( msglen > MAX_MSGLEN ) { + return qfalse; + } + ret = FS_Read( msg_read_buffer, msglen, f ); + if( ret != msglen ) { + return qfalse; } - memset( mvd->configstrings, 0, sizeof( mvd->configstrings ) ); - mvd->layout[0] = 0; + SZ_Init( &msg_read, msg_read_buffer, sizeof( msg_read_buffer ) ); + msg_read.cursize = msglen; - mvd->framenum = 0; - // intermission flag will be cleared in MVD_ChangeLevel + return qtrue; } -void MVD_BeginWaiting( mvd_t *mvd ) { - int maxDelay = mvd_wait_delay->value * 1000; +static qboolean demo_read_frame( mvd_t *mvd ) { + gtv_t *gtv = mvd->gtv; - mvd->state = MVD_WAITING; - mvd->waitTime = svs.realtime; - mvd->waitDelay = 5000 + 1000 * mvd->waitCount; - if( mvd->waitDelay > maxDelay ) { - mvd->waitDelay = maxDelay; + if( mvd->state == MVD_WAITING ) { + return qfalse; // paused by user + } + if( !gtv ) { + MVD_Destroyf( mvd, "End of MVD stream reached" ); } - mvd->waitCount++; -} -static void MVD_EmitGamestate( mvd_t *mvd ) { - char *string; - int i, j; - entity_state_t *es; - player_state_t *ps; - size_t length; - uint8_t *patch; - int flags, extra, portalbytes; - byte portalbits[MAX_MAP_AREAS/8]; + if( !demo_read_message( gtv->demoplayback ) ) { + demo_play_next( gtv, gtv->demoentry->next ); + return qtrue; + } - patch = SZ_GetSpace( &msg_write, 2 ); + MVD_ParseMessage( mvd ); + return qtrue; +} - // send the serverdata - MSG_WriteByte( mvd_serverdata ); - MSG_WriteLong( PROTOCOL_VERSION_MVD ); - MSG_WriteShort( PROTOCOL_VERSION_MVD_CURRENT ); - MSG_WriteLong( mvd->servercount ); - MSG_WriteString( mvd->gamedir ); - MSG_WriteShort( mvd->clientNum ); +static void demo_play_next( gtv_t *gtv, string_entry_t *entry ) { + uint32_t magic = 0; - // 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; + if( !entry ) { + if( gtv->demoloop ) { + if( --gtv->demoloop == 0 ) { + gtv_destroyf( gtv, "End of play list reached" ); + } } + entry = gtv->demohead; + } - MSG_WriteShort( i ); - MSG_WriteData( string, length ); - MSG_WriteByte( 0 ); + // close previous file + if( gtv->demoplayback ) { + FS_FCloseFile( gtv->demoplayback ); + gtv->demoplayback = 0; } - MSG_WriteShort( MAX_CONFIGSTRINGS ); - // send baseline frame - portalbytes = CM_WritePortalBits( &sv.cm, portalbits ); - MSG_WriteByte( portalbytes ); - MSG_WriteData( portalbits, portalbytes ); - - // send base player states - flags = 0; - if( sv_mvd_noblend->integer ) { - flags |= MSG_PS_IGNORE_BLEND; + // open new file + FS_FOpenFile( entry->string, >v->demoplayback, FS_MODE_READ ); + if( !gtv->demoplayback ) { + gtv_destroyf( gtv, "Couldn't reopen %s", entry->string ); } - if( sv_mvd_nogun->integer ) { - flags |= MSG_PS_IGNORE_GUNINDEX|MSG_PS_IGNORE_GUNFRAMES; + + // figure out if file is compressed and check magic + if( FS_Read( &magic, 4, gtv->demoplayback ) != 4 ) { + gtv_destroyf( gtv, "Couldn't read magic from %s", entry->string ); } - for( i = 0; i < mvd->maxclients; i++ ) { - ps = &mvd->players[i].ps; - extra = 0; - if( !PPS_INUSE( ps ) ) { - extra |= MSG_PS_REMOVE; + if( ( ( LittleLong( magic ) & 0xe0ffffff ) == 0x00088b1f ) ) { + if( !FS_FilterFile( gtv->demoplayback ) ) { + gtv_destroyf( gtv, "Couldn't install gzip filter on %s", entry->string ); + } + if( FS_Read( &magic, 4, gtv->demoplayback ) != 4 ) { + gtv_destroyf( gtv, "Couldn't read magic from %s", entry->string ); } - MSG_WriteDeltaPlayerstate_Packet( NULL, ps, i, flags | extra ); } - MSG_WriteByte( CLIENTNUM_NONE ); + if( magic != MVD_MAGIC ) { + gtv_destroyf( gtv, "%s is not a MVD2 file", entry->string ); + } - // send base entity states - for( i = 1; i < mvd->pool.num_edicts; i++ ) { - es = &mvd->edicts[i].s; - flags = 0; - if( i <= mvd->maxclients ) { - ps = &mvd->players[ i - 1 ].ps; - if( PPS_INUSE( ps ) && ps->pmove.pm_type == PM_NORMAL ) { - flags |= MSG_ES_FIRSTPERSON; - } - } - if( ( j = es->number ) == 0 ) { - flags |= MSG_ES_REMOVE; - } - es->number = i; - MSG_WriteDeltaEntity( NULL, es, flags ); - es->number = j; + // read the first message + if( !demo_read_message( gtv->demoplayback ) ) { + gtv_destroyf( gtv, "Couldn't read first message from %s", entry->string ); } - MSG_WriteShort( 0 ); - // TODO: write private layouts/configstrings + // create MVD channel + if( !gtv->mvd ) { + gtv->mvd = create_channel( gtv ); + gtv->mvd->read_frame = demo_read_frame; + } - length = msg_write.cursize - 2; - patch[0] = length & 255; - patch[1] = ( length >> 8 ) & 255; -} + // parse gamestate + MVD_ParseMessage( gtv->mvd ); + if( !gtv->mvd->state ) { + gtv_destroyf( gtv, "First message of %s does not contain gamestate", entry->string ); + } -void MVD_SendGamestate( tcpClient_t *client ) { - MVD_EmitGamestate( client->mvd ); + gtv->mvd->state = MVD_READING; - Com_DPrintf( "Sent gamestate to MVD client %s\n", - NET_AdrToString( &client->stream.address ) ); - client->state = cs_spawned; + Com_Printf( "[%s] Reading from %s\n", gtv->name, entry->string ); - SV_HttpWrite( client, msg_write.data, msg_write.cursize ); - SZ_Clear( &msg_write ); + // reset state + gtv->demoentry = entry; + + // set channel address + Q_strlcpy( gtv->address, COM_SkipPath( entry->string ), sizeof( gtv->address ) ); } -void MVD_GetStatus( void ) { - char buffer[MAX_STRING_CHARS]; - mvd_t *mvd; - int count, len; +static void demo_destroy( gtv_t *gtv ) { + string_entry_t *entry, *next; + mvd_t *mvd = gtv->mvd; + + if( mvd ) { + mvd->gtv = NULL; + if( !mvd->state ) { + MVD_Free( mvd ); + } + } - SV_HttpPrintf( "HTTP/1.0 200 OK\r\n" ); + if( gtv->demoplayback ) { + FS_FCloseFile( gtv->demoplayback ); + gtv->demoplayback = 0; + } - if( http_client->method == HTTP_METHOD_HEAD ) { - SV_HttpPrintf( "\r\n" ); - SV_HttpDrop( http_client, "200 OK" ); - return; + for( entry = gtv->demohead; entry; entry = next ) { + next = entry->next; + Z_Free( entry ); } - SV_HttpPrintf( - "Content-Type: text/html; charset=us-ascii\r\n" - "\r\n" ); + Z_Free( gtv ); +} + - count = SV_CountClients(); - len = Q_EscapeMarkup( buffer, sv_hostname->string, sizeof( buffer ) ); - Q_snprintf( buffer + len, sizeof( buffer ) - len, " - %d/%d", - count, sv_maxclients->integer - sv_reserved_slots->integer ); +/* +==================================================================== - SV_HttpHeader( buffer ); +GTV CONNECTIONS - buffer[len] = 0; - SV_HttpPrintf( "<h1>%s</h1><p>This server has ", buffer ); +==================================================================== +*/ - count = List_Count( &mvd_ready ); - if( count ) { - SV_HttpPrintf( "%d channel%s available, %d active. ", - count, count == 1 ? "" : "s", List_Count( &mvd_active ) ); - } else { - SV_HttpPrintf( "no channels available. " ); +static qboolean gtv_wait_stop( mvd_t *mvd ) { + int usage; + + // see how many frames are buffered + if( mvd->num_packets >= mvd->min_packets ) { + Com_Printf( "[%s] Waiting finished, reading...\n", mvd->name ); + mvd->state = MVD_READING; + return qtrue; } - count = List_Count( &mvd_waitingRoom.udpClients ); - if( count ) { - SV_HttpPrintf( "Waiting room has %d spectator%s.</p>", - count, count == 1 ? "" : "s" ); - } else { - SV_HttpPrintf( "Waiting room is empty.</p>" ); + // see how much data is buffered + usage = FIFO_Percent( &mvd->delay ); + if( usage >= mvd_wait_percent->value ) { + Com_Printf( "[%s] Buffering finished, reading...\n", mvd->name ); + mvd->state = MVD_READING; + return qtrue; } - - if( !LIST_EMPTY( &mvd_ready ) ) { - SV_HttpPrintf( - "<table border=\"1\"><tr>" - "<th>ID</th><th>Name</th><th>Map</th><th>Specs</th><th>Players</th></tr>" ); - - LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) { - SV_HttpPrintf( "<tr><td>" ); - if( sv_mvd_enable->integer ) { - SV_HttpPrintf( "<a href=\"http://%s/mvdstream/%d\">%d</a>", - http_host, mvd->id, mvd->id ); - } else { - SV_HttpPrintf( "%d", mvd->id ); - } - SV_HttpPrintf( "</td><td>" ); - - Q_EscapeMarkup( buffer, mvd->name, sizeof( buffer ) ); - if( sv_mvd_enable->integer ) { - SV_HttpPrintf( "<a href=\"http://%s/mvdstream/%d\">%s</a>", - http_host, mvd->id, buffer ); - } else { - SV_HttpPrintf( "%s", buffer ); - } - Q_EscapeMarkup( buffer, mvd->mapname, sizeof( buffer ) ); - count = List_Count( &mvd->udpClients ); - SV_HttpPrintf( "</td><td>%s</td><td>%d</td><td>%d</td></tr>", - buffer, count, mvd->numplayers ); - } - SV_HttpPrintf( "</table>" ); + return qfalse; +} + +// ran out of buffers +static void gtv_wait_start( mvd_t *mvd ) { + int tr = mvd_wait_delay->value * 10; + + // if not connected, kill it + if( !mvd->gtv ) { + MVD_Destroyf( mvd, "End of MVD stream reached" ); } - SV_HttpPrintf( - "<p><a href=\"quake2://%s\">Join this server</a></p>", http_host ); + Com_Printf( "[%s] Buffering data...\n", mvd->name ); - SV_HttpFooter(); + // notify spectators + if( Com_IsDedicated() ) { + MVD_BroadcastPrintf( mvd, PRINT_HIGH, 0, + "[MVD] Buffering data, please wait...\n" ); + } - SV_HttpDrop( http_client, "200 OK" ); + mvd->state = MVD_WAITING; + mvd->min_packets = 50 + 10 * mvd->underflows; + if( mvd->min_packets > tr ) { + mvd->min_packets = tr; + } + mvd->underflows++; } -static mvd_t *MVD_SetStream( const char *uri ) { - mvd_t *mvd; - int id; +static qboolean gtv_read_frame( mvd_t *mvd ) { + uint16_t msglen; - if( LIST_EMPTY( &mvd_ready ) ) { - SV_HttpReject( "503 Service Unavailable", - "No MVD streams are available on this server." ); - return NULL; + switch( mvd->state ) { + case MVD_WAITING: + if( !gtv_wait_stop( mvd ) ) { + return qfalse; + } + break; + case MVD_READING: + if( !mvd->num_packets ) { + gtv_wait_start( mvd ); + return qfalse; + } + break; + default: + MVD_Destroyf( mvd, "%s: bad mvd->state", __func__ ); } - if( *uri == '/' ) { - uri++; + // NOTE: if we got here, delay buffer MUST contain + // at least one complete, non-empty packet + + // parse msglen + if( FIFO_Read( &mvd->delay, &msglen, 2 ) != 2 ) { + MVD_Destroyf( mvd, "%s: partial data", __func__ ); } - if( *uri == 0 ) { - if( List_Count( &mvd_ready ) == 1 ) { - return LIST_FIRST( mvd_t, &mvd_ready, ready ); - } - strcpy( http_header, "Cache-Control: no-cache\r\n" ); - SV_HttpReject( "300 Multiple Choices", - "Please specify an exact stream ID." ); - return NULL; + msglen = LittleShort( msglen ); + if( msglen < 1 || msglen > MAX_MSGLEN ) { + MVD_Destroyf( mvd, "%s: invalid msglen", __func__ ); } - id = atoi( uri ); - LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) { - if( mvd->id == id ) { - return mvd; - } + // read this message + if( !FIFO_ReadMessage( &mvd->delay, msglen ) ) { + MVD_Destroyf( mvd, "%s: partial data", __func__ ); } - SV_HttpReject( "404 Not Found", - "Requested MVD stream was not found on this server." ); - return NULL; -} + // decrement buffered packets counter + mvd->num_packets--; -void MVD_GetStream( const char *uri ) { - mvd_t *mvd; + // parse it + MVD_ParseMessage( mvd ); + return qtrue; +} - mvd = MVD_SetStream( uri ); - if( !mvd ) { - return; +static void write_stream( gtv_t *gtv, void *data, size_t len ) { + if( !FIFO_TryWrite( >v->stream.send, data, len ) ) { + gtv_destroyf( gtv, "Send buffer overflowed" ); } - 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; - } + // don't timeout + gtv->last_sent = svs.realtime; +} - List_Append( &mvd->tcpClients, &http_client->mvdEntry ); - http_client->mvd = mvd; +static void write_message( gtv_t *gtv, gtv_clientop_t op ) { + byte header[3]; + size_t len = msg_write.cursize + 1; - SV_MvdInitStream(); + header[0] = len & 255; + header[1] = ( len >> 8 ) & 255; + header[2] = op; + write_stream( gtv, header, sizeof( header ) ); - MVD_SendGamestate( http_client ); + write_stream( gtv, msg_write.data, msg_write.cursize ); } -void MVD_ChangeLevel( mvd_t *mvd ) { - udpClient_t *client; +#if USE_ZLIB +static voidpf gtv_zalloc OF(( voidpf opaque, uInt items, uInt size )) { + return MVD_Malloc( items * size ); +} - if( sv.state != ss_broadcast ) { - MVD_Spawn_f(); // the game is just starting - return; +static void gtv_zfree OF(( voidpf opaque, voidpf address )) { + Z_Free( address ); +} +#endif + +static void parse_hello( gtv_t *gtv ) { + int flags; + + if( gtv->state >= GTV_CONNECTED ) { + gtv_destroyf( gtv, "Duplicated server hello" ); } - // cause all UDP clients to reconnect - MSG_WriteByte( svc_stufftext ); - MSG_WriteString( va( "changing map=%s; reconnect\n", mvd->mapname ) ); + flags = MSG_ReadLong(); - LIST_FOR_EACH( udpClient_t, client, &mvd->udpClients, entry ) { - if( mvd->intermission && client->cl->state == cs_spawned ) { - // make them switch to previous target instead of MVD dummy - client->target = client->oldtarget; - client->oldtarget = NULL; +#if USE_ZLIB + if( flags & GTF_DEFLATE ) { + if( !gtv->z_str.state ) { + gtv->z_str.zalloc = gtv_zalloc; + gtv->z_str.zfree = gtv_zfree; + if( inflateInit( >v->z_str ) != Z_OK ) { + gtv_destroyf( gtv, "inflateInit() failed: %s", + gtv->z_str.msg ); + } + } + if( !gtv->z_buf.data ) { + gtv->z_buf.data = MVD_Malloc( MAX_MSGLEN ); + gtv->z_buf.size = MAX_MSGLEN; } - SV_ClientReset( client->cl ); - client->cl->spawncount = mvd->servercount; - SV_ClientAddMessage( client->cl, MSG_RELIABLE ); + gtv->z_act = qtrue; // remaining data is deflated } +#endif - SZ_Clear( &msg_write ); + gtv->state = GTV_CONNECTED; - mvd->intermission = qfalse; + Com_Printf( "[%s] Sending stream request...\n", gtv->name ); - SV_SendAsyncPackets(); + // send stream request + write_message( gtv, GTC_STREAM_START ); } -static void MVD_PlayNext( mvd_t *mvd, string_entry_t *entry ) { - uint32_t magic = 0; +static void parse_stream_start( gtv_t *gtv ) { + if( gtv->state != GTV_CONNECTED ) { + gtv_destroyf( gtv, "Unexpected stream start packet" ); + } - if( !entry ) { - if( mvd->demoloop ) { - if( --mvd->demoloop == 0 ) { - MVD_Destroyf( mvd, "End of play list reached" ); - return; - } + // create the channel + if( !gtv->mvd ) { + mvd_t *mvd = create_channel( gtv ); + + // allocate delay buffer + mvd->delay.data = MVD_Malloc( MAX_MSGLEN * 2 ); + mvd->delay.size = MAX_MSGLEN * 2; + mvd->read_frame = gtv_read_frame; + + gtv->mvd = mvd; + } else { + if( Com_IsDedicated() ) { + // notify spectators + MVD_BroadcastPrintf( gtv->mvd, PRINT_HIGH, 0, + "[MVD] Stream has been resumed.\n" ); } - entry = mvd->demohead; + // reset delay to default + gtv->mvd->min_packets = mvd_wait_delay->value * 10; + gtv->mvd->underflows = 0; } - if( mvd->demoplayback ) { - FS_FCloseFile( mvd->demoplayback ); - mvd->demoplayback = 0; - } + Com_Printf( "[%s] Stream start marker received.\n", gtv->name ); + + gtv->state = GTV_ACTIVE; +} - FS_FOpenFile( entry->string, &mvd->demoplayback, FS_MODE_READ ); - if( !mvd->demoplayback ) { - MVD_Destroyf( mvd, "Couldn't reopen %s", entry->string ); +static void parse_stream_stop( gtv_t *gtv ) { + if( gtv->state < GTV_ACTIVE ) { + gtv_destroyf( gtv, "Unexpected stream stop packet" ); } - if( FS_Read( &magic, 4, mvd->demoplayback ) != 4 ) { - MVD_Destroyf( mvd, "Couldn't read magic out of %s", entry->string ); + if( gtv->mvd && Com_IsDedicated() && gtv->state == GTV_ACTIVE ) { + // notify spectators + MVD_BroadcastPrintf( gtv->mvd, PRINT_HIGH, 0, + "[MVD] Stream has been suspended.\n" ); } - if( ( ( LittleLong( magic ) & 0xe0ffffff ) == 0x00088b1f ) ) { - if( !FS_FilterFile( mvd->demoplayback ) ) { - MVD_Destroyf( mvd, "Couldn't install gzip filter on %s", entry->string ); - } - if( FS_Read( &magic, 4, mvd->demoplayback ) != 4 ) { - MVD_Destroyf( mvd, "Couldn't read magic out of %s", entry->string ); - } + + Com_Printf( "[%s] Stream stop marker received.\n", gtv->name ); + + gtv->state = GTV_CONNECTED; +} + +static void parse_stream_data( gtv_t *gtv ) { + mvd_t *mvd = gtv->mvd; + + if( gtv->state < GTV_ACTIVE ) { + gtv_destroyf( gtv, "Unexpected stream data packet" ); } - if( magic != MVD_MAGIC ) { - MVD_Destroyf( mvd, "%s is not a MVD2 file", entry->string ); + + // ignore if still recovering from overflow + if( gtv->state == GTV_OVERFLOWED ) { + msg_read.readcount = msg_read.cursize; + return; } - Com_Printf( "[%s] Reading from %s\n", mvd->name, entry->string ); + if( !mvd->state ) { + // parse it in place + MVD_ParseMessage( mvd ); + } else { + byte *data = msg_read.data + 1; + size_t len = msg_read.cursize - 1; + uint16_t msglen; + + // see if this packet fits + if( FIFO_Write( &mvd->delay, NULL, len + 2 ) != len + 2 ) { + if( mvd->state == MVD_WAITING ) { + // if delay buffer overflowed in waiting state, + // something is seriously wrong, disconnect for safety + gtv_destroyf( gtv, "Delay buffer overflowed in waiting state" ); + } + + // oops, overflowed + Com_Printf( "[%s] Delay buffer overflowed!\n", gtv->name ); - // reset state - mvd->demoentry = entry; + if( Com_IsDedicated() ) { + // notify spectators + MVD_BroadcastPrintf( mvd, PRINT_HIGH, 0, + "[MVD] Delay buffer overflowed!\n" ); + } - // set channel address - Q_strlcpy( mvd->address, COM_SkipPath( entry->string ), sizeof( mvd->address ) ); + // clear entire delay buffer + FIFO_Clear( &mvd->delay ); + mvd->state = MVD_WAITING; + mvd->min_packets = 50; + mvd->overflows++; + + // request restart of the stream + write_message( gtv, GTC_STREAM_STOP ); + write_message( gtv, GTC_STREAM_START ); + + // ignore any pending data + gtv->state = GTV_OVERFLOWED; + return; + } + + // write it into delay buffer + msglen = LittleShort( len ); + FIFO_Write( &mvd->delay, &msglen, 2 ); + FIFO_Write( &mvd->delay, data, len ); + + // increment buffered packets counter + mvd->num_packets++; + + msg_read.readcount = msg_read.cursize; + } } -void MVD_Finish( mvd_t *mvd, const char *reason ) { - Com_Printf( "[%s] %s\n", mvd->name, reason ); +static void send_hello( gtv_t *gtv ) { + int flags, maxbuf; - if( mvd->demoentry ) { - MVD_PlayNext( mvd, mvd->demoentry->next ); - mvd->state = MVD_PREPARING; - List_Delete( &mvd_ready ); + flags = 0; +#if USE_ZLIB + flags |= GTF_DEFLATE; +#endif + + if( gtv->mvd ) { + maxbuf = gtv->mvd->min_packets - 10; } else { - MVD_Destroy( mvd ); + maxbuf = mvd_wait_delay->value * 10 - 10; + } + if( maxbuf < 0 ) { + maxbuf = 0; } - longjmp( mvd_jmpbuf, -1 ); + MSG_WriteShort( GTV_PROTOCOL_VERSION ); + MSG_WriteLong( flags ); + MSG_WriteShort( maxbuf ); + MSG_WriteLong( 0 ); + MSG_WriteString( NULL ); + MSG_WriteString( com_version->string ); + write_message( gtv, GTC_HELLO ); + SZ_Clear( &msg_write ); } -static void MVD_ReadDemo( mvd_t *mvd ) { - byte *data; - size_t length, read, total = 0; - do { - data = FIFO_Reserve( &mvd->stream.recv, &length ); - if( !length ) { - return; +static qboolean parse_message( gtv_t *gtv, fifo_t *fifo ) { + uint32_t magic; + uint16_t msglen; + int cmd; + + // check magic + if( gtv->state < GTV_PREPARING ) { + if( !FIFO_TryRead( fifo, &magic, 4 ) ) { + return qfalse; + } + if( magic != MVD_MAGIC ) { + gtv_destroyf( gtv, "Not a MVD/GTV stream" ); } - read = FS_Read( data, length, mvd->demoplayback ); - FIFO_Commit( &mvd->stream.recv, read ); - total += read; - } while( read ); + gtv->state = GTV_PREPARING; - if( !total ) { - MVD_Dropf( mvd, "End of MVD file reached" ); + // send client hello + send_hello( gtv ); + } + + // parse msglen + if( !gtv->msglen ) { + if( !FIFO_TryRead( fifo, &msglen, 2 ) ) { + return qfalse; + } + msglen = LittleShort( msglen ); + if( !msglen ) { + gtv_dropf( gtv, "End of MVD/GTV stream" ); + } + if( msglen > MAX_MSGLEN ) { + gtv_destroyf( gtv, "Oversize message" ); + } + gtv->msglen = msglen; } -} -static htcoding_t MVD_FindCoding( const char *name ) { - if( !Q_stricmp( name, "identity" ) ) { - return HTTP_CODING_NONE; + // read this message + if( !FIFO_ReadMessage( fifo, gtv->msglen ) ) { + return qfalse; } - if( !Q_stricmp( name, "gzip" ) ) { - return HTTP_CODING_GZIP; + + gtv->msglen = 0; + + cmd = MSG_ReadByte(); + + if( mvd_shownet->integer == -1 ) { + Com_Printf( "[%"PRIz"]%d ", msg_read.cursize, cmd ); } - if( !Q_stricmp( name, "x-gzip" ) ) { - return HTTP_CODING_GZIP; + + switch( cmd ) { + case GTS_HELLO: + parse_hello( gtv ); + break; + case GTS_PONG: + break; + case GTS_STREAM_START: + parse_stream_start( gtv ); + break; + case GTS_STREAM_STOP: + parse_stream_stop( gtv ); + break; + case GTS_STREAM_DATA: + parse_stream_data( gtv ); + break; + case GTS_ERROR: + gtv_destroyf( gtv, "Server side error occured." ); + break; + case GTS_BADREQUEST: + gtv_destroyf( gtv, "Server refused to process our request." ); + break; + case GTS_NOACCESS: + gtv_destroyf( gtv, + "You don't have permission to access " + "MVD/GTV stream on this server." ); + break; + case GTS_DISCONNECT: + gtv_destroyf( gtv, "Server has been shut down." ); + break; + case GTS_RECONNECT: + gtv_dropf( gtv, "Server has been restarted." ); + break; + default: + gtv_destroyf( gtv, "Unknown command byte" ); } - if( !Q_stricmp( name, "deflate" ) ) { - return HTTP_CODING_DEFLATE; + + if( msg_read.readcount > msg_read.cursize ) { + gtv_destroyf( gtv, "Read past end of message" ); } - return HTTP_CODING_UNKNOWN; + + gtv->last_rcvd = svs.realtime; // don't timeout + return qtrue; } -static qboolean MVD_ParseResponse( mvd_t *mvd ) { - char key[MAX_TOKEN_CHARS]; - char *p, *token; - const char *line; - byte *b, *data; - size_t length; +#if USE_ZLIB +static int inflate_stream( fifo_t *dst, fifo_t *src, z_streamp z ) { + byte *data; + size_t avail_in, avail_out; + int ret = Z_BUF_ERROR; - while( 1 ) { - data = FIFO_Peek( &mvd->stream.recv, &length ); - if( !length ) { + do { + data = FIFO_Peek( src, &avail_in ); + if( !avail_in ) { break; } - if( ( b = memchr( data, '\n', length ) ) != NULL ) { - length = b - data + 1; - } - if( mvd->responseLength + length > MAX_NET_STRING - 1 ) { - MVD_Dropf( mvd, "Response line exceeded maximum length" ); + z->next_in = data; + z->avail_in = ( uInt )avail_in; + + data = FIFO_Reserve( dst, &avail_out ); + if( !avail_out ) { + break; } + z->next_out = data; + z->avail_out = ( uInt )avail_out; - memcpy( mvd->response + mvd->responseLength, data, length ); - mvd->responseLength += length; - mvd->response[mvd->responseLength] = 0; + ret = inflate( z, Z_SYNC_FLUSH ); - FIFO_Decommit( &mvd->stream.recv, length ); + FIFO_Decommit( src, avail_in - z->avail_in ); + FIFO_Commit( dst, avail_out - z->avail_out ); + } while( ret == Z_OK ); - if( !b ) { - continue; - } + return ret; +} - line = mvd->response; - mvd->responseLength = 0; - - if( !mvd->statusCode ) { - // parse version - token = COM_SimpleParse( &line, NULL ); - if( !token[0] ) { - continue; // empty line? - } - if( strncmp( token, "HTTP/", 5 ) ) { - MVD_Dropf( mvd, "Malformed HTTP version" ); - } +static void inflate_more( gtv_t *gtv ) { + int ret = inflate_stream( >v->z_buf, >v->stream.recv, >v->z_str ); - // parse status code - token = COM_SimpleParse( &line, NULL ); - mvd->statusCode = atoi( token ); - if( !mvd->statusCode ) { - MVD_Dropf( mvd, "Malformed HTTP status code" ); - } + switch( ret ) { + case Z_BUF_ERROR: + case Z_OK: + break; + case Z_STREAM_END: + inflateReset( >v->z_str ); + gtv->z_act = qfalse; + break; + default: + gtv_destroyf( gtv, "inflate() failed: %s", gtv->z_str.msg ); + } +} +#endif - // parse reason phrase - if( line ) { - while( *line && *line <= ' ' ) { - line++; - } - Q_ClearStr( mvd->statusText, line, MAX_QPATH ); - } - } else { - // parse header fields - token = COM_SimpleParse( &line, NULL ); - if( !token[0] ) { - return qtrue; // end of header - } - strcpy( key, token ); - p = strchr( key, ':' ); - if( !p ) { - MVD_Dropf( mvd, "Malformed HTTP header field" ); - } - *p = 0; - Q_strlwr( key ); - - token = COM_SimpleParse( &line, NULL ); - if( !strcmp( key, "content-type" ) ) { - } else if( !strcmp( key, "content-encoding" ) ) { - mvd->contentCoding = MVD_FindCoding( token ); - } else if( !strcmp( key, "content-length" ) ) { - mvd->contentLength = atoi( token ); - } else if( !strcmp( key, "transfer-encoding" ) ) { - } else if( !strcmp( key, "location" ) ) { - } - } +static neterr_t run_connect( gtv_t *gtv ) { + neterr_t ret; + uint32_t magic; + + // run connection + if( ( ret = NET_RunConnect( >v->stream ) ) != NET_OK ) { + return ret; } - return qfalse; + Com_Printf( "[%s] Connected to the game server!\n", gtv->name ); + + // allocate buffers + if( !gtv->data ) { + gtv->data = MVD_Malloc( MAX_MSGLEN + 128 ); + } + gtv->stream.recv.data = gtv->data; + gtv->stream.recv.size = MAX_MSGLEN; + gtv->stream.send.data = gtv->data + MAX_MSGLEN; + gtv->stream.send.size = 128; + + // don't timeout + gtv->last_rcvd = svs.realtime; + + // send magic + magic = MVD_MAGIC; + write_stream( gtv, &magic, 4 ); + + return NET_OK; } -static inline float MVD_BufferPercent( mvd_t *mvd ) { - size_t usage = FIFO_Usage( &mvd->stream.recv ); - size_t size = mvd->stream.recv.size; +static neterr_t run_stream( gtv_t *gtv ) { + neterr_t ret; + + // run network stream + if( ( ret = NET_RunStream( >v->stream ) ) != NET_OK ) { + return ret; + } -#ifdef USE_ZLIB - usage += FIFO_Usage( &mvd->zbuf ); - size += mvd->zbuf.size; +#if USE_ZLIB + if( gtv->z_act ) { + do { + // decompress more data + if( gtv->z_act ) { + inflate_more( gtv ); + } + } while( parse_message( gtv, >v->z_buf ) ); + } else #endif + while( parse_message( gtv, >v->stream.recv ) ) + ; - if( !size ) { - return 0; + if( mvd_shownet->integer == -1 ) { + Com_Printf( "\n" ); } - return usage * 100.0f / size; + return NET_OK; } -int MVD_Frame( void ) { - mvd_t *mvd, *next; - neterr_t ret; - float usage; - int connections = 0; +static void check_timeouts( gtv_t *gtv ) { + unsigned timeout = mvd_timeout->value * 1000; - LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_channels, entry ) { - if( mvd->state <= MVD_DEAD || mvd->state >= MVD_DISCONNECTED ) { - continue; - } + // drop if no data has been received for too long + if( svs.realtime - gtv->last_rcvd > timeout ) { + gtv_dropf( gtv, "Server connection timed out." ); + } - if( setjmp( mvd_jmpbuf ) ) { - continue; + // ping if no data has been sent for too long + if( gtv->state < GTV_CONNECTED ) { + return; + } + if( svs.realtime - gtv->last_sent > 60000 ) { + write_message( gtv, GTC_PING ); + } +} + +static qboolean check_reconnect( gtv_t *gtv ) { + netadr_t adr; + + if( svs.realtime - gtv->retry_time < gtv->retry_backoff ) { + return qfalse; + } + + Com_Printf( "[%s] Attempting to reconnect to %s...\n", + gtv->name, gtv->address ); + + gtv->state = GTV_CONNECTING; + + // don't timeout + gtv->last_sent = gtv->last_rcvd = svs.realtime; + + if( !NET_StringToAdr( gtv->address, &adr, PORT_SERVER ) ) { + gtv_dropf( gtv, "Unable to lookup %s\n", gtv->address ); + } + + if( NET_Connect( &adr, >v->stream ) == NET_ERROR ) { + gtv_dropf( gtv, "%s to %s\n", NET_ErrorString(), + NET_AdrToString( &adr ) ); + } + + return qtrue; +} + +static void gtv_run( gtv_t *gtv ) { + neterr_t ret = NET_AGAIN; + + // check if it is time to reconnect + if( !gtv->state ) { + if( !check_reconnect( gtv ) ) { + return; } + } - if( mvd->demoentry ) { - if( mvd->demoplayback ) { - MVD_ReadDemo( mvd ); - } - if( mvd->state == MVD_PREPARING ) { - MVD_Parse( mvd ); - } - continue; + // run network stream + switch( gtv->stream.state ) { + case NS_CONNECTING: + ret = run_connect( gtv ); + if( ret == NET_AGAIN ) { + return; + } + if( ret == NET_OK ) { + case NS_CONNECTED: + ret = run_stream( gtv ); } + break; + default: + return; + } - connections++; + switch( ret ) { + case NET_AGAIN: + case NET_OK: + check_timeouts( gtv ); + break; + case NET_ERROR: + gtv_dropf( gtv, "%s to %s", NET_ErrorString(), + NET_AdrToString( >v->stream.address ) ); + break; + case NET_CLOSED: + gtv_dropf( gtv, "Server has closed connection." ); + break; + } +} - // process network stream - ret = NET_Run( &mvd->stream ); - switch( ret ) { - case NET_AGAIN: - // check timeout - if( mvd->lastReceived > svs.realtime ) { - mvd->lastReceived = svs.realtime; - } - if( svs.realtime - mvd->lastReceived > mvd_timeout->value * 1000 ) { - MVD_Dropf( mvd, "Connection timed out" ); - } - continue; - case NET_ERROR: - MVD_Dropf( mvd, "%s to %s", NET_ErrorString(), - NET_AdrToString( &mvd->stream.address ) ); - case NET_CLOSED: - MVD_Dropf( mvd, "Connection closed" ); - case NET_OK: - break; +static void gtv_destroy( gtv_t *gtv ) { + mvd_t *mvd = gtv->mvd; + + // any associated MVD channel is orphaned + if( mvd ) { + mvd->gtv = NULL; + if( !mvd->state ) { + // free it here, since it is not yet + // added to global channel list + MVD_Free( mvd ); + } else if( Com_IsDedicated() ) { + // notify spectators + MVD_BroadcastPrintf( mvd, PRINT_HIGH, 0, + "[MVD] Disconnected from the game server!\n" ); } + } - // don't timeout - mvd->lastReceived = svs.realtime; + // make sure network connection is closed + NET_Close( >v->stream ); - // run MVD state machine - switch( mvd->state ) { - case MVD_CONNECTING: - Com_Printf( "[%s] Connected, awaiting response...\n", mvd->name ); - mvd->state = MVD_CONNECTED; - // fall through - case MVD_CONNECTED: - if( !MVD_ParseResponse( mvd ) ) { - continue; - } - if( mvd->statusCode != 200 ) { - MVD_Dropf( mvd, "HTTP request failed: %d %s", - mvd->statusCode, mvd->statusText ); - } - switch( mvd->contentCoding ) { - case HTTP_CODING_NONE: - break; + // unlink from the list of connections + List_Remove( >v->entry ); + + // free all memory buffers #if USE_ZLIB - case HTTP_CODING_GZIP: - case HTTP_CODING_DEFLATE: - if( inflateInit2( &mvd->z, 47 ) != Z_OK ) { - MVD_Dropf( mvd, "inflateInit2() failed: %s", mvd->z.msg ); - } - mvd->zbuf.data = MVD_Malloc( MAX_MSGLEN * 2 ); - mvd->zbuf.size = MAX_MSGLEN * 2; - break; + inflateEnd( >v->z_str ); + Z_Free( gtv->z_buf.data ); #endif - default: - MVD_Dropf( mvd, "Unsupported content encoding: %d", - mvd->contentCoding ); - break; - } - Com_Printf( "[%s] Got response, awaiting gamestate...\n", mvd->name ); - mvd->state = MVD_CHECKING; - // fall through - case MVD_CHECKING: - case MVD_PREPARING: - MVD_Parse( mvd ); - if( mvd->state <= MVD_PREPARING ) { - break; - } - // fall through - case MVD_WAITING: - if( svs.realtime - mvd->waitTime >= mvd->waitDelay ) { - Com_Printf( "[%s] Waiting finished, reading...\n", mvd->name ); - mvd->state = MVD_READING; - break; - } - usage = MVD_BufferPercent( mvd ); - if( usage >= mvd_wait_percent->value ) { - Com_Printf( "[%s] Buffering finished, reading...\n", mvd->name ); - mvd->state = MVD_READING; - } - break; - default: - break; + Z_Free( gtv->data ); + Z_Free( gtv ); +} + +static void gtv_drop( gtv_t *gtv ) { + if( gtv->stream.state < NS_CONNECTED ) { + gtv->retry_backoff += 15*1000; + } else { + // notify spectators + if( Com_IsDedicated() && gtv->mvd ) { + MVD_BroadcastPrintf( gtv->mvd, PRINT_HIGH, 0, + "[MVD] Lost connection to the game server!\n" ); + } + + if( gtv->state == GTV_CONNECTED ) { + gtv->retry_backoff = GTV_DEFAULT_BACKOFF; + } else { + gtv->retry_backoff += 30*1000; } } - return connections; + if( gtv->retry_backoff > GTV_MAXIMUM_BACKOFF ) { + gtv->retry_backoff = GTV_MAXIMUM_BACKOFF; + } + Com_Printf( "[%s] Reconnecting in %d seconds.\n", + gtv->name, gtv->retry_backoff / 1000 ); + + NET_Close( >v->stream ); +#if USE_ZLIB + inflateReset( >v->z_str ); + FIFO_Clear( >v->z_buf ); + gtv->z_act = qfalse; +#endif + gtv->msglen = 0; + gtv->state = GTV_DISCONNECTED; + gtv->retry_time = svs.realtime; } @@ -830,43 +1121,6 @@ OPERATOR COMMANDS ==================================================================== */ -mvd_t *MVD_SetChannel( int arg ) { - char *s = Cmd_Argv( arg ); - mvd_t *mvd; - int id; - - if( LIST_EMPTY( &mvd_channels ) ) { - Com_Printf( "No active channels.\n" ); - return NULL; - } - - if( !*s ) { - if( List_Count( &mvd_channels ) == 1 ) { - return LIST_FIRST( mvd_t, &mvd_channels, entry ); - } - Com_Printf( "Please specify an exact channel ID.\n" ); - return NULL; - } - - if( COM_IsUint( s ) ) { - id = atoi( s ); - LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { - if( mvd->id == id ) { - return mvd; - } - } - } else { - LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { - if( !strcmp( mvd->name, s ) ) { - return mvd; - } - } - } - - Com_Printf( "No such channel ID: %s\n", s ); - return NULL; -} - void MVD_Spawn_f( void ) { SV_InitGame( qtrue ); @@ -884,7 +1138,7 @@ void MVD_Spawn_f( void ) { sv.state = ss_broadcast; } -void MVD_ListChannels_f( void ) { +static void MVD_ListChannels_f( void ) { mvd_t *mvd; int usage; @@ -893,37 +1147,62 @@ void MVD_ListChannels_f( void ) { return; } - Com_Printf( "id name map spc plr stat buf address \n" - "-- ------------ -------- --- --- ---- --- --------------\n" ); + Com_Printf( "id name map spc plr stat buf pack address \n" + "-- ------------ -------- --- --- ---- --- ---- --------------\n" ); LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { Com_Printf( "%2d %-12.12s %-8.8s %3d %3d ", mvd->id, mvd->name, mvd->mapname, - List_Count( &mvd->udpClients ), mvd->numplayers ); + List_Count( &mvd->clients ), mvd->numplayers ); 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: + } + usage = FIFO_Percent( &mvd->delay ); + Com_Printf( " %3d %4u %s\n", usage, mvd->num_packets, + mvd->gtv ? mvd->gtv->address : "" ); + } +} + +static void MVD_ListGtvs_f( void ) { + gtv_t *gtv; + + if( LIST_EMPTY( &mvd_gtvs ) ) { + Com_Printf( "No GTV connections.\n" ); + return; + } + + Com_Printf( "id name stat address \n" + "-- ------------ ---- --------------\n" ); + + LIST_FOR_EACH( gtv_t, gtv, &mvd_gtvs, entry ) { + Com_Printf( "%2d %-12.12s ", gtv->id, gtv->name ); + switch( gtv->state ) { + case GTV_DISCONNECTED: Com_Printf( "DISC" ); break; + case GTV_CONNECTING: + case GTV_PREPARING: + Com_Printf( "CNCT" ); + break; + case GTV_CONNECTED: + Com_Printf( "IDLE" ); + break; + case GTV_ACTIVE: + Com_Printf( "STRM" ); + break; + case GTV_OVERFLOWED: + Com_Printf( "OVER" ); + break; } - usage = MVD_BufferPercent( mvd ); - Com_Printf( " %3d %s\n", usage, mvd->address ); + Com_Printf( " %s\n", NET_AdrToString( >v->stream.address ) ); } } @@ -951,6 +1230,76 @@ void MVD_StreamedStop_f( void ) { Com_Printf( "[%s] Stopped recording.\n", mvd->name ); } +static void MVD_EmitGamestate( mvd_t *mvd ) { + char *string; + int i; + edict_t *ent; + player_state_t *ps; + size_t length; + int flags, portalbytes; + byte portalbits[MAX_MAP_AREAS/8]; + + // send the serverdata + MSG_WriteByte( mvd_serverdata ); + MSG_WriteLong( PROTOCOL_VERSION_MVD ); + MSG_WriteShort( PROTOCOL_VERSION_MVD_CURRENT ); + 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 baseline frame + portalbytes = CM_WritePortalBits( &sv.cm, portalbits ); + MSG_WriteByte( portalbytes ); + MSG_WriteData( portalbits, portalbytes ); + + // send base player states + for( i = 0; i < mvd->maxclients; i++ ) { + ps = &mvd->players[i].ps; + flags = 0; + if( !PPS_INUSE( ps ) ) { + flags |= MSG_PS_REMOVE; + } + MSG_WriteDeltaPlayerstate_Packet( NULL, ps, i, flags ); + } + MSG_WriteByte( CLIENTNUM_NONE ); + + // send base entity states + for( i = 1; i < mvd->pool.num_edicts; i++ ) { + ent = &mvd->edicts[i]; + flags = 0; + if( i <= mvd->maxclients ) { + ps = &mvd->players[ i - 1 ].ps; + if( PPS_INUSE( ps ) && ps->pmove.pm_type == PM_NORMAL ) { + flags |= MSG_ES_FIRSTPERSON; + } + } + if( !ent->inuse ) { + flags |= MSG_ES_REMOVE; + } + MSG_WriteDeltaEntity( NULL, &ent->s, flags ); + } + MSG_WriteShort( 0 ); + + // TODO: write private layouts/configstrings +} + extern const cmd_option_t o_mvdrecord[]; void MVD_StreamedRecord_f( void ) { @@ -958,6 +1307,7 @@ void MVD_StreamedRecord_f( void ) { fileHandle_t f; mvd_t *mvd; uint32_t magic; + uint16_t msglen; qboolean gzip = qfalse; int c; size_t len; @@ -972,6 +1322,8 @@ void MVD_StreamedRecord_f( void ) { case 'z': gzip = qtrue; break; + default: + return; } } @@ -991,11 +1343,6 @@ void MVD_StreamedRecord_f( void ) { return; } - if( mvd->state < MVD_WAITING ) { - Com_Printf( "[%s] Channel not ready.\n", mvd->name ); - return; - } - // // open the demo file // @@ -1012,7 +1359,7 @@ void MVD_StreamedRecord_f( void ) { return; } - Com_Printf( "[%s] Recording into %s.\n", mvd->name, buffer ); + Com_Printf( "[%s] Recording into %s\n", mvd->name, buffer ); if( gzip ) { FS_FilterFile( f ); @@ -1022,197 +1369,119 @@ void MVD_StreamedRecord_f( void ) { MVD_EmitGamestate( mvd ); + // write magic magic = MVD_MAGIC; - FS_Write( &magic, 4, mvd->demorecording ); - FS_Write( msg_write.data, msg_write.cursize, mvd->demorecording ); + FS_Write( &magic, 4, f ); + + // write gamestate + msglen = LittleShort( msg_write.cursize ); + FS_Write( &msglen, 2, f ); + FS_Write( msg_write.data, msg_write.cursize, f ); SZ_Clear( &msg_write ); } static const cmd_option_t o_mvdconnect[] = { { "h", "help", "display this message" }, - { "e:string", "encoding", "specify default encoding as <string>" }, - { "i:number", "id", "specify remote stream ID as <number>" }, { "n:string", "name", "specify channel name as <string>" }, - { "r:string", "referer", "specify referer as <string> in HTTP request" }, + //{ "u:string", "user", "specify username as <string>" }, + //{ "p:string", "pass", "specify password as <string>" }, { NULL } }; -void MVD_Connect_c( genctx_t *ctx, int argnum ) { +static void MVD_Connect_c( genctx_t *ctx, int argnum ) { Cmd_Option_c( o_mvdconnect, Com_Address_g, ctx, argnum ); } /* ============== MVD_Connect_f - -[http://]host[:port][/resource] ============== */ -void MVD_Connect_f( void ) { +static 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 *id = "", *name = NULL, *referer = NULL, *host, *p; - htcoding_t coding = HTTP_CODING_NONE; - mvd_t *mvd; - uint16_t port; + char *name = NULL; + gtv_t *gtv; int c; while( ( c = Cmd_ParseOptions( o_mvdconnect ) ) != -1 ) { switch( c ) { case 'h': - Cmd_PrintUsage( o_mvdconnect, "<uri>" ); - Com_Printf( "Create new MVD channel and connect to URI.\n" ); + Cmd_PrintUsage( o_mvdconnect, "<address[:port]>" ); + Com_Printf( "Connect to the specified MVD/GTV server.\n" ); Cmd_PrintHelp( o_mvdconnect ); - Com_Printf( - "Full URI syntax: [http://][user:pass@]<host>[:port][/resource]\n" - "If resource is given, default port is 80 and stream ID is ignored.\n" - "Otherwise, default port is %d and stream ID is undefined.\n\n" -#if USE_ZLIB - "Accepted content encodings: gzip, deflate.\n" -#endif - , PORT_SERVER ); return; - case 'e': - coding = MVD_FindCoding( cmd_optarg ); - if( coding == HTTP_CODING_UNKNOWN ) { - Com_Printf( "Unknown content encoding: %s.\n", cmd_optarg ); - Cmd_PrintHint(); - return; - } - break; - case 'i': - id = cmd_optarg; - break; case 'n': name = cmd_optarg; break; - case 'r': - referer = cmd_optarg; - break; default: return; } } if( !cmd_optarg[0] ) { - Com_Printf( "Missing URI argument.\n" ); + Com_Printf( "Missing address argument.\n" ); Cmd_PrintHint(); return; } - Cmd_ArgvBuffer( cmd_optind, buffer, sizeof( buffer ) ); - - // skip optional http:// prefix - host = buffer; - if( !strncmp( host, "http://", 7 ) ) { - host += 7; - } - - // parse credentials - p = strchr( host, '@' ); - if( p ) { - *p = 0; - strcpy( credentials, host ); - host = p + 1; - } else { - credentials[0] = 0; - } - - // parse resource - p = strchr( host, '/' ); - if( p ) { - *p = 0; - strcpy( resource, p + 1 ); - port = 80; - } else { - Q_concat( resource, sizeof( resource ), "mvdstream/", id, NULL ); - port = PORT_SERVER; - } - // resolve hostname - if( !NET_StringToAdr( host, &adr, port ) ) { - Com_Printf( "Bad server address: %s\n", host ); + if( !NET_StringToAdr( cmd_optarg, &adr, PORT_SERVER ) ) { + Com_Printf( "Bad server address: %s\n", cmd_optarg ); return; } + // don't allow multiple connections + LIST_FOR_EACH( gtv_t, gtv, &mvd_gtvs, entry ) { + if( NET_IsEqualAdr( &adr, >v->stream.address ) ) { + Com_Printf( "Already connected to %s\n", + NET_AdrToString( &adr ) ); + return; + } + } + + // create new socket and start connecting if( NET_Connect( &adr, &stream ) == NET_ERROR ) { - Com_Printf( "%s to %s\n", NET_ErrorString(), + Com_EPrintf( "%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 ) ); - mvd->id = mvd_chanid++; - mvd->state = MVD_CONNECTING; - mvd->contentCoding = coding; - 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; - mvd->pool.edicts = mvd->edicts; - mvd->pool.edict_size = sizeof( edict_t ); - mvd->pool.max_edicts = MAX_EDICTS; - mvd->pm_type = PM_SPECTATOR; - mvd->lastReceived = svs.realtime; - mvd->waitDelay = mvd_wait_delay->value * 1000; - List_Init( &mvd->udpClients ); - List_Init( &mvd->tcpClients ); - List_Append( &mvd_channels, &mvd->entry ); - List_Init( &mvd->ready ); - List_Init( &mvd->active ); + // create new connection + gtv = MVD_Mallocz( sizeof( *gtv ) ); + gtv->id = mvd_chanid++; + gtv->state = GTV_CONNECTING; + gtv->stream = stream; + gtv->last_sent = gtv->last_rcvd = svs.realtime; + gtv->run = gtv_run; + gtv->drop = gtv_drop; + gtv->destroy = gtv_destroy; + List_Append( &mvd_gtvs, >v->entry ); // set channel name if( name ) { - Q_strlcpy( mvd->name, name, sizeof( mvd->name ) ); + Q_strlcpy( gtv->name, name, sizeof( gtv->name ) ); } else { - Q_snprintf( mvd->name, sizeof( mvd->name ), "net%d", mvd->id ); + Q_snprintf( gtv->name, sizeof( gtv->name ), "net%d", gtv->id ); } - Q_strlcpy( mvd->address, host, sizeof( mvd->address ) ); + Q_strlcpy( gtv->address, cmd_optarg, sizeof( gtv->address ) ); - Com_Printf( "[%s] Connecting to %s...\n", mvd->name, NET_AdrToString( &adr ) ); - - MVD_HttpPrintf( mvd, - "GET /%s HTTP/1.0\r\n" - "Host: %s\r\n" - "User-Agent: " APPLICATION "/" VERSION "\r\n" -#if 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 ); - } - if( referer ) { - MVD_HttpPrintf( mvd, "Referer: %s\r\n", referer ); - } - MVD_HttpPrintf( mvd, "\r\n" ); + Com_Printf( "[%s] Connecting to %s...\n", + gtv->name, NET_AdrToString( &adr ) ); } static void MVD_Disconnect_f( void ) { - mvd_t *mvd; - - mvd = MVD_SetChannel( 1 ); - if( !mvd ) { - return; - } + gtv_t *gtv; - if( mvd->state == MVD_DISCONNECTED ) { - Com_Printf( "[%s] Already disconnected.\n", mvd->name ); + gtv = gtv_set_conn( 1 ); + if( !gtv ) { return; } - Com_Printf( "[%s] Channel was disconnected.\n", mvd->name ); - MVD_Drop( mvd ); + Com_Printf( "[%s] Connection destroyed.\n", gtv->name ); + gtv->destroy( gtv ); } static void MVD_Kill_f( void ) { @@ -1235,17 +1504,21 @@ static void MVD_Pause_f( void ) { return; } + if( !mvd->gtv || !mvd->gtv->demoplayback ) { + Com_Printf( "[%s] Only demo channels can be paused.\n", mvd->name ); + return; + } + switch( mvd->state ) { case MVD_WAITING: - Com_Printf( "[%s] Channel was resumed.\n", mvd->name ); + //Com_Printf( "[%s] Channel was resumed.\n", mvd->name ); mvd->state = MVD_READING; break; case MVD_READING: - Com_Printf( "[%s] Channel was paused.\n", mvd->name ); - MVD_BeginWaiting( mvd ); + //Com_Printf( "[%s] Channel was paused.\n", mvd->name ); + mvd->state = MVD_WAITING; break; default: - Com_Printf( "[%s] Channel is not ready.\n", mvd->name ); break; } } @@ -1305,8 +1578,8 @@ static void MVD_Control_f( void ) { Q_strlcpy( mvd->name, name, sizeof( mvd->name ) ); } if( loop != -1 ) { - Com_Printf( "[%s] Loop count changed to %d.\n", mvd->name, loop ); - mvd->demoloop = loop; + //Com_Printf( "[%s] Loop count changed to %d.\n", mvd->name, loop ); + //mvd->demoloop = loop; } } @@ -1325,12 +1598,12 @@ static void MVD_Play_c( genctx_t *ctx, int argnum ) { Cmd_Option_c( o_mvdplay, MVD_File_g, ctx, argnum ); } -void MVD_Play_f( void ) { +static void MVD_Play_f( void ) { char *name = NULL, *s; char buffer[MAX_OSPATH]; int loop = 1; size_t len; - mvd_t *mvd; + gtv_t *gtv; int c, argc; string_entry_t *entry, *head; int i; @@ -1367,6 +1640,7 @@ void MVD_Play_f( void ) { return; } + // build the playlist head = NULL; for( i = argc - 1; i >= cmd_optind; i-- ) { s = Cmd_Argv( i ); @@ -1384,7 +1658,7 @@ void MVD_Play_f( void ) { } len = strlen( buffer ); - entry = Z_Malloc( sizeof( *entry ) + len ); + entry = MVD_Malloc( sizeof( *entry ) + len ); memcpy( entry->string, buffer, len + 1 ); entry->next = head; head = entry; @@ -1394,52 +1668,47 @@ void MVD_Play_f( void ) { return; } - Z_TagReserve( sizeof( *mvd ) + MAX_MSGLEN * 2, TAG_MVD ); - - mvd = Z_ReservedAllocz( sizeof( *mvd ) ); - mvd->id = mvd_chanid++; - mvd->state = MVD_PREPARING; - mvd->demohead = head; - mvd->demoloop = loop; - mvd->stream.recv.data = Z_ReservedAlloc( MAX_MSGLEN * 2 ); - mvd->stream.recv.size = MAX_MSGLEN * 2; - mvd->pool.edicts = mvd->edicts; - mvd->pool.edict_size = sizeof( edict_t ); - mvd->pool.max_edicts = MAX_EDICTS; - mvd->pm_type = PM_SPECTATOR; - List_Init( &mvd->udpClients ); - List_Init( &mvd->tcpClients ); - List_Append( &mvd_channels, &mvd->entry ); - List_Init( &mvd->ready ); - List_Init( &mvd->active ); + // create new connection + gtv = MVD_Mallocz( sizeof( *gtv ) ); + gtv->id = mvd_chanid++; + gtv->state = GTV_CONNECTED; + gtv->demohead = head; + gtv->demoloop = loop; + gtv->drop = demo_destroy; + gtv->destroy = demo_destroy; + //List_Append( &mvd_gtvs, >v->entry ); // set channel name if( name ) { - Q_strlcpy( mvd->name, name, sizeof( mvd->name ) ); + Q_strlcpy( gtv->name, name, sizeof( gtv->name ) ); } else { - Q_snprintf( mvd->name, sizeof( mvd->name ), "dem%d", mvd->id ); + Q_snprintf( gtv->name, sizeof( gtv->name ), "dem%d", gtv->id ); } - MVD_PlayNext( mvd, mvd->demohead ); + demo_play_next( gtv, gtv->demohead ); } void MVD_Shutdown( void ) { - mvd_t *mvd, *next; + gtv_t *gtv, *gtv_next; + mvd_t *mvd, *mvd_next; - LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_channels, entry ) { - MVD_Disconnect( mvd ); + // kill all connections + LIST_FOR_EACH_SAFE( gtv_t, gtv, gtv_next, &mvd_gtvs, entry ) { + gtv->destroy( gtv ); + } + + // kill all channels + LIST_FOR_EACH_SAFE( mvd_t, mvd, mvd_next, &mvd_channels, entry ) { MVD_Free( mvd ); } + List_Init( &mvd_gtvs ); List_Init( &mvd_channels ); - List_Init( &mvd_ready ); List_Init( &mvd_active ); - if( mvd_clients ) { - Z_Free( mvd_clients ); - mvd_clients = NULL; - } + Z_Free( mvd_clients ); + mvd_clients = NULL; mvd_chanid = 0; @@ -1453,6 +1722,7 @@ static const cmdreg_t c_mvd[] = { { "mvdkill", MVD_Kill_f }, { "mvdspawn", MVD_Spawn_f }, { "mvdchannels", MVD_ListChannels_f }, + { "mvdgtvs", MVD_ListGtvs_f }, { "mvdcontrol", MVD_Control_f }, { "mvdpause", MVD_Pause_f }, @@ -1467,10 +1737,9 @@ MVD_Register */ void MVD_Register( void ) { mvd_shownet = Cvar_Get( "mvd_shownet", "0", 0 ); - mvd_debug = Cvar_Get( "mvd_debug", "0", 0 ); - mvd_timeout = Cvar_Get( "mvd_timeout", "120", 0 ); + mvd_timeout = Cvar_Get( "mvd_timeout", "90", 0 ); mvd_wait_delay = Cvar_Get( "mvd_wait_delay", "20", 0 ); - mvd_wait_percent = Cvar_Get( "mvd_wait_percent", "50", 0 ); + mvd_wait_percent = Cvar_Get( "mvd_wait_percent", "35", 0 ); mvd_chase_msgs = Cvar_Get( "mvd_chase_msgs", "0", 0 ); Cmd_Register( c_mvd ); |