mirror of
https://github.com/nillerusr/source-engine.git
synced 2026-08-08 17:59:37 +00:00
1
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
//========= 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
|
||||
Reference in New Issue
Block a user