//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #if defined( REPLAY_ENABLED ) #include #include #include #include #include "replaydemo.h" #include "replayserver.h" #include "demo.h" #include "host_cmd.h" #include "proto_version.h" #include "demofile/demoformat.h" #include "filesystem_engine.h" #include "net.h" #include "networkstringtable.h" #include "dt_common_eng.h" #include "host.h" #include "server.h" #include "networkstringtableclient.h" #include "replay_internal.h" #include "GameEventManager.h" #include "replay/ireplaysystem.h" #include "replay/ireplaysessionrecorder.h" #include "replay/shared_defs.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern CNetworkStringTableContainer *networkStringTableContainerServer; extern CGlobalVars g_ServerGlobalVariables; extern IServerReplayContext *g_pServerReplayContext; static ConVar *replay_record_voice = NULL; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// CReplayDemoRecorder::CReplayDemoRecorder( CReplayServer* pServer ) { m_bIsRecording = false; Assert( pServer ); m_pReplayServer = pServer; m_nStartTick = -1; } CReplayDemoRecorder::~CReplayDemoRecorder() { StopRecording(); } void CReplayDemoRecorder::GetUniqueDemoFilename( char *pOut, int nLength ) { Assert( pOut ); tm today; VCRHook_LocalTime( &today ); Q_snprintf( pOut, nLength, "%04i%02i%02i-%02i%02i%02i-%s.dem", 1900 + today.tm_year, today.tm_mon+1, today.tm_mday, today.tm_hour, today.tm_min, today.tm_sec, m_pReplayServer->GetMapName() ); } void CReplayDemoRecorder::StartRecording() { // Get a proper filename and cache it for later GetUniqueDemoFilename( m_szDumpFilename, sizeof( m_szDumpFilename ) ); // Start recording to the temporary location in the game dir StartRecording( TMP_REPLAY_FILENAME, false ); } const char *CReplayDemoRecorder::GetDemoFilename() { static char s_szDemoFilename[ MAX_OSPATH ]; const char *pFilename = replay->m_DemoRecorder.GetRecordingFilename(); Assert( pFilename && pFilename[0] ); V_strcpy( s_szDemoFilename, pFilename ); return s_szDemoFilename; } void CReplayDemoRecorder::StartRecording( const char *pFilename, bool bContinuously ) { SETUP_CVAR_REF( replay_recording ); StopRecording(); // stop if we're already recording // Attempt to "open" the demo file ConVarRef replay_buffersize( "replay_buffersize" ); const int nBufferSize = 1024 * 1024 * ( replay_buffersize.IsValid() ? replay_buffersize.GetInt() : 16 ); if ( !m_DemoFile.Open( NULL, false, true, nBufferSize, false ) ) { Warning( "Failed to start recording - couldn't open demo file %s.\n", pFilename ); return; } // Using this tickcount allows us to sync up client-side recorded ragdolls later with replay demos on clients m_nStartTick = g_ServerGlobalVariables.tickcount; demoheader_t *dh = &m_DemoFile.m_DemoHeader; // open demo header file containing sigon data Q_memset( dh, 0, sizeof(demoheader_t) ); Q_strncpy( dh->demofilestamp, DEMO_HEADER_ID, sizeof(dh->demofilestamp) ); dh->demoprotocol = DEMO_PROTOCOL; dh->networkprotocol = PROTOCOL_VERSION; Q_strncpy( dh->mapname, m_pReplayServer->GetMapName(), sizeof( dh->mapname ) ); char szGameDir[MAX_OSPATH]; Q_strncpy(szGameDir, com_gamedir, sizeof( szGameDir ) ); Q_FileBase ( szGameDir, dh->gamedirectory, sizeof( dh->gamedirectory ) ); Q_strncpy( dh->servername, host_name.GetString(), sizeof( dh->servername ) ); Q_strncpy( dh->clientname, "Replay Demo", sizeof( dh->servername ) ); // write demo file header info m_DemoFile.WriteDemoHeader(); dh->signonlength = WriteSignonData(); // demoheader will be written when demo is closed m_nFrameCount = 0; // Demo playback should read this as an incoming message. // Write the client's realtime value out so we can synchronize the reads. m_DemoFile.WriteCmdHeader( dem_synctick, 0 ); m_bIsRecording = true; m_SequenceInfo = 1; m_nDeltaTick = -1; replay_recording.SetValue( 1 ); extern ConVar replay_debug; if ( replay_debug.GetBool() ) ConMsg( "%f: Recording Replay...\n", host_time ); g_pServerReplayContext->GetSessionRecorder()->SetCurrentRecordingStartTick( m_nStartTick ); } bool CReplayDemoRecorder::IsRecording() { return m_bIsRecording; } void CReplayDemoRecorder::StopRecording() { if ( !IsRecording() ) return; // Wipe the demo (does not write to disk) m_DemoFile.Close(); // Set recording flag m_bIsRecording = false; // clear writing data buffer if ( m_MessageData.GetBasePointer() ) { delete [] m_MessageData.GetBasePointer(); m_MessageData.StartWriting( NULL, 0 ); } // replay_stoprecording gets set to 0 from within the replay session recorder, but only if we aren't starting to record a new round } CDemoFile *CReplayDemoRecorder::GetDemoFile() { return &m_DemoFile; } int CReplayDemoRecorder::GetRecordingTick() { return g_ServerGlobalVariables.tickcount - m_nStartTick; } void CReplayDemoRecorder::WriteServerInfo() { ALIGN4 byte buffer[ NET_MAX_PAYLOAD ] ALIGN4_POST; bf_write msg( "CReplayDemoRecorder::WriteServerInfo", buffer, sizeof( buffer ) ); SVC_ServerInfo serverinfo; // create serverinfo message // on the master demos are using sv object, on relays replay CBaseServer *pServer = (CBaseServer*)&sv; m_pReplayServer->FillServerInfo( serverinfo ); // fill rest of info message serverinfo.WriteToBuffer( msg ); // send first tick NET_Tick signonTick( m_nSignonTick, 0, 0 ); signonTick.WriteToBuffer( msg ); // Write replicated ConVars to non-listen server clients only NET_SetConVar convars; // build a list of all replicated convars Host_BuildConVarUpdateMessage( &convars, FCVAR_REPLICATED, true ); // write convars to demo convars.WriteToBuffer( msg ); // write stringtable baselines #ifndef SHARED_NET_STRING_TABLES m_pReplayServer->m_StringTables->WriteBaselines( msg ); #endif // send signon state NET_SignonState signonMsg( SIGNONSTATE_NEW, pServer->GetSpawnCount() ); signonMsg.WriteToBuffer( msg ); WriteMessages( dem_signon, msg ); } void CReplayDemoRecorder::RecordCommand( const char *cmdstring ) { if ( !IsRecording() ) return; if ( !cmdstring || !cmdstring[0] ) return; GET_REPLAY_DBG_REF(); if ( replay_debug.GetBool() ) Msg( "recording command, \"%s\"\n", cmdstring ); m_DemoFile.WriteConsoleCommand( cmdstring, GetRecordingTick() ); } void CReplayDemoRecorder::RecordServerClasses( ServerClass *pClasses ) { MEM_ALLOC_CREDIT(); char *pBigBuffer; CUtlBuffer bigBuff; int buffSize = 256*1024; if ( !IsX360() ) { pBigBuffer = (char*)stackalloc( buffSize ); } else { // keep temp large allocations off of stack bigBuff.EnsureCapacity( buffSize ); pBigBuffer = (char*)bigBuff.Base(); } bf_write buf( pBigBuffer, buffSize ); // Send SendTable info. DataTable_WriteSendTablesBuffer( pClasses, &buf ); // Send class descriptions. DataTable_WriteClassInfosBuffer( pClasses, &buf ); // Now write the buffer into the demo file m_DemoFile.WriteNetworkDataTables( &buf, GetRecordingTick() ); } void CReplayDemoRecorder::RecordStringTables() { // !KLUDGE! It would be nice if the bit buffer could write into a stream // with the power to grow itself. But it can't. Hence this really bad // kludge void *data = NULL; int dataLen = 512 * 1024; while ( dataLen <= DEMO_FILE_MAX_STRINGTABLE_SIZE ) { data = realloc( data, dataLen ); bf_write buf( data, dataLen ); buf.SetDebugName("CReplayDemoRecorder::RecordStringTables"); buf.SetAssertOnOverflow( false ); // Doesn't turn off all the spew / asserts, but turns off one networkStringTableContainerServer->WriteStringTables( buf ); // Did we fit? if ( !buf.IsOverflowed() ) { // Now write the buffer into the demo file m_DemoFile.WriteStringTables( &buf, GetRecordingTick() ); break; } // Didn't fit. Try doubling the size of the buffer dataLen *= 2; } if ( dataLen > DEMO_FILE_MAX_STRINGTABLE_SIZE ) { Warning( "Failed to RecordStringTables. Trying to record string table that's bigger than max string table size\n" ); } free(data); } int CReplayDemoRecorder::WriteSignonData() { int start = m_DemoFile.GetCurPos( false ); // on the master demos are using sv object, on relays replay CBaseServer *pServer = (CBaseServer*)&sv; m_nSignonTick = pServer->m_nTickCount; WriteServerInfo(); RecordServerClasses( serverGameDLL->GetAllServerClasses() ); RecordStringTables(); ALIGN4 byte buffer[ NET_MAX_PAYLOAD ] ALIGN4_POST; bf_write msg( "CReplayDemo::WriteSignonData", buffer, sizeof( buffer ) ); // use your class infos, CRC is correct SVC_ClassInfo classmsg( true, pServer->serverclasses ); classmsg.WriteToBuffer( msg ); // Write the regular signon now msg.WriteBits( m_pReplayServer->m_Signon.GetData(), m_pReplayServer->m_Signon.GetNumBitsWritten() ); // write new state NET_SignonState signonMsg1( SIGNONSTATE_PRESPAWN, pServer->GetSpawnCount() ); signonMsg1.WriteToBuffer( msg ); WriteMessages( dem_signon, msg ); msg.Reset(); // set view entity SVC_SetView viewent( m_pReplayServer->m_nViewEntity ); viewent.WriteToBuffer( msg ); // Spawned into server, not fully active, though NET_SignonState signonMsg2( SIGNONSTATE_SPAWN, pServer->GetSpawnCount() ); signonMsg2.WriteToBuffer( msg ); WriteMessages( dem_signon, msg ); return m_DemoFile.GetCurPos( false ) - start; } void CReplayDemoRecorder::WriteFrame( CReplayFrame *pFrame ) { ALIGN4 byte buffer[ NET_MAX_PAYLOAD ] ALIGN4_POST; bf_write msg( "CReplayDemo::RecordFrame", buffer, sizeof( buffer ) ); //first write reliable data bf_write *data = &pFrame->m_Messages[REPLAY_BUFFER_RELIABLE]; if ( data->GetNumBitsWritten() ) msg.WriteBits( data->GetBasePointer(), data->GetNumBitsWritten() ); //now send snapshot data // send tick time NET_Tick tickmsg( pFrame->tick_count, host_frametime_unbounded, host_frametime_stddeviation ); tickmsg.WriteToBuffer( msg ); #ifndef SHARED_NET_STRING_TABLES // Update shared client/server string tables. Must be done before sending entities sv.m_StringTables->WriteUpdateMessage( NULL, MAX( m_nSignonTick, m_nDeltaTick ), msg ); #endif // get delta frame CClientFrame *deltaFrame = m_pReplayServer->GetClientFrame( m_nDeltaTick ); // NULL if m_nDeltaTick is not found or -1 // send entity update, delta compressed if deltaFrame != NULL sv.WriteDeltaEntities( m_pReplayServer->m_MasterClient, pFrame, deltaFrame, msg ); // send all unreliable temp ents between last and current frame CFrameSnapshot * fromSnapshot = deltaFrame?deltaFrame->GetSnapshot():NULL; sv.WriteTempEntities( m_pReplayServer->m_MasterClient, pFrame->GetSnapshot(), fromSnapshot, msg, 255 ); // write sound data data = &pFrame->m_Messages[REPLAY_BUFFER_SOUNDS]; if ( data->GetNumBitsWritten() ) msg.WriteBits( data->GetBasePointer(), data->GetNumBitsWritten() ); // write voice data if ( replay_record_voice == NULL ) { replay_record_voice = g_pCVar->FindVar( "replay_record_voice" ); Assert( replay_record_voice != NULL ); } if ( replay_record_voice && replay_record_voice->GetBool() ) { data = &pFrame->m_Messages[REPLAY_BUFFER_VOICE]; if ( data->GetNumBitsWritten() ) msg.WriteBits( data->GetBasePointer(), data->GetNumBitsWritten() ); } // last write unreliable data data = &pFrame->m_Messages[REPLAY_BUFFER_UNRELIABLE]; if ( data->GetNumBitsWritten() ) msg.WriteBits( data->GetBasePointer(), data->GetNumBitsWritten() ); // update delta tick just like fake clients do m_nDeltaTick = pFrame->tick_count; // write packet to demo file WriteMessages( dem_packet, msg ); } void CReplayDemoRecorder::WriteMessages( unsigned char cmd, bf_write &message ) { int len = message.GetNumBytesWritten(); if (len <= 0) return; // fill last bits in last byte with NOP if necessary int nRemainingBits = message.GetNumBitsWritten() % 8; if ( nRemainingBits > 0 && nRemainingBits <= (8-NETMSG_TYPE_BITS) ) { message.WriteUBitLong( net_NOP, NETMSG_TYPE_BITS ); } Assert( len < NET_MAX_MESSAGE ); // if signondata read as fast as possible, no rewind // and wait for packet time // byte cmd = (m_pDemoFileHeader != NULL) ? dem_signon : dem_packet; if ( cmd == dem_packet ) { m_nFrameCount++; } // write command & time m_DemoFile.WriteCmdHeader( cmd, GetRecordingTick() ); // write NULL democmdinfo just to keep same format as client demos democmdinfo_t info; Q_memset( &info, 0, sizeof( info ) ); m_DemoFile.WriteCmdInfo( info ); // write continously increasing sequence numbers m_DemoFile.WriteSequenceInfo( m_SequenceInfo, m_SequenceInfo ); m_SequenceInfo++; // Output the buffer. Skip the network packet stuff. m_DemoFile.WriteRawData( (char*)message.GetBasePointer(), len ); } void CReplayDemoRecorder::RecordMessages(bf_read &data, int bits) { // create buffer if not there yet if ( m_MessageData.GetBasePointer() == NULL ) { m_MessageData.StartWriting( new unsigned char[NET_MAX_PAYLOAD], NET_MAX_PAYLOAD ); } if ( bits>0 ) { m_MessageData.WriteBitsFromBuffer( &data, bits ); Assert( !m_MessageData.IsOverflowed() ); } } void CReplayDemoRecorder::RecordPacket() { Assert( !"Does this ever get called? I can't find anywhere where it does." ); if( m_MessageData.GetBasePointer() ) { WriteMessages( dem_packet, m_MessageData ); m_MessageData.Reset(); // clear message buffer } } const char *CReplayDemoRecorder::GetRecordingFilename() { AssertMsg( 0, "Do we ever call this? " ); if ( !IsRecording() ) { Assert( 0 ); return NULL; } return m_szDumpFilename; } #endif