diff options
Diffstat (limited to 'src/cl_demo.c')
-rw-r--r-- | src/cl_demo.c | 838 |
1 files changed, 838 insertions, 0 deletions
diff --git a/src/cl_demo.c b/src/cl_demo.c new file mode 100644 index 0000000..f8fc6f2 --- /dev/null +++ b/src/cl_demo.c @@ -0,0 +1,838 @@ +/* +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. + +*/ + +// +// cl_demo.c - demo recording and playback +// + +#include "cl_local.h" + +static byte demo_buffer[MAX_PACKETLEN_WRITABLE_DEFAULT]; + +// ========================================================================= + +/* +==================== +CL_WriteDemoMessage + +Dumps the current demo message, prefixed by the length. +==================== +*/ +void CL_WriteDemoMessage( sizebuf_t *buf ) { + uint32_t msglen; + + if( buf->overflowed ) { + SZ_Clear( buf ); + Com_WPrintf( "Demo message overflowed (should never happen).\n" ); + return; + } + if( !buf->cursize ) { + return; + } + + msglen = LittleLong( buf->cursize ); + FS_Write( &msglen, 4, cls.demo.recording ); + FS_Write( buf->data, buf->cursize, cls.demo.recording ); + + SZ_Clear( buf ); +} + +/* +============= +CL_EmitPacketEntities + +Writes a delta update of an entity_state_t list to the message. +============= +*/ +static void CL_EmitPacketEntities( server_frame_t *from, server_frame_t *to ) { + entity_state_t *oldent, *newent; + int oldindex, newindex; + int oldnum, newnum; + int i, from_num_entities; + + if( !from ) + from_num_entities = 0; + else + from_num_entities = from->numEntities; + + newindex = 0; + oldindex = 0; + oldent = newent = 0; + while( newindex < to->numEntities || oldindex < from_num_entities ) { + if( newindex >= to->numEntities ) { + newnum = 9999; + } else { + i = ( to->firstEntity + newindex ) & PARSE_ENTITIES_MASK; + newent = &cl.entityStates[i]; + newnum = newent->number; + } + + if( oldindex >= from_num_entities ) { + oldnum = 9999; + } else { + i = ( from->firstEntity + oldindex ) & PARSE_ENTITIES_MASK; + oldent = &cl.entityStates[i]; + oldnum = oldent->number; + } + + if( newnum == oldnum ) { + // delta update from old position + // because the force parm is false, this will not result + // in any bytes being emited if the entity has not changed at all + MSG_WriteDeltaEntity( oldent, newent, 0 ); + oldindex++; + newindex++; + continue; + } + + if( newnum < oldnum ) { + // this is a new entity, send it from the baseline + MSG_WriteDeltaEntity( &cl.baselines[newnum], newent, + MSG_ES_FORCE|MSG_ES_NEWENTITY ); + newindex++; + continue; + } + + if( newnum > oldnum ) { + // the old entity isn't present in the new message + MSG_WriteDeltaEntity( oldent, NULL, MSG_ES_FORCE ); + oldindex++; + continue; + } + } + + MSG_WriteShort( 0 ); // end of packetentities +} + +/* +==================== +CL_EmitDemoFrame + +Writes delta from the last frame we got to the current frame. +==================== +*/ +void CL_EmitDemoFrame( void ) { + server_frame_t *oldframe; + player_state_t *oldstate; + int lastframe; + + if( cls.demo.paused ) { + return; + } + + if( cl.demoframe < 0 ) { + oldframe = NULL; + oldstate = NULL; + lastframe = -1; + } else { + oldframe = &cl.frames[cl.demoframe & UPDATE_MASK]; + oldstate = &oldframe->ps; + lastframe = cl.demoframe + cl.demodelta; + if( oldframe->number != cl.demoframe || !oldframe->valid || + cl.numEntityStates - oldframe->firstEntity > MAX_PARSE_ENTITIES ) + { + oldframe = NULL; + oldstate = NULL; + lastframe = -1; + } + } + + MSG_WriteByte( svc_frame ); + MSG_WriteLong( cl.frame.number + cl.demodelta ); + MSG_WriteLong( lastframe ); // what we are delta'ing from + MSG_WriteByte( 0 ); // rate dropped packets + + // send over the areabits + MSG_WriteByte( cl.frame.areabytes ); + MSG_WriteData( cl.frame.areabits, cl.frame.areabytes ); + + // delta encode the playerstate + MSG_WriteByte( svc_playerinfo ); + MSG_WriteDeltaPlayerstate_Default( oldstate, &cl.frame.ps ); + + // delta encode the entities + MSG_WriteByte( svc_packetentities ); + CL_EmitPacketEntities( oldframe, &cl.frame ); + + if( cls.demo.buffer.cursize + msg_write.cursize > cls.demo.buffer.maxsize ) { + Com_DPrintf( "Demo frame overflowed\n" ); + } else { + SZ_Write( &cls.demo.buffer, msg_write.data, msg_write.cursize ); + cl.demoframe = cl.frame.number; + } + + SZ_Clear( &msg_write ); +} + +void CL_EmitZeroFrame( void ) { + cl.demodelta++; // insert new zero frame + + MSG_WriteByte( svc_frame ); + MSG_WriteLong( cl.frame.number + cl.demodelta ); + MSG_WriteLong( cl.frame.number + cl.demodelta - 1 ); // what we are delta'ing from + MSG_WriteByte( 0 ); // rate dropped packets + + // send over the areabits + MSG_WriteByte( cl.frame.areabytes ); + MSG_WriteData( cl.frame.areabits, cl.frame.areabytes ); + + MSG_WriteByte( svc_playerinfo ); + MSG_WriteShort( 0 ); + MSG_WriteLong( 0 ); + + MSG_WriteByte( svc_packetentities ); + MSG_WriteShort( 0 ); + + CL_WriteDemoMessage( &msg_write ); + + SZ_Clear( &msg_write ); +} + + +/* +==================== +CL_Stop_f + +stop recording a demo +==================== +*/ +void CL_Stop_f( void ) { + uint32_t msglen; + ssize_t pos; + + if( !cls.demo.recording ) { + Com_Printf( "Not recording a demo.\n" ); + return; + } + + if( cls.netchan && cls.serverProtocol >= PROTOCOL_VERSION_R1Q2 ) { + // tell the server we finished recording + MSG_WriteByte( clc_setting ); + MSG_WriteShort( CLS_RECORDING ); + MSG_WriteShort( 0 ); + MSG_FlushTo( &cls.netchan->message ); + } + +// finish up + msglen = ( uint32_t )-1; + FS_Write( &msglen, 4, cls.demo.recording ); + + FS_Flush( cls.demo.recording ); + pos = FS_Tell( cls.demo.recording ); + +// close demofile + FS_FCloseFile( cls.demo.recording ); + cls.demo.recording = 0; + cls.demo.paused = qfalse; + + if( pos < 0 ) { + Com_Printf( "Stopped demo.\n" ); + } else { + Com_Printf( "Stopped demo (%d bytes written).\n", (int)pos ); + } +} + +/* +==================== +CL_Record_f + +record <demoname> + +Begins recording a demo from the current position +==================== +*/ +static void CL_Record_f( void ) { + char name[MAX_OSPATH]; + int i, c; + size_t len; + entity_state_t *ent; + char *string; + qhandle_t f; + unsigned mode = FS_MODE_WRITE; + + if( cls.demo.recording ) { + Com_Printf( "Already recording.\n" ); + return; + } + + if( cls.state != ca_active ) { + Com_Printf( "You must be in a level to record.\n" ); + return; + } + + while( ( c = Cmd_ParseOptions( o_record ) ) != -1 ) { + switch( c ) { + case 'h': + Cmd_PrintUsage( o_record, "[/]<filename>" ); + Com_Printf( "Begin client demo recording.\n" ); + Cmd_PrintHelp( o_record ); + return; + case 'z': + mode |= FS_FLAG_GZIP; + break; + default: + return; + } + } + + if( !cmd_optarg[0] ) { + Com_Printf( "Missing filename argument.\n" ); + Cmd_PrintHint(); + return; + } + + // + // open the demo file + // + f = FS_EasyOpenFile( name, sizeof( name ), mode, + "demos/", cmd_optarg, ".dm2" ); + if( !f ) { + return; + } + + Com_Printf( "Recording client demo to %s.\n", name ); + + cls.demo.recording = f; + cls.demo.paused = qfalse; + + SZ_Init( &cls.demo.buffer, demo_buffer, sizeof( demo_buffer ) ); + + // the first frame will be delta uncompressed + cl.demoframe = -1; + cl.demodelta = 0; + + if( cls.netchan && cls.serverProtocol >= PROTOCOL_VERSION_R1Q2 ) { + // tell the server we are recording + MSG_WriteByte( clc_setting ); + MSG_WriteShort( CLS_RECORDING ); + MSG_WriteShort( 1 ); + MSG_FlushTo( &cls.netchan->message ); + } + + // + // write out messages to hold the startup information + // + + // send the serverdata + MSG_WriteByte( svc_serverdata ); + MSG_WriteLong( PROTOCOL_VERSION_DEFAULT ); + MSG_WriteLong( 0x10000 + cl.servercount ); + MSG_WriteByte( 1 ); // demos are always attract loops + MSG_WriteString( cl.gamedir ); + MSG_WriteShort( cl.clientNum ); + MSG_WriteString( cl.configstrings[CS_NAME] ); + + // configstrings + for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { + string = cl.configstrings[i]; + if( !string[0] ) { + continue; + } + + len = strlen( string ); + if( len > MAX_QPATH ) { + len = MAX_QPATH; + } + + if( msg_write.cursize + len + 4 > MAX_PACKETLEN_WRITABLE_DEFAULT ) { + CL_WriteDemoMessage( &msg_write ); + } + + MSG_WriteByte( svc_configstring ); + MSG_WriteShort( i ); + MSG_WriteData( string, len ); + MSG_WriteByte( 0 ); + } + + // baselines + for( i = 1; i < MAX_EDICTS; i++ ) { + ent = &cl.baselines[i]; + if( !ent->number ) { + continue; + } + + if( msg_write.cursize + 64 > MAX_PACKETLEN_WRITABLE_DEFAULT ) { + CL_WriteDemoMessage( &msg_write ); + } + + MSG_WriteByte( svc_spawnbaseline ); + MSG_WriteDeltaEntity( NULL, ent, MSG_ES_FORCE ); + } + + MSG_WriteByte( svc_stufftext ); + MSG_WriteString( "precache\n" ); + + // write it to the demo file + CL_WriteDemoMessage( &msg_write ); + + // the rest of the demo file will be individual frames +} + +static void CL_Suspend_f( void ) { + int i, j, index; + size_t length, total = 0; + + if( !cls.demo.recording ) { + Com_Printf( "Not recording a demo.\n" ); + return; + } + if( !cls.demo.paused ) { + Com_Printf( "Suspended demo.\n" ); + cls.demo.paused = qtrue; + return; + } + + // XXX: embed these in frame instead? + for( i = 0; i < CS_BITMAP_LONGS; i++ ) { + if( (( uint32_t * )cl.dcs)[i] == 0 ) { + continue; + } + index = i << 5; + for( j = 0; j < 32; j++, index++ ) { + if( !Q_IsBitSet( cl.dcs, index ) ) { + continue; + } + length = strlen( cl.configstrings[index] ); + if( length > MAX_QPATH ) { + length = MAX_QPATH; + } + if( msg_write.cursize + length + 4 > MAX_PACKETLEN_WRITABLE_DEFAULT ) { + CL_WriteDemoMessage( &msg_write ); + } + MSG_WriteByte( svc_configstring ); + MSG_WriteShort( index ); + MSG_WriteData( cl.configstrings[index], length ); + MSG_WriteByte( 0 ); + total += length + 4; + } + } + + // write it to the demo file + CL_WriteDemoMessage( &msg_write ); + + Com_Printf( "Resumed demo (%"PRIz" bytes flushed).\n", total ); + + cl.demodelta += cl.demoframe - cl.frame.number; // do not create holes + cls.demo.paused = qfalse; + + // clear dirty configstrings + memset( cl.dcs, 0, sizeof( cl.dcs ) ); +} + +static int read_first_message( qhandle_t f ) { + uint32_t ul; + uint16_t us; + size_t msglen; + ssize_t read; + qerror_t ret; + int type; + + // read magic/msglen + read = FS_Read( &ul, 4, f ); + if( read != 4 ) { + return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; + } + + // check for gzip header + if( ( ( LittleLong( ul ) & 0xe0ffffff ) == 0x00088b1f ) ) { + ret = FS_FilterFile( f ); + if( ret ) { + return ret; + } + read = FS_Read( &ul, 4, f ); + if( read != 4 ) { + return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; + } + } + + // determine demo type + if( ul == MVD_MAGIC ) { + read = FS_Read( &us, 2, f ); + if( read != 2 ) { + return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; + } + if( !us ) { + return Q_ERR_UNEXPECTED_EOF; + } + msglen = LittleShort( us ); + type = 1; + } else { + if( ul == ( uint32_t )-1 ) { + return Q_ERR_UNEXPECTED_EOF; + } + msglen = LittleLong( ul ); + type = 0; + } + + if( msglen < 64 || msglen > sizeof( msg_read_buffer ) ) { + return Q_ERR_INVALID_FORMAT; + } + + SZ_Init( &msg_read, msg_read_buffer, sizeof( msg_read_buffer ) ); + msg_read.cursize = msglen; + + // read packet data + read = FS_Read( msg_read.data, msglen, f ); + if( read != msglen ) { + return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; + } + + return type; +} + +static int read_next_message( qhandle_t f ) { + uint32_t msglen; + ssize_t read; + + // read msglen + read = FS_Read( &msglen, 4, f ); + if( read != 4 ) { + return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; + } + + // check for EOF packet + if( msglen == ( uint32_t )-1 ) { + return 0; + } + + msglen = LittleLong( msglen ); + if( msglen > sizeof( msg_read_buffer ) ) { + return Q_ERR_INVALID_FORMAT; + } + + SZ_Init( &msg_read, msg_read_buffer, sizeof( msg_read_buffer ) ); + msg_read.cursize = msglen; + + // read packet data + read = FS_Read( msg_read.data, msglen, f ); + if( read != msglen ) { + return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; + } + + return 1; +} + +static void parse_next_message( void ) { + int ret; + + ret = read_next_message( cls.demo.playback ); + if( ret <= 0 ) { + char *s = Cvar_VariableString( "nextserver" ); + + if( !s[0] ) { + if( ret == 0 ) { + Com_Error( ERR_SILENT, "Demo finished" ); + } else { + Com_Error( ERR_DROP, "Couldn't read demo: %s", Q_ErrorString( ret ) ); + } + } + + FS_FCloseFile( cls.demo.playback ); + memset( &cls.demo, 0, sizeof( cls.demo ) ); + + Cbuf_AddText( &cmd_buffer, s ); + Cbuf_AddText( &cmd_buffer, "\n" ); + Cvar_Set( "nextserver", "" ); + cls.state = ca_connected; + return; + } + + CL_ParseServerMessage(); + + if( cls.demo.file_size ) { + ssize_t pos = FS_Tell( cls.demo.playback ); + + if( pos > cls.demo.file_offset ) { + cls.demo.file_percent = ( pos - cls.demo.file_offset ) * 100 / cls.demo.file_size; + } + } +} + +/* +==================== +CL_PlayDemo_f +==================== +*/ +static void CL_PlayDemo_f( void ) { + char name[MAX_OSPATH]; + qhandle_t demofile; + char *arg; + ssize_t len, ofs; + int type, argc = Cmd_Argc(); + + if( argc < 2 ) { + Com_Printf( "Usage: %s <filename> [...]\n", Cmd_Argv( 0 ) ); + return; + } + + arg = Cmd_Argv( 1 ); + if( arg[0] == '/' ) { + // Assume full path is given + len = Q_strlcpy( name, arg + 1, sizeof( name ) ); + if( len >= sizeof( name ) ) { + len = Q_ERR_NAMETOOLONG; + goto fail; + } + len = FS_FOpenFile( name, &demofile, FS_MODE_READ ); + } else { + // Search for matching extensions + len = Q_concat( name, sizeof( name ), "demos/", arg, NULL ); + if( len >= sizeof( name ) ) { + len = Q_ERR_NAMETOOLONG; + goto fail; + } + len = FS_FOpenFile( name, &demofile, FS_MODE_READ ); + if( !demofile ) { + len = COM_DefaultExtension( name, ".dm2", sizeof( name ) ); + if( len >= sizeof( name ) ) { + len = Q_ERR_NAMETOOLONG; + goto fail; + } + len = FS_FOpenFile( name, &demofile, FS_MODE_READ ); + } + } + + if( !demofile ) { +fail: + Com_Printf( "Couldn't open %s: %s\n", name, Q_ErrorString( len ) ); + return; + } + +#if 0 + // add trailing filenames to play list + for( i = 2; i < argc; i++ ) { + arg = Cmd_Argv( i ); + length = strlen( arg ); + entry = Z_Malloc( sizeof( *entry ) + length ); + memcpy( entry->filename, arg, length + 1 ); + } +#endif + + type = read_first_message( demofile ); + if( type < 0 ) { + Com_Printf( "Couldn't read %s: %s\n", name, Q_ErrorString( type ) ); + FS_FCloseFile( demofile ); + return; + } + + if( type == 1 ) { + Cbuf_InsertText( &cmd_buffer, va( "mvdplay --replace @@ /%s\n", name ) ); + FS_FCloseFile( demofile ); + return; + } + + if( sv_running->integer ) { + // if running a local server, kill it and reissue + SV_Shutdown( "Server was killed\n", KILL_DROP ); + } + + CL_Disconnect( ERR_DISCONNECT, NULL ); + + Con_Close(); + + cls.demo.playback = demofile; + cls.state = ca_connected; + Q_strlcpy( cls.servername, COM_SkipPath( name ), sizeof( cls.servername ) ); + cls.serverAddress.type = NA_LOOPBACK; + + SCR_UpdateScreen(); + + CL_ParseServerMessage(); + while( cls.state == ca_connected ) { + Cbuf_Execute( &cl_cmdbuf ); + parse_next_message(); + } + + ofs = FS_Tell( demofile ); + if( ofs > 0 ) { + cls.demo.file_offset = ofs; + cls.demo.file_size = len - ofs; + } + + if( com_timedemo->integer ) { + cls.demo.time_frames = 0; + cls.demo.time_start = Sys_Milliseconds(); + } +} + +static void CL_Demo_c( genctx_t *ctx, int argnum ) { + if( argnum == 1 ) { + FS_File_g( "demos", "*.dm2;*.dm2.gz;*.mvd2;*.mvd2.gz", FS_SEARCH_SAVEPATH | FS_SEARCH_BYFILTER, ctx ); + } +} + +static void parse_info_string( demoInfo_t *info, int clientNum, int index, const char *string ) { + size_t len; + char *p; + + if( index >= CS_PLAYERSKINS && index < CS_PLAYERSKINS + MAX_CLIENTS ) { + if( index - CS_PLAYERSKINS == clientNum ) { + Q_strlcpy( info->pov, string, sizeof( info->pov ) ); + p = strchr( info->pov, '\\' ); + if( p ) { + *p = 0; + } + } + } else if( index == CS_MODELS + 1 ) { + len = strlen( string ); + if( len > 9 ) { + memcpy( info->map, string + 5, len - 9 ); // skip "maps/" + info->map[ len - 9 ] = 0; // cut off ".bsp" + } + } +} + +/* +==================== +CL_GetDemoInfo +==================== +*/ +demoInfo_t *CL_GetDemoInfo( const char *path, demoInfo_t *info ) { + qhandle_t f; + int c, index; + char string[MAX_QPATH]; + int clientNum, type; + + FS_FOpenFile( path, &f, FS_MODE_READ ); + if( !f ) { + return NULL; + } + + type = read_first_message( f ); + if( type < 0 ) { + goto fail; + } + + if( type == 0 ) { + if( MSG_ReadByte() != svc_serverdata ) { + goto fail; + } + if( MSG_ReadLong() != PROTOCOL_VERSION_DEFAULT ) { + goto fail; + } + MSG_ReadLong(); + MSG_ReadByte(); + MSG_ReadString( NULL, 0); + clientNum = MSG_ReadShort(); + MSG_ReadString( NULL, 0 ); + + while( 1 ) { + c = MSG_ReadByte(); + if( c == -1 ) { + if( read_next_message( f ) <= 0 ) { + break; + } + continue; // parse new message + } + if( c != svc_configstring ) { + break; + } + index = MSG_ReadShort(); + if( index < 0 || index >= MAX_CONFIGSTRINGS ) { + goto fail; + } + MSG_ReadString( string, sizeof( string ) ); + parse_info_string( info, clientNum, index, string ); + } + } else { + if( MSG_ReadByte() != mvd_serverdata ) { + goto fail; + } + if( MSG_ReadLong() != PROTOCOL_VERSION_MVD ) { + goto fail; + } + MSG_ReadShort(); + MSG_ReadLong(); + MSG_ReadString( NULL, 0 ); + clientNum = MSG_ReadShort(); + + while( 1 ) { + index = MSG_ReadShort(); + if( index == MAX_CONFIGSTRINGS ) { + break; + } + if( index < 0 || index >= MAX_CONFIGSTRINGS ) { + goto fail; + } + MSG_ReadString( string, sizeof( string ) ); + parse_info_string( info, clientNum, index, string ); + } + } + + FS_FCloseFile( f ); + return info; + +fail: + FS_FCloseFile( f ); + return NULL; + +} + +// ========================================================================= + + +/* +==================== +CL_DemoFrame +==================== +*/ +void CL_DemoFrame( void ) { + if( cls.state < ca_connected ) { + return; + } + if( cls.state != ca_active ) { + parse_next_message(); + return; + } + + if( com_timedemo->integer ) { + parse_next_message(); + cl.time = cl.servertime; + cls.demo.time_frames++; + return; + } + + while( cl.servertime < cl.time ) { + parse_next_message(); + if( cls.state != ca_active ) { + break; + } + } +} + +static const cmdreg_t c_demo[] = { + { "demo", CL_PlayDemo_f, CL_Demo_c }, + { "record", CL_Record_f, CL_Demo_c }, + { "stop", CL_Stop_f }, + { "suspend", CL_Suspend_f }, + + { NULL } +}; + +/* +==================== +CL_InitDemos +==================== +*/ +void CL_InitDemos( void ) { + Cmd_Register( c_demo ); +} + + |