You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
488 lines
14 KiB
488 lines
14 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
//=============================================================================// |
|
|
|
#if defined( REPLAY_ENABLED ) |
|
|
|
#include <tier1/strtools.h> |
|
#include <eiface.h> |
|
#include <bitbuf.h> |
|
#include <time.h> |
|
#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
|
|
|