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.
2406 lines
62 KiB
2406 lines
62 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
#include "client_pch.h" |
|
#include "enginestats.h" |
|
#include "iprediction.h" |
|
#include "cl_demo.h" |
|
#include "cl_demoactionmanager.h" |
|
#include "cl_pred.h" |
|
|
|
#include "baseautocompletefilelist.h" |
|
#include "demofile/demoformat.h" |
|
#include "gl_matsysiface.h" |
|
#include "materialsystem/imaterialsystemhardwareconfig.h" |
|
#include "tier0/etwprof.h" |
|
#include "tier0/icommandline.h" |
|
#include "vengineserver_impl.h" |
|
#include "console.h" |
|
#include "dt_common_eng.h" |
|
#include "net_chan.h" |
|
#include "gl_model_private.h" |
|
#include "decal.h" |
|
#include "icliententitylist.h" |
|
#include "icliententity.h" |
|
#include "cl_demouipanel.h" |
|
#include "materialsystem/materialsystem_config.h" |
|
#include "tier2/tier2.h" |
|
#include "vgui_baseui_interface.h" |
|
#include "con_nprint.h" |
|
#include "networkstringtableclient.h" |
|
|
|
#ifdef SWDS |
|
#include "server.h" |
|
#endif |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
static ConVar demo_recordcommands( "demo_recordcommands", "1", FCVAR_CHEAT, "Record commands typed at console into .dem files." ); |
|
static ConVar demo_quitafterplayback( "demo_quitafterplayback", "0", 0, "Quits game after demo playback." ); |
|
static ConVar demo_debug( "demo_debug", "0", 0, "Demo debug info." ); |
|
static ConVar demo_interpolateview( "demo_interpolateview", "1", 0, "Do view interpolation during dem playback." ); |
|
static ConVar demo_pauseatservertick( "demo_pauseatservertick", "0", 0, "Pauses demo playback at server tick" ); |
|
static ConVar timedemo_runcount( "timedemo_runcount", "0", 0, "Runs time demo X number of times." ); |
|
|
|
// singeltons: |
|
static char g_pStatsFile[MAX_OSPATH] = { 0 }; |
|
static bool s_bBenchframe = false; |
|
|
|
static CDemoRecorder s_ClientDemoRecorder; |
|
CDemoRecorder *g_pClientDemoRecorder = &s_ClientDemoRecorder; |
|
IDemoRecorder *demorecorder = g_pClientDemoRecorder; |
|
|
|
static CDemoPlayer s_ClientDemoPlayer; |
|
CDemoPlayer *g_pClientDemoPlayer = &s_ClientDemoPlayer; |
|
IDemoPlayer *demoplayer = g_pClientDemoPlayer; |
|
|
|
extern CNetworkStringTableContainer *networkStringTableContainerClient; |
|
|
|
// This is the number of units under which we are allowed to interpolate, otherwise pop. |
|
// This fixes problems with in-level transitions. |
|
static ConVar demo_interplimit( "demo_interplimit", "4000", 0, "How much origin velocity before it's considered to have 'teleported' causing interpolation to reset." ); |
|
static ConVar demo_avellimit( "demo_avellimit", "2000", 0, "Angular velocity limit before eyes considered snapped for demo playback." ); |
|
|
|
#define DEMO_HEADER_FILE "demoheader.tmp" |
|
|
|
// Fast forward convars |
|
static ConVar demo_fastforwardstartspeed( "demo_fastforwardstartspeed", "2", 0, "Go this fast when starting to hold FF button." ); |
|
static ConVar demo_fastforwardfinalspeed( "demo_fastforwardfinalspeed", "20", 0, "Go this fast when starting to hold FF button." ); |
|
static ConVar demo_fastforwardramptime( "demo_fastforwardramptime", "5", 0, "How many seconds it takes to get to full FF speed." ); |
|
|
|
float scr_demo_override_fov = 0.0f; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Implements IDemo and handles demo file i/o |
|
// Demos are more or less driven off of network traffic, but there are a few |
|
// other kinds of data items that are also included in the demo file: specifically |
|
// commands that the client .dll itself issued to the engine are recorded, though they |
|
// probably were not the result of network traffic. |
|
// At the start of a connection to a map/server, all of the signon, etc. network packets |
|
// are buffered. This allows us to actually decide to start recording the demo at a later |
|
// time. Once we actually issue the recording command, we don't actually start recording |
|
// network traffic, but instead we ask the server for an "uncompressed" packet (otherwise |
|
// we wouldn't be able to deal with the incoming packets during playback because we'd be missing the |
|
// data to delta from ) and go into a waiting state. Once an uncompressed packet is received, |
|
// we unset the waiting state and start recording network packets from that point forward. |
|
// Demo's record the elapsed time based on the current client clock minus the time the demo was started |
|
// During playback, the elapsed time for playback ( based on the host_time, which is subject to the |
|
// host_frametime cvar ) is compared with the elapsed time on the message from the demo file. |
|
// If it's not quite time for the message yet, the demo input stream is rewound |
|
// The demo system sits at the point where the client is checking to see if any network messages |
|
// have arrived from the server. If the message isn't ready for processing, the demo system |
|
// just responds that there are no messages waiting and the client continues on |
|
// Once a true network message with entity data is read from the demo stream, a couple of other |
|
// actions occur. First, the timestamp in the demo file and the view origin/angles corresponding |
|
// to the message are cached off. Then, we search ahead (into the future) to find out the next true network message |
|
// we are going to read from the demo file. We store of it's elapsed time and view origin/angles |
|
// Every frame that the client is rendering, even if there is no data from the demo system, |
|
// the engine asks the demo system to compute an interpolated origin and view angles. This |
|
// is done by taking the current time on the host and figuring out how far that puts us between |
|
// the last read origin from the demo file and the time when we'll actually read out and use the next origin |
|
// We use Quaternions to avoid gimbel lock on interpolating the view angles |
|
// To make a movie recorded at a fixed frame rate you would simply set the host_framerate to the |
|
// desired playback fps ( e.g., 0.02 == 50 fps ), then issue the startmovie command, and then |
|
// play the demo. The demo system will think that the engine is running at 50 fps and will pull |
|
// messages accordingly, even though movie recording kills the actually framerate. |
|
// It will also render frames with render origin/angles interpolated in-between the previous and next origins |
|
// even if the recording framerate was not 50 fps or greater. The interpolation provides a smooth visual playback |
|
// of the demo information to the client without actually adding latency to the view position (because we are |
|
// looking into the future for the position, not buffering the past data ). |
|
//----------------------------------------------------------------------------- |
|
|
|
static bool IsControlCommand( unsigned char cmd ) |
|
{ |
|
return ( (cmd == dem_signon) || (cmd == dem_stop) || |
|
(cmd == dem_synctick) || (cmd == dem_datatables ) || |
|
(cmd == dem_stringtables) ); |
|
} |
|
|
|
|
|
// Puts a flashing overlay on the screen during demo recording/playback |
|
static ConVar cl_showdemooverlay( "cl_showdemooverlay", "0", 0, "How often to flash demo recording/playback overlay (0 - disable overlay, -1 - show always)" ); |
|
|
|
class DemoOverlay |
|
{ |
|
public: |
|
DemoOverlay(); |
|
~DemoOverlay(); |
|
|
|
public: |
|
void Tick(); |
|
void DrawOverlay( float fSetting ); |
|
|
|
protected: |
|
float m_fLastTickTime; |
|
float m_fLastTickOverlay; |
|
enum Overlay { OVR_NONE = 0, OVR_REC = 1 << 1, OVR_PLAY = 1 << 2 }; |
|
bool m_bTick; |
|
int m_maskDrawnOverlay; |
|
} g_DemoOverlay; |
|
|
|
DemoOverlay::DemoOverlay() : |
|
m_fLastTickTime( 0.f ), m_fLastTickOverlay( 0.f ), m_bTick( false ), m_maskDrawnOverlay( OVR_NONE ) |
|
{ |
|
} |
|
|
|
DemoOverlay::~DemoOverlay() |
|
{ |
|
} |
|
|
|
void DemoOverlay::Tick() |
|
{ |
|
if ( !m_bTick ) |
|
{ |
|
m_bTick = true; |
|
|
|
float const fRealTime = Sys_FloatTime(); |
|
if ( m_fLastTickTime != fRealTime ) |
|
{ |
|
m_fLastTickTime = fRealTime; |
|
|
|
float const fDelta = m_fLastTickTime - m_fLastTickOverlay; |
|
float const fSettingDelta = cl_showdemooverlay.GetFloat(); |
|
|
|
if ( fSettingDelta <= 0.f || |
|
fDelta >= fSettingDelta ) |
|
{ |
|
m_fLastTickOverlay = m_fLastTickTime; |
|
DrawOverlay( fSettingDelta ); |
|
} |
|
} |
|
|
|
m_bTick = false; |
|
} |
|
} |
|
|
|
void DemoOverlay::DrawOverlay( float fSetting ) |
|
{ |
|
int maskDrawnOverlay = OVR_NONE; |
|
|
|
if ( fSetting < 0.f ) |
|
{ |
|
// Keep drawing |
|
maskDrawnOverlay = |
|
( demorecorder->IsRecording() ? OVR_REC : 0 ) | |
|
( demoplayer->IsPlayingBack() ? OVR_PLAY : 0 ); |
|
} |
|
else if ( fSetting == 0.f ) |
|
{ |
|
// None |
|
maskDrawnOverlay = OVR_NONE; |
|
} |
|
else |
|
{ |
|
// Flash |
|
maskDrawnOverlay = ( !m_maskDrawnOverlay ) ? ( |
|
( demorecorder->IsRecording() ? OVR_REC : 0 ) | |
|
( demoplayer->IsPlayingBack() ? OVR_PLAY : 0 ) |
|
) : OVR_NONE; |
|
} |
|
|
|
int const idx = 1; |
|
|
|
if ( OVR_NONE == maskDrawnOverlay && |
|
OVR_NONE != m_maskDrawnOverlay ) |
|
{ |
|
con_nprint_s xprn; |
|
memset( &xprn, 0, sizeof( xprn ) ); |
|
xprn.index = idx; |
|
xprn.time_to_live = -1; |
|
Con_NXPrintf( &xprn, "" ); |
|
} |
|
|
|
if ( OVR_PLAY & maskDrawnOverlay ) |
|
{ |
|
con_nprint_s xprn; |
|
memset( &xprn, 0, sizeof( xprn ) ); |
|
xprn.index = idx; |
|
xprn.color[0] = 0.f; |
|
xprn.color[1] = 1.f; |
|
xprn.color[2] = 0.f; |
|
xprn.fixed_width_font = true; |
|
xprn.time_to_live = ( fSetting > 0.f ) ? fSetting : 1.f; |
|
Con_NXPrintf( &xprn, " PLAY " ); |
|
} |
|
|
|
if ( OVR_REC & maskDrawnOverlay ) |
|
{ |
|
con_nprint_s xprn; |
|
memset( &xprn, 0, sizeof( xprn ) ); |
|
xprn.index = idx; |
|
xprn.color[0] = 1.f; |
|
xprn.color[1] = 0.f; |
|
xprn.color[2] = 0.f; |
|
xprn.fixed_width_font = true; |
|
xprn.time_to_live = ( fSetting > 0.f ) ? fSetting : 1.f; |
|
Con_NXPrintf( &xprn, " REC " ); |
|
} |
|
|
|
m_maskDrawnOverlay = maskDrawnOverlay; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Mark whether we are waiting for the first uncompressed update packet |
|
// Input : waiting - |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::SetSignonState(int state) |
|
{ |
|
if ( demoplayer->IsPlayingBack() ) |
|
return; |
|
|
|
if ( state == SIGNONSTATE_NEW ) |
|
{ |
|
if ( m_DemoFile.IsOpen() ) |
|
{ |
|
// we are already recording a demo file |
|
CloseDemoFile(); |
|
|
|
// prepare for recording next demo |
|
m_nDemoNumber++; |
|
} |
|
|
|
StartupDemoHeader(); |
|
} |
|
else if ( state == SIGNONSTATE_SPAWN ) |
|
{ |
|
// close demo file header when this packet is finished |
|
m_bCloseDemoFile = true; |
|
} |
|
else if ( state == SIGNONSTATE_FULL ) |
|
{ |
|
if ( m_bRecording ) |
|
{ |
|
StartupDemoFile(); |
|
} |
|
} |
|
} |
|
|
|
int CDemoRecorder::GetRecordingTick( void ) |
|
{ |
|
if ( cl.m_nMaxClients > 1 ) |
|
{ |
|
return TIME_TO_TICKS( net_time ) - m_nStartTick; |
|
} |
|
else |
|
{ |
|
return cl.GetClientTickCount() - m_nStartTick; |
|
} |
|
} |
|
|
|
void CDemoRecorder::ResyncDemoClock() |
|
{ |
|
if ( cl.m_nMaxClients > 1 ) |
|
{ |
|
m_nStartTick = TIME_TO_TICKS( net_time ); |
|
} |
|
else |
|
{ |
|
m_nStartTick = cl.GetClientTickCount(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : info - |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::GetClientCmdInfo( democmdinfo_t& info ) |
|
{ |
|
info.flags = FDEMO_NORMAL; |
|
|
|
if( m_bResetInterpolation ) |
|
{ |
|
info.flags |= FDEMO_NOINTERP; |
|
m_bResetInterpolation = false; |
|
} |
|
|
|
g_pClientSidePrediction->GetViewOrigin( info.viewOrigin ); |
|
#ifndef SWDS |
|
info.viewAngles = cl.viewangles; |
|
#endif |
|
g_pClientSidePrediction->GetLocalViewAngles( info.localViewAngles ); |
|
|
|
// Nothing by default |
|
info.viewOrigin2.Init(); |
|
info.viewAngles2.Init(); |
|
info.localViewAngles2.Init(); |
|
} |
|
|
|
void CDemoRecorder::WriteBSPDecals() |
|
{ |
|
decallist_t *decalList = (decallist_t*)malloc( sizeof(decallist_t) * Draw_DecalMax() ); |
|
|
|
int decalcount = DecalListCreate( decalList ); |
|
|
|
char data[NET_MAX_PAYLOAD]; |
|
bf_write msg; |
|
|
|
msg.StartWriting( data, NET_MAX_PAYLOAD ); |
|
msg.SetDebugName( "DemoFileWriteBSPDecals" ); |
|
|
|
for ( int i = 0; i < decalcount; i++ ) |
|
{ |
|
decallist_t *entry = &decalList[ i ]; |
|
|
|
SVC_BSPDecal decal; |
|
|
|
bool found = false; |
|
|
|
IClientEntity *clientEntity = entitylist->GetClientEntity( entry->entityIndex ); |
|
|
|
if ( !clientEntity ) |
|
continue; |
|
|
|
|
|
const model_t * pModel = clientEntity->GetModel(); |
|
|
|
decal.m_Pos = entry->position; |
|
decal.m_nEntityIndex = entry->entityIndex; |
|
decal.m_nDecalTextureIndex = Draw_DecalIndexFromName( entry->name, &found ); |
|
decal.m_nModelIndex = 0; |
|
|
|
if ( pModel ) |
|
{ |
|
decal.m_nModelIndex = cl.LookupModelIndex( modelloader->GetName( pModel ) ); |
|
} |
|
|
|
decal.WriteToBuffer( msg ); |
|
} |
|
|
|
WriteMessages( msg ); |
|
|
|
free( decalList ); |
|
} |
|
|
|
void CDemoRecorder::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 CDemoRecorder::RecordStringTables() |
|
{ |
|
MEM_ALLOC_CREDIT(); |
|
|
|
// !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("CDemoRecorder::RecordStringTables"); |
|
buf.SetAssertOnOverflow( false ); // Doesn't turn off all the spew / asserts, but turns off one |
|
networkStringTableContainerClient->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); |
|
} |
|
|
|
void CDemoRecorder::RecordUserInput( int cmdnumber ) |
|
{ |
|
char buffer[256]; |
|
bf_write msg( "CDemo::WriteUserCmd", buffer, sizeof(buffer) ); |
|
|
|
g_ClientDLL->EncodeUserCmdToBuffer( msg, cmdnumber ); |
|
|
|
m_DemoFile.WriteUserCmd( cmdnumber, buffer, msg.GetNumBytesWritten(), GetRecordingTick() ); |
|
} |
|
|
|
void CDemoRecorder::ResetDemoInterpolation( void ) |
|
{ |
|
m_bResetInterpolation = true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: saves all cvars falgged with FVAR_DEMO to demo file |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::WriteDemoCvars() |
|
{ |
|
const ConCommandBase *var; |
|
|
|
for ( var= g_pCVar->GetCommands() ; var ; var=var->GetNext() ) |
|
{ |
|
if ( var->IsCommand() ) |
|
continue; |
|
|
|
const ConVar *pCvar = ( const ConVar * )var; |
|
|
|
if ( !pCvar->IsFlagSet( FCVAR_DEMO ) ) |
|
continue; |
|
|
|
char cvarcmd[MAX_OSPATH]; |
|
|
|
Q_snprintf( cvarcmd, sizeof(cvarcmd),"%s \"%s\"", |
|
pCvar->GetName(), Host_CleanupConVarStringValue( pCvar->GetString() ) ); |
|
|
|
m_DemoFile.WriteConsoleCommand( cvarcmd, GetRecordingTick() ); |
|
} |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *cmdname - |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::RecordCommand( const char *cmdstring ) |
|
{ |
|
if ( !IsRecording() ) |
|
return; |
|
|
|
if ( !cmdstring || !cmdstring[0] ) |
|
return; |
|
|
|
if ( !demo_recordcommands.GetInt() ) |
|
return; |
|
|
|
m_DemoFile.WriteConsoleCommand( cmdstring, GetRecordingTick() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::StartupDemoHeader( void ) |
|
{ |
|
CloseDemoFile(); // make sure it's closed |
|
|
|
// Note: this is replacing tmpfile() |
|
if ( !m_DemoFile.Open( DEMO_HEADER_FILE, false ) ) |
|
{ |
|
ConDMsg ("ERROR: couldn't open temporary header file.\n"); |
|
return; |
|
} |
|
|
|
m_bIsDemoHeader = true; |
|
|
|
Assert( m_MessageData.GetBasePointer() == NULL ); |
|
|
|
// setup writing data buffer |
|
m_MessageData.StartWriting( new unsigned char[NET_MAX_PAYLOAD], NET_MAX_PAYLOAD ); |
|
m_MessageData.SetDebugName( "DemoHeaderWriteBuffer" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::StartupDemoFile( void ) |
|
{ |
|
if ( !m_bRecording ) |
|
return; |
|
|
|
// Already recording!!! |
|
if ( m_DemoFile.IsOpen() ) |
|
return; |
|
|
|
char demoFileName[MAX_OSPATH]; |
|
|
|
if ( m_nDemoNumber <= 1 ) |
|
{ |
|
V_sprintf_safe( demoFileName, "%s.dem", m_szDemoBaseName ); |
|
} |
|
else |
|
{ |
|
V_sprintf_safe( demoFileName, "%s_%i.dem", m_szDemoBaseName, m_nDemoNumber ); |
|
} |
|
|
|
// strip any trailing whitespace |
|
Q_StripPrecedingAndTrailingWhitespace( demoFileName ); |
|
|
|
// make sure the .dem extension is still present |
|
char ext[10]; |
|
Q_ExtractFileExtension( demoFileName, ext, sizeof( ext ) ); |
|
if ( Q_strcasecmp( ext, "dem" ) ) |
|
{ |
|
ConMsg( "StartupDemoFile: invalid filename.\n" ); |
|
return; |
|
} |
|
|
|
if ( !m_DemoFile.Open( demoFileName, false ) ) |
|
return; |
|
|
|
// open demo header file containing sigondata |
|
FileHandle_t hDemoHeader = g_pFileSystem->Open( DEMO_HEADER_FILE, "rb" ); |
|
if ( hDemoHeader == FILESYSTEM_INVALID_HANDLE ) |
|
{ |
|
ConMsg ("StartupDemoFile: couldn't open demo file header.\n"); |
|
return; |
|
} |
|
|
|
Assert( m_MessageData.GetBasePointer() == NULL ); |
|
|
|
// setup writing data buffer |
|
m_MessageData.StartWriting( new unsigned char[NET_MAX_PAYLOAD], NET_MAX_PAYLOAD ); |
|
m_MessageData.SetDebugName( "DemoFileWriteBuffer" ); |
|
|
|
// fill demo header info |
|
demoheader_t *dh = &m_DemoFile.m_DemoHeader; |
|
Q_memset(dh, 0, sizeof(demoheader_t)); |
|
|
|
dh->demoprotocol = DEMO_PROTOCOL; |
|
dh->networkprotocol = PROTOCOL_VERSION; |
|
Q_strncpy(dh->demofilestamp, DEMO_HEADER_ID, sizeof(dh->demofilestamp) ); |
|
|
|
Q_FileBase( modelloader->GetName( host_state.worldmodel ), dh->mapname, 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, cl.m_szRetryAddress, sizeof( dh->servername ) ); |
|
Q_strncpy( dh->clientname, cl_name.GetString(), sizeof( dh->clientname ) ); |
|
|
|
|
|
// get size signon data size |
|
dh->signonlength = g_pFileSystem->Size(hDemoHeader); |
|
|
|
// write demo file header info |
|
m_DemoFile.WriteDemoHeader(); |
|
|
|
// copy signon data from header file to demo file |
|
m_DemoFile.WriteFileBytes( hDemoHeader, dh->signonlength ); |
|
|
|
// close but keep header file, we might need it for a second record |
|
g_pFileSystem->Close( hDemoHeader ); |
|
|
|
m_nFrameCount = 0; |
|
m_bIsDemoHeader = false; |
|
|
|
ResyncDemoClock(); // reset demo clock |
|
|
|
// tell client to sync demo clock too |
|
m_DemoFile.WriteCmdHeader( dem_synctick, 0 ); |
|
|
|
RecordStringTables(); |
|
|
|
// Demo playback should read this as an incoming message. |
|
WriteDemoCvars(); // save all cvars marked with FCVAR_DEMO |
|
|
|
WriteBSPDecals(); |
|
|
|
g_ClientDLL->HudReset(); |
|
|
|
// tell server that we started recording a demo |
|
cl.SendStringCmd( "demorestart" ); |
|
|
|
ConMsg ("Recording to %s...\n", demoFileName); |
|
|
|
g_ClientDLL->OnDemoRecordStart( m_szDemoBaseName ); |
|
} |
|
|
|
CDemoRecorder::CDemoRecorder() |
|
{ |
|
} |
|
|
|
CDemoRecorder::~CDemoRecorder() |
|
{ |
|
CloseDemoFile(); |
|
} |
|
|
|
CDemoFile *CDemoRecorder::GetDemoFile() |
|
{ |
|
return &m_DemoFile; |
|
} |
|
|
|
void CDemoRecorder::ResumeRecording() |
|
{ |
|
|
|
} |
|
|
|
void CDemoRecorder::PauseRecording() |
|
{ |
|
|
|
} |
|
|
|
|
|
void CDemoRecorder::CloseDemoFile() |
|
{ |
|
if ( m_DemoFile.IsOpen()) |
|
{ |
|
if ( !m_bIsDemoHeader ) |
|
{ |
|
// Demo playback should read this as an incoming message. |
|
m_DemoFile.WriteCmdHeader( dem_stop, GetRecordingTick() ); |
|
|
|
// update demo header infos |
|
m_DemoFile.m_DemoHeader.playback_ticks = GetRecordingTick(); |
|
m_DemoFile.m_DemoHeader.playback_time = host_state.interval_per_tick * GetRecordingTick(); |
|
m_DemoFile.m_DemoHeader.playback_frames = m_nFrameCount; |
|
|
|
// go back to header and write demoHeader with correct time and #frame again |
|
m_DemoFile.WriteDemoHeader(); |
|
|
|
ConMsg ("Completed demo, recording time %.1f, game frames %i.\n", |
|
m_DemoFile.m_DemoHeader.playback_time, m_DemoFile.m_DemoHeader.playback_frames ); |
|
} |
|
|
|
if ( demo_debug.GetInt() ) |
|
{ |
|
ConMsg ("Closed demo file, %i bytes.\n", m_DemoFile.GetSize() ); |
|
} |
|
|
|
m_DemoFile.Close(); |
|
|
|
g_ClientDLL->OnDemoRecordStop(); |
|
} |
|
|
|
m_bCloseDemoFile = false; |
|
m_bIsDemoHeader = false; |
|
|
|
// clear writing data buffer |
|
if ( m_MessageData.GetBasePointer() ) |
|
{ |
|
delete [] m_MessageData.GetBasePointer(); |
|
m_MessageData.StartWriting( NULL, 0 ); |
|
} |
|
} |
|
|
|
void CDemoRecorder::RecordMessages(bf_read &data, int bits) |
|
{ |
|
if ( m_MessageData.GetBasePointer() && (bits>0) ) |
|
{ |
|
m_MessageData.WriteBitsFromBuffer( &data, bits ); |
|
|
|
Assert( !m_MessageData.IsOverflowed() ); |
|
} |
|
} |
|
|
|
void CDemoRecorder::RecordPacket() |
|
{ |
|
WriteMessages( m_MessageData ); |
|
|
|
m_MessageData.Reset(); // clear message buffer |
|
|
|
if ( m_bCloseDemoFile ) |
|
{ |
|
CloseDemoFile(); |
|
} |
|
} |
|
|
|
void CDemoRecorder::WriteMessages( 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 |
|
unsigned char cmd = m_bIsDemoHeader ? dem_signon : dem_packet; |
|
|
|
if ( cmd == dem_packet ) |
|
{ |
|
m_nFrameCount++; |
|
} |
|
|
|
// write command & time |
|
m_DemoFile.WriteCmdHeader( cmd, GetRecordingTick() ); |
|
|
|
democmdinfo_t info; |
|
// Snag current info |
|
GetClientCmdInfo( info ); |
|
|
|
// Store it |
|
m_DemoFile.WriteCmdInfo( info ); |
|
|
|
// write network channel sequencing infos |
|
int nOutSequenceNr, nInSequenceNr, nOutSequenceNrAck; |
|
cl.m_NetChannel->GetSequenceData( nOutSequenceNr, nInSequenceNr, nOutSequenceNrAck ); |
|
m_DemoFile.WriteSequenceInfo( nInSequenceNr, nOutSequenceNrAck ); |
|
|
|
// Output the messge buffer. |
|
m_DemoFile.WriteRawData( (char*) message.GetBasePointer(), len ); |
|
|
|
if ( demo_debug.GetInt() >= 1 ) |
|
{ |
|
Msg( "Writing demo message %i bytes at file pos %i\n", len, m_DemoFile.GetCurPos( false ) ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: stop recording a demo |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::StopRecording( void ) |
|
{ |
|
if ( !IsRecording() ) |
|
{ |
|
return; |
|
} |
|
|
|
if ( m_MessageData.GetBasePointer() ) |
|
{ |
|
delete[] m_MessageData.GetBasePointer(); |
|
m_MessageData.StartWriting( NULL, 0); |
|
} |
|
|
|
CloseDemoFile(); |
|
|
|
m_bRecording = false; |
|
m_nDemoNumber = 0; |
|
|
|
g_DemoOverlay.Tick(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *name - |
|
// track - |
|
//----------------------------------------------------------------------------- |
|
void CDemoRecorder::StartRecording( const char *name, bool bContinuously ) |
|
{ |
|
Q_strncpy( m_szDemoBaseName, name, sizeof(m_szDemoBaseName)); |
|
|
|
m_bRecording = true; |
|
m_nDemoNumber = 1; |
|
m_bResetInterpolation = false; |
|
|
|
|
|
g_DemoOverlay.Tick(); |
|
|
|
// request a full game update from server |
|
cl.ForceFullUpdate(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CDemoRecorder::IsRecording( void ) |
|
{ |
|
g_DemoOverlay.Tick(); |
|
|
|
return m_bRecording; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called when a demo file runs out, or the user starts a game |
|
// Output : void CDemo::StopPlayback |
|
//----------------------------------------------------------------------------- |
|
void CDemoPlayer::StopPlayback( void ) |
|
{ |
|
if ( !IsPlayingBack() ) |
|
return; |
|
|
|
demoaction->StopPlaying(); |
|
|
|
m_DemoFile.Close(); |
|
m_bPlayingBack = false; |
|
m_bLoading = false; |
|
m_bPlaybackPaused = false; |
|
m_flAutoResumeTime = 0.0f; |
|
m_nEndTick = 0; |
|
|
|
if ( m_bTimeDemo ) |
|
{ |
|
g_EngineStats.EndRun(); |
|
|
|
if ( !s_bBenchframe ) |
|
{ |
|
WriteTimeDemoResults(); |
|
} |
|
else |
|
{ |
|
mat_norendering.SetValue( 0 ); |
|
} |
|
|
|
m_bTimeDemo = false; |
|
} |
|
else |
|
{ |
|
int framecount = host_framecount - m_nTimeDemoStartFrame; |
|
float demotime = Sys_FloatTime() - m_flTimeDemoStartTime; |
|
|
|
if ( demotime > 0.0f ) |
|
{ |
|
DevMsg( "Demo playback finished ( %.1f seconds, %i render frames, %.2f fps).\n", demotime, framecount, framecount/demotime); |
|
} |
|
|
|
} |
|
|
|
m_flPlaybackRateModifier = 1.0f; |
|
|
|
delete[] m_DemoPacket.data; |
|
m_DemoPacket.data = NULL; |
|
|
|
scr_demo_override_fov = 0.0f; |
|
|
|
if ( timedemo_runcount.GetInt() > 1 ) |
|
{ |
|
timedemo_runcount.SetValue( timedemo_runcount.GetInt() - 1 ); |
|
|
|
Cbuf_AddText( va( "timedemo %s", m_DemoFile.m_szFileName ) ); |
|
} |
|
else if ( demo_quitafterplayback.GetBool() ) |
|
{ |
|
Cbuf_AddText( "quit\n" ); |
|
} |
|
|
|
g_ClientDLL->OnDemoPlaybackStop(); |
|
} |
|
|
|
CDemoFile *CDemoPlayer::GetDemoFile( void ) |
|
{ |
|
return &m_DemoFile; |
|
} |
|
|
|
#define SKIP_TO_TICK_FLAG uint32( uint32( 0x88 ) << 24 ) |
|
|
|
bool CDemoPlayer::IsSkipping( void ) |
|
{ |
|
return m_bPlayingBack && ( m_nSkipToTick != -1 ); |
|
} |
|
|
|
bool CDemoPlayer::IsLoading( void ) |
|
{ |
|
return m_bLoading; |
|
} |
|
|
|
int CDemoPlayer::GetTotalTicks(void) |
|
{ |
|
return m_DemoFile.m_DemoHeader.playback_ticks; |
|
} |
|
|
|
void CDemoPlayer::SkipToTick( int tick, bool bRelative, bool bPause ) |
|
{ |
|
if ( bRelative ) |
|
{ |
|
tick = GetPlaybackTick() + tick; |
|
} |
|
|
|
if ( tick < 0 ) |
|
return; |
|
|
|
if ( tick < GetPlaybackTick() ) |
|
{ |
|
// we have to reload the whole demo file |
|
// we need to create a temp copy of the filename |
|
char fileName[MAX_OSPATH]; |
|
Q_strncpy( fileName, m_DemoFile.m_szFileName, sizeof(fileName) ); |
|
|
|
// reload current demo file |
|
ETWMarkPrintf( "DemoPlayer: Reloading demo file '%s'", fileName ); |
|
StartPlayback( fileName, m_bTimeDemo ); |
|
|
|
// Make sure the proper skipping occurs after reload |
|
if ( tick > 0 ) |
|
tick |= SKIP_TO_TICK_FLAG; |
|
} |
|
|
|
m_nSkipToTick = tick; |
|
ETWMark1I( "DemoPlayer: SkipToTick", tick ); |
|
|
|
if ( bPause ) |
|
PausePlayback( -1 ); |
|
} |
|
|
|
void CDemoPlayer::SetEndTick( int tick ) |
|
{ |
|
if ( tick < 0 ) |
|
return; |
|
|
|
m_nEndTick = tick; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Read in next demo message and send to local client over network channel, if it's time. |
|
// Output : bool |
|
//----------------------------------------------------------------------------- |
|
bool CDemoPlayer::ParseAheadForInterval( int curtick, int intervalticks ) |
|
{ |
|
int tick = 0; |
|
int dummy; |
|
byte cmd = dem_stop; |
|
|
|
democmdinfo_t nextinfo; |
|
|
|
long starting_position = m_DemoFile.GetCurPos( true ); |
|
|
|
// remove all entrys older than 32 ticks |
|
while ( m_DestCmdInfo.Count() > 0 ) |
|
{ |
|
DemoCommandQueue& entry = m_DestCmdInfo[ 0 ]; |
|
|
|
if ( entry.tick >= (curtick - 32) ) |
|
break; |
|
|
|
if ( entry.filepos >= starting_position ) |
|
break; |
|
|
|
m_DestCmdInfo.Remove( 0 ); |
|
} |
|
|
|
if ( m_bTimeDemo ) |
|
return false; |
|
|
|
while ( true ) |
|
{ |
|
// skip forward to the next dem_packet or dem_signon |
|
bool swallowmessages = true; |
|
do |
|
{ |
|
m_DemoFile.ReadCmdHeader( cmd, tick ); |
|
|
|
// COMMAND HANDLERS |
|
switch ( cmd ) |
|
{ |
|
case dem_synctick: |
|
case dem_stop: |
|
{ |
|
m_DemoFile.SeekTo( starting_position, true ); |
|
return false; |
|
} |
|
break; |
|
case dem_consolecmd: |
|
{ |
|
m_DemoFile.ReadConsoleCommand(); |
|
} |
|
break; |
|
case dem_datatables: |
|
{ |
|
m_DemoFile.ReadNetworkDataTables( NULL ); |
|
} |
|
break; |
|
case dem_usercmd: |
|
{ |
|
m_DemoFile.ReadUserCmd( NULL, dummy ); |
|
} |
|
break; |
|
case dem_stringtables: |
|
{ |
|
m_DemoFile.ReadStringTables( NULL ); |
|
} |
|
break; |
|
default: |
|
{ |
|
swallowmessages = false; |
|
} |
|
break; |
|
} |
|
} |
|
while ( swallowmessages ); |
|
|
|
int curpos = m_DemoFile.GetCurPos( true ); |
|
|
|
// we read now a dem_packet |
|
m_DemoFile.ReadCmdInfo( nextinfo ); |
|
m_DemoFile.ReadSequenceInfo( dummy, dummy ); |
|
m_DemoFile.ReadRawData( NULL, 0 ); |
|
|
|
DemoCommandQueue entry; |
|
entry.info = nextinfo; |
|
entry.tick = tick; |
|
entry.filepos = curpos; |
|
|
|
int i = 0; |
|
int c = m_DestCmdInfo.Count(); |
|
for ( ; i < c; ++i ) |
|
{ |
|
if ( m_DestCmdInfo[ i ].filepos == entry.filepos ) |
|
break; // cmdinfo is already in list |
|
} |
|
|
|
if ( i >= c ) |
|
{ |
|
// add cmdinfo to list |
|
if ( c > 0 ) |
|
{ |
|
if ( m_DestCmdInfo[ c - 1 ].tick > tick ) |
|
{ |
|
m_DestCmdInfo.RemoveAll(); |
|
} |
|
} |
|
|
|
m_DestCmdInfo.AddToTail( entry ); |
|
} |
|
|
|
if ( ( tick - curtick ) > intervalticks ) |
|
break; |
|
} |
|
|
|
m_DemoFile.SeekTo( starting_position, true ); |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Read in next demo message and send to local client over network channel, if it's time. |
|
// Output : netpacket_t* -- NULL if there is no packet available at this time. |
|
//----------------------------------------------------------------------------- |
|
netpacket_t *CDemoPlayer::ReadPacket( void ) |
|
{ |
|
int tick = 0; |
|
byte cmd = dem_signon; |
|
long curpos = 0; |
|
|
|
if ( ! m_DemoFile.IsOpen() ) |
|
{ |
|
m_bPlayingBack = false; |
|
Host_EndGame( true, "Tried to read a demo message with no demo file\n" ); |
|
return NULL; |
|
} |
|
|
|
// If game is still shutting down, then don't read any demo messages from file quite yet |
|
if ( HostState_IsGameShuttingDown() ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
Assert( IsPlayingBack() ); |
|
|
|
if ( IsSkipping() ) |
|
{ |
|
// Every nMaxConsecutiveSkipPackets frames return NULL so that we don't build up an |
|
// endless supply of unprocessed packets. This avoids causing overflows and excessive |
|
// "highwater marks" in various Dota subsystems. |
|
++m_nSkipPacketsPlayed; |
|
if ( m_nSkipPacketsPlayed >= nMaxConsecutiveSkipPackets ) |
|
{ |
|
m_nSkipPacketsPlayed = 0; |
|
return NULL; |
|
} |
|
} |
|
else |
|
{ |
|
m_nSkipPacketsPlayed = 0; |
|
} |
|
|
|
// External editor has paused playback |
|
if ( CheckPausedPlayback() ) |
|
return NULL; |
|
|
|
bool bStopReading = false; |
|
|
|
while ( !bStopReading ) |
|
{ |
|
curpos = m_DemoFile.GetCurPos( true ); |
|
|
|
m_DemoFile.ReadCmdHeader( cmd, tick ); |
|
|
|
// always read control commands |
|
if ( !IsControlCommand( cmd ) ) |
|
{ |
|
int playbacktick = GetPlaybackTick(); |
|
|
|
#if defined( RAD_TELEMETRY_ENABLED ) |
|
g_Telemetry.playbacktick = playbacktick; |
|
#endif |
|
|
|
// If the end tick is set, check to see if we should bail |
|
if ( m_nEndTick > 0 && playbacktick >= m_nEndTick ) |
|
{ |
|
m_nEndTick = 0; |
|
return NULL; |
|
} |
|
|
|
if ( !m_bTimeDemo ) |
|
{ |
|
// Time demo ignores clocks and tries to synchronize frames to what was recorded |
|
// I.e., while frame is the same, read messages, otherwise, skip out. |
|
// If we're still signing on, then just parse messages until fully connected no matter what |
|
if ( cl.IsActive() && |
|
(tick > playbacktick) && !IsSkipping() ) |
|
{ |
|
// is not time yet |
|
bStopReading = true; |
|
} |
|
} |
|
else |
|
{ |
|
if ( m_nTimeDemoCurrentFrame == host_framecount ) |
|
{ |
|
// If we are playing back a timedemo, and we've already passed on a |
|
// frame update for this host_frame tag, then we'll just skip this mess |
|
bStopReading = true; |
|
} |
|
} |
|
|
|
if ( bStopReading ) |
|
{ |
|
demoaction->Update( false, playbacktick, TICKS_TO_TIME( playbacktick ) ); |
|
m_DemoFile.SeekTo( curpos, true ); // go back to start of current demo command |
|
return NULL; // Not time yet, dont return packet data. |
|
} |
|
} |
|
|
|
// COMMAND HANDLERS |
|
switch ( cmd ) |
|
{ |
|
case dem_synctick: |
|
{ |
|
if ( demo_debug.GetBool() ) |
|
{ |
|
Msg( "%d dem_synctick\n", tick ); |
|
} |
|
|
|
ResyncDemoClock(); |
|
|
|
// Once demo clock got resync-ed we can go ahead and |
|
// perform skipping logic normally |
|
if ( ( m_nSkipToTick != -1 ) && |
|
( ( m_nSkipToTick & SKIP_TO_TICK_FLAG ) == SKIP_TO_TICK_FLAG ) ) |
|
{ |
|
m_nSkipToTick &= ~SKIP_TO_TICK_FLAG; |
|
} |
|
} |
|
break; |
|
case dem_stop: |
|
{ |
|
if ( demo_debug.GetBool() ) |
|
{ |
|
Msg( "%d dem_stop\n", tick ); |
|
} |
|
|
|
OnStopCommand(); |
|
|
|
return NULL; |
|
} |
|
break; |
|
case dem_consolecmd: |
|
{ |
|
const char * command = m_DemoFile.ReadConsoleCommand(); |
|
|
|
if ( demo_debug.GetBool() ) |
|
{ |
|
Msg( "%d dem_consolecmd [%s]\n", tick, command ); |
|
} |
|
|
|
Cbuf_AddText( command ); |
|
Cbuf_Execute(); |
|
} |
|
break; |
|
case dem_datatables: |
|
{ |
|
if ( demo_debug.GetBool() ) |
|
{ |
|
Msg( "%d dem_datatables\n", tick ); |
|
} |
|
|
|
void *data = malloc( 256*1024 ); // X360TBD: How much memory is really needed here? |
|
bf_read buf( "dem_datatables", data, 256*1024 ); |
|
m_DemoFile.ReadNetworkDataTables( &buf ); |
|
buf.Seek( 0 ); // re-read data |
|
|
|
// support for older engine demos |
|
if ( !DataTable_LoadDataTablesFromBuffer( &buf, m_DemoFile.m_DemoHeader.demoprotocol ) ) |
|
{ |
|
Host_Error( "Error parsing network data tables during demo playback." ); |
|
} |
|
free( data ); |
|
} |
|
break; |
|
case dem_stringtables: |
|
{ |
|
void *data = NULL; |
|
int dataLen = 512 * 1024; |
|
while ( dataLen <= DEMO_FILE_MAX_STRINGTABLE_SIZE ) |
|
{ |
|
data = realloc( data, dataLen ); |
|
bf_read buf( "dem_stringtables", data, dataLen ); |
|
// did we successfully read |
|
if ( m_DemoFile.ReadStringTables( &buf ) > 0 ) |
|
{ |
|
buf.Seek( 0 ); |
|
if ( !networkStringTableContainerClient->ReadStringTables( buf ) ) |
|
{ |
|
Host_Error( "Error parsing string tables during demo playback." ); |
|
} |
|
break; |
|
} |
|
|
|
// Didn't fit. Try doubling the size of the buffer |
|
dataLen *= 2; |
|
} |
|
|
|
if ( dataLen > DEMO_FILE_MAX_STRINGTABLE_SIZE ) |
|
{ |
|
Warning( "ReadPacket failed to read string tables. Trying to read string tables that's bigger than max string table size\n" ); |
|
} |
|
|
|
free( data ); |
|
} |
|
break; |
|
case dem_usercmd: |
|
{ |
|
|
|
if ( demo_debug.GetBool() ) |
|
{ |
|
Msg( "%d dem_usercmd\n", tick ); |
|
} |
|
|
|
char buffer[256]; |
|
int length = sizeof(buffer); |
|
int outgoing_sequence = m_DemoFile.ReadUserCmd( buffer, length ); |
|
|
|
// put it into a bitbuffer |
|
bf_read msg( "CDemo::ReadUserCmd", buffer, length ); |
|
|
|
g_ClientDLL->DecodeUserCmdFromBuffer( msg, outgoing_sequence ); |
|
|
|
// Note, we need to have the current outgoing sequence correct so we can do prediction |
|
// correctly during playback |
|
cl.lastoutgoingcommand = outgoing_sequence; |
|
|
|
} |
|
break; |
|
default: |
|
{ |
|
bStopReading = true; |
|
|
|
if ( IsSkipping() ) |
|
{ |
|
// adjust playback host_tickcount when skipping |
|
m_nStartTick = host_tickcount - tick; |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
|
|
if ( cmd == dem_packet ) |
|
{ |
|
// remember last frame we read a dem_packet update |
|
m_nTimeDemoCurrentFrame = host_framecount; |
|
} |
|
|
|
int inseq, outseqack, outseq = 0; |
|
|
|
m_DemoFile.ReadCmdInfo( m_LastCmdInfo ); |
|
|
|
m_DemoFile.ReadSequenceInfo( inseq, outseqack ); |
|
cl.m_NetChannel->SetSequenceData( outseq, inseq, outseqack ); |
|
|
|
int length = m_DemoFile.ReadRawData( (char*)m_DemoPacket.data, NET_MAX_PAYLOAD ); |
|
|
|
if ( demo_debug.GetBool() ) |
|
{ |
|
Msg( "%d network packet [%d]\n", tick, length ); |
|
} |
|
|
|
if ( length > 0 ) |
|
{ |
|
// succsessfully read new demopacket |
|
m_DemoPacket.received = realtime; |
|
m_DemoPacket.size = length; |
|
m_DemoPacket.message.StartReading( m_DemoPacket.data, m_DemoPacket.size ); |
|
|
|
if ( demo_debug.GetInt() >= 1 ) |
|
{ |
|
Msg( "Demo message, tick %i, %i bytes\n", GetPlaybackTick(), length ); |
|
} |
|
} |
|
|
|
// Try and jump ahead one frame |
|
m_bInterpolateView = ParseAheadForInterval( tick, 8 ); |
|
|
|
// ConMsg( "Reading message for %i : %f skip %i\n", m_nFrameCount, fElapsedTime, forceskip ? 1 : 0 ); |
|
|
|
// Skip a few ticks before doing any timing |
|
if ( (m_nTimeDemoStartFrame < 0) && GetPlaybackTick() > 100 ) |
|
{ |
|
m_nTimeDemoStartFrame = host_framecount; |
|
m_flTimeDemoStartTime = Sys_FloatTime(); |
|
m_flTotalFPSVariability = 0.0f; |
|
|
|
if ( m_bTimeDemo ) |
|
{ |
|
g_EngineStats.BeginRun(); |
|
} |
|
} |
|
|
|
if ( m_nSnapshotTick > 0 && m_nSnapshotTick <= GetPlaybackTick() ) |
|
{ |
|
const char *filename = "benchframe"; |
|
|
|
if ( m_SnapshotFilename[0] ) |
|
filename = m_SnapshotFilename; |
|
|
|
CL_TakeScreenshot( filename ); // take a screenshot |
|
m_nSnapshotTick = 0; |
|
|
|
if ( s_bBenchframe ) |
|
{ |
|
Cbuf_AddText( "stopdemo\n" ); |
|
} |
|
} |
|
|
|
return &m_DemoPacket; |
|
} |
|
|
|
void CDemoPlayer::InterpolateDemoCommand( int targettick, DemoCommandQueue& prev, DemoCommandQueue& next ) |
|
{ |
|
CUtlVector< DemoCommandQueue >& list = m_DestCmdInfo; |
|
int c = list.Count(); |
|
|
|
prev.info.Reset(); |
|
next.info.Reset(); |
|
|
|
if ( c < 2 ) |
|
{ |
|
// we need at least two entries to interpolate |
|
return; |
|
} |
|
|
|
int i = 0; |
|
int savedI = -1; |
|
|
|
DemoCommandQueue *entry1 = &list[ i ]; |
|
DemoCommandQueue *entry2 = &list[ i+1 ]; |
|
|
|
while ( true ) |
|
{ |
|
if ( (entry1->tick <= targettick) && (entry2->tick > targettick) ) |
|
{ |
|
// Means we hit a FDEMO_NOINTERP along the way to now |
|
if ( savedI != -1 ) |
|
{ |
|
prev = list[ savedI ]; |
|
next = list[ savedI + 1 ]; |
|
} |
|
else |
|
{ |
|
prev = *entry1; |
|
next = *entry2; |
|
} |
|
return; |
|
} |
|
|
|
// If any command between the previous target and now has the FDEMO_NOINTERP, we need to stop at the command just before that (entry), so we save off the I |
|
// We can't just return since we need to see if we actually get to a spanning pair (though we always should). Also, we only latch this final interp spot on |
|
/// the first FDEMO_NOINTERP we see |
|
if ( savedI == -1 && |
|
entry2->tick > m_nPreviousTick && |
|
entry2->tick <= targettick && |
|
entry2->info.flags & FDEMO_NOINTERP ) |
|
{ |
|
savedI = i; |
|
} |
|
|
|
if ( i+2 == c ) |
|
break; |
|
|
|
i++; |
|
entry1 = &list[ i ]; |
|
entry2 = &list[ i+1 ]; |
|
} |
|
|
|
Assert( 0 ); |
|
} |
|
|
|
static ConVar demo_legacy_rollback( "demo_legacy_rollback", "1", 0, "Use legacy view interpolation rollback amount in demo playback." ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CDemoPlayer::InterpolateViewpoint( void ) |
|
{ |
|
if ( !IsPlayingBack() ) |
|
return; |
|
|
|
democmdinfo_t outinfo; |
|
outinfo.Reset(); |
|
|
|
bool bHasValidData = |
|
m_LastCmdInfo.viewOrigin != vec3_origin || |
|
m_LastCmdInfo.viewAngles != vec3_angle || |
|
m_LastCmdInfo.localViewAngles != vec3_angle || |
|
m_LastCmdInfo.flags != 0; |
|
|
|
int nTargetTick = GetPlaybackTick(); |
|
|
|
// Player view needs to be one tick interval in the past like the client DLL entities |
|
if ( cl.m_nMaxClients == 1 ) |
|
{ |
|
if ( demo_legacy_rollback.GetBool() ) |
|
{ |
|
nTargetTick -= TIME_TO_TICKS( cl.GetClientInterpAmount() ) + 1; |
|
} |
|
else |
|
{ |
|
nTargetTick -= 1; |
|
} |
|
} |
|
|
|
float vel = 0.0f; |
|
float angVel = 0.0f; |
|
if ( m_bInterpolateView && demo_interpolateview.GetBool() && bHasValidData ) |
|
{ |
|
DemoCommandQueue prev, next; |
|
float frac = 0.0f; |
|
|
|
prev.info = m_LastCmdInfo; |
|
prev.tick = -1; |
|
next.info = m_LastCmdInfo; |
|
next.tick = -1; |
|
|
|
// Determine current time slice |
|
|
|
InterpolateDemoCommand( nTargetTick, prev, next ); |
|
|
|
float dt = TICKS_TO_TIME(next.tick-prev.tick); |
|
|
|
frac = (TICKS_TO_TIME(nTargetTick-prev.tick)+cl.m_tickRemainder)/dt; |
|
|
|
frac = clamp( frac, 0.0f, 1.0f ); |
|
|
|
// Now interpolate |
|
Vector delta; |
|
|
|
Vector startorigin = prev.info.GetViewOrigin(); |
|
Vector destorigin = next.info.GetViewOrigin(); |
|
|
|
// check for teleporting - since there can be multiple cmd packets between a game frame, |
|
// we need to check from the last actually ran command to see if there was a teleport |
|
VectorSubtract( destorigin, m_LastCmdInfo.GetViewOrigin(), delta ); |
|
float distmoved = delta.Length(); |
|
|
|
if ( dt > 0.0f ) |
|
{ |
|
vel = distmoved / dt; |
|
} |
|
|
|
if ( dt > 0.0f ) |
|
{ |
|
QAngle startang = prev.info.GetLocalViewAngles(); |
|
QAngle destang = next.info.GetLocalViewAngles(); |
|
|
|
for ( int i = 0; i < 3; ++i ) |
|
{ |
|
float dAng = AngleNormalizePositive( destang[ i ] ) - AngleNormalizePositive( startang[ i ] ); |
|
dAng = AngleNormalize( dAng ); |
|
float aVel = fabs( dAng ) / dt; |
|
if ( aVel > angVel ) |
|
{ |
|
angVel = aVel; |
|
} |
|
} |
|
} |
|
|
|
// FIXME: This should be velocity based maybe? |
|
if ( (vel > demo_interplimit.GetFloat()) || |
|
(angVel > demo_avellimit.GetFloat() ) || |
|
m_bResetInterpolation ) |
|
{ |
|
m_bResetInterpolation = false; |
|
|
|
// it's a teleport, just let it happen naturally next frame |
|
// setting frac to 1.0 (like it was previously) would just mean that we |
|
// are teleporting a frame ahead of when we should |
|
outinfo.viewOrigin = m_LastCmdInfo.GetViewOrigin(); |
|
outinfo.viewAngles = m_LastCmdInfo.GetViewAngles(); |
|
outinfo.localViewAngles = m_LastCmdInfo.GetLocalViewAngles(); |
|
} |
|
else |
|
{ |
|
outinfo.viewOrigin = startorigin + frac * ( destorigin - startorigin ); |
|
|
|
Quaternion src, dest; |
|
Quaternion result; |
|
|
|
AngleQuaternion( prev.info.GetViewAngles(), src ); |
|
AngleQuaternion( next.info.GetViewAngles(), dest ); |
|
QuaternionSlerp( src, dest, frac, result ); |
|
|
|
QuaternionAngles( result, outinfo.viewAngles ); |
|
|
|
AngleQuaternion( prev.info.GetLocalViewAngles(), src ); |
|
AngleQuaternion( next.info.GetLocalViewAngles(), dest ); |
|
QuaternionSlerp( src, dest, frac, result ); |
|
|
|
QuaternionAngles( result, outinfo.localViewAngles ); |
|
} |
|
} |
|
else if ( bHasValidData ) |
|
{ |
|
// don't interpolate, just copy values |
|
outinfo.viewOrigin = m_LastCmdInfo.GetViewOrigin(); |
|
outinfo.viewAngles = m_LastCmdInfo.GetViewAngles(); |
|
outinfo.localViewAngles = m_LastCmdInfo.GetLocalViewAngles(); |
|
} |
|
|
|
m_nPreviousTick = nTargetTick; |
|
|
|
// let any demo system override view ( drive, editor, smoother etc) |
|
bHasValidData |= OverrideView( outinfo ); |
|
|
|
if ( !bHasValidData ) |
|
return; // no validate data & no override, exit |
|
|
|
g_pClientSidePrediction->SetViewOrigin( outinfo.viewOrigin ); |
|
g_pClientSidePrediction->SetViewAngles( outinfo.viewAngles ); |
|
g_pClientSidePrediction->SetLocalViewAngles( outinfo.localViewAngles ); |
|
#ifndef SWDS |
|
VectorCopy( outinfo.viewAngles, cl.viewangles ); |
|
#endif |
|
|
|
|
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CDemoPlayer::IsPlayingTimeDemo( void ) |
|
{ |
|
return m_bTimeDemo && m_bPlayingBack; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CDemoPlayer::IsPlayingBack( void ) |
|
{ |
|
return m_bPlayingBack; |
|
} |
|
|
|
CDemoPlayer::CDemoPlayer() |
|
{ |
|
m_flAutoResumeTime = 0.0f; |
|
m_flPlaybackRateModifier = 1.0f; |
|
m_bTimeDemo = false; |
|
m_nTimeDemoStartFrame = -1; |
|
m_flTimeDemoStartTime = 0.0f; |
|
m_flTotalFPSVariability = 0.0f; |
|
m_nTimeDemoCurrentFrame = -1; |
|
m_bPlayingBack = false; |
|
m_bLoading = false; |
|
m_bPlaybackPaused = false; |
|
m_nSkipToTick = -1; |
|
m_nSkipPacketsPlayed = 0; |
|
m_nSnapshotTick = 0; |
|
m_SnapshotFilename[0] = 0; |
|
m_bResetInterpolation = false; |
|
m_nPreviousTick = 0; |
|
m_nEndTick = 0; |
|
} |
|
|
|
CDemoPlayer::~CDemoPlayer() |
|
{ |
|
StopPlayback(); |
|
if ( g_ClientDLL ) |
|
{ |
|
g_ClientDLL->OnDemoPlaybackStop(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Start's demo playback |
|
// Input : *name - |
|
//----------------------------------------------------------------------------- |
|
bool CDemoPlayer::StartPlayback( const char *filename, bool bAsTimeDemo ) |
|
{ |
|
m_bLoading = true; |
|
|
|
SCR_BeginLoadingPlaque(); |
|
|
|
// Disconnect from server or stop running one |
|
int oldn = cl.demonum; |
|
cl.demonum = -1; |
|
Host_Disconnect(false); |
|
cl.demonum = oldn; |
|
|
|
if ( !m_DemoFile.Open( filename, true ) ) |
|
{ |
|
cl.demonum = -1; // stop demo loop |
|
return false; |
|
} |
|
|
|
// Read in the m_DemoHeader |
|
demoheader_t *dh = m_DemoFile.ReadDemoHeader(); |
|
|
|
if ( !dh ) |
|
{ |
|
ConMsg( "Failed to read demo header.\n" ); |
|
m_DemoFile.Close(); |
|
cl.demonum = -1; |
|
return false; |
|
} |
|
|
|
ConMsg ("Playing demo from %s.\n", filename); |
|
|
|
// Now read in the directory structure. |
|
m_bPlayingBack = true; |
|
cl.m_nSignonState= SIGNONSTATE_CONNECTED; |
|
|
|
ResyncDemoClock(); |
|
|
|
// create a fake channel with a NULL address |
|
cl.m_NetChannel = NET_CreateNetChannel( NS_CLIENT, NULL, "DEMO", &cl, false, dh->networkprotocol ); |
|
|
|
if ( !cl.m_NetChannel ) |
|
{ |
|
ConMsg ("CDemo::Play: failed to create demo net channel\n" ); |
|
m_DemoFile.Close(); |
|
cl.demonum = -1; // stop demo loop |
|
Host_Disconnect(true); |
|
} |
|
|
|
cl.m_NetChannel->SetTimeout( -1.0f ); // never timeout |
|
|
|
Q_memset( &m_DemoPacket, 0, sizeof(m_DemoPacket) ); |
|
|
|
// setup demo packet data buffer |
|
m_DemoPacket.data = new unsigned char[NET_MAX_PAYLOAD]; |
|
m_DemoPacket.from.SetType( NA_LOOPBACK); |
|
|
|
cl.chokedcommands = 0; |
|
cl.lastoutgoingcommand = -1; |
|
cl.m_flNextCmdTime = net_time; |
|
|
|
m_bTimeDemo = bAsTimeDemo; |
|
m_nTimeDemoCurrentFrame = -1; |
|
m_nTimeDemoStartFrame = -1; |
|
|
|
if ( m_bTimeDemo ) |
|
{ |
|
SeedRandomNumberGenerator( true ); |
|
} |
|
|
|
demoaction->StartPlaying( filename ); |
|
|
|
// m_bFastForwarding = false; |
|
m_flAutoResumeTime = 0.0f; |
|
m_flPlaybackRateModifier = 1.0f; |
|
|
|
scr_demo_override_fov = 0.0f; |
|
|
|
m_bLoading = false; |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : flCurTime - |
|
//----------------------------------------------------------------------------- |
|
void CDemoPlayer::MarkFrame( float flFPSVariability ) |
|
{ |
|
m_flTotalFPSVariability += flFPSVariability; |
|
} |
|
|
|
void CDemoPlayer::WriteTimeDemoResults( void ) |
|
{ |
|
int frames; |
|
float time; |
|
frames = (host_framecount - m_nTimeDemoStartFrame) - 1; |
|
time = Sys_FloatTime() - m_flTimeDemoStartTime; |
|
if (!time) |
|
{ |
|
time = 1; |
|
} |
|
float flVariability = (m_flTotalFPSVariability / (float)frames); |
|
ConMsg ("%i frames %5.3f seconds %5.2f fps (%5.2f ms/f) %5.3f fps variability\n", frames, time, frames/time, 1000*time/frames, flVariability ); |
|
bool bFileExists = g_pFileSystem->FileExists( "SourceBench.csv" ); |
|
FileHandle_t fileHandle = g_pFileSystem->Open( "SourceBench.csv", "a+" ); |
|
int width, height; |
|
CMatRenderContextPtr pRenderContext( materials ); |
|
pRenderContext->GetWindowSize( width, height ); |
|
|
|
const MaterialSystem_Config_t &config = materials->GetCurrentConfigForVideoCard(); |
|
|
|
if( !bFileExists ) |
|
{ |
|
g_pFileSystem->FPrintf( fileHandle, "demofile," ); |
|
g_pFileSystem->FPrintf( fileHandle, "fps," ); |
|
g_pFileSystem->FPrintf( fileHandle, "framerate variability," ); |
|
g_pFileSystem->FPrintf( fileHandle, "totaltime," ); |
|
g_pFileSystem->FPrintf( fileHandle, "numframes," ); |
|
g_pFileSystem->FPrintf( fileHandle, "width," ); |
|
g_pFileSystem->FPrintf( fileHandle, "height," ); |
|
g_pFileSystem->FPrintf( fileHandle, "windowed," ); |
|
g_pFileSystem->FPrintf( fileHandle, "vsync," ); |
|
g_pFileSystem->FPrintf( fileHandle, "MSAA," ); |
|
g_pFileSystem->FPrintf( fileHandle, "Aniso," ); |
|
g_pFileSystem->FPrintf( fileHandle, "dxlevel," ); |
|
g_pFileSystem->FPrintf( fileHandle, "cmdline," ); |
|
g_pFileSystem->FPrintf( fileHandle, "driver name," ); |
|
g_pFileSystem->FPrintf( fileHandle, "vendor id," ); |
|
g_pFileSystem->FPrintf( fileHandle, "device id," ); |
|
|
|
// g_pFileSystem->FPrintf( fileHandle, "sound," ); |
|
g_pFileSystem->FPrintf( fileHandle, "Reduce fillrate," ); |
|
g_pFileSystem->FPrintf( fileHandle, "reflect entities," ); |
|
g_pFileSystem->FPrintf( fileHandle, "motion blur," ); |
|
g_pFileSystem->FPrintf( fileHandle, "flashlight shadows," ); |
|
g_pFileSystem->FPrintf( fileHandle, "mat_reduceparticles," ); |
|
g_pFileSystem->FPrintf( fileHandle, "r_dopixelvisibility," ); |
|
g_pFileSystem->FPrintf( fileHandle, "nulldevice," ); |
|
g_pFileSystem->FPrintf( fileHandle, "timedemo_comment," ); |
|
g_pFileSystem->FPrintf( fileHandle, "\n" ); |
|
} |
|
|
|
ConVarRef mat_vsync( "mat_vsync" ); |
|
ConVarRef mat_antialias( "mat_antialias" ); |
|
ConVarRef mat_forceaniso( "mat_forceaniso" ); |
|
ConVarRef r_waterforcereflectentities( "r_waterforcereflectentities" ); |
|
ConVarRef mat_motion_blur_enabled( "mat_motion_blur_enabled" ); |
|
ConVarRef r_flashlightdepthtexture( "r_flashlightdepthtexture" ); |
|
ConVarRef mat_reducefillrate( "mat_reducefillrate" ); |
|
ConVarRef mat_reduceparticles( "mat_reduceparticles" ); |
|
ConVarRef r_dopixelvisibility( "r_dopixelvisibility" ); |
|
|
|
g_pFileSystem->Seek( fileHandle, 0, FILESYSTEM_SEEK_TAIL ); |
|
MaterialAdapterInfo_t info; |
|
materials->GetDisplayAdapterInfo( materials->GetCurrentAdapter(), info ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", m_DemoFile.m_szFileName ); |
|
g_pFileSystem->FPrintf( fileHandle, "%5.1f,", frames/time ); |
|
g_pFileSystem->FPrintf( fileHandle, "%5.1f,", flVariability ); |
|
g_pFileSystem->FPrintf( fileHandle, "%5.1f,", time ); |
|
g_pFileSystem->FPrintf( fileHandle, "%i,", frames ); |
|
g_pFileSystem->FPrintf( fileHandle, "%i,", width ); |
|
g_pFileSystem->FPrintf( fileHandle, "%i,", height ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", config.Windowed() ? "windowed" : "fullscreen"); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", mat_vsync.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%d,", mat_antialias.GetInt() ); |
|
g_pFileSystem->FPrintf( fileHandle, "%d,", mat_forceaniso.GetInt() ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", COM_DXLevelToString( g_pMaterialSystemHardwareConfig->GetDXSupportLevel() ) ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", CommandLine()->GetCmdLine() ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", info.m_pDriverName ); |
|
g_pFileSystem->FPrintf( fileHandle, "0x%x,", info.m_VendorID ); |
|
g_pFileSystem->FPrintf( fileHandle, "0x%x,", info.m_DeviceID ); |
|
|
|
// g_pFileSystem->FPrintf( fileHandle, "%s,", CommandLine()->CheckParm( "-nosound" ) ? "off" : "on" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", mat_reducefillrate.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", r_waterforcereflectentities.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", mat_motion_blur_enabled.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", r_flashlightdepthtexture.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", mat_reduceparticles.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", r_dopixelvisibility.GetBool() ? "on" : "off" ); |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", CommandLine()->CheckParm( "-nulldevice" ) ? "yes" : "no" ); |
|
|
|
int itimedemo_comment = CommandLine()->FindParm( "-timedemo_comment" ); |
|
const char *timedemo_comment = itimedemo_comment ? CommandLine()->GetParm( itimedemo_comment + 1 ) : ""; |
|
g_pFileSystem->FPrintf( fileHandle, "%s,", timedemo_comment ); |
|
g_pFileSystem->FPrintf( fileHandle, "\n" ); |
|
g_pFileSystem->Close( fileHandle ); |
|
} |
|
|
|
|
|
void CDemoPlayer::PausePlayback( float seconds ) |
|
{ |
|
m_bPlaybackPaused = true; |
|
|
|
if ( seconds > 0.0f ) |
|
{ |
|
// Use true clock since everything else is frozen |
|
m_flAutoResumeTime = Sys_FloatTime() + seconds; |
|
} |
|
else |
|
{ |
|
m_flAutoResumeTime = 0.0f; |
|
} |
|
} |
|
|
|
void CDemoPlayer::ResumePlayback() |
|
{ |
|
m_bPlaybackPaused = false; |
|
m_flAutoResumeTime = 0.0f; |
|
} |
|
|
|
bool CDemoPlayer::CheckPausedPlayback() |
|
{ |
|
if ( demo_pauseatservertick.GetInt() > 0 ) |
|
{ |
|
if ( cl.GetServerTickCount() >= demo_pauseatservertick.GetInt() ) |
|
{ |
|
PausePlayback( -1 ); |
|
ETWMark1I( "DemoPlayer: Reached pause tick", cl.GetServerTickCount() ); |
|
m_nSkipToTick = -1; |
|
demo_pauseatservertick.SetValue( 0 ); |
|
Msg( "Demo paused at server tick %i\n", cl.GetServerTickCount() ); |
|
} |
|
} |
|
|
|
if ( IsSkipping() ) |
|
{ |
|
if ( ( m_nSkipToTick > GetPlaybackTick() ) || |
|
( ( m_nSkipToTick & SKIP_TO_TICK_FLAG ) == SKIP_TO_TICK_FLAG ) ) |
|
{ |
|
// we are skipping |
|
return false; |
|
} |
|
else |
|
{ |
|
// we can't skip back (or finished skipping), so disable skipping |
|
ETWMark1I( "DemoPlayer: SkipToTick done", GetPlaybackTick() ); |
|
m_nSkipToTick = -1; |
|
} |
|
} |
|
|
|
if ( !IsPlaybackPaused() ) |
|
return false; |
|
|
|
if ( m_bPlaybackPaused ) |
|
{ |
|
if ( (m_flAutoResumeTime > 0.0f) && |
|
(Sys_FloatTime() >= m_flAutoResumeTime) ) |
|
{ |
|
// it's time to unpause replay |
|
ResumePlayback(); |
|
} |
|
} |
|
|
|
return m_bPlaybackPaused; |
|
} |
|
|
|
bool CDemoPlayer::IsPlaybackPaused() |
|
{ |
|
if ( !IsPlayingBack() ) |
|
return false; |
|
|
|
// never pause while reading signon data |
|
if ( m_nTimeDemoCurrentFrame < 0 ) |
|
return false; |
|
|
|
// If skipping then do not pretend paused |
|
if ( IsSkipping() ) |
|
return false; |
|
|
|
return m_bPlaybackPaused; |
|
} |
|
|
|
int CDemoPlayer::GetPlaybackStartTick( void ) |
|
{ |
|
return m_nStartTick; |
|
} |
|
|
|
int CDemoPlayer::GetPlaybackTick( void ) |
|
{ |
|
return host_tickcount - m_nStartTick; |
|
} |
|
|
|
void CDemoPlayer::ResyncDemoClock() |
|
{ |
|
m_nStartTick = host_tickcount; |
|
m_nPreviousTick = m_nStartTick; |
|
} |
|
|
|
float CDemoPlayer::GetPlaybackTimeScale() |
|
{ |
|
return m_flPlaybackRateModifier; |
|
} |
|
|
|
void CDemoPlayer::SetPlaybackTimeScale(float timescale) |
|
{ |
|
m_flPlaybackRateModifier = timescale; |
|
} |
|
|
|
void CDemoPlayer::SetBenchframe( int tick, const char *filename ) |
|
{ |
|
m_nSnapshotTick = tick; |
|
|
|
if ( filename ) |
|
{ |
|
Q_strncpy( m_SnapshotFilename, filename, sizeof(m_SnapshotFilename) ); |
|
} |
|
} |
|
|
|
static bool ComputeNextIncrementalDemoFilename( char *name, int namesize ) |
|
{ |
|
FileHandle_t test; |
|
|
|
test = g_pFileSystem->Open( name, "rb" ); |
|
if ( FILESYSTEM_INVALID_HANDLE == test ) |
|
{ |
|
// file doesn't exist, so we can use that |
|
return true; |
|
} |
|
g_pFileSystem->Close( test ); |
|
|
|
char basename[ MAX_OSPATH ]; |
|
|
|
Q_StripExtension( name, basename, sizeof( basename ) ); |
|
|
|
// Start looking for a valid name |
|
int i = 0; |
|
for ( i = 0; i < 1000; i++ ) |
|
{ |
|
char newname[ MAX_OSPATH ]; |
|
Q_snprintf( newname, sizeof( newname ), "%s%03i.dem", basename, i ); |
|
|
|
test = g_pFileSystem->Open( newname, "rb" ); |
|
if ( FILESYSTEM_INVALID_HANDLE == test ) |
|
{ |
|
Q_strncpy( name, newname, namesize ); |
|
return true; |
|
} |
|
g_pFileSystem->Close( test ); |
|
} |
|
|
|
ConMsg( "Unable to find a valid incremental demo filename for %s, try clearing the directory of %snnn.dem\n", name, basename ); |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: List the contents of a demo file. |
|
//----------------------------------------------------------------------------- |
|
void CL_ListDemo_f( const CCommand &args ) |
|
{ |
|
if ( cmd_source != src_command ) |
|
return; |
|
|
|
// Find the file |
|
char name[MAX_OSPATH]; |
|
|
|
Q_snprintf (name, sizeof(name), "%s", args[1]); |
|
|
|
Q_DefaultExtension( name, ".dem", sizeof( name ) ); |
|
|
|
ConMsg ("Demo contents for %s:\n", name); |
|
|
|
CDemoFile demofile; |
|
|
|
if ( !demofile.Open( name, true ) ) |
|
{ |
|
ConMsg ("ERROR: couldn't open.\n"); |
|
return; |
|
} |
|
|
|
demofile.ReadDemoHeader(); |
|
|
|
demoheader_t *header = &demofile.m_DemoHeader; |
|
|
|
if ( !header ) |
|
{ |
|
ConMsg( "Failed reading demo header.\n" ); |
|
demofile.Close(); |
|
return; |
|
} |
|
|
|
if ( Q_strcmp ( header->demofilestamp, DEMO_HEADER_ID ) ) |
|
{ |
|
ConMsg( "%s is not a valid demo file\n", name); |
|
return; |
|
} |
|
|
|
ConMsg("Network protocol: %i\n", header->networkprotocol); |
|
ConMsg("Demo version : %i\n", header->demoprotocol); |
|
ConMsg("Server name : %s\n", header->servername); |
|
ConMsg("Map name : %s\n", header->mapname); |
|
ConMsg("Game : %s\n", header->gamedirectory); |
|
ConMsg("Player name : %s\n", header->clientname); |
|
ConMsg("Time : %.1f\n", header->playback_time); |
|
ConMsg("Ticks : %i\n", header->playback_ticks); |
|
ConMsg("Frames : %i\n", header->playback_frames); |
|
ConMsg("Signon size : %i\n", header->signonlength); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CON_COMMAND( stop, "Finish recording demo." ) |
|
{ |
|
if ( cmd_source != src_command ) |
|
return; |
|
|
|
if ( !demorecorder->IsRecording() ) |
|
{ |
|
ConDMsg ("Not recording a demo.\n"); |
|
return; |
|
} |
|
|
|
demorecorder->StopRecording(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CON_COMMAND_F( record, "Record a demo.", FCVAR_DONTRECORD ) |
|
{ |
|
if ( g_ClientDLL == NULL ) |
|
{ |
|
ConMsg ("Can't record on dedicated server.\n"); |
|
return; |
|
} |
|
|
|
if ( args.ArgC() != 2 && args.ArgC() != 3 ) |
|
{ |
|
ConMsg ("record <demoname> [incremental]\n"); |
|
return; |
|
} |
|
|
|
bool incremental = false; |
|
if ( args.ArgC() == 3 ) |
|
{ |
|
if ( !Q_stricmp( args[2], "incremental" ) ) |
|
{ |
|
incremental = true; |
|
} |
|
} |
|
|
|
if ( demorecorder->IsRecording() ) |
|
{ |
|
ConMsg ("Already recording.\n"); |
|
return; |
|
} |
|
|
|
if ( demoplayer->IsPlayingBack() ) |
|
{ |
|
ConMsg ("Can't record during demo playback.\n"); |
|
return; |
|
} |
|
|
|
// check path first |
|
if ( !COM_IsValidPath( args[1] ) ) |
|
{ |
|
ConMsg( "record %s: invalid path.\n", args[1] ); |
|
return; |
|
} |
|
|
|
char name[ MAX_OSPATH ]; |
|
|
|
if ( !g_ClientDLL->CanRecordDemo( name, sizeof( name ) ) ) |
|
{ |
|
ConMsg( "%s\n", name ); // re-use name as the error string if the client prevents us from starting a demo |
|
return; |
|
} |
|
|
|
// remove .dem extension if user added it |
|
Q_StripExtension( args[1], name, sizeof( name ) ); |
|
|
|
if ( incremental ) |
|
{ |
|
// If file exists, construct a better name |
|
if ( !ComputeNextIncrementalDemoFilename( name, sizeof( name ) ) ) |
|
{ |
|
return; |
|
} |
|
} |
|
// Record it |
|
demorecorder->StartRecording( name, incremental ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CL_PlayDemo_f( const CCommand &args ) |
|
{ |
|
if ( cmd_source != src_command ) |
|
return; |
|
|
|
if ( args.ArgC() != 2 ) |
|
{ |
|
ConMsg ("playdemo <demoname> : plays a demo file\n"); |
|
return; |
|
} |
|
|
|
// Get the demo filename |
|
char name[ MAX_OSPATH ]; |
|
Q_strncpy( name, args[1], sizeof( name ) ); |
|
Q_DefaultExtension( name, ".dem", sizeof( name ) ); |
|
|
|
// set current demo player to replay demo player? |
|
demoplayer = g_pClientDemoPlayer; |
|
|
|
// |
|
// open the demo file |
|
// |
|
if ( demoplayer->StartPlayback( name, false ) ) |
|
{ |
|
// Remove extension |
|
char basename[ MAX_OSPATH ]; |
|
V_StripExtension( name, basename, sizeof( basename ) ); |
|
g_ClientDLL->OnDemoPlaybackStart( basename ); |
|
} |
|
else |
|
{ |
|
SCR_EndLoadingPlaque(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CL_TimeDemo_f( const CCommand &args ) |
|
{ |
|
if ( cmd_source != src_command ) |
|
return; |
|
|
|
if ( args.ArgC() < 2 || args.ArgC() > 4 ) |
|
{ |
|
ConMsg ("timedemo <demoname> <optional stats.txt> : gets demo speeds, starting from optional frame\n"); |
|
return; |
|
} |
|
|
|
if( args.ArgC() >= 3 ) |
|
{ |
|
Q_strncpy( g_pStatsFile, args[ 2 ], sizeof( g_pStatsFile ) ); |
|
} |
|
else |
|
{ |
|
Q_strncpy( g_pStatsFile, "UNKNOWN", sizeof( g_pStatsFile ) ); |
|
} |
|
|
|
// set current demo player to client demo player |
|
demoplayer = g_pClientDemoPlayer; |
|
|
|
// open the demo file |
|
char name[ MAX_OSPATH ]; |
|
Q_strncpy (name, args[1], sizeof( name ) ); |
|
Q_DefaultExtension( name, ".dem", sizeof( name ) ); |
|
|
|
if ( !demoplayer->StartPlayback( name, true ) ) |
|
{ |
|
SCR_EndLoadingPlaque(); |
|
} |
|
} |
|
|
|
void CL_TimeDemoQuit_f( const CCommand &args ) |
|
{ |
|
demo_quitafterplayback.SetValue( 1 ); |
|
CL_TimeDemo_f( args ); |
|
} |
|
|
|
void CL_BenchFrame_f( const CCommand &args ) |
|
{ |
|
if ( cmd_source != src_command ) |
|
return; |
|
|
|
if ( args.ArgC() != 4 ) |
|
{ |
|
ConMsg ("benchframe <demoname> <frame> <tgafilename>: takes a snapshot of a particular frame in a demo\n"); |
|
return; |
|
} |
|
|
|
g_pClientDemoPlayer->SetBenchframe( max( 0, atoi( args[2] ) ), args[3] ); |
|
|
|
s_bBenchframe = true; |
|
|
|
mat_norendering.SetValue( 1 ); |
|
|
|
// set current demo player to client demo player |
|
demoplayer = g_pClientDemoPlayer; |
|
|
|
// open the demo file |
|
char name[ MAX_OSPATH ]; |
|
Q_strncpy (name, args[1], sizeof( name ) ); |
|
Q_DefaultExtension( name, ".dem", sizeof( name ) ); |
|
|
|
if ( !demoplayer->StartPlayback( name, true ) ) |
|
{ |
|
SCR_EndLoadingPlaque(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CON_COMMAND( vtune, "Controls VTune's sampling." ) |
|
{ |
|
if ( args.ArgC() != 2 ) |
|
{ |
|
ConMsg ("vtune \"pause\" | \"resume\" : Suspend or resume VTune's sampling.\n"); |
|
return; |
|
} |
|
|
|
if( !Q_strcasecmp( args[1], "pause" ) ) |
|
{ |
|
if(!vtune(false)) |
|
{ |
|
ConMsg("Failed to find \"VTPause()\" in \"vtuneapi.dll\".\n"); |
|
return; |
|
} |
|
|
|
ConMsg("VTune sampling paused.\n"); |
|
} |
|
|
|
else if( !Q_strcasecmp( args[1], "resume" ) ) |
|
{ |
|
if(!vtune(true)) |
|
{ |
|
ConMsg("Failed to find \"VTResume()\" in \"vtuneapi.dll\".\n"); |
|
return; |
|
} |
|
|
|
ConMsg("VTune sampling resumed.\n"); |
|
} |
|
|
|
else |
|
{ |
|
ConMsg("Unknown vtune option.\n"); |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
CON_COMMAND_AUTOCOMPLETEFILE( playdemo, CL_PlayDemo_f, "Play a recorded demo file (.dem ).", NULL, dem ); |
|
CON_COMMAND_AUTOCOMPLETEFILE( timedemo, CL_TimeDemo_f, "Play a demo and report performance info.", NULL, dem ); |
|
CON_COMMAND_AUTOCOMPLETEFILE( timedemoquit, CL_TimeDemoQuit_f, "Play a demo, report performance info, and then exit", NULL, dem ); |
|
CON_COMMAND_AUTOCOMPLETEFILE( listdemo, CL_ListDemo_f, "List demo file contents.", NULL, dem ); |
|
CON_COMMAND_AUTOCOMPLETEFILE( benchframe, CL_BenchFrame_f, "Takes a snapshot of a particular frame in a time demo.", NULL, dem ); |
|
|
|
CON_COMMAND( demo_pause, "Pauses demo playback." ) |
|
{ |
|
float seconds = -1.0; |
|
|
|
if ( args.ArgC() == 2 ) |
|
{ |
|
seconds = atof( args[1] ); |
|
} |
|
|
|
demoplayer->PausePlayback( seconds ); |
|
} |
|
|
|
CON_COMMAND( demo_resume, "Resumes demo playback." ) |
|
{ |
|
demoplayer->ResumePlayback(); |
|
} |
|
|
|
CON_COMMAND( demo_togglepause, "Toggles demo playback." ) |
|
{ |
|
if ( !demoplayer->IsPlayingBack() ) |
|
return; |
|
|
|
if ( demoplayer->IsPlaybackPaused() ) |
|
{ |
|
demoplayer->ResumePlayback(); |
|
} |
|
else |
|
{ |
|
demoplayer->PausePlayback( -1 ); |
|
} |
|
} |
|
|
|
CON_COMMAND( demo_gototick, "Skips to a tick in demo." ) |
|
{ |
|
bool bRelative = false; |
|
bool bPause = false; |
|
|
|
if ( args.ArgC() < 2 ) |
|
{ |
|
Msg("Syntax: demo_gototick <tick> [relative] [pause]\n"); |
|
return; |
|
} |
|
|
|
int nTick = atoi( args[1] ); |
|
|
|
if ( args.ArgC() >= 3 ) |
|
{ |
|
bRelative = Q_atoi( args[2] ) != 0; |
|
} |
|
|
|
if ( args.ArgC() >= 4 ) |
|
{ |
|
bPause = Q_atoi( args[3] ) != 0; |
|
} |
|
|
|
demoplayer->SkipToTick( nTick, bRelative, bPause ); |
|
} |
|
|
|
CON_COMMAND( demo_setendtick, "Sets end demo playback tick. Set to 0 to disable." ) |
|
{ |
|
if ( args.ArgC() != 2 ) |
|
{ |
|
Msg( "Syntax: demo_setendtick <tick>\n" ); |
|
return; |
|
} |
|
|
|
int nTick = atoi( args[1] ); |
|
|
|
demoplayer->SetEndTick( nTick ); |
|
} |
|
|
|
CON_COMMAND( demo_timescale, "Sets demo replay speed." ) |
|
{ |
|
float fScale = 1.0f; |
|
|
|
if ( args.ArgC() == 2 ) |
|
{ |
|
fScale = atof( args[1] ); |
|
fScale = clamp( fScale, 0.0f, 100.0f ); |
|
} |
|
|
|
demoplayer->SetPlaybackTimeScale( fScale ); |
|
} |
|
|
|
bool CDemoPlayer::OverrideView( democmdinfo_t& info ) |
|
{ |
|
if ( g_pDemoUI && g_pDemoUI->OverrideView( info, GetPlaybackTick() ) ) |
|
return true; |
|
|
|
if ( g_pDemoUI2 && g_pDemoUI2->OverrideView( info, GetPlaybackTick() ) ) |
|
return true; |
|
|
|
if ( demoaction && demoaction->OverrideView( info, GetPlaybackTick() ) ) |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
void CDemoPlayer::OnStopCommand() |
|
{ |
|
cl.Disconnect( "Demo stopped", true); |
|
} |
|
|
|
void CDemoPlayer::ResetDemoInterpolation( void ) |
|
{ |
|
m_bResetInterpolation = true; |
|
} |
|
|
|
int CDemoPlayer::GetProtocolVersion() |
|
{ |
|
Assert( IsPlayingBack() ); |
|
if ( !IsPlayingBack() ) |
|
return PROTOCOL_VERSION; |
|
|
|
return m_DemoFile.GetProtocolVersion(); |
|
}
|
|
|