/* Copyright (C) 2003-2006 Andrey Nazarov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // // mvd_client.c // #include "sv_local.h" #include "mvd_local.h" #include LIST_DECL( mvd_channels ); LIST_DECL( mvd_ready ); mvd_t mvd_waitingRoom; qboolean mvd_dirty; 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; // ==================================================================== void MVD_Disconnect( mvd_t *mvd ) { if( mvd->demorecording ) { uint16_t msglen = 0; FS_Write( &msglen, 2, mvd->demorecording ); FS_FCloseFile( mvd->demorecording ); mvd->demorecording = 0; } if( mvd->stream.state ) { NET_Close( &mvd->stream ); } if( mvd->demoplayback ) { FS_FCloseFile( mvd->demoplayback ); mvd->demoplayback = 0; } mvd->state = MVD_DISCONNECTED; } void MVD_Free( mvd_t *mvd ) { #if USE_ZLIB if( mvd->z.state ) { inflateEnd( &mvd->z ); } if( mvd->zbuf.data ) { Z_Free( mvd->zbuf.data ); } #endif Z_Free( mvd->players ); 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; // cause UDP clients to reconnect LIST_FOR_EACH_SAFE( udpClient_t, u, unext, &mvd->udpClients, entry ) { MVD_SwitchChannel( u, &mvd_waitingRoom ); } // send EOF to TCP clients and kick them length = 0; LIST_FOR_EACH( tcpClient_t, t, &mvd->tcpClients, mvdEntry ) { SV_HttpWrite( t, &length, 2 ); SV_HttpFinish( t ); SV_HttpDrop( t, "channel destroyed" ); NET_Run( &t->stream ); } MVD_Disconnect( mvd ); MVD_ClearState( mvd ); MVD_Free( mvd ); mvd_dirty = qtrue; } 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]; va_start( argptr, fmt ); Q_vsnprintf( text, sizeof( text ), fmt, argptr ); va_end( argptr ); Com_Printf( "[%s] %s\n", mvd->name, text ); MVD_Destroy( mvd ); longjmp( mvd_jmpbuf, -1 ); } void MVD_Dropf( mvd_t *mvd, const char *fmt, ... ) { va_list argptr; char text[MAXPRINTMSG]; va_start( argptr, fmt ); Q_vsnprintf( text, sizeof( text ), fmt, argptr ); va_end( argptr ); Com_Printf( "[%s] %s\n", mvd->name, text ); MVD_Drop( mvd ); longjmp( mvd_jmpbuf, -1 ); } void MVD_DPrintf( const char *fmt, ... ) { va_list argptr; char text[MAXPRINTMSG]; if( !mvd_debug->integer ) { return; } va_start( argptr, fmt ); Q_vsnprintf( text, sizeof( text ), fmt, argptr ); va_end( argptr ); Com_Printf( S_COLOR_BLUE "%s", text ); } static void MVD_HttpPrintf( mvd_t *mvd, const char *fmt, ... ) { char buffer[MAX_STRING_CHARS]; va_list argptr; size_t len; va_start( argptr, fmt ); len = Q_vsnprintf( buffer, sizeof( buffer ), fmt, argptr ); va_end( argptr ); if( FIFO_Write( &mvd->stream.send, buffer, len ) != len ) { MVD_Dropf( mvd, "%s: overflow", __func__ ); } } void MVD_ClearState( mvd_t *mvd ) { mvd_cs_t *cs, *nextcs; mvd_player_t *player; edict_t *ent; int i; for( i = 0; i < mvd->pool.num_edicts; i++ ) { ent = &mvd->edicts[i]; memset( &ent->s, 0, sizeof( ent->s ) ); ent->inuse = qfalse; } mvd->pool.num_edicts = 0; for( i = 0; i < mvd->maxclients; i++ ) { player = &mvd->players[i]; for( cs = player->configstrings; cs; cs = nextcs ) { nextcs = cs->next; Z_Free( cs ); } player->configstrings = NULL; memset( &player->ps, 0, sizeof( player->ps ) ); player->inuse = qfalse; } CM_FreeMap( &mvd->cm ); memset( mvd->configstrings, 0, sizeof( mvd->configstrings ) ); mvd->layout[0] = 0; mvd->framenum = 0; mvd->intermission = qfalse; } void MVD_BeginWaiting( mvd_t *mvd ) { //int maxDelay = mvd_wait_delay->value * 1000; mvd->state = MVD_WAITING; mvd->waitTime = svs.realtime; #if 0 mvd->waitDelay += 5000; if( maxDelay < 5000 ) { maxDelay = 5000; } if( mvd->waitDelay > maxDelay ) { mvd->waitDelay = maxDelay; } #endif } 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]; patch = SZ_GetSpace( &msg_write, 2 ); // 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 flags = 0; if( sv_mvd_noblend->integer ) { flags |= MSG_PS_IGNORE_BLEND; } if( sv_mvd_nogun->integer ) { flags |= MSG_PS_IGNORE_GUNINDEX|MSG_PS_IGNORE_GUNFRAMES; } for( i = 0; i < mvd->maxclients; i++ ) { ps = &mvd->players[i].ps; extra = 0; if( !PPS_INUSE( ps ) ) { extra |= MSG_PS_REMOVE; } MSG_WriteDeltaPlayerstate_Packet( NULL, ps, i, flags | extra ); } MSG_WriteByte( CLIENTNUM_NONE ); // 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; } MSG_WriteShort( 0 ); // TODO: write private layouts/configstrings length = msg_write.cursize - 2; patch[0] = length & 255; patch[1] = ( length >> 8 ) & 255; } void MVD_SendGamestate( tcpClient_t *client ) { MVD_EmitGamestate( client->mvd ); Com_DPrintf( "Sent gamestate to MVD client %s\n", NET_AdrToString( &client->stream.address ) ); client->state = cs_spawned; SV_HttpWrite( client, msg_write.data, msg_write.cursize ); SZ_Clear( &msg_write ); } void MVD_GetStatus( void ) { char buffer[MAX_STRING_CHARS]; mvd_t *mvd; int count, len; SV_HttpPrintf( "HTTP/1.0 200 OK\r\n" ); if( http_client->method == HTTP_METHOD_HEAD ) { SV_HttpPrintf( "\r\n" ); SV_HttpDrop( http_client, "200 OK" ); return; } SV_HttpPrintf( "Content-Type: text/html; charset=us-ascii\r\n" "\r\n" ); count = SV_CountClients(); len = Q_EscapeMarkup( buffer, sv_hostname->string, sizeof( buffer ) ); Com_sprintf( buffer + len, sizeof( buffer ) - len, " - %d/%d", count, sv_maxclients->integer - sv_reserved_slots->integer ); SV_HttpHeader( buffer ); buffer[len] = 0; SV_HttpPrintf( "

%s

This server has ", buffer ); count = List_Count( &mvd_ready ); if( count ) { SV_HttpPrintf( "%d channel%s available. ", count, count == 1 ? "" : "s" ); } else { SV_HttpPrintf( "no channels available. " ); } count = List_Count( &mvd_waitingRoom.udpClients ); if( count ) { SV_HttpPrintf( "Waiting room has %d client%s.

", count, count == 1 ? "" : "s" ); } else { SV_HttpPrintf( "Waiting room is empty.

" ); } if( !LIST_EMPTY( &mvd_ready ) ) { SV_HttpPrintf( "" "" ); LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) { SV_HttpPrintf( "", buffer, count ); } SV_HttpPrintf( "
IDNameMapClients
" ); if( sv_mvd_enable->integer ) { SV_HttpPrintf( "%d", http_host, mvd->id, mvd->id ); } else { SV_HttpPrintf( "%d", mvd->id ); } SV_HttpPrintf( "" ); Q_EscapeMarkup( buffer, mvd->name, sizeof( buffer ) ); if( sv_mvd_enable->integer ) { SV_HttpPrintf( "%s", http_host, mvd->id, buffer ); } else { SV_HttpPrintf( "%s", buffer ); } Q_EscapeMarkup( buffer, mvd->mapname, sizeof( buffer ) ); count = List_Count( &mvd->udpClients ); SV_HttpPrintf( "%s%d
" ); } SV_HttpPrintf( "

Join this server

", http_host ); SV_HttpFooter(); SV_HttpDrop( http_client, "200 OK" ); } static mvd_t *MVD_SetStream( const char *uri ) { mvd_t *mvd; int id; if( LIST_EMPTY( &mvd_ready ) ) { SV_HttpReject( "503 Service Unavailable", "No MVD streams are available on this server." ); return NULL; } if( *uri == '/' ) { uri++; } 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; } id = atoi( uri ); LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) { if( mvd->id == id ) { return mvd; } } SV_HttpReject( "404 Not Found", "Requested MVD stream was not found on this server." ); return NULL; } void MVD_GetStream( const char *uri ) { mvd_t *mvd; mvd = MVD_SetStream( uri ); if( !mvd ) { return; } 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; } List_Append( &mvd->tcpClients, &http_client->mvdEntry ); http_client->mvd = mvd; SV_MvdInitStream(); MVD_SendGamestate( http_client ); } void MVD_ChangeLevel( mvd_t *mvd ) { udpClient_t *u; if( sv.state != ss_broadcast ) { MVD_Spawn_f(); // the game is just starting return; } // cause all UDP clients to reconnect MSG_WriteByte( svc_stufftext ); MSG_WriteString( "changing; reconnect\n" ); LIST_FOR_EACH( udpClient_t, u, &mvd->udpClients, entry ) { SV_ClientReset( u->cl ); SV_ClientAddMessage( u->cl, MSG_RELIABLE ); } SZ_Clear( &msg_write ); SV_SendAsyncPackets(); } static void MVD_PlayNext( mvd_t *mvd, string_entry_t *entry ) { uint32_t magic = 0; if( !entry ) { if( mvd->demoloop ) { if( --mvd->demoloop == 0 ) { MVD_Destroyf( mvd, "End of play list reached" ); return; } } entry = mvd->demohead; } if( mvd->demoplayback ) { FS_FCloseFile( mvd->demoplayback ); mvd->demoplayback = 0; } FS_FOpenFile( entry->string, &mvd->demoplayback, FS_MODE_READ ); if( !mvd->demoplayback ) { MVD_Destroyf( mvd, "Couldn't reopen %s", entry->string ); } FS_Read( &magic, 4, mvd->demoplayback ); if( magic != MVD_MAGIC ) { MVD_Destroyf( mvd, "%s is not a MVD2 file", entry->string ); } Com_Printf( "[%s] Reading from %s\n", mvd->name, entry->string ); // reset state mvd->demoentry = entry; // set channel address Q_strncpyz( mvd->address, COM_SkipPath( entry->string ), sizeof( mvd->address ) ); } void MVD_Finish( mvd_t *mvd, const char *reason ) { Com_Printf( "[%s] %s\n", mvd->name, reason ); if( mvd->demoentry ) { MVD_PlayNext( mvd, mvd->demoentry->next ); mvd->state = MVD_PREPARING; List_Delete( &mvd_ready ); } else { MVD_Destroy( mvd ); } longjmp( mvd_jmpbuf, -1 ); } 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; } read = FS_Read( data, length, mvd->demoplayback ); FIFO_Commit( &mvd->stream.recv, read ); total += read; } while( read ); if( !total ) { MVD_Dropf( mvd, "End of MVD file reached" ); } } static htcoding_t MVD_FindCoding( const char *name ) { if( !Q_stricmp( name, "identity" ) ) { return HTTP_CODING_NONE; } if( !Q_stricmp( name, "gzip" ) ) { return HTTP_CODING_GZIP; } if( !Q_stricmp( name, "x-gzip" ) ) { return HTTP_CODING_GZIP; } if( !Q_stricmp( name, "deflate" ) ) { return HTTP_CODING_DEFLATE; } return HTTP_CODING_UNKNOWN; } static qboolean MVD_ParseResponse( mvd_t *mvd ) { char key[MAX_TOKEN_CHARS]; char *p, *token; const char *line; byte *b, *data; size_t length; while( 1 ) { data = FIFO_Peek( &mvd->stream.recv, &length ); if( !length ) { break; } if( ( b = memchr( data, '\n', length ) ) != NULL ) { length = b - data + 1; } if( mvd->responseLength + length > MAX_NET_STRING - 1 ) { MVD_Dropf( mvd, "Response line exceeded maximum length" ); } memcpy( mvd->response + mvd->responseLength, data, length ); mvd->responseLength += length; mvd->response[mvd->responseLength] = 0; FIFO_Decommit( &mvd->stream.recv, length ); if( !b ) { continue; } line = mvd->response; mvd->responseLength = 0; if( !mvd->statusCode ) { // parse version token = COM_SimpleParse( &line, NULL ); if( !token[0] ) { continue; // empty line? } if( strncmp( token, "HTTP/", 5 ) ) { MVD_Dropf( mvd, "Malformed HTTP version" ); } // parse status code token = COM_SimpleParse( &line, NULL ); mvd->statusCode = atoi( token ); if( !mvd->statusCode ) { MVD_Dropf( mvd, "Malformed HTTP status code" ); } // parse reason phrase if( line ) { while( *line && *line <= ' ' ) { line++; } Q_ClearStr( mvd->statusText, line, MAX_QPATH ); } } else { // parse header fields token = COM_SimpleParse( &line, 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" ) ) { } } } return qfalse; } static inline float MVD_BufferPercent( mvd_t *mvd ) { size_t usage = FIFO_Usage( &mvd->stream.recv ); size_t size = mvd->stream.recv.size; #ifdef USE_ZLIB usage += FIFO_Usage( &mvd->zbuf ); size += mvd->zbuf.size; #endif if( !size ) { return 0; } return usage * 100.0f / size; } int MVD_Frame( void ) { mvd_t *mvd, *next; neterr_t ret; float usage; int connections = 0; LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_channels, entry ) { if( mvd->state <= MVD_DEAD || mvd->state >= MVD_DISCONNECTED ) { continue; } if( setjmp( mvd_jmpbuf ) ) { continue; } if( mvd->demoentry ) { if( mvd->demoplayback ) { MVD_ReadDemo( mvd ); } if( mvd->state == MVD_PREPARING ) { MVD_Parse( mvd ); } continue; } connections++; // 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; } // don't timeout mvd->lastReceived = svs.realtime; // 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; #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; #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( mvd->waitTime > svs.realtime ) { mvd->waitTime = svs.realtime; } 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; } } return connections; } /* ==================================================================== 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 ); Cvar_SetInteger( "sv_running", ss_broadcast ); Cvar_SetInteger( "sv_paused", 0 ); Cvar_SetInteger( "timedemo", 0 ); sv.spawncount = ( rand() | ( rand() << 16 ) ) ^ Sys_Milliseconds(); sv.spawncount &= 0x7FFFFFFF; sv.state = ss_broadcast; } void MVD_ListChannels_f( void ) { mvd_t *mvd; int usage; if( LIST_EMPTY( &mvd_channels ) ) { Com_Printf( "No active channels.\n" ); return; } Com_Printf( "id name map stat buf address \n" "-- ---------------- -------- ---- --- --------------\n" ); LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { Com_Printf( "%2d %-16.16s %-8.8s ", mvd->id, mvd->name, mvd->mapname ); switch( mvd->state ) { case MVD_DEAD: Com_Printf( "DEAD" ); break; case MVD_CONNECTING: case MVD_CONNECTED: Com_Printf( "CNCT" ); break; case MVD_CHECKING: case MVD_PREPARING: Com_Printf( "PREP" ); break; case MVD_WAITING: Com_Printf( "WAIT" ); break; case MVD_READING: Com_Printf( "READ" ); break; case MVD_DISCONNECTED: Com_Printf( "DISC" ); break; } usage = MVD_BufferPercent( mvd ); Com_Printf( " %3d %s\n", usage, mvd->address ); } } void MVD_StreamedStop_f( void ) { mvd_t *mvd; uint16_t msglen; mvd = MVD_SetChannel( 1 ); if( !mvd ) { Com_Printf( "Usage: %s [chanid]\n", Cmd_Argv( 0 ) ); return; } if( !mvd->demorecording ) { Com_Printf( "[%s] Not recording a demo.\n", mvd->name ); return; } msglen = 0; FS_Write( &msglen, 2, mvd->demorecording ); FS_FCloseFile( mvd->demorecording ); mvd->demorecording = 0; Com_Printf( "[%s] Stopped recording.\n", mvd->name ); } void MVD_StreamedRecord_f( void ) { char buffer[MAX_OSPATH]; char *name; fileHandle_t f; mvd_t *mvd; uint32_t magic; if( Cmd_Argc() < 2 || ( mvd = MVD_SetChannel( 2 ) ) == NULL ) { Com_Printf( "Usage: %s [/] [chanid]\n", Cmd_Argv( 0 ) ); return; } if( mvd->demorecording ) { Com_Printf( "[%s] Already recording.\n", mvd->name ); return; } if( mvd->state < MVD_WAITING ) { Com_Printf( "[%s] Channel not ready.\n", mvd->name ); return; } // // open the demo file // name = Cmd_Argv( 1 ); if( name[0] == '/' ) { Q_strncpyz( buffer, name + 1, sizeof( buffer ) ); } else { Q_concat( buffer, sizeof( buffer ), "demos/", name, NULL ); COM_AppendExtension( buffer, ".mvd2", sizeof( buffer ) ); } FS_FOpenFile( buffer, &f, FS_MODE_WRITE ); if( !f ) { Com_EPrintf( "Couldn't open %s for writing\n", buffer ); return; } Com_Printf( "[%s] Recording into %s.\n", mvd->name, buffer ); mvd->demorecording = f; MVD_EmitGamestate( mvd ); magic = MVD_MAGIC; FS_Write( &magic, 4, mvd->demorecording ); FS_Write( msg_write.data, msg_write.cursize, mvd->demorecording ); SZ_Clear( &msg_write ); } static const cmd_option_t o_mvdconnect[] = { { "h", "help", "display this message" }, { "e:string", "encoding", "specify default encoding as " }, { "i:number", "id", "specify remote stream ID as " }, { "n:string", "name", "specify channel name as " }, { "r:string", "referer", "specify referer as in HTTP request" }, { NULL } }; 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 ) { 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; int c; while( ( c = Cmd_ParseOptions( o_mvdconnect ) ) != -1 ) { switch( c ) { case 'h': Cmd_PrintUsage( o_mvdconnect, "" ); Com_Printf( "Create new MVD channel and connect to URI.\n" ); Cmd_PrintHelp( o_mvdconnect ); Com_Printf( "Full URI syntax: [http://][user:pass@][: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" ); 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 ); return; } if( NET_Connect( &adr, &stream ) == NET_ERROR ) { Com_Printf( "%s to %s\n", NET_ErrorString(), NET_AdrToString( &adr ) ); return; } Z_TagReserve( sizeof( *mvd ) + MAX_MSGLEN * 2 + 256, TAG_MVD ); mvd = Z_ReservedAllocz( sizeof( *mvd ) ); 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_Init( &mvd->ready ); List_Append( &mvd_channels, &mvd->entry ); // set channel name if( name ) { Q_strncpyz( mvd->name, name, sizeof( mvd->name ) ); } else { Com_sprintf( mvd->name, sizeof( mvd->name ), "net%d", mvd->id ); } Q_strncpyz( mvd->address, host, sizeof( mvd->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" ); } static void MVD_Disconnect_f( void ) { mvd_t *mvd; mvd = MVD_SetChannel( 1 ); if( !mvd ) { return; } if( mvd->state == MVD_DISCONNECTED ) { Com_Printf( "[%s] Already disconnected.\n", mvd->name ); return; } Com_Printf( "[%s] Channel was disconnected.\n", mvd->name ); MVD_Drop( mvd ); } static void MVD_Kill_f( void ) { mvd_t *mvd; mvd = MVD_SetChannel( 1 ); if( !mvd ) { return; } Com_Printf( "[%s] Channel was killed.\n", mvd->name ); MVD_Destroy( mvd ); } static void MVD_Pause_f( void ) { mvd_t *mvd; mvd = MVD_SetChannel( 1 ); if( !mvd ) { return; } switch( mvd->state ) { case MVD_WAITING: 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 ); break; default: Com_Printf( "[%s] Channel is not ready.\n", mvd->name ); break; } } static void MVD_Control_f( void ) { static const cmd_option_t options[] = { { "h", "help", "display this message" }, { "l:number", "loop", "replay of times (0 means forever)" }, { "n:string", "name", "specify channel name as " }, { NULL } }; mvd_t *mvd; char *name = NULL; int loop = -1; int todo = 0; int c; while( ( c = Cmd_ParseOptions( options ) ) != -1 ) { switch( c ) { case 'h': Cmd_PrintUsage( options, "[chanid]" ); Com_Printf( "Change attributes of existing MVD channel.\n" ); Cmd_PrintHelp( options ); return; case 'l': loop = atoi( cmd_optarg ); if( loop < 0 ) { Com_Printf( "Invalid value for %s option.\n", cmd_optopt ); Cmd_PrintHint(); return; } todo |= 1; break; case 'n': name = cmd_optarg; todo |= 2; break; default: return; } } if( !todo ) { Com_Printf( "At least one option needed.\n" ); Cmd_PrintHint(); return; } mvd = MVD_SetChannel( cmd_optind ); if( !mvd ) { Cmd_PrintHint(); return; } if( name ) { Com_Printf( "[%s] Channel renamed to %s.\n", mvd->name, name ); Q_strncpyz( mvd->name, name, sizeof( mvd->name ) ); } if( loop != -1 ) { Com_Printf( "[%s] Loop count changed to %d.\n", mvd->name, loop ); mvd->demoloop = loop; } } static const cmd_option_t o_mvdplay[] = { { "h", "help", "display this message" }, { "l:number", "loop", "replay of times (0 means forever)" }, { "n:string", "name", "specify channel name as " }, { NULL } }; void MVD_File_g( genctx_t *ctx ) { FS_File_g( "demos", "*.mvd2;*.mvd2.gz", FS_SEARCH_SAVEPATH | FS_SEARCH_BYFILTER, ctx ); } 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 ) { char *name = NULL, *s; char buffer[MAX_OSPATH]; int loop = 1; size_t len; mvd_t *mvd; int c, argc; string_entry_t *entry, *head; int i; while( ( c = Cmd_ParseOptions( o_mvdplay ) ) != -1 ) { switch( c ) { case 'h': Cmd_PrintUsage( o_mvdplay, "[/]" ); Com_Printf( "Create new MVD channel and begin demo playback.\n" ); Cmd_PrintHelp( o_mvdplay ); Com_Printf( "Final path is formatted as demos/.mvd2.\n" "Prepend slash to specify raw path.\n" ); return; case 'l': loop = atoi( cmd_optarg ); if( loop < 0 ) { Com_Printf( "Invalid value for %s option.\n", cmd_optopt ); Cmd_PrintHint(); return; } break; case 'n': name = cmd_optarg; break; default: return; } } argc = Cmd_Argc(); if( cmd_optind == argc ) { Com_Printf( "Missing filename argument.\n" ); Cmd_PrintHint(); return; } head = NULL; for( i = argc - 1; i >= cmd_optind; i-- ) { s = Cmd_Argv( i ); if( *s == '/' ) { Q_strncpyz( buffer, s + 1, sizeof( buffer ) ); } else { Q_concat( buffer, sizeof( buffer ), "demos/", s, NULL ); if( FS_LoadFile( buffer, NULL ) == INVALID_LENGTH ) { COM_AppendExtension( buffer, ".mvd2", sizeof( buffer ) ); } } if( FS_LoadFile( buffer, NULL ) == INVALID_LENGTH ) { Com_Printf( "Ignoring non-existent entry: %s\n", buffer ); continue; } len = strlen( buffer ); entry = Z_Malloc( sizeof( *entry ) + len ); memcpy( entry->string, buffer, len + 1 ); entry->next = head; head = entry; } if( !head ) { 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_Init( &mvd->ready ); List_Append( &mvd_channels, &mvd->entry ); // set channel name if( name ) { Q_strncpyz( mvd->name, name, sizeof( mvd->name ) ); } else { Com_sprintf( mvd->name, sizeof( mvd->name ), "dem%d", mvd->id ); } MVD_PlayNext( mvd, mvd->demohead ); } void MVD_Shutdown( void ) { mvd_t *mvd, *next; LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_channels, entry ) { MVD_Disconnect( mvd ); MVD_ClearState( mvd ); MVD_Free( mvd ); } List_Init( &mvd_channels ); List_Init( &mvd_ready ); if( mvd_clients ) { Z_Free( mvd_clients ); mvd_clients = NULL; } mvd_chanid = 0; Z_LeakTest( TAG_MVD ); } static const cmdreg_t c_mvd[] = { { "mvdplay", MVD_Play_f, MVD_Play_c }, { "mvdconnect", MVD_Connect_f, MVD_Connect_c }, { "mvdisconnect", MVD_Disconnect_f }, { "mvdkill", MVD_Kill_f }, { "mvdspawn", MVD_Spawn_f }, { "mvdchannels", MVD_ListChannels_f }, { "mvdcontrol", MVD_Control_f }, { "mvdpause", MVD_Pause_f }, { NULL } }; /* ============== MVD_Register ============== */ void MVD_Register( void ) { mvd_shownet = Cvar_Get( "mvd_shownet", "0", 0 ); mvd_debug = Cvar_Get( "mvd_debug", "0", 0 ); mvd_timeout = Cvar_Get( "mvd_timeout", "120", 0 ); mvd_wait_delay = Cvar_Get( "mvd_wait_delay", "20", 0 ); mvd_wait_percent = Cvar_Get( "mvd_wait_percent", "50", 0 ); Cmd_Register( c_mvd ); }