This commit is contained in:
FluorescentCIAAfricanAmerican
2020-04-22 12:56:21 -04:00
commit 3bf9df6b27
15370 changed files with 5489726 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "baserecordingsession.h"
#include "baserecordingsessionblock.h"
#include "replay/irecordingsessionblockmanager.h"
#include "replay/replayutils.h"
#include "replay/iclientreplaycontext.h"
#include "replay/shared_defs.h"
#include "KeyValues.h"
#include "replay/replayutils.h"
#include "replay/ireplaycontext.h"
#include "filesystem.h"
#include "iserver.h"
#include "replaysystem.h"
#include "utlbuffer.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CBaseRecordingSession::CBaseRecordingSession( IReplayContext *pContext )
: m_pContext( pContext ),
m_bRecording( false ),
m_bAutoDelete( false ),
m_bBlocksLoaded( false ),
m_flStartTime( 0.0f )
{
}
CBaseRecordingSession::~CBaseRecordingSession()
{
}
void CBaseRecordingSession::AddBlock( CBaseRecordingSessionBlock *pBlock )
{
AddBlock( pBlock, false );
}
bool CBaseRecordingSession::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_strName = pIn->GetString( "name" );
if ( m_strName.IsEmpty() )
{
CUtlBuffer buf;
pIn->RecursiveSaveToFile( buf, 0 );
IF_REPLAY_DBG( Warning( "Session with no session name found - aborting load for this session. Data:\n---\n%s\n---\n", (const char *)buf.Base() ) );
return false;
}
m_bRecording = pIn->GetBool( "recording" );
m_strBaseDownloadURL = pIn->GetString( "base_download_url" );
m_nServerStartRecordTick = pIn->GetInt( "server_start_record_tick", -1 );
return true;
}
void CBaseRecordingSession::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetString( "name", m_strName.Get() );
pOut->SetInt( "recording", m_bRecording ? 1 : 0 );
pOut->SetString( "base_download_url", m_strBaseDownloadURL.Get() );
pOut->SetInt( "server_start_record_tick", m_nServerStartRecordTick );
}
const char *CBaseRecordingSession::GetSubKeyTitle() const
{
return m_strName.Get();
}
const char *CBaseRecordingSession::GetPath() const
{
return Replay_va( "%s%s%c", m_pContext->GetBaseDir(), SUBDIR_SESSIONS, CORRECT_PATH_SEPARATOR );
}
const char *CBaseRecordingSession::GetSessionInfoURL() const
{
return Replay_va( "%s%s.%s", m_strBaseDownloadURL.Get(), m_strName.Get(), GENERIC_FILE_EXTENSION );
}
void CBaseRecordingSession::LoadBlocksForSession()
{
if ( m_bBlocksLoaded )
return;
IRecordingSessionBlockManager *pBlockManager = m_pContext->GetRecordingSessionBlockManager();
// Peek in directory and load files based on what's there
FileFindHandle_t hFind;
CFmtStr fmtPath( "%s%s*.%s", pBlockManager->GetBlockPath(), m_strName.Get(), GENERIC_FILE_EXTENSION );
const char *pFilename = g_pFullFileSystem->FindFirst( fmtPath.Access(), &hFind );
while ( pFilename )
{
// Load the block - this will add the block to this session
pBlockManager->LoadBlockFromFileName( pFilename, this );
// Get next file
pFilename = g_pFullFileSystem->FindNext( hFind );
}
// Blocks loaded
m_bBlocksLoaded = true;
}
void CBaseRecordingSession::OnDelete()
{
BaseClass::OnDelete();
// Dynamically load blocks if necessary, then delete from the block manager and from disk
DeleteBlocks();
}
void CBaseRecordingSession::DeleteBlocks()
{
if ( !m_bBlocksLoaded )
{
// Load blocks now based on the session name
LoadBlocksForSession();
}
// Delete all blocks associated w/ the session
FOR_EACH_VEC( m_vecBlocks, i )
{
CBaseRecordingSessionBlock *pCurBlock = m_vecBlocks[ i ];
m_pContext->GetRecordingSessionBlockManager()->DeleteBlock( pCurBlock );
}
}
void CBaseRecordingSession::OnUnload()
{
BaseClass::OnUnload();
FOR_EACH_VEC( m_vecBlocks, i )
{
CBaseRecordingSessionBlock *pCurBlock = m_vecBlocks[ i ];
m_pContext->GetRecordingSessionBlockManager()->UnloadBlock( pCurBlock );
}
}
void CBaseRecordingSession::PopulateWithRecordingData( int nCurrentRecordingStartTick )
{
Assert( nCurrentRecordingStartTick >= 0 );
m_strBaseDownloadURL = Replay_GetDownloadURL();
m_bRecording = true;
m_nServerStartRecordTick = nCurrentRecordingStartTick;
}
void CBaseRecordingSession::AddBlock( CBaseRecordingSessionBlock *pBlock, bool bFlagForFlush )
{
Assert( pBlock->m_hSession == GetHandle() );
Assert( m_vecBlocks.Find( pBlock ) == m_vecBlocks.InvalidIndex() );
m_vecBlocks.Insert( pBlock );
if ( bFlagForFlush )
{
// Mark as dirty
m_pContext->GetRecordingSessionManager()->FlagSessionForFlush( this, false );
}
m_bBlocksLoaded = true;
}
int CBaseRecordingSession::FindBlock( CBaseRecordingSessionBlock *pBlock ) const
{
int itResult = m_vecBlocks.Find( pBlock );
if ( itResult == m_vecBlocks.InvalidIndex() )
return -1;
return itResult;
}
bool CBaseRecordingSession::ShouldDitchSession() const
{
return m_bAutoDelete;
}
//----------------------------------------------------------------------------------------
bool CBaseRecordingSession::CLessFunctor::Less( const CBaseRecordingSessionBlock *pSrc1, const CBaseRecordingSessionBlock *pSrc2, void *pContext )
{
return pSrc1->m_iReconstruction < pSrc2->m_iReconstruction;
}
//----------------------------------------------------------------------------------------
+97
View File
@@ -0,0 +1,97 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef BASERECORDINGSESSION_H
#define BASERECORDINGSESSION_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "tier0/platform.h"
#include "utlstring.h"
#include "utllinkedlist.h"
#include "replay/replayhandle.h"
#include "replay/basereplayserializeable.h"
#include "replay/irecordingsession.h"
#include "UtlSortVector.h"
//----------------------------------------------------------------------------------------
class CBaseRecordingSessionBlock;
class KeyValues;
class IReplayContext;
//----------------------------------------------------------------------------------------
// A recording session (e.g. round), including a list of blocks
class CBaseRecordingSession : public CBaseReplaySerializeable,
public IRecordingSession
{
typedef CBaseReplaySerializeable BaseClass;
public:
CBaseRecordingSession( IReplayContext *pContext );
~CBaseRecordingSession();
//
// IRecordingSession
//
virtual void AddBlock( CBaseRecordingSessionBlock *pBlock );
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
virtual const char *GetSubKeyTitle() const;
virtual const char *GetPath() const;
virtual void OnDelete();
virtual void OnUnload();
const char *GetSessionInfoURL() const;
virtual void PopulateWithRecordingData( int nCurrentRecordingStartTick );
void OnStopRecording() { m_bRecording = false; }
class CLessFunctor
{
public:
bool Less( const CBaseRecordingSessionBlock *pSrc1, const CBaseRecordingSessionBlock *pSrc2, void *pContext );
};
typedef CUtlSortVector< CBaseRecordingSessionBlock *, CLessFunctor > BlockContainer_t;
inline int GetNumBlocks() const { return m_vecBlocks.Count(); }
void AddBlock( CBaseRecordingSessionBlock *pBlock, bool bFlagForFlush );
int FindBlock( CBaseRecordingSessionBlock *pBlock ) const; // Returns -1 on fail
const BlockContainer_t &GetBlocks() const { return m_vecBlocks; }
// Determines whether or not a session gets nuked at the end of a round or not.
virtual bool ShouldDitchSession() const;
// Dynamically load blocks
void LoadBlocksForSession();
void DeleteBlocks();
// Persistent:
bool m_bRecording; // Is this session currently recording?
CUtlString m_strName; // A unique session name, given by the server at the start of recording based on time/date
CUtlString m_strBaseDownloadURL; // The download URL, with no filename, e.g., "http://someserver:80/somepath/"
int m_nServerStartRecordTick; // The tick at which the server began recording the given session (based on g_ServerGlobalVariables.tickcount) -
// which is the tick client spawn/death ticks are relative to
float m_flStartTime;
// Non-persistent:
IReplayContext *m_pContext;
bool m_bAutoDelete; // Set to true if a session is removed while it's recording - if flagged, session will auto-delete once recording ends
bool m_bBlocksLoaded;
protected:
BlockContainer_t m_vecBlocks; // A list of session blocks for the given session - NOTE: Blocks should not be free'd
};
//----------------------------------------------------------------------------------------
#endif // BASERECORDINGSESSION_H
+168
View File
@@ -0,0 +1,168 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "baserecordingsessionblock.h"
#include "replay/replayutils.h"
#include "replay/ireplaycontext.h"
#include "replay/irecordingsessionmanager.h"
#include "replay/shared_defs.h"
#include "KeyValues.h"
#include "qlimits.h"
#include "utlbuffer.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CBaseRecordingSessionBlock::CBaseRecordingSessionBlock( IReplayContext *pContext )
: m_pContext( pContext ),
m_nRemoteStatus( STATUS_INVALID ),
m_nHttpError( ERROR_NONE ),
m_hSession( REPLAY_HANDLE_INVALID ),
m_bHashValid( false ),
m_iReconstruction( -1 ),
m_uFileSize( 0 ),
m_uUncompressedSize( 0 ),
m_nCompressorType( COMPRESSORTYPE_INVALID )
{
m_szFullFilename[ 0 ] = '\0';
V_memset( m_aHash, 0, sizeof( m_aHash ) );
}
const char *CBaseRecordingSessionBlock::GetSubKeyTitle() const
{
CBaseRecordingSession *pOwnerSession = m_pContext->GetRecordingSessionManager()->FindSession( m_hSession );
if ( !pOwnerSession )
{
AssertMsg( 0, "Owner session not found" );
return "";
}
return Replay_va( "%s_part_%i", pOwnerSession->m_strName.Get(), m_iReconstruction );
}
const char *CBaseRecordingSessionBlock::GetPath() const
{
return Replay_va( "%s%s%c", m_pContext->GetBaseDir(), SUBDIR_BLOCKS, CORRECT_PATH_SEPARATOR );
}
bool CBaseRecordingSessionBlock::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_nRemoteStatus = (RemoteStatus_t)pIn->GetInt( "remote_status", (int)STATUS_INVALID ); Assert( m_nRemoteStatus != STATUS_INVALID );
m_nHttpError = (Error_t)pIn->GetInt( "error", (int)ERROR_NONE );
m_iReconstruction = pIn->GetInt( "recon_index", -1 ); Assert( m_iReconstruction >= 0 );
m_hSession = (ReplayHandle_t)pIn->GetInt( "session", REPLAY_HANDLE_INVALID ); Assert( m_hSession != REPLAY_HANDLE_INVALID );
m_uFileSize = pIn->GetInt( "size", 0 ); Assert( m_uFileSize > 0 );
m_uUncompressedSize = pIn->GetInt( "usize", 0 );
m_nCompressorType = (CompressorType_t)pIn->GetInt( "compressor", 0 );
ReadHash( pIn, "hash" );
return true;
}
void CBaseRecordingSessionBlock::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "remote_status", (int)m_nRemoteStatus );
pOut->SetInt( "error", (int)m_nHttpError );
pOut->SetInt( "recon_index", m_iReconstruction );
pOut->SetInt( "session", (int)m_hSession );
pOut->SetInt( "size", m_uFileSize );
pOut->SetInt( "usize", m_uUncompressedSize );
pOut->SetInt( "compressor", (int)m_nCompressorType );
WriteHash( pOut, "hash" );
// NOTE: Filename written in subclasses, since it's handled differently for client vs. server
}
void CBaseRecordingSessionBlock::OnDelete()
{
BaseClass::OnDelete();
// NOTE: The actual .block files get deleted in subclasses, since each handle the case differently.
}
bool CBaseRecordingSessionBlock::ReadHash( KeyValues *pIn, const char *pHashName )
{
const char *pHashStr = pIn->GetString( pHashName );
bool bResult = false;
if ( V_strlen( pHashStr ) > 0 )
{
int iHash = 0;
char *p = strtok( const_cast< char * >( pHashStr ), " " );
while ( p )
{
// Should have no more than 3 characters
if ( V_strlen( p ) > 3 )
{
break;
}
m_aHash[ iHash++ ] = (uint8)atoi( p );
p = strtok( NULL, " " );
bResult = true;
}
}
// Keep track of whether we have a valid hash or not
m_bHashValid = bResult;
AssertMsg( bResult, "Invalid hash string" );
return bResult;
}
void CBaseRecordingSessionBlock::WriteHash( KeyValues *pOut, const char *pHashName ) const
{
CFmtStr fmtHash( "%03i %03i %03i %03i %03i %03i %03i %03i %03i %03i %03i %03i %03i %03i %03i %03i",
m_aHash[0], m_aHash[1], m_aHash[2], m_aHash[3], m_aHash[4], m_aHash[5], m_aHash[6], m_aHash[7],
m_aHash[8], m_aHash[9], m_aHash[10], m_aHash[11], m_aHash[12], m_aHash[13], m_aHash[14], m_aHash[15]
);
pOut->SetString( pHashName, fmtHash.Access() );
}
bool CBaseRecordingSessionBlock::HasValidHash() const
{
return m_bHashValid;
}
void CBaseRecordingSessionBlock::WriteSessionInfoDataToBuffer( CUtlBuffer &buf ) const
{
RecordingSessionBlockSpec_t blob;
blob.m_iReconstruction = (int32)m_iReconstruction;
blob.m_uRemoteStatus = (uint8)m_nRemoteStatus;
blob.m_uFileSize = m_uFileSize;
blob.m_nCompressorType = (int8)m_nCompressorType; // Can be COMPRESSORTYPE_INVALID if not compressed
blob.m_uUncompressedSize = m_uUncompressedSize; // Can be 0 if not compressed
V_memcpy( blob.m_aHash, m_aHash, sizeof( m_aHash ) );
// Write the blob at the appropriate position in the buffer
Assert( m_iReconstruction >= 0 );
buf.SeekPut( CUtlBuffer::SEEK_HEAD, m_iReconstruction * sizeof( blob ) );
buf.Put( &blob, sizeof( RecordingSessionBlockSpec_t ) );
}
//----------------------------------------------------------------------------------------
/*static*/ const char *CBaseRecordingSessionBlock::GetRemoteStatusStringSafe( RemoteStatus_t nStatus )
{
switch ( nStatus )
{
case STATUS_INVALID: return "invalid";
case STATUS_ERROR: return "error";
case STATUS_WRITING: return "writing";
case STATUS_READYFORDOWNLOAD: return "ready for download";
default: return "unknown";
}
}
//----------------------------------------------------------------------------------------
+108
View File
@@ -0,0 +1,108 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef BASERECORDINGSESSIONBLOCK_H
#define BASERECORDINGSESSIONBLOCK_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/replayhandle.h"
#include "replay/basereplayserializeable.h"
#include "genericpersistentmanager.h"
#include "baserecordingsession.h"
#include "utlstring.h"
#include "compression.h"
//----------------------------------------------------------------------------------------
class KeyValues;
class CBaseReplayContext;
class IReplayContext;
//----------------------------------------------------------------------------------------
class CBaseRecordingSessionBlock : public CBaseReplaySerializeable
{
typedef CBaseReplaySerializeable BaseClass;
public:
CBaseRecordingSessionBlock( IReplayContext *pContext );
//
// IReplaySerializeable
//
virtual const char *GetSubKeyTitle() const;
virtual const char *GetPath() const;
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
virtual void OnDelete();
enum RemoteStatus_t
{
STATUS_INVALID = -2,
STATUS_ERROR = -1,
STATUS_WRITING,
STATUS_READYFORDOWNLOAD,
MAX_STATUS
};
static const char *GetRemoteStatusStringSafe( RemoteStatus_t nStatus );
enum Error_t
{
ERROR_NONE,
ERROR_NOTONDISK, // The replay was lost somehow - eg a server crash before the replay had a chance to write to disk
ERROR_WRITEFAILED, // The disk write somehow failed
};
bool ReadHash( KeyValues *pIn, const char *pHashName );
void WriteHash( KeyValues *pOut, const char *pHashName ) const;
bool HasValidHash() const;
// Get a filled out sub key specifically for writing to the session info file
void WriteSessionInfoDataToBuffer( CUtlBuffer &buf ) const;
RemoteStatus_t m_nRemoteStatus; // This represents the block's status on the server
Error_t m_nHttpError;
int32 m_iReconstruction; // For client-side reconstruction of sessions, this represents the index of the given block
ReplayHandle_t m_hSession; // What session is this partial replay a part of?
uint32 m_uFileSize; // Size in bytes of the binary block file (if compressed, this represents the compressed file size)
uint32 m_uUncompressedSize; // If compressed, this represents the uncompressed file size
char m_szFullFilename[512]; // Filename for the .block file itself.
// NOTE: On the server, full path info is written - on client, only filename is written
CompressorType_t m_nCompressorType; // What type of compressor/decompressor was/should be used, if any? Can be COMPRESSORTYPE_INVALID if not compressed.
uint8 m_aHash[16]; // Server sets this and client compares to validate downloaded block data
bool m_bHashValid; // Do we have a valid hash?
IReplayContext *m_pContext;
};
//----------------------------------------------------------------------------------------
//
// For serializing blocks - format version implied in header.
//
// In case this format changes, we have some legroom (m_aUnused).
//
struct RecordingSessionBlockSpec_t
{
int32 m_iReconstruction;
uint8 m_uRemoteStatus;
uint8 m_aHash[16];
int8 m_nCompressorType;
uint32 m_uFileSize;
uint32 m_uUncompressedSize;
uint8 m_aUnused[8];
};
#define MIN_SESSION_INFO_PAYLOAD_SIZE sizeof( RecordingSessionBlockSpec_t )
//----------------------------------------------------------------------------------------
#endif // BASERECORDINGSESSIONBLOCK_H
+100
View File
@@ -0,0 +1,100 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "baserecordingsessionblockmanager.h"
#include "baserecordingsessionblock.h"
#include "replay/replayutils.h"
#include "replay/ireplaycontext.h"
#include "replay/shared_defs.h"
#include "KeyValues.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define RECORDINGSESSIONBLOCKMANAGER_VERSION 0
//----------------------------------------------------------------------------------------
CBaseRecordingSessionBlockManager::CBaseRecordingSessionBlockManager( IReplayContext *pContext )
: m_pContext( pContext )
{
}
bool CBaseRecordingSessionBlockManager::Init()
{
// Call CGenericPersistentManager::Init() to do setup, but don't actually load any blocks on the server.
return BaseClass::Init( ShouldLoadBlocks() );
}
const char *CBaseRecordingSessionBlockManager::GetRelativeIndexPath() const
{
return Replay_va( "%s%c", SUBDIR_BLOCKS, CORRECT_PATH_SEPARATOR );
}
float CBaseRecordingSessionBlockManager::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 0.1f;
}
int CBaseRecordingSessionBlockManager::GetVersion() const
{
return RECORDINGSESSIONBLOCKMANAGER_VERSION;
}
CBaseRecordingSessionBlock *CBaseRecordingSessionBlockManager::GetBlock( ReplayHandle_t hBlock )
{
return Find( hBlock );
}
void CBaseRecordingSessionBlockManager::DeleteBlock( CBaseRecordingSessionBlock *pBlock )
{
Remove( pBlock );
}
void CBaseRecordingSessionBlockManager::UnloadBlock( CBaseRecordingSessionBlock *pBlock )
{
FlagForUnload( pBlock );
}
CBaseRecordingSessionBlock *CBaseRecordingSessionBlockManager::FindBlockForSession( ReplayHandle_t hSession, int iReconstruction )
{
FOR_EACH_OBJ( this, i )
{
CBaseRecordingSessionBlock *pCurBlock = m_vecObjs[ i ];
if ( pCurBlock->m_hSession == hSession && pCurBlock->m_iReconstruction == iReconstruction )
{
return pCurBlock;
}
}
return NULL;
}
const char *CBaseRecordingSessionBlockManager::GetSavePath() const
{
return Replay_va(
"%s%c%s%c%s%c",
SUBDIR_REPLAY, CORRECT_PATH_SEPARATOR,
m_pContext->GetReplaySubDir(), CORRECT_PATH_SEPARATOR,
SUBDIR_BLOCKS, CORRECT_PATH_SEPARATOR
);
}
const char *CBaseRecordingSessionBlockManager::GetBlockPath() const
{
return GetSavePath();
}
void CBaseRecordingSessionBlockManager::LoadBlockFromFileName( const char *pFilename, IRecordingSession *pSession )
{
CBaseRecordingSessionBlock *pBlock;
if ( ReadObjFromFile( pFilename, pBlock, true ) )
{
pSession->AddBlock( pBlock );
}
}
//----------------------------------------------------------------------------------------
+78
View File
@@ -0,0 +1,78 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef BASERECORDINGSESSIONBLOCKMANAGER_H
#define BASERECORDINGSESSIONBLOCKMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "genericpersistentmanager.h"
#include "replay/replayhandle.h"
#include "replay/irecordingsessionblockmanager.h"
#include "utlstring.h"
#include "baserecordingsession.h"
#include "replay/basereplayserializeable.h"
#include "baserecordingsessionblock.h"
//----------------------------------------------------------------------------------------
class KeyValues;
class CBaseReplayContext;
class IReplayContext;
//----------------------------------------------------------------------------------------
//
// Maintains a persistent list of session blocks in a keyvalues file
//
class CBaseRecordingSessionBlockManager : public CGenericPersistentManager< CBaseRecordingSessionBlock >,
public IRecordingSessionBlockManager
{
typedef CGenericPersistentManager< CBaseRecordingSessionBlock > BaseClass;
public:
CBaseRecordingSessionBlockManager( IReplayContext *pContext );
virtual bool Init();
//
// IRecordingSessionBlockManager
//
CBaseRecordingSessionBlock *GetBlock( ReplayHandle_t hBlock );
virtual void DeleteBlock( CBaseRecordingSessionBlock *pBlock );
virtual void UnloadBlock( CBaseRecordingSessionBlock *pBlock );
virtual const char *GetBlockPath() const;
virtual void LoadBlockFromFileName( const char *pFilename, IRecordingSession *pSession ); // NOTE: This will not actually add the block to the session block manager - this is for loading blocks and adding them to recording sessions so they can be deleted from disk and cleaned up from the fileserver if necessary.
// Find the block for with the given reconstruction index for the given session
CBaseRecordingSessionBlock *FindBlockForSession( ReplayHandle_t hSession, int iReconstruction );
// Gets something like "replays/<client|server>/blocks/"
const char *GetSavePath() const;
protected:
virtual bool ShouldSerializeToIndividualFiles() const { return true; }
virtual const char *GetRelativeIndexPath() const;
virtual bool ShouldLoadBlocks() const = 0;
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
IReplayContext *m_pContext;
private:
virtual const char *GetDebugName() const { return "block manager"; }
virtual int GetVersion() const;
virtual const char *GetIndexFilename() const { return "blocks." GENERIC_FILE_EXTENSION; }
};
//----------------------------------------------------------------------------------------
#endif // BASERECORDINGSESSIONBLOCKMANAGER_H
+266
View File
@@ -0,0 +1,266 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "baserecordingsessionmanager.h"
#include "baserecordingsession.h"
#include "baserecordingsessionblock.h"
#include "replay/replayutils.h"
#include "replay/shared_defs.h"
#include "replaysystem.h"
#include "KeyValues.h"
#include "shared_replaycontext.h"
#include "filesystem.h"
#include "iserver.h"
#include "vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
inline const char *GetSessionsFullFilename()
{
return Replay_va( "%s" SUBDIR_SESSIONS "%c", Replay_GetBaseDir(), CORRECT_PATH_SEPARATOR );
}
//----------------------------------------------------------------------------------------
CBaseRecordingSessionManager::CBaseRecordingSessionManager( IReplayContext *pContext )
: m_pContext( pContext ),
m_pRecordingSession( NULL ),
m_bLastSessionDitched( false )
{
}
CBaseRecordingSessionManager::~CBaseRecordingSessionManager()
{
}
bool CBaseRecordingSessionManager::Init()
{
if ( !BaseClass::Init() )
return false;
// Go through each block handle and attempt find the block in the block manager
typedef CGenericPersistentManager< CBaseRecordingSessionBlock > BaseBlockManager_t;
BaseBlockManager_t *pBlockManager = dynamic_cast< BaseBlockManager_t * >( m_pContext->GetRecordingSessionBlockManager() );
FOR_EACH_OBJ( pBlockManager, it )
{
CBaseRecordingSessionBlock *pCurBlock = pBlockManager->m_vecObjs[ it ];
// Find the session for the current block
CBaseRecordingSession *pSession = m_pContext->GetRecordingSessionManager()->FindSession( pCurBlock->m_hSession );
if ( !pSession )
{
m_pContext->GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Load_CouldNotFindSession" );
continue;
}
// Add the block
pSession->AddBlock( pCurBlock, false );
}
return true;
}
CBaseRecordingSession *CBaseRecordingSessionManager::OnSessionStart( int nCurrentRecordingStartTick, const char *pSessionName )
{
// Add a new session if one w/ the given name doesn't already exist.
// This is necessary on the client, where a session may already exist if, for example,
// the client reconnects to a server where they were already playing/saved replays.
// On the server, NULL will always be passed in for pSessionName.
CBaseRecordingSession *pNewSession = pSessionName ? FindSessionByName( pSessionName ) : NULL;
if ( !pNewSession )
{
pNewSession = CreateAndGenerateHandle();
Add( pNewSession );
}
// Initialize
pNewSession->PopulateWithRecordingData( nCurrentRecordingStartTick );
Save();
// Update recording session
m_pRecordingSession = pNewSession;
return m_pRecordingSession;
}
void CBaseRecordingSessionManager::OnSessionEnd()
{
if ( m_pRecordingSession )
{
// If we don't care about the given session, ditch it
// NOTE: ShouldDitchSession() checks auto-delete flag!
if ( m_pRecordingSession->ShouldDitchSession() )
{
m_bLastSessionDitched = true;
DBG( "Marking session for ditch!\n" );
MarkSessionForDelete( m_pRecordingSession->GetHandle() );
}
else
{
m_bLastSessionDitched = false;
// Save
FlagForFlush( m_pRecordingSession, false );
// Unload from memory?
if ( ShouldUnloadSessions() )
{
FlagForUnload( m_pRecordingSession );
}
}
}
m_pRecordingSession = NULL;
}
void CBaseRecordingSessionManager::DeleteSession( ReplayHandle_t hSession, bool bForce )
{
CBaseRecordingSession *pSession = Find( hSession );
if ( !pSession )
{
AssertMsg( 0, "Trying to delete a non-existent session - should never happen!" );
return;
}
AssertMsg( !pSession->IsLocked(), "Shouldn't be free'ing a locked session!" );
// If the given session is recording, flag for delete but don't actually remove now
if ( pSession == m_pRecordingSession && !bForce )
{
pSession->m_bAutoDelete = true;
return;
}
// Remove the session and save
Remove( pSession );
Save();
}
void CBaseRecordingSessionManager::MarkSessionForDelete( ReplayHandle_t hSession )
{
m_lstSessionsToDelete.AddToTail( hSession );
}
const char *CBaseRecordingSessionManager::GetCurrentSessionName() const
{
if ( !m_pRecordingSession )
{
AssertMsg( 0, "GetCurrentSessionName() called w/o a session context" );
return NULL;
}
return m_pRecordingSession->m_strName.Get();
}
int CBaseRecordingSessionManager::GetCurrentSessionBlockIndex() const
{
if ( !m_pRecordingSession )
{
AssertMsg( 0, "GetCurrentPartialIndex() called w/o a session context" );
return -1;
}
// Need this MAX() here since GetNumBlocks() will return 0 until the first block is actually written.
return MAX( 0, m_pRecordingSession->GetNumBlocks() - 1 );
}
void CBaseRecordingSessionManager::FlagSessionForFlush( CBaseRecordingSession *pSession, bool bForceImmediate )
{
FlagForFlush( pSession, bForceImmediate );
}
int CBaseRecordingSessionManager::GetServerStartTickForSession( ReplayHandle_t hSession )
{
CBaseRecordingSession *pSession = FindSession( hSession );
if ( !pSession )
return -1;
return pSession->m_nServerStartRecordTick;
}
CBaseRecordingSession *CBaseRecordingSessionManager::FindSession( ReplayHandle_t hSession )
{
return Find( hSession );
}
const CBaseRecordingSession *CBaseRecordingSessionManager::FindSession( ReplayHandle_t hSession ) const
{
return const_cast< CBaseRecordingSessionManager * >( this )->Find( hSession );
}
CBaseRecordingSession *CBaseRecordingSessionManager::FindSessionByName( const char *pSessionName )
{
if ( !pSessionName || !pSessionName[0] )
return NULL;
FOR_EACH_OBJ( this, i )
{
CBaseRecordingSession *pCurSession = m_vecObjs[ i ];
if ( !V_stricmp( pSessionName, pCurSession->m_strName.Get() ) )
return pCurSession;
}
return NULL;
}
const char *CBaseRecordingSessionManager::GetRelativeIndexPath() const
{
return Replay_va( "%s%c", SUBDIR_SESSIONS, CORRECT_PATH_SEPARATOR );
}
void CBaseRecordingSessionManager::Think()
{
VPROF_BUDGET( "CBaseRecordingSessionManager::Think", VPROF_BUDGETGROUP_REPLAY );
DeleteSessionThink();
BaseClass::Think();
}
void CBaseRecordingSessionManager::DeleteSessionThink()
{
DoSessionCleanup();
}
void CBaseRecordingSessionManager::DoSessionCleanup()
{
bool bDeletedASession = false;
for ( int i = m_lstSessionsToDelete.Head(); i != m_lstSessionsToDelete.InvalidIndex(); )
{
ReplayHandle_t hSession = m_lstSessionsToDelete[ i ];
const int itNext = m_lstSessionsToDelete.Next( i );
if ( CanDeleteSession( hSession ) )
{
DBG( "Unloading session.\n" );
DeleteSession( hSession, true );
m_lstSessionsToDelete.Remove( i );
bDeletedASession = true;
}
i = itNext;
}
// If we just deleted the last session, let the derived class do any post-work
if ( !m_lstSessionsToDelete.Count() && bDeletedASession )
{
OnAllSessionsDeleted();
}
}
float CBaseRecordingSessionManager::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 0.1f;
}
//----------------------------------------------------------------------------------------
+103
View File
@@ -0,0 +1,103 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef BASERECORDINGSESSIONMANAGER_H
#define BASERECORDINGSESSIONMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "tier0/platform.h"
#include "utlstring.h"
#include "utllinkedlist.h"
#include "replay/replayhandle.h"
#include "replay/irecordingsessionmanager.h"
#include "genericpersistentmanager.h"
//----------------------------------------------------------------------------------------
class CBaseRecordingSession;
class CBaseRecordingSessionBlock;
class KeyValues;
class IReplayContext;
//----------------------------------------------------------------------------------------
//
// Manages and serializes all replay recording session data - instanced on both the client
// and server via CClientRecordingSessionManager and CServerRecordingSessionManager.
//
class CBaseRecordingSessionManager : public CGenericPersistentManager< CBaseRecordingSession >,
public IRecordingSessionManager
{
typedef CGenericPersistentManager< CBaseRecordingSession > BaseClass;
public:
CBaseRecordingSessionManager( IReplayContext *pContext );
virtual ~CBaseRecordingSessionManager();
virtual bool Init();
virtual CBaseRecordingSession *OnSessionStart( int nCurrentRecordingStartTick, const char *pSessionName );
virtual void OnSessionEnd();
void DeleteSession( ReplayHandle_t hSession, bool bForce );
const char *GetCurrentSessionName() const;
int GetCurrentSessionBlockIndex() const;
CBaseRecordingSession *FindSessionByName( const char *pSessionName );
// This is here so that server-side cleanup can be done for a ditched session. See calling code for details.
void DoSessionCleanup();
//
// IRecordingSessionManager
//
virtual CBaseRecordingSession *FindSession( ReplayHandle_t hSession );
virtual const CBaseRecordingSession *FindSession( ReplayHandle_t hSession ) const;
virtual void FlagSessionForFlush( CBaseRecordingSession *pSession, bool bForceImmediate );
virtual int GetServerStartTickForSession( ReplayHandle_t hSession );
// Get the recording session in progress
CBaseRecordingSession *GetRecordingSessionInProgress() { return m_pRecordingSession; }
bool LastSessionDitched() const { return m_bLastSessionDitched; }
protected:
//
// CGenericPersistentManager
//
virtual const char *GetIndexFilename() const { return "sessions." GENERIC_FILE_EXTENSION; }
virtual const char *GetRelativeIndexPath() const;
virtual const char *GetDebugName() const { return "session manager"; }
virtual void Think();
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
IReplayContext *m_pContext;
CBaseRecordingSession *m_pRecordingSession; // Currently recording session, or NULL if not recording
bool m_bLastSessionDitched;
virtual bool CanDeleteSession( ReplayHandle_t hSession ) const { return true; }
virtual bool ShouldUnloadSessions() const { return false; }
virtual void OnAllSessionsDeleted() {}
private:
void DeleteSessionThink();
void MarkSessionForDelete( ReplayHandle_t hSession );
CUtlLinkedList< ReplayHandle_t, int > m_lstSessionsToDelete;
};
//----------------------------------------------------------------------------------------
#endif // BASERECORDINGSESSIONMANAGER_H
+47
View File
@@ -0,0 +1,47 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "basethinker.h"
#include "ithinkmanager.h"
#include "replay/ienginereplay.h"
#include "dbg.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IThinkManager *g_pThinkManager;
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
CBaseThinker::CBaseThinker()
: m_flNextThinkTime( 0.0f )
{
g_pThinkManager->AddThinker( this );
}
CBaseThinker::~CBaseThinker()
{
g_pThinkManager->RemoveThinker( this );
}
void CBaseThinker::Think()
{
AssertMsg( ShouldThink(), "Thinking before ready - Think() being called explicitly? Let the think manager call Think()." );
}
bool CBaseThinker::ShouldThink() const
{
const float flHostTime = g_pEngine->GetHostTime();
return m_flNextThinkTime >= 0.0f && flHostTime >= m_flNextThinkTime;
}
void CBaseThinker::PostThink()
{
m_flNextThinkTime = GetNextThinkTime();
}
//----------------------------------------------------------------------------------------
+42
View File
@@ -0,0 +1,42 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef BASETHINKER_H
#define BASETHINKER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "ithinker.h"
//----------------------------------------------------------------------------------------
//
// Adds/removes itself from think manager and implements a default ShouldThink().
//
class CBaseThinker : public IThinker
{
public:
CBaseThinker();
virtual ~CBaseThinker();
protected:
virtual void Think();
virtual bool ShouldThink() const;
virtual void PostThink();
// Derived classes must implement this.
// Return 0 to think every frame.
// Return -1 to never think.
virtual float GetNextThinkTime() const = 0;
private:
float m_flNextThinkTime;
};
//----------------------------------------------------------------------------------------
#endif // BASETHINKER_H
+346
View File
@@ -0,0 +1,346 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_screenshotmanager.h"
#include "convar.h"
#include "replaysystem.h"
#include "netmessages.h"
#include "cl_replaymanager.h"
#include "cl_sessionblockdownloader.h"
#include "cl_recordingsession.h"
#include "cl_renderqueue.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern ConVar replay_postdeathrecordtime;
//----------------------------------------------------------------------------------------
CON_COMMAND( save_replay, "Save a replay of the current life if possible." )
{
// Is the user running a listen server?
if ( g_pEngineClient->IsListenServer() )
{
Replay_HudMsg( "#Replay_NoListenServer", "replay\\record_fail.wav", true );
return;
}
// Replay enabled on the server?
if ( !g_pReplay->IsReplayEnabled() )
{
Replay_HudMsg( "#Replay_NotEnabled", "replay\\record_fail.wav", true );
return;
}
// Are we recording?
if ( !g_pReplay->IsRecording() )
{
Replay_HudMsg( "#Replay_NotRecording", "replay\\record_fail.wav", true );
return;
}
// Is replay disabled on the client?
if ( g_pClientReplayContextInternal->IsClientSideReplayDisabled() )
{
Replay_HudMsg( "#Replay_ClientSideDisabled", NULL, true );
return;
}
// Get the replay for the current life
CReplay *pReplayForCurrentLife = CL_GetReplayManager()->m_pReplayThisLife;
// Already saved this replay?
if ( !pReplayForCurrentLife || pReplayForCurrentLife->m_bRequestedByUser || pReplayForCurrentLife->m_bSaved )
{
Replay_HudMsg( "#Replay_AlreadySaved", "replay\\record_fail.wav" );
return;
}
// Take a screenshot and write it to disk if one hasn't been taken already
if ( !pReplayForCurrentLife->GetScreenshotCount() )
{
CaptureScreenshotParams_t params;
V_memset( &params, 0, sizeof( params ) );
params.m_flDelay = 0.0f;
params.m_bPrimary = true;
CL_GetScreenshotManager()->CaptureScreenshot( params );
}
// Send a message to the server, regardless of whether the player is alive or dead, requesting
// that a demo be written. Format a file name with the client's steam id and a timestamp
// (gpGlobals->tickcount).
CLC_SaveReplay msgSaveReplay;
g_pEngineClient->GetNetChannel()->SendNetMsg( msgSaveReplay, true );
// Get the session
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->Find( pReplayForCurrentLife->m_hSession ) );
if ( !pSession )
{
AssertMsg( 0, "Replay points to a non-existent session - should never happen!" );
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_ReplayBadSession" );
return;
}
// Replay for current life is complete (ie, player is dead and replay is ready to be committed)
if ( pReplayForCurrentLife->m_bComplete )
{
CL_GetReplayManager()->CommitPendingReplayAndBeginDownload();
}
else
{
// Mark the replay as requested by the user, so we can commit automatically as soon as
// the replay is complete (ie when the player dies, etc.).
pReplayForCurrentLife->m_bRequestedByUser = true;
}
// Cache replay pointer in owning session
pSession->CacheReplay( pReplayForCurrentLife );
// Make sure downloading is enabled
pSession->EnsureDownloadingEnabled();
// Add the new entry to the replay browser
g_pClient->OnSaveReplay( pReplayForCurrentLife->GetHandle(), true );
}
//----------------------------------------------------------------------------------------
CON_COMMAND( replay_add_fake_replays, "Adds a set of fake replays" )
{
if ( args.ArgC() < 2 )
{
DevMsg( "Use \'replay_add_fake_replays\' <num fake replays to add> <today only>\n" );
return;
}
// bool bTodayOnly = args.ArgC() >= 3 && args[2][0] == '1';
for ( int i = 0; i < atoi(args[1]); ++i )
{
// TODO:
}
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_confirmquit, "Make sure all replays are rendered before quitting", FCVAR_HIDDEN | FCVAR_DONTRECORD )
{
// TODO: Check to see if any replays are downloading - warn user. If user wants to
// quit anyway, make sure to set any blocks to not downloaded, save, and delete any
// files that were only partially downloaded.
// Unrendered replays? Display the quit confirmation dialog with the option to render all and quit
if ( CL_GetReplayManager()->GetUnrenderedReplayCount() > 0 && g_pClient->OnConfirmQuit() )
{
// Play a sound.
g_pClient->PlaySound( "replay\\confirmquit.wav" );
}
else
{
g_pEngine->Cbuf_AddText( "quit" );
g_pEngine->Cbuf_AddText( "\n" );
}
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_deleteclientreplays, "Deletes all replays from client replay history, as well as all files associated with each replay.", FCVAR_DONTRECORD )
{
CUtlVector< ReplayHandle_t > vecReplayHandles;
FOR_EACH_REPLAY( i )
{
vecReplayHandles.AddToTail( GET_REPLAY_AT( i )->GetHandle() );
}
FOR_EACH_VEC( vecReplayHandles, i )
{
CL_GetReplayManager()->DeleteReplay( vecReplayHandles[ i ], true );
}
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_removeclientreplay, "Remove the replay at the given index.", FCVAR_DONTRECORD )
{
if ( args.ArgC() != 2 )
{
Msg( "Not enough parameters.\n" );
return;
}
CL_GetReplayManager()->DeleteReplay( atoi(args[ 1 ]), true );
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_printclientreplays, "Prints out all client replay info", FCVAR_DONTRECORD )
{
FOR_EACH_REPLAY( i )
{
const CReplay *pReplay = GET_REPLAY_AT( i );
if ( !pReplay )
continue;
int nMonth, nDay, nYear;
pReplay->m_RecordTime.GetDate( nDay, nMonth, nYear );
int nHour, nMin, nSec;
pReplay->m_RecordTime.GetTime( nHour, nMin, nSec );
int nSpawnTick = pReplay->m_nSpawnTick;
int nDeathTick = pReplay->m_nDeathTick;
// TODO: All of this should go into a virtual function in CReplay, rather than some here and some in DumpGameSpecificData()
char szTitle[MAX_REPLAY_TITLE_LENGTH];
g_pVGuiLocalize->ConvertUnicodeToANSI( pReplay->m_wszTitle, szTitle, sizeof( szTitle ) );
Msg( "replay %i: \"%s\"\n", i, szTitle );
Msg( " handle: %i\n", pReplay->GetHandle() );
Msg( " spawn/death tick: %i / %i\n", nSpawnTick, nDeathTick );
Msg( " date: %i/%i/%i\n", nMonth, nDay, nYear );
Msg( " time: %i:%i:%i\n", nHour, nMin, nSec );
Msg( " map: %s\n", pReplay->m_szMapName );
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pReplay->m_hSession ) );
const char *pSessionName = pSession ? pSession->m_strName.Get() : NULL;
Msg( " session name: %s\n", pSessionName ? pSessionName : "" );
if ( pSession )
{
Msg( " last block downloaded: %i\n", pSession->GetGreatestConsecutiveBlockDownloaded() );
Msg( " last block to download: %i\n", pSession->GetLastBlockToDownload() );
}
int nScreenshotCount = pReplay->GetScreenshotCount();
Msg( "\n" );
Msg( " # screenshots: %i\n", nScreenshotCount );
Msg( " session handle: %i\n", (int)pReplay->m_hSession );
for ( int i = 0; i < nScreenshotCount; ++i )
{
const CReplayScreenshot *pScreenshot = pReplay->GetScreenshot( i );
Msg( " screenshot %i:\n", i );
Msg( " dimensions: w=%i, h=%i\n", pScreenshot->m_nWidth, pScreenshot->m_nHeight );
Msg( " base filename: %s\n", pScreenshot->m_szBaseFilename );
}
int nPerfCount = pReplay->GetPerformanceCount();
Msg( "\n" );
Msg( "# performances: %i\n", nPerfCount );
for ( int i = 0; i < nPerfCount; ++i )
{
const CReplayPerformance *pCurPerformance = pReplay->GetPerformance( i );
g_pVGuiLocalize->ConvertUnicodeToANSI( pCurPerformance->m_wszTitle, szTitle, sizeof( szTitle ) );
Msg( " performance %i:\n", i );
Msg( " title: %s\n", szTitle );
Msg( " ticks: in=%i out=%i\n", pCurPerformance->m_nTickIn, pCurPerformance->m_nTickOut );
Msg( " filename: %s\n", pCurPerformance->m_szBaseFilename );
}
Msg( "\n" );
pReplay->DumpGameSpecificData();
// Print replay status
const char *pStatus;
switch ( pReplay->m_nStatus )
{
case CReplay::REPLAYSTATUS_INVALID: pStatus = "invalid"; break;
case CReplay::REPLAYSTATUS_DOWNLOADPHASE: pStatus = "download phase"; break;
case CReplay::REPLAYSTATUS_READYTOCONVERT: pStatus = "ready to convert"; break;
case CReplay::REPLAYSTATUS_RENDERING: pStatus = "rendering"; break;
case CReplay::REPLAYSTATUS_RENDERED: pStatus = "rendered"; break;
case CReplay::REPLAYSTATUS_ERROR: pStatus = "error"; break;
default: pStatus = "";
}
Msg( " status: %s\n\n\n", pStatus );
}
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_renderpause, "Pause Replay rendering.", FCVAR_DONTRECORD )
{
if ( !CL_GetMovieManager()->IsRendering() )
return;
if ( g_pReplayDemoPlayer->IsReplayPaused() )
{
Msg( "Replay rendering already paused.\n" );
return;
}
// Pause playback
g_pReplayDemoPlayer->PauseReplay();
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_renderunpause, "Unpause Replay rendering.", FCVAR_DONTRECORD )
{
if ( !CL_GetMovieManager()->IsRendering() )
return;
if ( !g_pReplayDemoPlayer->IsReplayPaused() )
{
Msg( "Replay rendering not paused.\n" );
return;
}
// Unpause
g_pReplayDemoPlayer->ResumeReplay();
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_printqueuedtakes, "Print a list of takes queued for rendering.", FCVAR_DONTRECORD )
{
const int nCount = CL_GetRenderQueue()->GetCount();
if ( !nCount )
{
ConMsg( "No takes queued for render.\n" );
return;
}
ConMsg( "Takes queued for render:\n" );
ConMsg( " %65s%65s\n", "Replay Name", "Take Name" );
for ( int i = 0; i < nCount; ++i )
{
ReplayHandle_t hReplay;
int iPerf;
CL_GetRenderQueue()->GetEntryData( i, &hReplay, &iPerf );
const CReplay *pReplay = CL_GetReplayManager()->GetReplay( hReplay );
if ( !pReplay )
continue;
char szTakeName[MAX_REPLAY_TITLE_LENGTH];
if ( iPerf == -1 )
{
V_strcpy( szTakeName, "original" );
}
else
{
const CReplayPerformance *pPerformance = pReplay->GetPerformance( iPerf );
if ( !pPerformance )
continue;
V_wcstostr( pPerformance->m_wszTitle, -1, szTakeName, sizeof( szTakeName ) );
}
char szReplayTitle[MAX_REPLAY_TITLE_LENGTH];
V_wcstostr( pReplay->m_wszTitle, -1, szReplayTitle, sizeof( szReplayTitle ) );
ConMsg( " %02i:%65s%65s\n", i, szReplayTitle, szTakeName );
}
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_clearqueuedtakes, "Clear takes from render queue.", FCVAR_DONTRECORD )
{
CL_GetRenderQueue()->Clear();
ConMsg( "Cleared.\n" );
}
//----------------------------------------------------------------------------------------
+61
View File
@@ -0,0 +1,61 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "convar.h"
#include "replaysystem.h"
#include "replay/ireplayscreenshotsystem.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
void OnReplayScreenshotResolutionChanged( IConVar *var, const char *pOldValue, float flOldValue )
{
ConVar *pCvar = static_cast<ConVar*>(var);
if ( pCvar->GetInt() != (int)flOldValue )
{
// Re-allocate screenshot memory in the client since we are will be taking screenshots
// of a different dimension.
if ( g_pClient )
{
g_pClient->GetReplayScreenshotSystem()->UpdateReplayScreenshotCache();
}
}
}
//----------------------------------------------------------------------------------------
ConVar replay_postdeathrecordtime( "replay_postdeathrecordtime", "5", FCVAR_DONTRECORD, "The amount of time (seconds) to be recorded after you die for a given replay.", true, 0.0f, true, 10.0f );
ConVar replay_postwinreminderduration( "replay_postwinreminderduration", "5", FCVAR_DONTRECORD, "The number of seconds to show a Replay reminder, post-win/lose.", true, 0.0f, false, 0.0f );
ConVar replay_sessioninfo_updatefrequency( "replay_sessioninfo_updatefrequency", "5", FCVAR_DONTRECORD, "If a replay has not been downloaded, the replay browser will update the status of a given replay on the server based on this cvar (in seconds).", true, 5.0f, true, 120.0f );
ConVar replay_enableeventbasedscreenshots( "replay_enableeventbasedscreenshots", "0", FCVAR_DONTRECORD | FCVAR_ARCHIVE, "If disabled, only take a screenshot when a replay is saved. If enabled, take up to replay_maxscreenshotsperreplay screenshots, with a minimum of replay_mintimebetweenscreenshots seconds in between, at key events. Events include kills, ubers (if you are a medic), sentry kills (if you are an engineer), etc. NOTE: Turning this on may affect performance!" );
ConVar replay_screenshotresolution( "replay_screenshotresolution", "0", FCVAR_DONTRECORD, "0 for low-res screenshots (width=512), 1 for hi-res (width=1024)", true, 0.0f, true, 1.0f, OnReplayScreenshotResolutionChanged );
ConVar replay_maxscreenshotsperreplay( "replay_maxscreenshotsperreplay", "8", FCVAR_DONTRECORD, "The maximum number of screenshots that can be taken for any given replay.", true, 8, false, 0 );
ConVar replay_mintimebetweenscreenshots( "replay_mintimebetweenscreenshots", "5", FCVAR_DONTRECORD, "The minimum time (in seconds) that must pass between screenshots being taken.", true, 1, false, 0 );
ConVar replay_screenshotkilldelay( "replay_screenshotkilldelay", ".4", FCVAR_DONTRECORD, "Delay before taking a screenshot when you kill someone, in seconds.", true, 0.0f, true, 1.0f );
ConVar replay_screenshotsentrykilldelay( "replay_screenshotsentrykilldelay", ".30", FCVAR_DONTRECORD, "Delay before taking a screenshot when you kill someone, in seconds.", true, 0.0f, true, 1.0f );
ConVar replay_deathcammaxverticaloffset( "replay_deathcammaxverticaloffset", "150", FCVAR_DONTRECORD, "Vertical offset for player death camera" );
ConVar replay_sentrycammaxverticaloffset( "replay_sentrycammaxverticaloffset", "10", FCVAR_DONTRECORD, "Vertical offset from a sentry on sentry kill", true, 10.0f, false, 0.0f );
ConVar replay_playerdeathscreenshotdelay( "replay_playerdeathscreenshotdelay", "2", FCVAR_DONTRECORD, "Amount of time to wait after player is killed before taking a screenshot" );
ConVar replay_sentrycamoffset_frontback( "replay_sentrycamoffset_frontback", "-50", FCVAR_DONTRECORD, "Front/back offset for sentry POV screenshot" );
ConVar replay_sentrycamoffset_leftright( "replay_sentrycamoffset_leftright", "-25", FCVAR_DONTRECORD, "Left/right offset for sentry POV screenshot" );
ConVar replay_sentrycamoffset_updown( "replay_sentrycamoffset_updown", "22", FCVAR_DONTRECORD, "Up/down offset for sentry POV screenshot" );
ConVar replay_maxconcurrentdownloads( "replay_maxconcurrentdownloads", "3", FCVAR_DONTRECORD, "The maximum number of concurrent downloads allowed.", true, 1.0f, true, 16.0f );
ConVar replay_forcereconstruct( "replay_forcereconstruct", "0", FCVAR_DONTRECORD, "Force the reconstruction of replays each time." );
#if _DEBUG
ConVar replay_simulate_size_discrepancy( "replay_simulate_size_discrepancy", "0", FCVAR_DONTRECORD, "(Client-side) Simulate a downloaded session info saying a block should be X bytes and the downloaded data being Y bytes" );
ConVar replay_simulate_bad_hash( "replay_simulate_bad_hash", "0", FCVAR_DONTRECORD, "(Client-side) Simulate a downloaded session info specifying an MD5 digest that is different than what is calculated for a downloaded block on the client" );
ConVar replay_simulate_evil_download_size( "replay_simulate_evil_download_size", "0", FCVAR_DONTRECORD, "(Client-side) Simulate a maliciously large block or session info file size." );
ConVar replay_fake_render( "replay_fake_render", "0", FCVAR_DONTRECORD, "(Client-side) For fast render simulation - don't actually render any frames." );
ConVar replay_simulatedownloadfailure( "replay_simulatedownloadfailure", "0", FCVAR_DONTRECORD, "(Client-side) Simulate download failures. 0 = no failures; 1 = all HTTP downloads; 2 = session info files; 3 = session blocks" );
#endif
ConVar replay_dodiskcleanup( "replay_dodiskcleanup", "1", FCVAR_HIDDEN | FCVAR_ARCHIVE, "If 1, cleanup unneeded recording session blocks." );
ConVar replay_voice_during_playback( "replay_voice_during_playback", "0", FCVAR_ARCHIVE, "Play player voice chat during replay playback" );
//----------------------------------------------------------------------------------------
+300
View File
@@ -0,0 +1,300 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#if defined( WIN32 )
#include "winlite.h"
#include <WinInet.h>
#endif
#include "cl_downloader.h"
#include "engine/requestcontext.h"
#include "replaysystem.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineClientReplay *g_pEngineClient;
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
CHttpDownloader::CHttpDownloader( IDownloadHandler *pHandler )
: m_pHandler( pHandler ),
m_flNextThinkTime( 0.0f ),
m_uBytesDownloaded( 0 ),
m_uSize( 0 ),
m_pThreadState( NULL ),
m_bDone( false ),
m_nHttpError( HTTP_ERROR_NONE ),
m_nHttpStatus( HTTP_INVALID ),
m_pBytesDownloaded( NULL ),
m_pUserData( NULL )
{
}
CHttpDownloader::~CHttpDownloader()
{
CleanupThreadIfDone();
}
bool CHttpDownloader::CleanupThreadIfDone()
{
if ( !m_pThreadState || !m_pThreadState->threadDone )
return false;
// NOTE: The context's "data" member will have already been cleaned up by the
// download thread at this point.
delete m_pThreadState;
m_pThreadState = NULL;
return true;
}
bool CHttpDownloader::BeginDownload( const char *pURL, const char *pGamePath, void *pUserData, uint32 *pBytesDownloaded )
{
if ( !pURL || !pURL[0] )
return false;
m_pThreadState = new RequestContext_t();
if ( !m_pThreadState )
return false;
// Cache any user data
m_pUserData = pUserData;
// Cache bytes downloaded
m_pBytesDownloaded = pBytesDownloaded;
// Setup request context
Replay_CrackURL( pURL, m_pThreadState->baseURL, m_pThreadState->urlPath );
m_pThreadState->bAsHTTP = true;
if ( pGamePath )
{
m_pThreadState->bSuppressFileWrite = false;
V_strcpy( m_pThreadState->gamePath, pGamePath );
// Generate the actual filename to save. Well, it's not
// absolute, but this will work.
V_strcpy_safe( m_pThreadState->absLocalPath, g_pEngine->GetGameDir() );
V_AppendSlash( m_pThreadState->absLocalPath, sizeof(m_pThreadState->absLocalPath) );
V_strcat_safe( m_pThreadState->absLocalPath, pGamePath );
}
else
{
m_pThreadState->bSuppressFileWrite = true;
}
// Cache URL - for debugging
V_strcpy( m_szURL, pURL );
// Spawn the download thread
extern IDownloadSystem *g_pDownloadSystem;
return g_pDownloadSystem->CreateDownloadThread( m_pThreadState ) != 0;
}
void CHttpDownloader::AbortDownloadAndCleanup()
{
// Make sure that this function isn't executed simultaneously by
// multiple threads in order to avoid use-after-free crashes during
// shutdown.
AUTO_LOCK( m_lock );
if ( !m_pThreadState )
return;
// Thread already completed?
if ( m_pThreadState->threadDone )
{
CleanupThreadIfDone();
return;
}
// Loop until the thread cleans up
m_pThreadState->shouldStop = true;
while ( !m_pThreadState->threadDone )
;
// Cache state for handler
m_nHttpError = m_pThreadState->error;
m_nHttpStatus = HTTP_ABORTED; // Force this to be safe
m_uBytesDownloaded = 0;
m_uSize = m_pThreadState->nBytesTotal;
m_bDone = true;
InvokeHandler();
CleanupThreadIfDone();
}
void CHttpDownloader::Think()
{
const float flHostTime = g_pEngine->GetHostTime();
if ( m_flNextThinkTime > flHostTime )
return;
if ( !m_pThreadState )
return;
// If thread is done, cleanup now
if ( CleanupThreadIfDone() )
return;
// If we haven't already set shouldStop, check the download status
if ( !m_pThreadState->shouldStop )
{
// Security measure: make sure the file size isn't outrageous
const bool bEvilFileSize = m_pThreadState->nBytesTotal &&
m_pThreadState->nBytesTotal >= DOWNLOAD_MAX_SIZE;
#if _DEBUG
extern ConVar replay_simulate_evil_download_size;
if ( replay_simulate_evil_download_size.GetBool() || bEvilFileSize )
#else
if ( bEvilFileSize )
#endif
{
AbortDownloadAndCleanup();
return;
}
bool bConnecting = false; // For fall-through in HTTP_CONNECTING case.
#if _DEBUG
extern ConVar replay_simulatedownloadfailure;
if ( replay_simulatedownloadfailure.GetInt() == 1 )
{
m_pThreadState->status = HTTP_ERROR;
}
#endif
switch ( m_pThreadState->status )
{
case HTTP_CONNECTING:
// Call connecting handler
if ( m_pHandler )
{
m_pHandler->OnConnecting( this );
}
bConnecting = true;
// Fall-through
case HTTP_FETCH:
m_uBytesDownloaded = (uint32)m_pThreadState->nBytesCurrent;
m_uSize = m_pThreadState->nBytesTotal;
Assert( m_uBytesDownloaded <= m_uSize );
// Call fetch handle
if ( !bConnecting && m_pHandler )
{
m_pHandler->OnFetch( this );
}
break;
case HTTP_ABORTED:
case HTTP_DONE:
case HTTP_ERROR:
// Cache state
m_nHttpError = m_pThreadState->error;
m_nHttpStatus = m_pThreadState->status;
m_uBytesDownloaded = (uint32)m_pThreadState->nBytesCurrent;
m_uSize = m_pThreadState->nBytesTotal; // NOTE: Need to do this here in the case that a file is small enough that we never hit HTTP_FETCH
m_bDone = true;
// Call handler
InvokeHandler();
// Tell the thread to cleanup so we can free it
m_pThreadState->shouldStop = true;
break;
}
}
// Write bytes for user if changed
if ( m_pBytesDownloaded && *m_pBytesDownloaded != m_uBytesDownloaded )
{
*m_pBytesDownloaded = m_uBytesDownloaded;
IF_REPLAY_DBG( Warning( "%s: Downloaded %i/%i bytes\n", m_szURL, m_uBytesDownloaded, m_uSize ) );
}
// Set next think time
m_flNextThinkTime = flHostTime + 0.1f;
}
void CHttpDownloader::InvokeHandler()
{
if ( !m_pHandler )
return;
// NOTE: Don't delete the downloader in OnDownloadComplete()!
m_pHandler->OnDownloadComplete( this, m_pThreadState->data );
}
// This does not increment the "ErrorCounter" field and should only be called from code
// that eventually calls into OGS_ReportGenericError().
KeyValues *CHttpDownloader::GetOgsRow( int nErrorCounter ) const
{
KeyValues *pResult = new KeyValues( "TF2ReplayHttpDownloadErrors" );
pResult->SetInt( "ErrorCounter", nErrorCounter );
pResult->SetInt( "BytesDownloaded", (int)m_uBytesDownloaded );
pResult->SetInt( "BytesTotal", (int)m_uSize );
pResult->SetInt( "HttpStatus", m_nHttpStatus );
pResult->SetInt( "HttpError", m_nHttpError );
pResult->SetString( "URL", m_szURL );
return pResult;
}
/*static*/ const char *CHttpDownloader::GetHttpErrorToken( HTTPError_t nError )
{
switch ( nError )
{
case HTTP_ERROR_ZERO_LENGTH_FILE: return "#HTTPError_ZeroLengthFile";
case HTTP_ERROR_CONNECTION_CLOSED: return "#HTTPError_ConnectionClosed";
case HTTP_ERROR_INVALID_URL: return "#HTTPError_InvalidURL";
case HTTP_ERROR_INVALID_PROTOCOL: return "#HTTPError_InvalidProtocol";
case HTTP_ERROR_CANT_BIND_SOCKET: return "#HTTPError_CantBindSocket";
case HTTP_ERROR_CANT_CONNECT: return "#HTTPError_CantConnect";
case HTTP_ERROR_NO_HEADERS: return "#HTTPError_NoHeaders";
case HTTP_ERROR_FILE_NONEXISTENT: return "#HTTPError_NonExistent";
}
return "#HTTPError_Unknown";
}
//----------------------------------------------------------------------------------------
#ifdef _DEBUG
CHttpDownloader *g_pTestDownload = NULL;
ConVar replay_forcedownloadurl( "replay_forcedownloadurl", "" );
CON_COMMAND( replay_testdownloader_start, "" )
{
const char *pGamePath = Replay_va( "%s%s", CL_GetRecordingSessionBlockManager()->GetSavePath(), "testdownload" );
g_pTestDownload = new CHttpDownloader();
g_pTestDownload->BeginDownload( args[1], pGamePath );
}
CON_COMMAND( replay_testdownloader_abort, "" )
{
if ( !g_pTestDownload )
return;
g_pTestDownload->AbortDownloadAndCleanup();
delete g_pTestDownload;
g_pTestDownload = NULL;
}
#endif
+109
View File
@@ -0,0 +1,109 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_DOWNLOADER_H
#define CL_DOWNLOADER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "platform.h"
#include "engine/http.h"
#include "interface.h"
#include "tier0/threadtools.h"
//----------------------------------------------------------------------------------------
struct RequestContext_t;
class CHttpDownloader;
class KeyValues;
//----------------------------------------------------------------------------------------
class IDownloadHandler
{
public:
virtual void OnConnecting( CHttpDownloader *pDownloader ) = 0;
virtual void OnFetch( CHttpDownloader *pDownloader ) = 0;
// Called when the download is done successfully, errors out, or is aborted.
// NOTE: pDownloader should NOT be deleted from within OnDownloadComplete().
// pData contains the downloaded data for processing.
virtual void OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData ) = 0;
};
//----------------------------------------------------------------------------------------
//
// Generic downloader class - downloads a single file on its own thread, and maintains
// state for that data.
//
// TODO: Derive from CBaseThinker and remove explicit calls to Think() - will make this
// class less bug-prone (easy to forget to call Think()).
//
class CHttpDownloader
{
public:
// Pass in a callback
CHttpDownloader( IDownloadHandler *pHandler = NULL );
~CHttpDownloader();
//
// Download the file at the given URL (HTTP/HTTPS support only)
// pGamePath - Game path where we should put the file - can be NULL if we don't
// want to save to the file to disk
// pUserData - Passed back to IDownloadHandler, if one has been set - can be NULL
// pBytesDownloaded - If non-NULL, # of bytes downloaded written.
// Returns true on success.
//
bool BeginDownload( const char *pURL, const char *pGamePath = NULL,
void *pUserData = NULL, uint32 *pBytesDownloaded = NULL );
//
// Abort the download (if there is one), wait for the download to shutdown and
// do cleanup.
//
void AbortDownloadAndCleanup();
inline bool IsDone() const { return m_bDone; } // Download done?
inline bool CanDelete() const { return m_pThreadState == NULL; } // Can free?
inline HTTPStatus_t GetStatus() const { return m_nHttpStatus; }
inline HTTPError_t GetError() const { return m_nHttpError; }
inline void *GetUserData() const { return m_pUserData; }
inline uint32 GetBytesDownloaded() const { return m_uBytesDownloaded; }
inline uint32 GetSize() const { return m_uSize; } // File size in bytes - NOTE: Not valid until the download is complete, aborted, or errored out
inline const char *GetURL() const { return m_szURL; }
void Think();
KeyValues *GetOgsRow( int nErrorCounter ) const;
static const char *GetHttpErrorToken( HTTPError_t nError );
private:
bool CleanupThreadIfDone();
void InvokeHandler();
RequestContext_t *m_pThreadState;
float m_flNextThinkTime;
bool m_bDone;
HTTPError_t m_nHttpError;
HTTPStatus_t m_nHttpStatus;
uint32 m_uBytesDownloaded;
uint32 *m_pBytesDownloaded; // Passed into BeginDownload()
uint32 m_uSize;
IDownloadHandler *m_pHandler;
void *m_pUserData;
char m_szURL[512];
// Use this to make sure that AbortDownloadAndCleanup isn't executed simultaneously
// by two threads. This was causing use-after-free crashes during shutdown.
CThreadMutex m_lock;
};
//----------------------------------------------------------------------------------------
#endif // CL_DOWNLOADER_H
+35
View File
@@ -0,0 +1,35 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_PERFORMANCE_COMMON_H
#define CL_PERFORMANCE_COMMON_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/performance.h"
//----------------------------------------------------------------------------------------
enum PerformanceEventType_t
{
EVENTTYPE_INVALID,
EVENTTYPE_CAMERA_CHANGE_BEGIN,
EVENTTYPE_CAMERA_CHANGE_FIRSTPERSON = EVENTTYPE_CAMERA_CHANGE_BEGIN,
EVENTTYPE_CAMERA_CHANGE_THIRDPERSON,
EVENTTYPE_CAMERA_CHANGE_FREE,
EVENTTYPE_CAMERA_CHANGE_END = EVENTTYPE_CAMERA_CHANGE_FREE,
EVENTTYPE_CHANGEPLAYER = EVENTTYPE_CAMERA_CHANGE_END + 512, // Leave plenty of room for camera types
EVENTTYPE_CAMERA_SETVIEW,
EVENTTYPE_TIMESCALE,
};
//----------------------------------------------------------------------------------------
#endif // CL_PERFORMANCE_COMMON_H
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef PERFORMANCECONTROLLER_H
#define PERFORMANCECONTROLLER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplayperformancecontroller.h"
#include "basethinker.h"
#include "replay/replayhandle.h"
#include <utllinkedlist.h>
//----------------------------------------------------------------------------------------
class IReplayPerformancePlaybackHandler;
class IReplayPerformanceEditor;
class KeyValues;
class Vector;
class QAngle;
class CReplayPerformance;
class CReplay;
class CJob;
//----------------------------------------------------------------------------------------
class CPerformanceController : public CBaseThinker,
public IReplayPerformanceController
{
public:
CPerformanceController();
~CPerformanceController();
//
// IReplayPerforamnceController
//
virtual void SetEditor( IReplayPerformanceEditor *pEditor );
virtual void StartRecording( CReplay *pReplay, bool bSnip );
virtual void Stop(); // Stop playback/recording
virtual bool SaveAsync();
virtual bool SaveAsAsync( const wchar_t *pTitle );
virtual bool IsSaving() const;
virtual void SaveThink();
virtual bool GetLastSaveStatus() const;
virtual bool IsRecording() const;
virtual bool IsPlaying() const;
virtual bool IsPlaybackDataLeft();
virtual bool IsDirty() const;
virtual void NotifyDirty();
virtual void OnSignonStateFull();
virtual float GetPlaybackTimeScale() const;
private:
//
// Common to recording/playback
//
void Cleanup();
void CleanupDbgStream();
void CleanupStream(); // Cleans up m_pRoot if necessary
float GetTime() const; // Get m_pCurEvent time
void ClearDirtyFlag();
enum State_t
{
STATE_DORMANT = -1, // Not playing back or recording
STATE_RECORDING,
STATE_PLAYING,
};
State_t m_nState;
//
// CBaseThinker
//
virtual void Think();
virtual float GetNextThinkTime() const;
//
// Recorder-specific:
//
virtual void NotifyPauseState( bool bPaused );
virtual void Snip();
virtual void NotifyRewinding();
virtual void ClearRewinding();
virtual bool IsRewinding() const { return m_bRewinding; }
virtual const KeyValues *GetUnsavedRecordingData() const { return m_pRoot; }
virtual void AddEvent_Camera_Change_FirstPerson( float flTime, int nEntityIndex );
virtual void AddEvent_Camera_Change_ThirdPerson( float flTime, int nEntityIndex );
virtual void AddEvent_Camera_Change_Free( float flTime );
virtual void AddEvent_Camera_ChangePlayer( float flTime, int nEntIndex );
virtual void AddEvent_Camera_SetView( const SetViewParams_t &params );
virtual void AddEvent_TimeScale( float flTime, float flScale );
void CreateNewScratchPerformance( CReplay *pReplay );
bool DumpStreamToFileAsync( const char *pFullFilename );
bool FlushReplay();
bool IsCameraChangeEvent( int nType ) const;
void AddEvent( KeyValues *pEvent );
void RemoveDuplicateEventsFromQueue();
ReplayHandle_t m_hReplay;
CReplayPerformance *m_pSavedPerformance; // Points to the saved performance - scratch copies to saved - should not be modified directly
CReplayPerformance *m_pScratchPerformance; // The working performance, ie the temporary performance we muck with until the user saves or discards
bool m_bRewinding;
bool m_bPaused; // Maintain our own state for paused/playing for event queueing
CUtlLinkedList< KeyValues * > m_EventQueue; // If user pauses and changes camera, etc, it gets queued here - if multiple camera changes occur, previous is stomped
IReplayPerformanceEditor *m_pEditor; // Pointer to the editor UI in the client
//
// Playback-specific
//
void PlaybackThink();
void ReadSetViewEvent( KeyValues *pEventSubKey, Vector &origin, QAngle &angles, float &fov,
float *pAccel, float *pSpeed, float *pRotFilter );
void DebugRender();
bool SetupPlaybackHandler();
void SetupPlaybackFromPerformance( CReplayPerformance *pPerformance );
void SetupPlaybackExistingStream(); // Don't load anything from disk - use what's already in memory - used for rewind
void FinishBeginPerformancePlayback();
virtual CReplayPerformance *GetPerformance();
virtual CReplayPerformance *GetSavedPerformance();
virtual bool HasSavedPerformance();
KeyValues *m_pRoot;
KeyValues *m_pCurEvent;
KeyValues *m_pDbgRoot; // Copy of performance data for debug rendering
KeyValues *m_pSetViewEvent;
bool m_bViewOverrideMode;
bool m_bDirty; // If we recorded at all, was anything changed?
float m_flLastCamSetViewTime;
float m_flTimeScale;
CJob *m_pSaveJob;
bool m_bLastSaveStatus;
IReplayPerformancePlaybackHandler *m_pPlaybackHandler;
};
//----------------------------------------------------------------------------------------
#endif // PERFORMANCECONTROLLER_H
+71
View File
@@ -0,0 +1,71 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_performancemanager.h"
#include "cl_replaymanager.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define PERFORMANCE_INDEX_VERSION 0
//----------------------------------------------------------------------------------------
CReplayPerformanceManager::CReplayPerformanceManager()
{
}
CReplayPerformanceManager::~CReplayPerformanceManager()
{
}
void CReplayPerformanceManager::Init()
{
g_pFullFileSystem->CreateDirHierarchy( Replay_va( "%s%s", CL_GetBasePath(), SUBDIR_PERFORMANCES ) );
}
void CReplayPerformanceManager::DeletePerformance( CReplayPerformance *pPerformance )
{
// Delete the performance file
const char *pFullFilename = pPerformance->GetFullPerformanceFilename();
g_pFullFileSystem->RemoveFile( pFullFilename );
// Remove from replay list
CReplay *pOwnerReplay = pPerformance->m_pReplay;
pOwnerReplay->m_vecPerformances.FindAndRemove( pPerformance ); // This can fail if the replay doesn't own the performance yet - which is no problem.
// Free
delete pPerformance;
CL_GetReplayManager()->FlagReplayForFlush( pOwnerReplay, true );
}
const char *CReplayPerformanceManager::GetRelativePath() const
{
return Replay_va( "%s%s%c", CL_GetRelativeBasePath(), SUBDIR_PERFORMANCES, CORRECT_PATH_SEPARATOR );
}
const char *CReplayPerformanceManager::GetFullPath() const
{
return Replay_va( "%s%c%s", g_pEngine->GetGameDir(), CORRECT_PATH_SEPARATOR, GetRelativePath() );
}
CReplayPerformance *CReplayPerformanceManager::CreatePerformance( CReplay *pReplay )
{
return new CReplayPerformance( pReplay );
}
const char *CReplayPerformanceManager::GeneratePerformanceFilename( CReplay *pReplay )
{
static char s_szBaseFilename[ MAX_OSPATH ];
char szIdealBaseFilename[ MAX_OSPATH ];
V_strcpy_safe( szIdealBaseFilename, Replay_va( "replay_%i_edit", (int)pReplay->GetHandle() ) );
Replay_GetFirstAvailableFilename( s_szBaseFilename, sizeof( s_szBaseFilename ), szIdealBaseFilename, "." GENERIC_FILE_EXTENSION,
CL_GetPerformanceManager()->GetRelativePath(), pReplay->GetPerformanceCount() );
return s_szBaseFilename;
}
//----------------------------------------------------------------------------------------
+44
View File
@@ -0,0 +1,44 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAYPERFORMANCEMANAGER_H
#define REPLAYPERFORMANCEMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplayperformancemanager.h"
#include "replay/performance.h"
//----------------------------------------------------------------------------------------
class KeyValues;
class CReplay;
class IReplayPerformancePlaybackController;
//----------------------------------------------------------------------------------------
class CReplayPerformanceManager : public IReplayPerformanceManager
{
public:
CReplayPerformanceManager();
~CReplayPerformanceManager();
void Init();
//
// IReplayPerformanceManager
//
virtual const char *GetRelativePath() const;
virtual const char *GetFullPath() const;
virtual CReplayPerformance *CreatePerformance( CReplay *pReplay );
virtual void DeletePerformance( CReplayPerformance *pPerformance );
virtual const char *GeneratePerformanceFilename( CReplay *pReplay );
};
//----------------------------------------------------------------------------------------
#endif // REPLAYPERFORMANCEMANAGER_H
+451
View File
@@ -0,0 +1,451 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_recordingsession.h"
#include "cl_sessioninfodownloader.h"
#include "cl_recordingsessionmanager.h"
#include "cl_replaymanager.h"
#include "cl_recordingsessionblock.h"
#include "cl_sessionblockdownloader.h"
#include "replay/ienginereplay.h"
#include "KeyValues.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
#define MAX_SESSION_INFO_DOWNLOAD_ATTEMPTS 3
//----------------------------------------------------------------------------------------
CClientRecordingSession::CClientRecordingSession( IReplayContext *pContext )
: CBaseRecordingSession( pContext ),
m_iLastBlockToDownload( -1 ),
m_iGreatestConsecutiveBlockDownloaded( -1 ),
m_nSessionInfoDownloadAttempts( 0 ),
m_flLastUpdateTime( -1.0f ),
m_pSessionInfoDownloader( NULL ),
m_bTimedOut( false ),
m_bAllBlocksDownloaded( false )
{
}
CClientRecordingSession::~CClientRecordingSession()
{
delete m_pSessionInfoDownloader;
}
bool CClientRecordingSession::AllReplaysReconstructed() const
{
FOR_EACH_LL( m_lstReplays, it )
{
const CReplay *pCurReplay = m_lstReplays[ it ];
if ( !pCurReplay->HasReconstructedReplay() )
return false;
}
return true;
}
void CClientRecordingSession::DeleteBlocks()
{
// Only delete blocks if all replays have been reconstructed for this session
if ( !AllReplaysReconstructed() )
return;
// Delete each block
FOR_EACH_VEC( m_vecBlocks, i )
{
m_pContext->GetRecordingSessionBlockManager()->DeleteBlock( m_vecBlocks[ i ] );
}
// Clear out the list
m_vecBlocks.RemoveAll();
// Clear these out so we don't try to download the blocks again
m_iLastBlockToDownload = -1;
m_iGreatestConsecutiveBlockDownloaded = -1;
}
void CClientRecordingSession::SyncSessionBlocks()
{
// If the last update time hasn't been initialized yet, initialize it now since this will be the first time
// we are attempting to download the session info file.
if ( m_flLastUpdateTime < 0.0f )
{
m_flLastUpdateTime = g_pEngine->GetHostTime();
}
Assert( !m_pSessionInfoDownloader );
IF_REPLAY_DBG( Warning( "Downloading session info...\n" ) );
m_pSessionInfoDownloader = new CSessionInfoDownloader();
m_pSessionInfoDownloader->DownloadSessionInfoAndUpdateBlocks( this );
}
void CClientRecordingSession::OnReplayDeleted( CReplay *pReplay )
{
m_lstReplays.FindAndRemove( pReplay );
// This will load session blocks and delete them from disk if possible. In the case
// that all other replays for a session have already been reconstructed and pReplay
// was the last replay that was never reconstructed, we should delete session's blocks now.
// Note that these calls will only (a) load blocks if they aren't loaded already, and
// (b) Delete blocks if all associated replays have been reconstructed.
LoadBlocksForSession();
DeleteBlocks();
}
bool CClientRecordingSession::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_iLastBlockToDownload = pIn->GetInt( "last_block_to_download", -1 );
m_iGreatestConsecutiveBlockDownloaded = pIn->GetInt( "last_consec_block_downloaded", -1 );
// m_bTimedOut = pIn->GetBool( "timed_out" );
m_uServerSessionID = pIn->GetUint64( "server_session_id" );
m_bAllBlocksDownloaded = pIn->GetBool( "all_blocks_downloaded" );
return true;
}
void CClientRecordingSession::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "last_block_to_download", m_iLastBlockToDownload );
pOut->SetInt( "last_consec_block_downloaded", m_iGreatestConsecutiveBlockDownloaded );
// pOut->SetInt( "timed_out", (int)m_bTimedOut );
pOut->SetUint64( "server_session_id", m_uServerSessionID );
pOut->SetInt( "all_blocks_downloaded", (int)m_bAllBlocksDownloaded );
}
void CClientRecordingSession::AdjustLastBlockToDownload( int iNewLastBlockToDownload )
{
Assert( m_iLastBlockToDownload > iNewLastBlockToDownload );
m_iLastBlockToDownload = iNewLastBlockToDownload;
// Adjust any replays that refer to this session
FOR_EACH_LL( m_lstReplays, i )
{
CReplay *pCurReplay = m_lstReplays[ i ];
if ( pCurReplay->m_iMaxSessionBlockRequired > iNewLastBlockToDownload )
{
// Adjust replay
pCurReplay->m_iMaxSessionBlockRequired = iNewLastBlockToDownload;
}
}
}
int CClientRecordingSession::UpdateLastBlockToDownload()
{
// Here we calculate the block we'll need in order to reconstruct the replay at the post-death time,
// based on replay_postdeathrecordtime, NOT the current time. The index calculated here may be greater
// than the actual last block the server writes, since the round may end or the map may change. This
// is adjusted for when we actually download the blocks.
extern ConVar replay_postdeathrecordtime;
CClientRecordingSessionManager::ServerRecordingState_t *pServerState = &CL_GetRecordingSessionManager()->m_ServerRecordingState;
const int nCurBlock = pServerState->m_nCurrentBlock;
const int nDumpInterval = pServerState->m_nDumpInterval; Assert( nDumpInterval > 0 );
const int nAddedBlocks = (int)ceil( replay_postdeathrecordtime.GetFloat() / nDumpInterval ); // Round up
const int iPostDeathBlock = nCurBlock + nAddedBlocks;
IF_REPLAY_DBG( Warning( "nCurBlock: %i\n", nCurBlock ) );
IF_REPLAY_DBG( Warning( "nDumpInterval: %i\n", nDumpInterval ) );
IF_REPLAY_DBG( Warning( "nAddedBlocks: %i\n", nAddedBlocks ) );
IF_REPLAY_DBG( Warning( "iPostDeathBlock: %i\n", iPostDeathBlock ) );
// Never assign less blocks than we already need
m_iLastBlockToDownload = MAX( m_iLastBlockToDownload, iPostDeathBlock );
CL_GetRecordingSessionManager()->FlagForFlush( this, false );
IF_REPLAY_DBG( ConColorMsg( 0, Color(0,255,0), "Max block currently needed: %i\n", m_iLastBlockToDownload ) );
return m_iLastBlockToDownload;
}
void CClientRecordingSession::Think()
{
CBaseThinker::Think();
// If the session info downloader's done and can be deleted, free it.
if ( m_pSessionInfoDownloader &&
m_pSessionInfoDownloader->IsDone() &&
m_pSessionInfoDownloader->CanDelete() )
{
// Failure?
if ( m_pSessionInfoDownloader->m_nError != CSessionInfoDownloader::ERROR_NONE )
{
// If there was an error, increment the error count and update the appropriate replays if
// we've tried a sufficient number of times.
++m_nSessionInfoDownloadAttempts;
if ( m_nSessionInfoDownloadAttempts >= MAX_SESSION_INFO_DOWNLOAD_ATTEMPTS )
{
FOR_EACH_LL( m_lstReplays, i )
{
CReplay *pCurReplay = m_lstReplays[ i ];
// If this replay has already been set to "ready to convert" state (or beyond), skip.
if ( pCurReplay->m_nStatus >= CReplay::REPLAYSTATUS_READYTOCONVERT )
continue;
// Update status
pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_ERROR;
// Display an error message
ShowDownloadFailedMessage( pCurReplay );
// Save now
CL_GetReplayManager()->FlagReplayForFlush( pCurReplay, true );
}
}
}
IF_REPLAY_DBG( Warning( "...session info download complete. Freeing.\n" ) );
delete m_pSessionInfoDownloader;
m_pSessionInfoDownloader = NULL;
}
}
float CClientRecordingSession::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 0.5f;
}
void CClientRecordingSession::UpdateAllBlocksDownloaded()
{
// We're only "done" if this session is no longer recording and all blocks are downloaded.
const bool bOld = m_bAllBlocksDownloaded;
m_bAllBlocksDownloaded = !m_bRecording && ( m_iGreatestConsecutiveBlockDownloaded >= m_iLastBlockToDownload );
// Flag as modified if changed
if ( bOld != m_bAllBlocksDownloaded )
{
CL_GetRecordingSessionManager()->FlagForFlush( this, false );
}
}
void CClientRecordingSession::EnsureDownloadingEnabled()
{
m_bAllBlocksDownloaded = false;
}
void CClientRecordingSession::UpdateGreatestConsecutiveBlockDownloaded()
{
// Assumes m_vecBlocks is sorted in ascending order (for both reconstruction indices and handle, which should be parallel)
int j = 0;
int iGreatestConsecutiveBlockDownloaded = 0;
FOR_EACH_VEC( m_vecBlocks, i )
{
CClientRecordingSessionBlock *pCurBlock = CL_CastBlock( m_vecBlocks[ i ] );
AssertMsg( pCurBlock->m_iReconstruction == j, "Session blocks must be sorted!" );
// If the block hasn't been downloaded, stop here
if ( pCurBlock->m_nDownloadStatus != CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADED )
break;
// Block has been downloaded - update the counter
iGreatestConsecutiveBlockDownloaded = MAX( iGreatestConsecutiveBlockDownloaded, pCurBlock->m_iReconstruction );
++j;
}
Assert( iGreatestConsecutiveBlockDownloaded >= 0 );
Assert( iGreatestConsecutiveBlockDownloaded < m_vecBlocks.Count() );
// Cache
m_iGreatestConsecutiveBlockDownloaded = iGreatestConsecutiveBlockDownloaded;
// Mark session as dirty
CL_GetRecordingSessionManager()->FlagForFlush( this, false );
}
void CClientRecordingSession::UpdateReplayStatuses( CClientRecordingSessionBlock *pBlock )
{
AssertMsg( m_vecBlocks.Find( pBlock ) != m_vecBlocks.InvalidIndex(), "Block doesn't belong to session or was not added" );
// If the download was successful, update the greatest consecutive block downloaded index
if ( pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADED )
{
UpdateGreatestConsecutiveBlockDownloaded();
UpdateAllBlocksDownloaded();
}
// Block in error state?
const bool bFailed = pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_ERROR;
// Go through all replays that refer to this session and update their status if necessary
FOR_EACH_LL( m_lstReplays, i )
{
CReplay *pCurReplay = m_lstReplays[ i ];
// If this replay has already been set to "ready to convert" state (or beyond), skip.
if ( pCurReplay->m_nStatus >= CReplay::REPLAYSTATUS_READYTOCONVERT )
continue;
bool bFlush = false;
// If the download failed and the block is required for this replay, mark as such
if ( bFailed && pCurReplay->m_iMaxSessionBlockRequired >= pBlock->m_iReconstruction )
{
pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_ERROR;
bFlush = true;
// Display an error message
ShowDownloadFailedMessage( pCurReplay );
}
// Have we downloaded all blocks required for the given replay?
else if ( !bFailed && pCurReplay->m_iMaxSessionBlockRequired <= m_iGreatestConsecutiveBlockDownloaded )
{
// Update replay's status and mark as dirty
pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_READYTOCONVERT;
// Display a message on the client
g_pClient->DisplayReplayMessage( "#Replay_DownloadComplete", false, false, "replay\\downloadcomplete.wav" );
bFlush = true;
}
// Mark replay as dirty?
if ( bFlush )
{
CL_GetReplayManager()->FlagForFlush( pCurReplay, false );
}
}
}
void CClientRecordingSession::OnDownloadTimeout()
{
m_bTimedOut = true;
// Go through all replays that refer to this session and update their status if necessary
FOR_EACH_LL( m_lstReplays, i )
{
CReplay *pCurReplay = m_lstReplays[ i ];
// If this replay has already been set to "ready to convert" state (or beyond), skip.
if ( pCurReplay->m_nStatus >= CReplay::REPLAYSTATUS_READYTOCONVERT )
continue;
// Check to see if we have enough block info for the current replay
if ( m_iGreatestConsecutiveBlockDownloaded >= pCurReplay->m_iMaxSessionBlockRequired )
continue;
// Update replay status
pCurReplay->m_nStatus = CReplay::REPLAYSTATUS_ERROR;
// Display an error message
ShowDownloadFailedMessage( pCurReplay );
// Save the replay
CL_GetReplayManager()->FlagForFlush( pCurReplay, false );
}
}
void CClientRecordingSession::RefreshLastUpdateTime()
{
m_flLastUpdateTime = g_pEngine->GetHostTime();
}
void CClientRecordingSession::ShowDownloadFailedMessage( const CReplay *pReplay )
{
// Don't show the download failed message for replays that were saved during this run of the game.
if ( !pReplay || !pReplay->m_bSavedDuringThisSession )
return;
// Display an error message
g_pClient->DisplayReplayMessage( "#Replay_DownloadFailed", true, false, "replay\\downloadfailed.wav" );
}
void CClientRecordingSession::CacheReplay( CReplay *pReplay )
{
Assert( m_lstReplays.Find( pReplay ) == m_lstReplays.InvalidIndex() );
m_lstReplays.AddToTail( pReplay );
// We should no longer auto-delete this session if CacheReplay() is being called. This
// can happen if the user connects to a server, saves a replay, deletes the replay (at
// which point auto-delete is flagged for the recording session), and then saves another
// replay. In this situation, we obviously don't want to delete the session anymore.
if ( m_bAutoDelete )
{
m_bAutoDelete = false;
}
}
bool CClientRecordingSession::ShouldSyncBlocksWithServer() const
{
// Already downloaded all blocks?
if ( m_bAllBlocksDownloaded )
return false;
// If block count is out of sync with the m_iLastBlockDownloaded we need to sync up
const bool bReachedMaxDownloadAttempts = m_nSessionInfoDownloadAttempts >= MAX_SESSION_INFO_DOWNLOAD_ATTEMPTS;
const bool bNeedToDownloadBlocks = m_iLastBlockToDownload >= 0;
// const bool bAlreadyDownloadedAllNeededBlocks = m_iLastBlockToDownload <= m_iGreatestConsecutiveBlockDownloaded;
const bool bAlreadyDownloadedAllNeededBlocks = m_iLastBlockToDownload < m_vecBlocks.Count(); // NOTE/TODO: Shouldn't this look at m_iGreatestConsecutiveBlockDownloaded? Tried for a week, but it caused bugs. Reverting for now. TODO
const bool bTimedOut = false;//TimedOut();
const bool bResult = !bReachedMaxDownloadAttempts &&
bNeedToDownloadBlocks &&
!bAlreadyDownloadedAllNeededBlocks &&
!bTimedOut;
if ( bResult )
{
IF_REPLAY_DBG( Warning( "Blocks out of sync for session %i - downloading session info now.\n", GetHandle() ) );
}
else
{
DBG3( "NOT syncing because:\n" );
if ( bReachedMaxDownloadAttempts ) DBG3( " - Reached maximum download attempts\n" );
if ( !bNeedToDownloadBlocks ) DBG3( " - No replay saved yet\n" );
if ( bAlreadyDownloadedAllNeededBlocks ) DBG3( " - Already downloaded all needed blocks\n" );
if ( bTimedOut ) DBG3( " - Download timed out (session info file didn't change after 90 seconds)\n" );
}
return bResult;
}
void CClientRecordingSession::PopulateWithRecordingData( int nCurrentRecordingStartTick )
{
BaseClass::PopulateWithRecordingData( nCurrentRecordingStartTick );
CClientRecordingSessionManager::ServerRecordingState_t *pServerState = &CL_GetRecordingSessionManager()->m_ServerRecordingState;
m_strName = pServerState->m_strSessionName;
// Get download URL from replicated cvars
m_strBaseDownloadURL = Replay_GetDownloadURL();
// Get server session ID
m_uServerSessionID = g_pClient->GetServerSessionId();
}
bool CClientRecordingSession::ShouldDitchSession() const
{
return BaseClass::ShouldDitchSession() || m_lstReplays.Count() == 0;
}
void CClientRecordingSession::OnDelete()
{
// Abort any session block downloads now
CL_GetSessionBlockDownloader()->AbortDownloadsAndCleanup( this );
if ( m_pSessionInfoDownloader )
{
m_pSessionInfoDownloader->CleanupDownloader();
}
// Delete blocks
BaseClass::OnDelete();
}
//----------------------------------------------------------------------------------------
+102
View File
@@ -0,0 +1,102 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_RECORDINGSESSION_H
#define CL_RECORDINGSESSION_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsession.h"
#include "basethinker.h"
//----------------------------------------------------------------------------------------
class CReplay;
class CSessionInfoDownloader;
class CClientRecordingSessionBlock;
//----------------------------------------------------------------------------------------
class CClientRecordingSession : public CBaseRecordingSession,
public CBaseThinker
{
typedef CBaseRecordingSession BaseClass;
public:
CClientRecordingSession( IReplayContext *pContext );
~CClientRecordingSession();
void DeleteBlocks();
void UpdateAllBlocksDownloaded(); // Sets the all-blocks-downloaded flag if this session is no longer recording and all blocks have been downloaded
void EnsureDownloadingEnabled();
//
// CGenericPersistentManager
//
bool Read( KeyValues *pIn );
void Write( KeyValues *pOut );
void AdjustLastBlockToDownload( int iNewLastBlockToDownload ); // When the client predicts more blocks than the server is ever going to write (if the round ends and the player just died, for example), this allows the session info downloader to adjust the max # of blocks to download - also does adjustments for any offending replays
int UpdateLastBlockToDownload(); // Calling this will implicitly cause new blocks to be downloaded for the given session
void SyncSessionBlocks(); // Creates session info downloader and fires off the download - creates and syncs blocks as necessary
void OnReplayDeleted( CReplay *pReplay );
void UpdateReplayStatuses( CClientRecordingSessionBlock *pBlock ); // Called after a given block is downloaded/failed
void CacheReplay( CReplay *pReplay );
int GetLastBlockToDownload() const { return m_iLastBlockToDownload; }
bool ShouldSyncBlocksWithServer() const; // Returns true if the number of blocks for this session is out of sync with m_iLastBlockToDownload
bool HasSessionInfoDownloader() const { return m_pSessionInfoDownloader != NULL; }
int GetGreatestConsecutiveBlockDownloaded() const { return m_iGreatestConsecutiveBlockDownloaded; }
void OnDownloadTimeout(); // Called if blocks were expected to appear or become downloadable but never showed up or updated.
void RefreshLastUpdateTime();
bool TimedOut() const { return m_bTimedOut; }
float GetLastUpdateTime() const { return m_flLastUpdateTime; }
bool AllReplaysReconstructed() const;
//
// CBaseRecordingSession
//
virtual void PopulateWithRecordingData( int nCurrentRecordingStartTick );
virtual bool ShouldDitchSession() const;
virtual void OnDelete();
uint64 GetServerSessionID() const { return m_uServerSessionID; }
private:
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
virtual void Think();
void UpdateGreatestConsecutiveBlockDownloaded();
void ShowDownloadFailedMessage( const CReplay *pReplay );
float m_flLastUpdateTime; // The last time a block was added or updated during session info download (see CSessionInfoDownloader::OnDownloadComplete())
int m_iLastBlockToDownload; // The last block we should download for the given session (in terms of reconstruction index)
int m_iGreatestConsecutiveBlockDownloaded; // The greatest consecutive block downloaded (in terms of reconstruction index)
CSessionInfoDownloader *m_pSessionInfoDownloader;
int m_nSessionInfoDownloadAttempts;
bool m_bTimedOut; // We can't test time-out state based on m_flLastUpdateTime - the session has to be put into the timed-out state explicitly by
// the session info downloader. "Time out" in this context means that nothing updated in the session info file for more than
// 90 seconds or whatever.
bool m_bAllBlocksDownloaded;
CUtlLinkedList< CReplay *, int > m_lstReplays; // List of replays that refer to this session
uint64 m_uServerSessionID; // A globally unique ID for the round
};
//----------------------------------------------------------------------------------------
inline CClientRecordingSession *CL_CastSession( IReplaySerializeable *pSession )
{
return static_cast< CClientRecordingSession * >( pSession );
}
//----------------------------------------------------------------------------------------
#endif // CL_RECORDINGSESSION_H
+112
View File
@@ -0,0 +1,112 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_recordingsessionblock.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CClientRecordingSessionBlock::CClientRecordingSessionBlock( IReplayContext *pContext )
: CBaseRecordingSessionBlock( pContext ),
m_uBytesDownloaded( 0 ),
m_nDownloadStatus( DOWNLOADSTATUS_WAITING ),
m_nHttpError( HTTP_ERROR_NONE ),
m_bDataInvalid( false ),
m_nDownloadAttempts( 0 )
{
}
bool CClientRecordingSessionBlock::NeedsUpdate() const
{
// TODO: Is this correct?
return m_nDownloadStatus != DOWNLOADSTATUS_DOWNLOADED;
}
bool CClientRecordingSessionBlock::ShouldDownloadNow() const
{
return m_nRemoteStatus == STATUS_READYFORDOWNLOAD &&
m_nDownloadStatus == DOWNLOADSTATUS_READYTODOWNLOAD;
}
bool CClientRecordingSessionBlock::DownloadedSuccessfully() const
{
return m_nDownloadStatus == DOWNLOADSTATUS_DOWNLOADED &&
m_nHttpError == HTTP_ERROR_NONE;
}
bool CClientRecordingSessionBlock::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_nDownloadStatus = (DownloadStatus_t)pIn->GetInt( "download_status" );
m_nHttpError = (HTTPError_t)pIn->GetInt( "download_error" );
m_uBytesDownloaded = (uint32)pIn->GetInt( "bytes_downloaded", 0 );
m_bDataInvalid = pIn->GetInt( "data_invalid", 0 ) != 0;
// Read relative path and construct full path - must have a filename
const char *pBlockFile = pIn->GetString( "filename" );
if ( !V_strlen( pBlockFile ) )
{
AssertMsg( 0, "No block filename!" );
return false;
}
V_snprintf( m_szFullFilename, sizeof( m_szFullFilename ), "%s%s", GetPath(), pBlockFile );
return true;
}
void CClientRecordingSessionBlock::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "download_status", (int)m_nDownloadStatus );
pOut->SetInt( "download_error", (int)m_nHttpError );
pOut->SetInt( "bytes_downloaded", m_uBytesDownloaded );
pOut->SetInt( "data_invalid", (int)m_bDataInvalid );
// Get just the filename and write that
pOut->SetString( "filename", V_UnqualifiedFileName( m_szFullFilename ) );
}
void CClientRecordingSessionBlock::OnDelete()
{
BaseClass::OnDelete();
// Remove the actual binary block file itself
g_pFullFileSystem->RemoveFile( m_szFullFilename );
}
bool CClientRecordingSessionBlock::AttemptToResetForDownload()
{
// Attempt to download again?
if ( ++m_nDownloadAttempts < 3 )
{
DBG( "Trying downloading again.\n" );
m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_READYTODOWNLOAD;
return true;
}
return false;
}
bool CClientRecordingSessionBlock::ValidateData( const void *pData, int nSize, unsigned char *pOutHash/*=NULL*/ ) const
{
unsigned char aDigest[16];
if ( !g_pEngine->MD5_HashBuffer( aDigest, (const unsigned char *)pData, nSize, NULL ) )
return false;
if ( pOutHash )
{
V_memcpy( pOutHash, aDigest, sizeof( aDigest ) );
}
return V_memcmp( aDigest, m_aHash, sizeof( m_aHash ) ) == 0;
}
//----------------------------------------------------------------------------------------
+80
View File
@@ -0,0 +1,80 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_RECORDINGSESSIONBLOCK_H
#define CL_RECORDINGSESSIONBLOCK_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionblock.h"
#include "engine/http.h"
//----------------------------------------------------------------------------------------
class CClientRecordingSessionBlock : public CBaseRecordingSessionBlock
{
typedef CBaseRecordingSessionBlock BaseClass;
public:
CClientRecordingSessionBlock( IReplayContext *pContext );
bool NeedsUpdate() const;
bool ShouldDownloadNow() const;
bool DownloadedSuccessfully() const;
int GetNumDownloadAttempts() const { return m_nDownloadAttempts; }
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
virtual void OnDelete();
// Resets the download status to be "ready for download" if the # of download attempts
// is under 3. Returns false if reset failed, otherwise true.
bool AttemptToResetForDownload();
// Checks data against the block's md5 digest
bool ValidateData( const void *pData, int nSize, unsigned char *pOutHash = NULL ) const;
enum DownloadStatus_t
{
DOWNLOADSTATUS_ABORTED, // Download was aborted for some reason
DOWNLOADSTATUS_ERROR, // Refer to m_nError for more detail
DOWNLOADSTATUS_WAITING, // Waiting for the file to be ready on the server
DOWNLOADSTATUS_READYTODOWNLOAD, // File is ready to be downloaded
DOWNLOADSTATUS_CONNECTING, // Connecting to file server
DOWNLOADSTATUS_DOWNLOADING, // Currently downloading
DOWNLOADSTATUS_DOWNLOADED, // Successfully downloaded file
MAX_DOWNLOADSTATUS
};
// Persistent:
DownloadStatus_t m_nDownloadStatus;
uint32 m_uBytesDownloaded;
bool m_bDataInvalid; // Hash didn't match data?
HTTPError_t m_nHttpError;
private:
// Non-persistent:
int m_nDownloadAttempts; // Should be modified via AttemptToResetForDownload()
};
//----------------------------------------------------------------------------------------
inline CClientRecordingSessionBlock *CL_CastBlock( IReplaySerializeable *pBlock )
{
return static_cast< CClientRecordingSessionBlock * >( pBlock );
}
inline const CClientRecordingSessionBlock *CL_CastBlock( const IReplaySerializeable *pBlock )
{
return static_cast< const CClientRecordingSessionBlock * >( pBlock );
}
//----------------------------------------------------------------------------------------
#endif // CL_RECORDINGSESSIONBLOCK_H
@@ -0,0 +1,40 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_recordingsessionblockmanager.h"
#include "cl_recordingsessionblock.h"
#include "cl_recordingsession.h"
#include "cl_replaycontext.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CClientRecordingSessionBlockManager::CClientRecordingSessionBlockManager( IReplayContext *pContext )
: CBaseRecordingSessionBlockManager( pContext )
{
}
CBaseRecordingSessionBlock *CClientRecordingSessionBlockManager::Create()
{
return new CClientRecordingSessionBlock( m_pContext );
}
IReplayContext *CClientRecordingSessionBlockManager::GetReplayContext() const
{
return g_pClientReplayContextInternal;
}
float CClientRecordingSessionBlockManager::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 0.5f;
}
void CClientRecordingSessionBlockManager::Think()
{
BaseClass::Think();
}
//----------------------------------------------------------------------------------------
+46
View File
@@ -0,0 +1,46 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef RECORDINGSESSIONBLOCKMANAGER_H
#define RECORDINGSESSIONBLOCKMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionblockmanager.h"
//----------------------------------------------------------------------------------------
class CClientRecordingSessionBlockManager : public CBaseRecordingSessionBlockManager
{
typedef CBaseRecordingSessionBlockManager BaseClass;
public:
CClientRecordingSessionBlockManager( IReplayContext *pContext );
//
// CGenericPersistentManager
//
virtual CBaseRecordingSessionBlock *Create();
private:
//
// CGenericPersistentManager
//
virtual IReplayContext *GetReplayContext() const;
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
virtual void Think();
virtual bool ShouldLoadBlocks() const { return false; }
};
//----------------------------------------------------------------------------------------
#endif // RECORDINGSESSIONBLOCKMANAGER_H
+309
View File
@@ -0,0 +1,309 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_recordingsessionmanager.h"
#include "replaysystem.h"
#include "cl_replaymanager.h"
#include "cl_recordingsession.h"
#include "cl_sessionblockdownloader.h"
#include "vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define CLIENTRECORDINGSESSIONMANAGER_VERSION 0
//----------------------------------------------------------------------------------------
CClientRecordingSessionManager::CClientRecordingSessionManager( IReplayContext *pContext )
: CBaseRecordingSessionManager( pContext ),
m_nNumSessionBlockDownloaders( 0 ),
m_flNextBlockUpdateTime( 0.0f ),
m_flNextPossibleDownloadTime( 0.0f )
{
}
CClientRecordingSessionManager::~CClientRecordingSessionManager()
{
}
bool CClientRecordingSessionManager::Init()
{
AddEventsForListen();
return BaseClass::Init();
}
void CClientRecordingSessionManager::CleanupUnneededBlocks()
{
Msg( "Cleaning up unneeded replay block data...\n" );
FOR_EACH_OBJ( this, i )
{
CClientRecordingSession *pCurSession = CL_CastSession( m_vecObjs[ i ] );
pCurSession->LoadBlocksForSession();
pCurSession->DeleteBlocks();
}
Msg( "Replay cleanup done.\n" );
}
void CClientRecordingSessionManager::AddEventsForListen()
{
g_pGameEventManager->AddListener( this, "replay_endrecord", false );
g_pGameEventManager->AddListener( this, "replay_sessioninfo", false );
g_pGameEventManager->AddListener( this, "player_death", false );
}
const char *CClientRecordingSessionManager::GetNewSessionName() const
{
return m_ServerRecordingState.m_strSessionName;
}
CBaseRecordingSession *CClientRecordingSessionManager::OnSessionStart( int nCurrentRecordingStartTick, const char *pSessionName )
{
return BaseClass::OnSessionStart( nCurrentRecordingStartTick, pSessionName );
}
void CClientRecordingSessionManager::OnSessionEnd()
{
if ( m_pRecordingSession )
{
// Update whether all blocks have been downloaded
AssertMsg( !m_pRecordingSession->m_bRecording, "This flag should have been cleared already! See CBaseRecordingSession::OnStopRecording()" );
CL_CastSession( m_pRecordingSession )->UpdateAllBlocksDownloaded();
}
BaseClass::OnSessionEnd();
m_ServerRecordingState.Clear();
}
void CClientRecordingSessionManager::FireGameEvent( IGameEvent *pEvent )
{
DBG( "CReplayHistoryManager::FireGameEvent()\n" );
if ( g_pEngineClient->IsDemoPlayingBack() )
return;
const char *pEventName = pEvent->GetName();
if ( !V_stricmp( "replay_sessioninfo", pEventName ) )
{
DBG( " replay_sessioninfo\n" );
bool bDisableReplayOnClient = false;
const CUtlString strSessionName = pEvent->GetString( "sn" );
if ( strSessionName.IsEmpty() )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_SessionInfo_BadSessionName" );
bDisableReplayOnClient = true;
}
const int nDumpInterval = pEvent->GetInt( "di", 0 );
if ( nDumpInterval < MIN_SERVER_DUMP_INTERVAL ||
nDumpInterval > MAX_SERVER_DUMP_INTERVAL )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_SessionInfo_BadDumpInterval" );
bDisableReplayOnClient = true;
}
const int nCurrentBlock = pEvent->GetInt( "cb", -1 );
if ( nCurrentBlock < 0 )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_SessionInfo_BadCurrentBlock" );
bDisableReplayOnClient = true;
}
const int nStartTick = pEvent->GetInt( "st", -1 );
if ( nStartTick < 0 )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "Replay_Err_SessionInfo_BadStartTick" );
bDisableReplayOnClient = true;
}
// Cache session state
m_ServerRecordingState.m_strSessionName = strSessionName;
m_ServerRecordingState.m_nDumpInterval = nDumpInterval;
m_ServerRecordingState.m_nCurrentBlock = nCurrentBlock + 1; // This will account for any different between when the server actually dumps a block and the client predicts a dump
m_ServerRecordingState.m_nStartTick = nStartTick;
// If the server's in a weird state, disable replay on the client so they don't
// create any replays that don't play, etc.
g_pClientReplayContextInternal->DisableReplayOnClient( bDisableReplayOnClient );
if ( bDisableReplayOnClient )
return;
OnSessionStart( nStartTick, strSessionName.Get() );
CL_GetReplayManager()->OnSessionStart();
// Update the current block based on the dump interval passed down from
// the server so the client stays in sync w/ the current block index.
m_flNextBlockUpdateTime = g_pEngine->GetHostTime() + nDumpInterval;
}
else if ( !V_stricmp( "replay_endrecord", pEventName ) )
{
DBG( " replay_stoprecord\n" );
// Clear pending replay URL cache, complete any pending replay
CL_GetReplayManager()->OnSessionEnd();
// Notify the session - it will mark itself as no longer recording.
if ( m_pRecordingSession )
{
m_pRecordingSession->OnStopRecording();
}
// Resets current session pointer
OnSessionEnd();
}
// When the player dies, we fill out the rest of the data here
else if ( !V_stricmp( "player_death", pEventName ) &&
pEvent->GetInt( "victim_entindex" ) == g_pEngineClient->GetPlayerSlot() + 1 &&
g_pClient->ShouldCompletePendingReplay( pEvent ) )
{
CL_GetReplayManager()->CompletePendingReplay();
}
}
int CClientRecordingSessionManager::GetVersion() const
{
return CLIENTRECORDINGSESSIONMANAGER_VERSION;
}
void CClientRecordingSessionManager::Think()
{
VPROF_BUDGET( "CClientRecordingSessionManager::Think", VPROF_BUDGETGROUP_REPLAY );
BaseClass::Think();
// Manage all session block downloads
DownloadThink();
if ( !g_pReplay->IsRecording() )
return;
if ( g_pEngineClient->IsDemoPlayingBack() )
return;
const float flHostTime = g_pEngine->GetHostTime();
if ( replay_debug.GetBool() )
{
extern ConVar replay_postdeathrecordtime;
g_pEngineClient->Con_NPrintf( 100, "Time until block dump: ~%f", m_flNextBlockUpdateTime - flHostTime );
g_pEngineClient->Con_NPrintf( 101, "Post-death record time: %f", replay_postdeathrecordtime.GetFloat() );
}
if ( m_flNextBlockUpdateTime <= flHostTime )
{
// Increment current block
++m_ServerRecordingState.m_nCurrentBlock;
// NOTE: Now the number of blocks in the recording session in progress should be
// different from the number of blocks in its list - so it should spawn a download
// thread to grab the session info and create the new block.
IF_REPLAY_DBG( Warning( "# session blocks updating: %i\n", m_ServerRecordingState.m_nCurrentBlock ) );
// Setup next think
m_flNextBlockUpdateTime = flHostTime + m_ServerRecordingState.m_nDumpInterval;
}
}
void CClientRecordingSessionManager::DownloadThink()
{
bool bKickedOffDownload = false;
const float flHostTime = g_pEngine->GetHostTime();
// For session in progress - check predicted # of blocks on the server based on current number
// of blocks in our list - if different, download the session info and create any outstanding
// blocks on the client.
bool bEnoughTimeHasPassed = flHostTime >= m_flNextPossibleDownloadTime;
if ( !bEnoughTimeHasPassed )
return;
// Go through all sessions to see if we need to create session block downloaders
FOR_EACH_OBJ( this, i )
{
CClientRecordingSession *pCurSession = CL_CastSession( m_vecObjs[ i ] );
// Already have a session block downloader? NOTE: The think manager calls its Think().
if ( pCurSession->HasSessionInfoDownloader() )
continue;
// If the # of blocks on the client is out of sync with the number of blocks we need
// to eventually download, sync with the server, i.e. download the session info and
// create blocks/sync block data as needed.
if ( pCurSession->ShouldSyncBlocksWithServer() )
{
pCurSession->SyncSessionBlocks();
bKickedOffDownload = true;
}
}
// Set next possible download time if we just kicked off a download
if ( bKickedOffDownload )
{
m_flNextPossibleDownloadTime = flHostTime + MAX( MIN_SERVER_DUMP_INTERVAL, CL_GetRecordingSessionManager()->m_ServerRecordingState.m_nDumpInterval );
}
}
CBaseRecordingSession *CClientRecordingSessionManager::Create()
{
return new CClientRecordingSession( m_pContext );
}
IReplayContext *CClientRecordingSessionManager::GetReplayContext() const
{
return g_pClientReplayContextInternal;
}
void CClientRecordingSessionManager::OnObjLoaded( CBaseRecordingSession *pSession )
{
// Make sure the session doesn't try to start downloading if it's done
CL_CastSession( pSession )->UpdateAllBlocksDownloaded();
}
void CClientRecordingSessionManager::OnReplayDeleted( CReplay *pReplay )
{
// Notify the session that a replay has been deleted, in case it needs to do any cleanup.
CClientRecordingSession *pSession = CL_CastSession( FindSession( pReplay->m_hSession ) );
if ( pSession )
{
pSession->OnReplayDeleted( pReplay );
}
// Get the # of replays that depend on the given session
int nNumDependentReplays = CL_GetReplayManager()->GetNumReplaysDependentOnSession( pReplay->m_hSession );
if ( nNumDependentReplays == 1 )
{
// Delete the session - remove the item from the manager itself, delete the
// .dem file, and any .dmx.
DeleteSession( pReplay->m_hSession, false );
}
}
void CClientRecordingSessionManager::OnReplaysLoaded()
{
// Cache replay pointers in sessions for quick access
FOR_EACH_REPLAY( i )
{
CReplay *pCurReplay = GET_REPLAY_AT( i );
CClientRecordingSession *pOwnerSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pCurReplay->m_hSession ) ); Assert( pOwnerSession );
if ( !pOwnerSession )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Load_BadOwnerSession" );
continue;
}
pOwnerSession->CacheReplay( pCurReplay );
}
}
//----------------------------------------------------------------------------------------
+94
View File
@@ -0,0 +1,94 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_RECORDINGSESSIONMANAGER_H
#define CL_RECORDINGSESSIONMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionmanager.h"
#include "igameevents.h"
#include "replaysystem.h"
#include "replay/shared_defs.h"
//----------------------------------------------------------------------------------------
//
// Manages and serializes all replay recording session data on the client
//
class CClientRecordingSessionManager : public CBaseRecordingSessionManager,
public IGameEventListener2
{
typedef CBaseRecordingSessionManager BaseClass;
public:
CClientRecordingSessionManager( IReplayContext *pContext );
~CClientRecordingSessionManager();
virtual bool Init();
void CleanupUnneededBlocks();
virtual CBaseRecordingSession *OnSessionStart( int nCurrentRecordingStartTick, const char *pSessionName );
virtual void OnSessionEnd();
void ClearServerRecordingState() { m_ServerRecordingState.Clear(); }
void OnReplayDeleted( CReplay *pReplay );
void OnReplaysLoaded();
//
// CGenericPersistentManager
//
virtual CBaseRecordingSession *Create();
struct ServerRecordingState_t
{
ServerRecordingState_t() { Clear(); }
void Clear() { m_strSessionName = ""; m_nDumpInterval = m_nCurrentBlock = m_nStartTick = 0; }
CUtlString m_strSessionName; // Name of current recording session
int m_nDumpInterval; // The interval at which the server is dumping partial replays
int m_nCurrentBlock; // Current session block being written to on the server (approximation - may be ahead but never behind)
int m_nStartTick; // The tick on the server when the session began - used to calculate an adjusted spawn tick on the client
bool IsValid()
{
return !m_strSessionName.IsEmpty() &&
m_nDumpInterval >= MIN_SERVER_DUMP_INTERVAL &&
m_nDumpInterval <= MAX_SERVER_DUMP_INTERVAL;
}
}
m_ServerRecordingState;
private:
//
// CGenericPersistentManager
//
virtual int GetVersion() const;
virtual void Think();
virtual IReplayContext *GetReplayContext() const;
virtual void OnObjLoaded( CBaseRecordingSession *pSession );
//
// IRecordingSessionManager
//
virtual const char *GetNewSessionName() const;
virtual void FireGameEvent( IGameEvent *pEvent );
void AddEventsForListen();
void DownloadThink();
float m_flNextBlockUpdateTime;
float m_flNextPossibleDownloadTime;
int m_nNumSessionBlockDownloaders; // TODO: Manage the number of session block downloaders
};
//----------------------------------------------------------------------------------------
#endif // CL_RECORDINGSESSIONMANAGER_H
+93
View File
@@ -0,0 +1,93 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_renderqueue.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CRenderQueue::CRenderQueue()
{
}
CRenderQueue::~CRenderQueue()
{
Clear();
}
void CRenderQueue::Add( ReplayHandle_t hReplay, int iPerformance )
{
RenderInfo_t *pEntry = new RenderInfo_t;
pEntry->m_hReplay = hReplay;
pEntry->m_iPerformance = iPerformance;
m_vecQueue.AddToTail( pEntry );
}
void CRenderQueue::Remove( ReplayHandle_t hReplay, int iPerformance )
{
RenderInfo_t *pEntry = Find( hReplay, iPerformance );
if ( pEntry )
{
m_vecQueue.FindAndRemove( pEntry );
delete pEntry;
}
}
void CRenderQueue::Clear()
{
m_vecQueue.PurgeAndDeleteElements();
}
int CRenderQueue::GetCount() const
{
return m_vecQueue.Count();
}
bool CRenderQueue::GetEntryData( int iIndex, ReplayHandle_t *pHandleOut, int *pPerformanceOut ) const
{
if ( iIndex < 0 || iIndex >= GetCount() )
{
AssertMsg( 0, "Request for replay render queue data is out of bounds!" );
Warning( "Request for replay render queue data is out of bounds!" );
return false;
}
if ( !pHandleOut || !pPerformanceOut )
{
AssertMsg( 0, "Bad parameters" );
return false;
}
const RenderInfo_t *pEntry = m_vecQueue[ iIndex ];
*pHandleOut = pEntry->m_hReplay;
*pPerformanceOut = pEntry->m_iPerformance;
return true;
}
bool CRenderQueue::IsInQueue( ReplayHandle_t hReplay, int iPerformance ) const
{
return Find( hReplay, iPerformance ) != NULL;
}
CRenderQueue::RenderInfo_t *CRenderQueue::Find( ReplayHandle_t hReplay, int iPerformance )
{
FOR_EACH_VEC( m_vecQueue, i )
{
RenderInfo_t *pEntry = m_vecQueue[ i ];
if ( pEntry->m_hReplay == hReplay && pEntry->m_iPerformance == iPerformance )
return pEntry;
}
return NULL;
}
const CRenderQueue::RenderInfo_t *CRenderQueue::Find( ReplayHandle_t hReplay, int iPerformance ) const
{
return const_cast< CRenderQueue * >( this )->Find( hReplay, iPerformance );
}
//----------------------------------------------------------------------------------------
+47
View File
@@ -0,0 +1,47 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAYRENDERQUEUE_H
#define REPLAYRENDERQUEUE_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplayrenderqueue.h"
#include "utlvector.h"
//----------------------------------------------------------------------------------------
class CRenderQueue : public IReplayRenderQueue
{
public:
CRenderQueue();
~CRenderQueue();
virtual void Add( ReplayHandle_t hReplay, int iPerformance );
virtual void Remove( ReplayHandle_t hReplay, int iPerformance );
virtual void Clear();
virtual int GetCount() const;
virtual bool GetEntryData( int iIndex, ReplayHandle_t *pHandleOut, int *pPerformanceOut ) const;
virtual bool IsInQueue( ReplayHandle_t hReplay, int iPerformance ) const;
private:
struct RenderInfo_t
{
ReplayHandle_t m_hReplay;
int m_iPerformance;
};
RenderInfo_t *Find( ReplayHandle_t hReplay, int iPerformance );
const RenderInfo_t *Find( ReplayHandle_t hReplay, int iPerformance ) const;
CUtlVector< RenderInfo_t * > m_vecQueue;
};
//----------------------------------------------------------------------------------------
#endif // REPLAYRENDERQUEUE_H
+421
View File
@@ -0,0 +1,421 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_replaycontext.h"
#include "replaysystem.h"
#include "replay/iclientreplay.h"
#include "replay/ireplaymovierenderer.h"
#include "replay/shared_defs.h"
#include "cl_replaymanager.h"
#include "replay_dbg.h"
#include "baserecordingsessionmanager.h"
#include "baserecordingsessionblockmanager.h"
#include "cl_replaymoviemanager.h"
#include "cl_screenshotmanager.h"
#include "cl_performancemanager.h"
#include "cl_sessionblockdownloader.h"
#include "cl_downloader.h"
#include "cl_recordingsession.h"
#include "cl_recordingsessionblock.h"
#include "cl_renderqueue.h"
#include "replay_reconstructor.h"
#include "globalvars_base.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CClientReplayContext::CClientReplayContext()
: m_pReplayManager( NULL ),
m_pScreenshotManager( NULL ),
m_pMovieRenderer( NULL ),
m_pMovieManager( NULL ),
m_pPerformanceManager( NULL ),
m_pPerformanceController( NULL ),
m_pTestDownloader( NULL ),
m_pRenderQueue( NULL ),
m_bClientSideReplayDisabled( false )
{
}
CClientReplayContext::~CClientReplayContext()
{
delete m_pSessionBlockDownloader;
delete m_pReplayManager;
delete m_pScreenshotManager;
delete m_pMovieManager;
delete m_pPerformanceManager;
delete m_pShared;
delete m_pTestDownloader;
}
bool CClientReplayContext::Init( CreateInterfaceFn fnFactory )
{
m_pShared = new CSharedReplayContext( this );
m_pShared->m_strSubDir = SUBDIR_CLIENT;
m_pShared->m_pRecordingSessionManager = new CClientRecordingSessionManager( this );
m_pShared->m_pRecordingSessionBlockManager = new CClientRecordingSessionBlockManager( this );
m_pShared->m_pErrorSystem = new CErrorSystem( this );
if ( !m_pShared->Init( fnFactory ) )
return false;
m_pPerformanceManager = new CReplayPerformanceManager();
m_pPerformanceManager->Init();
m_pReplayManager = new CReplayManager();
m_pReplayManager->Init( fnFactory );
m_pScreenshotManager = new CScreenshotManager();
m_pScreenshotManager->Init();
m_pMovieManager = new CReplayMovieManager();
m_pMovieManager->Init();
m_pRenderQueue = new CRenderQueue();
if ( !m_pRenderQueue )
return false;
m_pSessionBlockDownloader = new CSessionBlockDownloader();
if ( !m_pSessionBlockDownloader )
return false;
m_pPerformanceController = new CPerformanceController();
// Cleanup any unneeded block data from disk - cleanup is done on the fly, but this will clean up
// blocks from the olden days, when block data was not cleaned up properly.
CleanupUnneededBlocks();
return true;
}
void CClientReplayContext::Shutdown()
{
// NOTE: Must come first, as any existing downloads are aborted and may cause status
// changes in replays, etc, which will need to be saved in CReplayManager::Shutdown(), etc.
m_pSessionBlockDownloader->Shutdown();
m_pShared->Shutdown();
m_pReplayManager->Shutdown();
m_pMovieManager->Shutdown();
}
void CClientReplayContext::DebugThink()
{
if ( !replay_debug.GetBool() )
return;
int iLine = 15;
// Recording session in progress
CClientRecordingSession *pRecordingSession = CL_GetRecordingSessionInProgress();
if ( pRecordingSession )
{
g_pEngineClient->Con_NPrintf( iLine++, "SESSION IN PROGRESS:" );
g_pEngineClient->Con_NPrintf( iLine++, " BLOCKS: %i", pRecordingSession->GetNumBlocks() );
g_pEngineClient->Con_NPrintf( iLine++, " NAME: %s", pRecordingSession->m_strName.Get() );
g_pEngineClient->Con_NPrintf( iLine++, " URL: %s", pRecordingSession->m_strBaseDownloadURL.Get() );
g_pEngineClient->Con_NPrintf( iLine++, " LAST CONSECUTIVE BLOCK DOWNLOADED: %i", pRecordingSession->GetGreatestConsecutiveBlockDownloaded() );
g_pEngineClient->Con_NPrintf( iLine++, " LAST BLOCK TO DOWNLOAD: %i", pRecordingSession->GetLastBlockToDownload() );
}
else
{
g_pEngineClient->Con_NPrintf( iLine++, "NO SESSION IN PROGRESS" );
}
iLine++;
// Server state
CClientRecordingSessionManager::ServerRecordingState_t *pServerState = &CL_GetRecordingSessionManager()->m_ServerRecordingState;
g_pEngineClient->Con_NPrintf( iLine++, "SERVER STATE:" );
g_pEngineClient->Con_NPrintf( iLine++, " NAME: %s\n", pServerState->m_strSessionName.Get() );
g_pEngineClient->Con_NPrintf( iLine++, " DUMP INTERVAL: %i\n", pServerState->m_nDumpInterval );
g_pEngineClient->Con_NPrintf( iLine++, " CURRENT BLOCK: %i\n", pServerState->m_nCurrentBlock );
}
void CClientReplayContext::Think()
{
DebugThink();
if ( m_pTestDownloader )
{
m_pTestDownloader->Think();
if ( m_pTestDownloader->IsDone() )
{
delete m_pTestDownloader;
m_pTestDownloader = NULL;
}
}
if ( !g_pReplay->IsReplayEnabled() )
return;
m_pShared->Think();
}
CReplay *CClientReplayContext::GetReplay( ReplayHandle_t hReplay )
{
return m_pReplayManager->GetReplay( hReplay );
}
IReplayManager *CClientReplayContext::GetReplayManager()
{
return m_pReplayManager;
}
IReplayScreenshotManager *CClientReplayContext::GetScreenshotManager()
{
return m_pScreenshotManager;
}
IReplayPerformanceManager *CClientReplayContext::GetPerformanceManager()
{
return m_pPerformanceManager;
}
IReplayPerformanceController *CClientReplayContext::GetPerformanceController()
{
return m_pPerformanceController;
}
IReplayRenderQueue *CClientReplayContext::GetRenderQueue()
{
return m_pRenderQueue;
}
void CClientReplayContext::SetMovieRenderer( IReplayMovieRenderer *pMovieRenderer )
{
m_pMovieRenderer = pMovieRenderer;
}
IReplayMovieRenderer *CClientReplayContext::GetMovieRenderer()
{
return m_pMovieRenderer;
}
IReplayMovieManager *CClientReplayContext::GetMovieManager()
{
return m_pMovieManager;
}
void CClientReplayContext::TestDownloader( const char *pURL )
{
// Don't overwrite existing test
if ( m_pTestDownloader )
return;
// Download the file
m_pTestDownloader = new CHttpDownloader();
m_pTestDownloader->BeginDownload( pURL, NULL );
}
void CClientReplayContext::OnSignonStateFull()
{
// Notify the demo player that we've reached full signon state
if ( g_pEngineClient->IsPlayingReplayDemo() )
{
g_pReplayDemoPlayer->OnSignonStateFull();
}
// Play a performance? This will play a performance from the beginning, if we're loading
// one (ie the 'watch' button in the details panel of the replay browser), or will continue
// playback if the user rewound while watching or editing a performance.
CL_GetPerformanceController()->OnSignonStateFull();
// If we're rendering, display the viewport
if ( CL_GetMovieManager()->IsRendering() )
{
extern IClientReplay *g_pClient;
g_pClient->OnRenderStart();
// Prepare audio system for recording.
g_pEngineClient->InitSoundRecord();
// Init renderer
IReplayMovie *pMovie = CL_GetMovieManager()->GetPendingMovie();
if ( !m_pMovieRenderer->SetupRenderer( CL_GetMovieManager()->GetRenderMovieSettings(), pMovie ) )
{
Warning( "Render failed!\n" );
CL_GetMovieManager()->CancelRender();
}
}
// If we're not rendering and are playing back a replay, initialize the performance editor -
// won't actually show until the user presses space, if they do at all.
else if ( g_pEngineClient->IsPlayingReplayDemo() )
{
const CReplay *pReplay = g_pReplayDemoPlayer->GetCurrentReplay();
if ( pReplay )
{
g_pClient->InitPerformanceEditor( pReplay->GetHandle() );
}
else
{
AssertMsg( 0, "Replay should exist here!" );
Warning( "No current replay in demo player!\n" );
}
}
}
void CClientReplayContext::OnClientSideDisconnect()
{
if ( !g_pEngine->IsSupportedModAndPlatform() )
return;
// Reset replay_recording or we'll continue to think we're recording
extern ConVar replay_recording;
replay_recording.SetValue( 0 );
if ( !g_pEngineClient->IsPlayingReplayDemo() )
{
// Saves dangling replay, if there is one, clears out everything
// NOTE: We need to let the replay manager deal before we end the session, otherwise the
// state of the session will be cleared.
m_pReplayManager->OnClientSideDisconnect();
// Mark the session as no longer recording.
CClientRecordingSession *pSession = CL_GetRecordingSessionInProgress();
if ( pSession )
{
pSession->OnStopRecording();
}
// Sets recording flag to false in session in progress, clears session in progress,
// and clears server state
CL_GetRecordingSessionManager()->OnSessionEnd();
}
}
void CClientReplayContext::PlayReplay( ReplayHandle_t hReplay, int iPerformance, bool bPlaySound )
{
CReplay *pReplay = m_pReplayManager->GetReplay( hReplay );
if ( !pReplay )
return;
if ( !ReconstructReplayIfNecessary( pReplay ) )
{
Replay_MsgBox( iPerformance < 0 ? "#Replay_Err_User_FailedToPlayReplay" : "#Replay_Err_User_FailedToPlayTake" );
return;
}
// Play a sound?
if ( bPlaySound )
{
g_pClient->PlaySound( iPerformance >= 0 ? "replay\\playperformance.wav" : "replay\\playoriginalreplay.wav" );
}
// Play the replay!
g_pReplayDemoPlayer->PlayReplay( hReplay, iPerformance );
}
bool CClientReplayContext::ReconstructReplayIfNecessary( CReplay *pReplay )
{
// If reconstruction hasn't happened yet, try to reconstruct
extern ConVar replay_forcereconstruct;
if ( !pReplay->HasReconstructedReplay() || replay_forcereconstruct.GetBool() )
{
if ( !Replay_Reconstruct( pReplay ) )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Reconstruction_Fail" );
return false;
}
}
return true;
}
void CClientReplayContext::OnPlayerSpawn()
{
DBG( "OnPlayerSpawn()\n" );
m_pReplayManager->AttemptToSetupNewReplay();
}
void CClientReplayContext::OnPlayerClassChanged()
{
DBG( "OnPlayerClassChanged()\n" );
m_pReplayManager->CompletePendingReplay();
}
void CClientReplayContext::GetPlaybackTimes( float &flOutTime, float &flOutLength, const CReplay *pReplay, const CReplayPerformance *pPerformance )
{
flOutTime = 0.0f;
flOutLength = 0.0f;
// Get server start record tick
const int nServerRecordStartTick = CL_GetRecordingSessionManager()->GetServerStartTickForSession( pReplay->m_hSession );
// Don't let it be -1. Take performance in tick into account.
int nStartTick = MAX( 0, pReplay->m_nSpawnTick );
if ( pPerformance && pPerformance->m_nTickIn >= 0 )
{
nStartTick = pPerformance->m_nTickIn;
}
// Calculate length
const int nReplayEndTick = pReplay->m_nSpawnTick + g_pEngine->TimeToTicks( pReplay->m_flLength );
const int nEndTick = ( pPerformance && pPerformance->m_nTickOut > 0 ) ? pPerformance->m_nTickOut : nReplayEndTick;
flOutLength = pPerformance ? g_pEngine->TicksToTime( nEndTick - nStartTick ) : pReplay->m_flLength;
// Calculate current time
const int nCurTick = MAX( g_pEngineClient->GetClientGlobalVars()->tickcount - nStartTick - nServerRecordStartTick, 0 );
flOutTime = MIN( g_pEngine->TicksToTime( nCurTick ), flOutLength );
}
uint64 CClientReplayContext::GetServerSessionId( ReplayHandle_t hReplay )
{
CReplay *pReplay = GetReplay( hReplay );
if ( !pReplay )
return 0;
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pReplay->m_hSession ) );
if ( !pSession )
return 0;
return pSession->GetServerSessionID();
}
void CClientReplayContext::CleanupUnneededBlocks()
{
CL_GetRecordingSessionManager()->CleanupUnneededBlocks();
}
void CClientReplayContext::ReportErrorsToUser( wchar_t *pErrorText )
{
// Display a message now
// Replay_MsgBox( pErrorText );
if ( !pErrorText || pErrorText[0] == L'\0' )
return;
const int nErrorLen = wcslen( pErrorText );
static char s_szError[1024];
wcstombs( s_szError, pErrorText, MIN( 1024, nErrorLen ) );
Warning( "Replay error system: %s\n", s_szError );
}
void CClientReplayContext::DisableReplayOnClient( bool bDisable )
{
if ( m_bClientSideReplayDisabled == bDisable )
return;
m_bClientSideReplayDisabled = bDisable;
// Display a message to the user
Replay_HudMsg( bDisable ? "#Replay_ClientSideDisabled" : "#Replay_ClientSideEnabled", NULL, true );
}
//----------------------------------------------------------------------------------------
CClientRecordingSessionManager *CL_GetRecordingSessionManager()
{
return static_cast< CClientRecordingSessionManager * >( g_pClientReplayContextInternal->GetRecordingSessionManager() );
}
CClientRecordingSession *CL_GetRecordingSessionInProgress()
{
return CL_CastSession( CL_GetRecordingSessionManager()->GetRecordingSessionInProgress() );
}
//----------------------------------------------------------------------------------------
+189
View File
@@ -0,0 +1,189 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_REPLAYCONTEXT_H
#define CL_REPLAYCONTEXT_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "shared_replaycontext.h"
#include "replay/iclientreplaycontext.h"
#include "igameevents.h"
#include "cl_recordingsessionmanager.h"
#include "cl_replaymoviemanager.h"
#include "cl_recordingsessionblockmanager.h"
#include "cl_performancemanager.h"
#include "cl_performancecontroller.h"
#include "errorsystem.h"
//----------------------------------------------------------------------------------------
class IReplayMovieRenderer;
class CScreenshotManager;
class CReplayManager;
class CReplayMovieManager;
class CClientRecordingSessionManager;
class CReplayPerformanceManager;
class CHttpDownloader;
class CSessionBlockDownloader;
class CClientRecordingSession;
class CPerformanceController;
class CRenderQueue;
//----------------------------------------------------------------------------------------
class CClientReplayContext : public IClientReplayContext,
public IErrorReporter
{
public:
LINK_TO_SHARED_REPLAYCONTEXT_IMP();
CClientReplayContext();
~CClientReplayContext();
virtual bool Init( CreateInterfaceFn fnFactory );
virtual void Shutdown();
virtual void Think(); // Called by engine
bool ReconstructReplayIfNecessary( CReplay *pReplay );
void DisableReplayOnClient( bool bDisable );
bool IsClientSideReplayDisabled() const { return m_bClientSideReplayDisabled; }
//
// IClientReplayContext
//
virtual CReplay *GetReplay( ReplayHandle_t hReplay );
virtual IReplayManager *GetReplayManager();
virtual IReplayMovieRenderer *GetMovieRenderer();
virtual IReplayMovieManager *GetMovieManager();
virtual IReplayScreenshotManager *GetScreenshotManager();
virtual IReplayPerformanceManager *GetPerformanceManager();
virtual IReplayPerformanceController *GetPerformanceController();
virtual IReplayRenderQueue *GetRenderQueue();
virtual void SetMovieRenderer( IReplayMovieRenderer *pMovieRenderer );
virtual void OnSignonStateFull();
virtual void OnClientSideDisconnect();
virtual void PlayReplay( ReplayHandle_t hReplay, int iPerformance, bool bPlaySound );
virtual void OnPlayerSpawn();
virtual void OnPlayerClassChanged();
virtual void GetPlaybackTimes( float &flOutTime, float &flOutLength, const CReplay *pReplay, const CReplayPerformance *pPerformance );
virtual uint64 GetServerSessionId( ReplayHandle_t hReplay );
virtual void CleanupUnneededBlocks();
//
// IErrorReporter
//
virtual void ReportErrorsToUser( wchar_t *pErrorText );
void TestDownloader( const char *pURL );
CReplayManager *m_pReplayManager;
CScreenshotManager *m_pScreenshotManager;
IReplayMovieRenderer *m_pMovieRenderer;
CReplayMovieManager *m_pMovieManager;
CReplayPerformanceManager *m_pPerformanceManager;
CPerformanceController *m_pPerformanceController;
CSessionBlockDownloader *m_pSessionBlockDownloader;
CRenderQueue *m_pRenderQueue;
CHttpDownloader *m_pTestDownloader;
private:
void DebugThink();
void ReplayThink();
bool m_bClientSideReplayDisabled;
};
//----------------------------------------------------------------------------------------
extern CClientReplayContext *g_pClientReplayContextInternal;
//----------------------------------------------------------------------------------------
//
// Helpers
//
inline const char *CL_GetBasePath()
{
return g_pClientReplayContextInternal->m_pShared->m_strBasePath;
}
inline const char *CL_GetRelativeBasePath()
{
return g_pClientReplayContextInternal->m_pShared->m_strRelativeBasePath.Get();
}
inline CReplayManager *CL_GetReplayManager()
{
return g_pClientReplayContextInternal->m_pReplayManager;
}
inline CClientRecordingSessionBlockManager *CL_GetRecordingSessionBlockManager()
{
return static_cast< CClientRecordingSessionBlockManager * >( g_pClientReplayContextInternal->GetRecordingSessionBlockManager() );
}
inline CScreenshotManager *CL_GetScreenshotManager()
{
return g_pClientReplayContextInternal->m_pScreenshotManager;
}
inline IReplayMovieRenderer *CL_GetMovieRenderer()
{
return g_pClientReplayContextInternal->m_pMovieRenderer;
}
inline CReplayMovieManager *CL_GetMovieManager()
{
return g_pClientReplayContextInternal->m_pMovieManager;
}
inline const char *CL_GetReplayBaseDir()
{
return g_pClientReplayContextInternal->m_pShared->m_strBasePath;
}
inline CErrorSystem *CL_GetErrorSystem()
{
return g_pClientReplayContextInternal->m_pShared->m_pErrorSystem;
}
inline CSessionBlockDownloader *CL_GetSessionBlockDownloader()
{
return g_pClientReplayContextInternal->m_pSessionBlockDownloader;
}
inline CReplayPerformanceManager *CL_GetPerformanceManager()
{
return g_pClientReplayContextInternal->m_pPerformanceManager;
}
inline CPerformanceController *CL_GetPerformanceController()
{
return g_pClientReplayContextInternal->m_pPerformanceController;
}
inline IThreadPool *CL_GetThreadPool()
{
return g_pClientReplayContextInternal->m_pShared->m_pThreadPool;
}
inline CRenderQueue *CL_GetRenderQueue()
{
return g_pClientReplayContextInternal->m_pRenderQueue;
}
//----------------------------------------------------------------------------------------
CClientRecordingSessionManager *CL_GetRecordingSessionManager();
CClientRecordingSession *CL_GetRecordingSessionInProgress();
//----------------------------------------------------------------------------------------
#endif // CL_REPLAYCONTEXT_H
+665
View File
@@ -0,0 +1,665 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_replaymanager.h"
#include "replay/ienginereplay.h"
#include "replay/iclientreplay.h"
#include "replay/ireplaymoviemanager.h"
#include "replay/ireplayfactory.h"
#include "replay/replayutils.h"
#include "replay/ireplaymovierenderer.h"
#include "replay/shared_defs.h"
#include "baserecordingsession.h"
#include "cl_screenshotmanager.h"
#include "cl_recordingsession.h"
#include "cl_recordingsessionblock.h"
#include "replaysystem.h"
#include "cl_replaymoviemanager.h"
#include "replay_dbg.h"
#include "inetchannel.h"
#include "cl_replaycontext.h"
#include <time.h>
#include "vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineClientReplay *g_pEngineClient;
extern ConVar replay_postdeathrecordtime;
//----------------------------------------------------------------------------------------
#define REPLAY_INDEX_VERSION 0
//----------------------------------------------------------------------------------------
CReplayManager::CReplayManager()
: m_pPendingReplay( NULL ),
m_pReplayLastLife( NULL ),
m_pReplayThisLife( NULL ),
m_flPlayerSpawnCreateReplayFailTime( 0.0f )
{
}
CReplayManager::~CReplayManager()
{
}
bool CReplayManager::Init( CreateInterfaceFn fnCreateFactory )
{
// Get out if the user is running an unsupported mod or platform
if ( !g_pEngine->IsSupportedModAndPlatform() )
return false;
// Clear anything already loaded (since we reuse the same instance)
Clear();
// Register replay factory
m_pReplayFactory = GetReplayFactory( fnCreateFactory ); Assert( m_pReplayFactory );
// Load all replays from disk
if ( !BaseClass::Init() )
{
Warning( "Failed to load replay history!\n" );
}
// Session manager init'd by this point - go through and link up replays to sessions
CL_GetRecordingSessionManager()->OnReplaysLoaded();
return true;
}
void CReplayManager::Shutdown()
{
// Get out if the user is running an unsupported mod or platform
if ( !g_pEngine->IsSupportedModAndPlatform() )
return;
// Make sure we aren't waiting to write
BaseClass::Shutdown(); // Saves
}
IReplayFactory *CReplayManager::GetReplayFactory( CreateInterfaceFn fnCreateFactory )
{
return (IReplayFactory *)fnCreateFactory( INTERFACE_VERSION_REPLAY_FACTORY, NULL );
}
void CReplayManager::OnSessionStart()
{
// The pending replay doesn't exist yet at this point as far as I've seen, since the "replay_sessioninfo"
// event comes down a frame or more after the "replay_recording" replicated cvar is set to 1, which is
// what triggers AttemptToSetupNewReplay().
if ( !m_pPendingReplay )
{
AttemptToSetupNewReplay();
}
if ( m_pPendingReplay )
{
// Link up the pending replay to the recording session in progress
if ( m_pPendingReplay->m_hSession == REPLAY_HANDLE_INVALID )
{
ReplayHandle_t hSessionInProgress = CL_GetRecordingSessionManager()->GetRecordingSessionInProgress()->GetHandle();
m_pPendingReplay->m_hSession = hSessionInProgress;
}
// Make sure the spawn tick has the proper server start tick subtracted out
if ( m_pPendingReplay->m_nSpawnTick < 0 )
{
const int nServerStartTick = CL_GetRecordingSessionManager()->m_ServerRecordingState.m_nStartTick; Assert( nServerStartTick > 0 );
m_pPendingReplay->m_nSpawnTick = MAX( 0, -m_pPendingReplay->m_nSpawnTick - nServerStartTick );
}
}
}
void CReplayManager::OnSessionEnd()
{
// Complete the pending replay, if there is one
CompletePendingReplay();
}
const char *CReplayManager::GetRelativeIndexPath() const
{
return Replay_va( "%s%c", SUBDIR_REPLAYS, CORRECT_PATH_SEPARATOR );
}
CReplay *CReplayManager::Create()
{
return m_pReplayFactory->Create();
}
IReplayContext *CReplayManager::GetReplayContext() const
{
return g_pClientReplayContextInternal;
}
bool CReplayManager::ShouldLoadObj( const CReplay *pReplay ) const
{
return pReplay && pReplay->m_bComplete;
}
void CReplayManager::OnObjLoaded( CReplay *pReplay )
{
if ( !pReplay )
return;
pReplay->m_bSavedDuringThisSession = false;
}
int CReplayManager::GetVersion() const
{
return REPLAY_INDEX_VERSION;
}
void CReplayManager::ClearPendingReplay()
{
m_pPendingReplay = NULL;
}
void CReplayManager::SanityCheckReplay( CReplay *pReplay )
{
if ( !pReplay )
return;
// DEBUG: Make sure this replay does not already exist in the list
FOR_EACH_VEC( Replays(), i )
{
if ( Replays()[ i ]->GetHandle() == pReplay->GetHandle() )
{
IF_REPLAY_DBG( Warning( "Replay %i already found in history!\n", pReplay->GetHandle() ) );
}
}
if ( pReplay->m_nDeathTick < pReplay->m_nSpawnTick )
{
IF_REPLAY_DBG( Warning( "Spawn tick (%i) is greater than death tick (%i)!\n", pReplay->m_nSpawnTick, pReplay->m_nDeathTick ) );
}
}
void CReplayManager::SaveDanglingReplay()
{
if ( !m_pReplayThisLife )
return;
if ( m_pReplayThisLife->m_bRequestedByUser )
{
CompletePendingReplay();
FlagReplayForFlush( m_pReplayThisLife, false );
}
}
void CReplayManager::FreeLifeIfNotSaved( CReplay *&pReplay )
{
if ( pReplay )
{
if ( !pReplay->m_bSaved && !IsDirty( pReplay ) )
{
CleanupReplay( pReplay );
}
else
{
// If it's been saved, don't free the memory, just clear the pointer
pReplay = NULL;
}
}
}
void CReplayManager::CleanupReplay( CReplay *&pReplay )
{
if ( !pReplay )
return;
// Get rid of a replay that was never committed:
// Remove screenshots taken
CL_GetScreenshotManager()->DeleteScreenshotsForReplay( pReplay );
// Free
delete pReplay;
pReplay = NULL;
}
void CReplayManager::OnReplayRecordingCvarChanged()
{
DBG( "OnReplayRecordingCvarChanged()\n" );
// If set to 0, get out - we don't care
extern ConVar replay_recording;
if ( !replay_recording.GetBool() )
{
DBG( " replay_recording is false...aborting\n" );
return;
}
// If OnPlayerSpawn() hasn't failed to create the scratch replay, get out
if ( m_flPlayerSpawnCreateReplayFailTime == 0.0f )
{
DBG( " m_flPlayerSpawnCreateReplayFailTime == 0.0f...aborting.\n" );
return;
}
DBG( " Calling AttemptToSetupNewReplay()\n" );
// Try to create & setup again
AttemptToSetupNewReplay();
// Reset
m_flPlayerSpawnCreateReplayFailTime = 0.0f;
}
void CReplayManager::OnClientSideDisconnect()
{
SaveDanglingReplay();
ClearPendingReplay();
FreeLifeIfNotSaved( m_pReplayLastLife );
FreeLifeIfNotSaved( m_pReplayThisLife );
m_flPlayerSpawnCreateReplayFailTime = 0.0f;
}
void CReplayManager::CommitPendingReplayAndBeginDownload()
{
// Update the last session block we should download
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( m_pReplayThisLife->m_hSession ) );
const int iPostDeathBlockIndex = pSession->UpdateLastBlockToDownload();
// Update the # of blocks required to reconstruct the replay
m_pReplayThisLife->m_iMaxSessionBlockRequired = iPostDeathBlockIndex;
Commit( m_pReplayThisLife );
}
void CReplayManager::CompletePendingReplay()
{
// Get out if no pending replay
if ( !m_pPendingReplay )
return;
// Get session associated w/ the replay
CBaseRecordingSession *pSession = CL_GetRecordingSessionManager()->FindSession( m_pPendingReplay->m_hSession );
// Sometimes the session isn't valid here, like when we're first joining a server
if ( !pSession )
return;
Assert( pSession->m_nServerStartRecordTick >= 0 );
// Cache death tick
m_pPendingReplay->m_nDeathTick = g_pEngineClient->GetLastServerTickTime() - pSession->m_nServerStartRecordTick;
SanityCheckReplay( m_pPendingReplay );
// Calc replay length
m_pPendingReplay->m_flLength = g_pEngine->TicksToTime(
m_pPendingReplay->m_nDeathTick -
m_pPendingReplay->m_nSpawnTick +
g_pEngine->TimeToTicks( replay_postdeathrecordtime.GetFloat() )
);
// Cache player slot so we can start playback of replays from recorder player's perspective
m_pPendingReplay->m_nPlayerSlot = g_pEngineClient->GetPlayerSlot() + 1;
// Setup status
m_pPendingReplay->m_nStatus = CReplay::REPLAYSTATUS_DOWNLOADPHASE;
// The replay is now "complete," ie has all the data needed
m_pPendingReplay->m_bComplete = true;
// Let derived classes do whatever it wants
m_pPendingReplay->OnComplete();
// If the replay was requested by the user already, update the # of blocks we should download & commit the replay
if ( m_pPendingReplay->m_bRequestedByUser )
{
#ifdef DBGFLAG_ASSERT
Assert( m_pReplayThisLife->m_bComplete );
#endif
CommitPendingReplayAndBeginDownload();
}
// Before we copy the pointer to "this life," end recording so the replay can do any cleanup (eg listening for game events)
m_pPendingReplay->OnEndRecording();
// Cache off scratch replay to "this life"
m_pReplayThisLife = m_pPendingReplay;
ClearPendingReplay();
}
bool CReplayManager::Commit( CReplay *pNewReplay )
{
if ( !g_pClientReplayContextInternal->IsInitialized() || !pNewReplay )
return false;
SanityCheckReplay( pNewReplay );
// NOTE: Marks index as dirty, as well as pNewReplay
Add( pNewReplay );
// Save now
Save();
return true;
}
//
// IReplayManager implementation
//
CReplay *CReplayManager::GetReplay( ReplayHandle_t hReplay )
{
if ( m_pReplayThisLife && m_pReplayThisLife->GetHandle() == hReplay )
return m_pReplayThisLife;
return Find( hReplay );
}
void CReplayManager::DeleteReplay( ReplayHandle_t hReplay, bool bNotifyUI )
{
CReplay *pReplay = GetReplay( hReplay ); Assert( pReplay );
// The session manager will delete the .dem, the session .dmx and remove the session
// item itself if this is the last replay associated with it.
CL_GetRecordingSessionManager()->OnReplayDeleted( pReplay );
// Notify the replay browser if necessary
if ( bNotifyUI )
{
extern IClientReplay *g_pClient;
g_pClient->OnDeleteReplay( hReplay );
}
// Remove it
Remove( pReplay );
// If the replay deleted was just saved and we haven't respawned yet,
// we need to clear out some stuff so GetReplay() doesn't crash.
if ( m_pReplayThisLife == pReplay )
{
m_pReplayThisLife = NULL;
m_pPendingReplay = NULL;
}
if ( m_pReplayLastLife == pReplay )
{
m_pReplayLastLife = NULL;
}
}
void CReplayManager::FlagReplayForFlush( CReplay *pReplay, bool bForceImmediate )
{
FlagForFlush( pReplay, bForceImmediate );
}
int CReplayManager::GetUnrenderedReplayCount()
{
int nCount = 0;
FOR_EACH_VEC( m_vecObjs, i )
{
CReplay *pCurReplay = m_vecObjs[ i ];
if ( !pCurReplay->m_bRendered &&
pCurReplay->m_nStatus == CReplay::REPLAYSTATUS_READYTOCONVERT )
{
++nCount;
}
}
return nCount;
}
void CReplayManager::InitReplay( CReplay *pReplay )
{
// Setup record time right now
pReplay->m_RecordTime.InitDateAndTimeToNow();
// Store start time
pReplay->m_flStartTime = g_pEngine->GetHostTime();
// Get map name (w/o the path)
V_FileBase( g_pEngineClient->GetLevelName(), m_pPendingReplay->m_szMapName, sizeof( m_pPendingReplay->m_szMapName ) );
// Give the replay a default name
pReplay->AutoNameTitleIfEmpty();
}
CReplay *CReplayManager::CreatePendingReplay()
{
Assert( m_pPendingReplay == NULL );
m_pPendingReplay = CreateAndGenerateHandle();
// If we've already begun recording, link to the session now, otherwise link once
// we start recording.
CBaseRecordingSession *pSessionInProgress = CL_GetRecordingSessionInProgress();
if ( pSessionInProgress )
{
m_pPendingReplay->m_hSession = pSessionInProgress->GetHandle();
}
InitReplay( m_pPendingReplay );
// Setup replay handle for screenshots
CL_GetScreenshotManager()->SetScreenshotReplay( m_pPendingReplay->GetHandle() );
return m_pPendingReplay;
}
void CReplayManager::AttemptToSetupNewReplay()
{
DBG( "AttemptToSetupNewReplay()\n" );
if ( !g_pReplay->IsRecording() || g_pEngineClient->IsPlayingReplayDemo() )
{
DBG( " Aborting...not recording, or playing back replay.\n" );
m_flPlayerSpawnCreateReplayFailTime = g_pEngine->GetHostTime();
return;
}
// Create the replay if necessary - we only do setup if we're creating
// a new replay, because on a full update this function may be called
// even though we're not actually spawning.
if ( !m_pPendingReplay )
{
DBG( " Creating new replay...\n" );
// If there is a "last life" replay that was not saved already, delete it
FreeLifeIfNotSaved( m_pReplayLastLife );
// Cache last life
m_pReplayLastLife = m_pReplayThisLife;
// Create the scratch replay (sets m_pPendingReplay and returns it)
CReplay *pPendingReplay = CreatePendingReplay();
SanityCheckReplay( pPendingReplay );
// "This life" is the scratch replay
m_pReplayThisLife = pPendingReplay;
// Setup spawn tick
const int nServerStartTick = CL_GetRecordingSessionManager()->m_ServerRecordingState.m_nStartTick;
pPendingReplay->m_nSpawnTick = g_pEngineClient->GetLastServerTickTime() - nServerStartTick;
if ( nServerStartTick == 0 )
{
// Didn't receive the replay_sessioninfo event yet - make spawn tick negative so when the
// event IS received, we can detect that the server start tick still needs to be subtracted.
pPendingReplay->m_nSpawnTick *= -1;
}
// Setup post-death record time
extern ConVar replay_postdeathrecordtime;
pPendingReplay->m_nPostDeathRecordTime = replay_postdeathrecordtime.GetFloat();
// Let the replay know we're recording
pPendingReplay->OnBeginRecording();
}
else
{
DBG( " NOT creating new replay.\n" );
}
// Served its purpose
m_flPlayerSpawnCreateReplayFailTime = 0.0f;
}
void CReplayManager::Think()
{
VPROF_BUDGET( "CReplayManager::Think", VPROF_BUDGETGROUP_REPLAY );
BaseClass::Think();
DebugThink();
// Only update pending replay, since it's recording
// NOTE: we use Sys_FloatTime() here, since the client sets the next update time with engine->Time(),
// which also uses Sys_FloatTime().
if ( m_pPendingReplay && m_pPendingReplay->m_flNextUpdateTime <= Sys_FloatTime() )
{
m_pPendingReplay->Update(); // Allow the replay's Update() function to set the next update time
}
}
void CReplayManager::DebugThink()
{
// Debugging
if ( replay_debug.GetBool() )
{
const char *pReplayNames[] = { "Scratch", "This life", "Last life" };
CReplay *pReplays[] = { m_pPendingReplay, m_pReplayThisLife, m_pReplayLastLife };
for ( int i = 0; i < 3; ++i )
{
CReplay *pCurReplay = pReplays[ i ];
if ( !pCurReplay )
{
g_pEngineClient->Con_NPrintf( i, "%s: NULL", pReplayNames[ i ] );
continue;
}
g_pEngineClient->Con_NPrintf( i, "%s: handle=%i [%i, %i] C? %s R? %s MaxBlock: %i", pReplayNames[ i ],
pCurReplay->GetHandle(), pCurReplay->m_nSpawnTick,
pCurReplay->m_nDeathTick, pCurReplay->m_bComplete ? "YES" : "NO",
pCurReplay->m_bRequestedByUser ? "YES" : "NO",
pCurReplay->m_iMaxSessionBlockRequired
);
// Screenshot handle
int nCurLine = 5;
g_pEngineClient->Con_NPrintf( nCurLine, "Screenshot replay: handle=%i", CL_GetScreenshotManager()->GetScreenshotReplay() );
nCurLine += 2;
// Saved replay handles
g_pEngineClient->Con_NPrintf( nCurLine++, "REPLAYS:" );
FOR_EACH_REPLAY( j )
{
CReplay *pReplay = GET_REPLAY_AT( j );
g_pEngineClient->Con_NPrintf( nCurLine++, "%i: handle=%i ticks=[%i %i]", i, pReplay->GetHandle(),
pReplay->m_nSpawnTick, pReplay->m_nDeathTick );
}
// Current tick:
g_pEngineClient->Con_NPrintf( ++nCurLine, "MAIN tick: %f", g_pEngineClient->GetLastServerTickTime() );
g_pEngineClient->Con_NPrintf( ++nCurLine, " server tick: %f", g_pEngineClient->GetLastServerTickTime() );
nCurLine += 2;
}
}
}
float CReplayManager::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 0.1f;
}
CReplay *CReplayManager::GetPlayingReplay()
{
return g_pReplayDemoPlayer->GetCurrentReplay();
}
CReplay *CReplayManager::GetReplayForCurrentLife()
{
return m_pReplayThisLife;
}
void CReplayManager::GetReplays( CUtlLinkedList< CReplay *, int > &lstReplays )
{
lstReplays.RemoveAll();
FOR_EACH_REPLAY( i )
{
lstReplays.AddToTail( GET_REPLAY_AT( i ) );
}
}
void CReplayManager::GetReplaysAsQueryableItems( CUtlLinkedList< IQueryableReplayItem *, int > &lstReplays )
{
lstReplays.RemoveAll();
FOR_EACH_REPLAY( i )
{
lstReplays.AddToHead( dynamic_cast< IQueryableReplayItem * >( GET_REPLAY_AT( i ) ) );
}
if ( m_pPendingReplay &&
!m_pPendingReplay->m_bComplete &&
m_pPendingReplay->m_bRequestedByUser )
{
Assert( lstReplays.Find( m_pPendingReplay ) == lstReplays.InvalidIndex() );
lstReplays.AddToHead( m_pPendingReplay );
}
}
int CReplayManager::GetNumReplaysDependentOnSession( ReplayHandle_t hSession )
{
int nResult = 0;
FOR_EACH_REPLAY( i )
{
CReplay *pCurReplay = GET_REPLAY_AT( i );
if ( pCurReplay->m_hSession == hSession )
{
++nResult;
}
}
return nResult;
}
const char *CReplayManager::GetReplaysDir() const
{
return GetIndexPath();
}
float CReplayManager::GetDownloadProgress( const CReplay *pReplay )
{
// Give each downloadable session block equal weight since we won't know the size of blocks that
// have not been created/written yet on the server.
// Go through all blocks in the replay and figure out how many bytes have been downloaded
float flSum = 0.0f;
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pReplay->m_hSession ) ); Assert( pSession );
if ( !pSession )
return 0.0f;
const CBaseRecordingSession::BlockContainer_t &vecBlocks = pSession->GetBlocks();
FOR_EACH_VEC( vecBlocks, i )
{
CClientRecordingSessionBlock *pCurBlock = CL_CastBlock( vecBlocks[ i ] );
if ( !pReplay->IsSignificantBlock( pCurBlock->m_iReconstruction ) )
continue;
// Calculate progress for this block
Assert( pCurBlock->m_uFileSize > 0 );
const float flSubProgress = pCurBlock->m_uFileSize == 0 ? 0.0f : clamp( (float)pCurBlock->m_uBytesDownloaded / pCurBlock->m_uFileSize, 0.0f, 1.0f );
flSum += flSubProgress;
}
// Account for blocks that haven't been created yet
// NOTE: This will cause a bug in download progress if the round ends and cuts the number of
// expected blocks down - but that situation is probably less likely to occur than the situation
// where a client is expecting more blocks that *will* be created. To avoid pops in the latter
// situation, we account for those blocks here.
const int nTotalSubBlocks = pReplay->m_iMaxSessionBlockRequired + 1;
// Calculate mean
Assert( nTotalSubBlocks > 0 );
return nTotalSubBlocks == 0 ? 0.0f : ( flSum / (float)nTotalSubBlocks );
}
//----------------------------------------------------------------------------------------
+128
View File
@@ -0,0 +1,128 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_REPLAYMANAGER_H
#define CL_REPLAYMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "genericpersistentmanager.h"
#include "replay/ireplaymanager.h"
#include "cl_replaycontext.h"
#include "replay/replay.h"
//----------------------------------------------------------------------------------------
class IReplayFactory;
//----------------------------------------------------------------------------------------
//
// Manages and serializes all replays on the client.
//
class CReplayManager : public CGenericPersistentManager< CReplay >,
public IReplayManager
{
typedef CGenericPersistentManager< CReplay > BaseClass;
public:
CReplayManager();
~CReplayManager();
virtual bool Init( CreateInterfaceFn fnCreateFactory );
void Shutdown();
void OnSessionStart();
void OnSessionEnd();
int GetNumReplaysDependentOnSession( ReplayHandle_t hSession );
// IReplayManager
virtual CReplay *GetReplay( ReplayHandle_t hReplay );
virtual void FlagReplayForFlush( CReplay *pReplay, bool bForceImmediate );
virtual int GetUnrenderedReplayCount();
virtual void DeleteReplay( ReplayHandle_t hReplay, bool bNotifyUI );
virtual CReplay *GetPlayingReplay();
virtual CReplay *GetReplayForCurrentLife();
virtual void GetReplays( CUtlLinkedList< CReplay *, int > &lstReplays );
virtual void GetReplaysAsQueryableItems( CUtlLinkedList< IQueryableReplayItem *, int > &lstReplays );
virtual int GetReplayCount() const { return Count(); }
virtual float GetDownloadProgress( const CReplay *pReplay );
virtual const char *GetReplaysDir() const;
void CommitPendingReplayAndBeginDownload();
void CompletePendingReplay();
void AddEventsForListen();
void ClearPendingReplay();
void SanityCheckReplay( CReplay *pReplay );
void SaveDanglingReplay();
void OnClientSideDisconnect();
inline ObjContainer_t &Replays() { return m_vecObjs; }
bool Commit( CReplay *pNewReplay );
void UpdateCurrentReplayDataFromServer();
void OnReplayRecordingCvarChanged();
void AttemptToSetupNewReplay();
CReplay *m_pReplayThisLife; // Valid only between replay completion (ie player death) and player respawn, otherwise NULL
//
// CGenericPersistentManager
//
virtual const char *GetRelativeIndexPath() const;
private:
//
// CGenericPersistentManager
//
virtual const char *GetDebugName() const { return "replay manager"; }
virtual const char *GetIndexFilename() const { return "replays." GENERIC_FILE_EXTENSION; }
virtual CReplay *Create();
virtual int GetVersion() const;
virtual void Think();
virtual IReplayContext *GetReplayContext() const;
virtual bool ShouldLoadObj( const CReplay *pReplay ) const;
virtual void OnObjLoaded( CReplay *pReplay );
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
void DebugThink();
void InitReplay( CReplay *pReplay );
IReplayFactory *GetReplayFactory( CreateInterfaceFn fnCreateFactory );
void CleanupReplay( CReplay *&pReplay );
void FreeLifeIfNotSaved( CReplay *&pReplay );
CReplay *CreatePendingReplay();
CReplay *m_pPendingReplay; // This is the replay we're currently recording - one which will
// either be committed (via Commit()) or not, depending on whether
// the player chooses to save the replay.
CReplay *m_pReplayLastLife; // The previous life (ie between the player's previous spawn and the current spawn), if any (otherwise NULL)
float m_flPlayerSpawnCreateReplayFailTime;
IReplayFactory *m_pReplayFactory;
};
//----------------------------------------------------------------------------------------
inline CReplay *GetReplay( ReplayHandle_t hReplay )
{
extern CClientReplayContext *g_pClientReplayContextInternal;
return g_pClientReplayContextInternal->m_pReplayManager->GetReplay( hReplay );
}
//----------------------------------------------------------------------------------------
#define FOR_EACH_REPLAY( _i ) FOR_EACH_OBJ( CL_GetReplayManager(), _i )
#define GET_REPLAY_AT( _i ) CL_GetReplayManager()->m_vecObjs[ _i ]
//----------------------------------------------------------------------------------------
#endif // CL_REPLAYMANAGER_H
+266
View File
@@ -0,0 +1,266 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_replaymovie.h"
#include "replay/replayutils.h"
#include "replay/shared_defs.h"
#include "KeyValues.h"
#include "replay/replay.h"
#include "cl_replaycontext.h"
#include "cl_replaymanager.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CReplayMovie::CReplayMovie()
: m_hReplay( REPLAY_HANDLE_INVALID ),
m_bRendered( false ),
m_bUploaded( false ),
m_flRenderTime( 0.0f ),
m_flLength( 0.0f ),
m_pUserData( NULL )
{
V_wcsncpy( m_wszTitle, L"Untitled", sizeof( m_wszTitle ) );
}
bool CReplayMovie::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_hReplay = (ReplayHandle_t)pIn->GetInt( "replay_handle", REPLAY_HANDLE_INVALID );
m_bRendered = pIn->GetInt( "rendered" ) != 0;
V_wcsncpy( m_wszTitle, pIn->GetWString( "title" ), sizeof( m_wszTitle ) );
m_strFilename = pIn->GetString( "filename" );
m_strUploadURL = pIn->GetString( "upload_url" );
m_bUploaded = pIn->GetInt( "uploaded" ) != 0;
m_flRenderTime = pIn->GetFloat( "rendertime" );
m_flLength = pIn->GetFloat( "length" );
m_RecordTime.Read( pIn );
return ReadRenderSettings( pIn );
}
void CReplayMovie::Write( KeyValues* pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "replay_handle", (int)m_hReplay );
pOut->SetInt( "rendered", (int)m_bRendered );
pOut->SetWString( "title", m_wszTitle );
pOut->SetString( "filename", m_strFilename.Get() );
pOut->SetString( "upload_url", m_strUploadURL.Get() );
pOut->SetInt( "uploaded", (int)m_bUploaded );
pOut->SetFloat( "rendertime", m_flRenderTime );
pOut->SetFloat( "length", m_flLength );
m_RecordTime.Write( pOut );
WriteRenderSettings( pOut );
}
bool CReplayMovie::ReadRenderSettings( KeyValues *pIn )
{
KeyValues *pRenderSettingsSubKey = pIn->FindKey( "rendersettings" );
if ( !pRenderSettingsSubKey )
{
AssertMsg( 0, "No render settings sub key found for movie!" );
return true; // Continue to load anyway
}
m_RenderSettings.m_nWidth = pRenderSettingsSubKey->GetInt( "width" );
m_RenderSettings.m_nHeight = pRenderSettingsSubKey->GetInt( "height" );
m_RenderSettings.m_nMotionBlurQuality = pRenderSettingsSubKey->GetInt( "motionblurquality" );
m_RenderSettings.m_FPS.SetRaw( pRenderSettingsSubKey->GetInt( "fps.ups" ), pRenderSettingsSubKey->GetInt( "fps.upf" ) );
m_RenderSettings.m_Codec = ( VideoEncodeCodec_t )pRenderSettingsSubKey->GetInt( "codec" );
m_RenderSettings.m_nEncodingQuality = pRenderSettingsSubKey->GetInt( "encoding_quality" );
m_RenderSettings.m_bMotionBlurEnabled = pRenderSettingsSubKey->GetBool( "mb_enabled" );
m_RenderSettings.m_bAAEnabled = pRenderSettingsSubKey->GetBool( "aa_enabled" );
m_RenderSettings.m_bRaw = pRenderSettingsSubKey->GetBool( "raw" );
return true;
}
void CReplayMovie::WriteRenderSettings( KeyValues *pOut )
{
KeyValues *pRenderSettingsSubKey = new KeyValues( "rendersettings" );
if ( !pRenderSettingsSubKey )
{
AssertMsg( 0, "Failed to allocate render settings sub key for movie!" );
return;
}
pOut->AddSubKey( pRenderSettingsSubKey );
pRenderSettingsSubKey->SetInt( "width", m_RenderSettings.m_nWidth );
pRenderSettingsSubKey->SetInt( "height", m_RenderSettings.m_nHeight );
pRenderSettingsSubKey->SetInt( "motionblurquality", m_RenderSettings.m_nMotionBlurQuality );
pRenderSettingsSubKey->SetInt( "fps.ups", m_RenderSettings.m_FPS.GetUnitsPerSecond() );
pRenderSettingsSubKey->SetInt( "fps.upf", m_RenderSettings.m_FPS.GetUnitsPerFrame() );
pRenderSettingsSubKey->SetInt( "codec", (int)m_RenderSettings.m_Codec );
pRenderSettingsSubKey->SetInt( "encoding_quality", m_RenderSettings.m_nEncodingQuality );
pRenderSettingsSubKey->SetInt( "mb_enabled", (int)m_RenderSettings.m_bMotionBlurEnabled );
pRenderSettingsSubKey->SetInt( "aa_enabled", (int)m_RenderSettings.m_bAAEnabled );
pRenderSettingsSubKey->SetInt( "raw", (int)m_RenderSettings.m_bRaw );
}
const char *CReplayMovie::GetSubKeyTitle() const
{
return Replay_va( "movie_%i", GetHandle() );
}
const char *CReplayMovie::GetPath() const
{
return Replay_va( "%s%s%c", g_pClientReplayContextInternal->GetBaseDir(), SUBDIR_MOVIES, CORRECT_PATH_SEPARATOR );
}
void CReplayMovie::OnDelete()
{
// Remove the actual movie from disk
g_pFullFileSystem->RemoveFile( Replay_va( "%s%s", CL_GetMovieManager()->GetRenderDir(), m_strFilename.Get() ) );
}
ReplayHandle_t CReplayMovie::GetMovieHandle() const
{
return GetHandle();
}
ReplayHandle_t CReplayMovie::GetReplayHandle() const
{
return m_hReplay;
}
const ReplayRenderSettings_t &CReplayMovie::GetRenderSettings()
{
return m_RenderSettings;
}
void CReplayMovie::GetFrameDimensions( int &nWidth, int &nHeight )
{
nWidth = m_RenderSettings.m_nWidth;
nHeight = m_RenderSettings.m_nHeight;
}
void CReplayMovie::SetIsRendered( bool bIsRendered )
{
m_bRendered = bIsRendered;
}
void CReplayMovie::SetMovieFilename( const char *pFilename )
{
m_strFilename = pFilename;
}
const char *CReplayMovie::GetMovieFilename() const
{
return m_strFilename.Get();
}
void CReplayMovie::SetMovieTitle( const wchar_t *pTitle )
{
V_wcsncpy( m_wszTitle, pTitle, sizeof( m_wszTitle ) );
}
void CReplayMovie::SetRenderTime( float flRenderTime )
{
m_flRenderTime = flRenderTime;
}
float CReplayMovie::GetRenderTime() const
{
return m_flRenderTime;
}
void CReplayMovie::CaptureRecordTime()
{
m_RecordTime.InitDateAndTimeToNow();
}
void CReplayMovie::SetLength( float flLength )
{
m_flLength = flLength;
}
CReplay *CReplayMovie::GetReplay() const
{
return static_cast< CReplay * >( ::GetReplay( m_hReplay ) );
}
bool CReplayMovie::IsUploaded() const
{
return m_bUploaded;
}
void CReplayMovie::SetUploaded( bool bUploaded )
{
m_bUploaded = bUploaded;
}
void CReplayMovie::SetUploadURL( const char *pURL )
{
m_strUploadURL = pURL;
}
const char *CReplayMovie::GetUploadURL() const
{
return m_strUploadURL.Get();
}
const CReplayTime &CReplayMovie::GetItemDate() const
{
return m_RecordTime;
}
bool CReplayMovie::IsItemRendered() const
{
return GetReplay()->IsItemRendered();
}
CReplay *CReplayMovie::GetItemReplay()
{
return GetReplay();
}
ReplayHandle_t CReplayMovie::GetItemReplayHandle() const
{
return m_hReplay;
}
QueryableReplayItemHandle_t CReplayMovie::GetItemHandle() const
{
return (QueryableReplayItemHandle_t)GetHandle();
}
const wchar_t *CReplayMovie::GetItemTitle() const
{
return m_wszTitle;
}
void CReplayMovie::SetItemTitle( const wchar_t *pTitle )
{
V_wcsncpy( m_wszTitle, pTitle, sizeof( m_wszTitle ) );
}
float CReplayMovie::GetItemLength() const
{
return m_flLength;
}
void *CReplayMovie::GetUserData()
{
return m_pUserData;
}
void CReplayMovie::SetUserData( void *pUserData )
{
m_pUserData = pUserData;
}
bool CReplayMovie::IsItemAMovie() const
{
return true;
}
//----------------------------------------------------------------------------------------
+104
View File
@@ -0,0 +1,104 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAYMOVIE_H
#define REPLAYMOVIE_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplaymovie.h"
#include "replay/basereplayserializeable.h"
#include "replay/replaytime.h"
#include "replay/rendermovieparams.h"
#include "utlstring.h"
//----------------------------------------------------------------------------------------
#define REPLAY_MOVIE_HANDLE_FIRST_VALID ((ReplayHandle_t)5000)
//----------------------------------------------------------------------------------------
class CReplayMovie : public CBaseReplaySerializeable,
public IReplayMovie
{
typedef CBaseReplaySerializeable BaseClass;
public:
CReplayMovie();
//
// IReplaySerializeable
//
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
virtual const char *GetSubKeyTitle() const;
virtual const char *GetPath() const;
virtual void OnDelete();
//
// IReplayMovie
//
virtual ReplayHandle_t GetMovieHandle() const;
virtual ReplayHandle_t GetReplayHandle() const;
virtual const ReplayRenderSettings_t &GetRenderSettings();
virtual void GetFrameDimensions( int &nWidth, int &nHeight );
virtual void SetIsRendered( bool bIsRendered );
virtual void SetMovieFilename( const char *pFilename );
virtual const char *GetMovieFilename() const;
virtual void SetMovieTitle( const wchar_t *pTitle );
virtual void SetRenderTime( float flRenderTime );
virtual float GetRenderTime() const;
virtual void CaptureRecordTime();
virtual void SetLength( float flLength );
virtual bool IsUploaded() const;
virtual void SetUploaded( bool bUploaded );
virtual void SetUploadURL( const char *pURL );
virtual const char *GetUploadURL() const;
//
// IQueryableReplayItem
//
virtual const CReplayTime & GetItemDate() const;
virtual bool IsItemRendered() const;
virtual CReplay *GetItemReplay();
virtual ReplayHandle_t GetItemReplayHandle() const;
virtual QueryableReplayItemHandle_t GetItemHandle() const;
virtual const wchar_t *GetItemTitle() const;
virtual void SetItemTitle( const wchar_t *pTitle );
virtual float GetItemLength() const;
virtual void *GetUserData();
virtual void SetUserData( void *pUserData );
virtual bool IsItemAMovie() const;
CReplay *GetReplay() const;
bool ReadRenderSettings( KeyValues *pIn );
void WriteRenderSettings( KeyValues *pOut );
ReplayHandle_t m_hReplay; // The replay associated with this movie, or 0 if the replay has been deleted
wchar_t m_wszTitle[256];// Title for the movie
CUtlString m_strFilename; // Relative (to game dir) path and filename of the movie
CUtlString m_strUploadURL; // Link to uploaded YouTube video
bool m_bRendered; // Has the movie finished rendering?
void *m_pUserData;
bool m_bUploaded;
float m_flRenderTime; // How many seconds it took to render the movie
CReplayTime m_RecordTime; // What date/time was this movie recorded?
float m_flLength; // The movie length
ReplayRenderSettings_t m_RenderSettings;
};
//----------------------------------------------------------------------------------------
inline CReplayMovie *ToMovie( IReplaySerializeable *pMovie )
{
return static_cast< CReplayMovie * >( pMovie );
}
//----------------------------------------------------------------------------------------
#endif // REPLAYMOVIE_H
+474
View File
@@ -0,0 +1,474 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_replaymoviemanager.h"
#include "replay/ireplaymoviemanager.h"
#include "replay/ireplaymovierenderer.h"
#include "replay/replay.h"
#include "replay/replayutils.h"
#include "replay/rendermovieparams.h"
#include "replay/shared_defs.h"
#include "cl_replaymovie.h"
#include "cl_renderqueue.h"
#include "cl_replaycontext.h"
#include "filesystem.h"
#include "KeyValues.h"
#include "replaysystem.h"
#include "cl_replaymanager.h"
#include "materialsystem/imaterialsystem.h"
#include "materialsystem/materialsystem_config.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define MOVIE_MANAGER_VERSION 1
//----------------------------------------------------------------------------------------
extern IMaterialSystem *materials;
//----------------------------------------------------------------------------------------
CReplayMovieManager::CReplayMovieManager()
: m_pPendingMovie( NULL ),
m_pVidModeSettings( NULL ),
m_pRenderMovieSettings( NULL ),
m_bIsRendering( false ),
m_bRenderingCancelled( false )
{
m_pVidModeSettings = new MaterialSystem_Config_t();
m_pRenderMovieSettings = new RenderMovieParams_t();
m_wszCachedMovieTitle[0] = L'\0';
}
CReplayMovieManager::~CReplayMovieManager()
{
delete m_pVidModeSettings;
}
bool CReplayMovieManager::Init()
{
// Create "rendered" dir
const char *pRenderedDir = Replay_va( "%s%s%c", CL_GetReplayBaseDir(), SUBDIR_RENDERED, CORRECT_PATH_SEPARATOR );
g_pFullFileSystem->CreateDirHierarchy( pRenderedDir );
return BaseClass::Init();
}
int CReplayMovieManager::GetMovieCount()
{
return Count();
}
void CReplayMovieManager::GetMovieList( CUtlLinkedList< IReplayMovie * > &list )
{
FOR_EACH_OBJ( this, i )
{
list.AddToTail( ToMovie( m_vecObjs[ i ] ) );
}
}
IReplayMovie *CReplayMovieManager::GetMovie( ReplayHandle_t hMovie )
{
return ToMovie( Find( hMovie ) );
}
void CReplayMovieManager::AddMovie( CReplayMovie *pNewMovie )
{
Add( pNewMovie );
Save();
}
CReplayMovie *CReplayMovieManager::Create()
{
return new CReplayMovie();
}
const char *CReplayMovieManager::GetRelativeIndexPath() const
{
return Replay_va( "%s%c", SUBDIR_MOVIES, CORRECT_PATH_SEPARATOR );
}
IReplayMovie *CReplayMovieManager::CreateAndAddMovie( ReplayHandle_t hReplay )
{
CReplayMovie *pNewMovie = CreateAndGenerateHandle(); // Sets m_hThis (which is accessed via GetHandle())
// Cache replay handle.
pNewMovie->m_hReplay = hReplay;
// Copy cached render settings to the movie itself.
V_memcpy( &pNewMovie->m_RenderSettings, &m_pRenderMovieSettings->m_Settings, sizeof( ReplayRenderSettings_t ) );
AddMovie( pNewMovie );
return pNewMovie;
}
void CReplayMovieManager::DeleteMovie( ReplayHandle_t hMovie )
{
// Cache owner replay
CReplayMovie *pMovie = Find( hMovie );
CReplay *pOwnerReplay = pMovie ? CL_GetReplayManager()->GetReplay( pMovie->m_hReplay ) : NULL;
Remove( hMovie );
// If no more movies for the given replay, mark as unrendered & save
if ( pOwnerReplay && GetNumMoviesDependentOnReplay( pOwnerReplay ) == 0 )
{
pOwnerReplay->m_bRendered = false;
CL_GetReplayManager()->FlagReplayForFlush( pOwnerReplay, false );
}
}
int CReplayMovieManager::GetNumMoviesDependentOnReplay( const CReplay *pReplay )
{
if ( !pReplay )
return 0;
// Go through all movies and find any that depend on the given replay
int nNumMovies = 0;
FOR_EACH_OBJ( this, i )
{
CReplayMovie *pMovie = ToMovie( m_vecObjs[ i ] );
if ( pMovie->m_hReplay == pReplay->GetHandle() )
{
++nNumMovies;
}
}
return nNumMovies;
}
void CReplayMovieManager::SetPendingMovie( IReplayMovie *pMovie )
{
m_pPendingMovie = pMovie;
}
IReplayMovie *CReplayMovieManager::GetPendingMovie()
{
return m_pPendingMovie;
}
void CReplayMovieManager::FlagMovieForFlush( IReplayMovie *pMovie, bool bImmediate )
{
FlagForFlush( CastMovie( pMovie ), bImmediate );
}
CReplayMovie *CReplayMovieManager::CastMovie( IReplayMovie *pMovie )
{
return static_cast< CReplayMovie * >( pMovie );
}
int CReplayMovieManager::GetVersion() const
{
return MOVIE_MANAGER_VERSION;
}
IReplayContext *CReplayMovieManager::GetReplayContext() const
{
return g_pClientReplayContextInternal;
}
float CReplayMovieManager::GetNextThinkTime() const
{
return 0.1f;
}
void CReplayMovieManager::CacheMovieTitle( const wchar_t *pTitle )
{
V_wcsncpy( m_wszCachedMovieTitle, pTitle, sizeof( m_wszCachedMovieTitle ) );
}
void CReplayMovieManager::GetCachedMovieTitleAndClear( wchar_t *pOut, int nOutBufLength )
{
const int nLength = wcslen( m_wszCachedMovieTitle );
wcsncpy( pOut, m_wszCachedMovieTitle, nOutBufLength );
pOut[ nLength ] = L'\0';
m_wszCachedMovieTitle[0] = L'\0';
}
void CReplayMovieManager::AddReplayForRender( CReplay *pReplay, int iPerformance )
{
if ( !g_pClientReplayContextInternal->ReconstructReplayIfNecessary( pReplay ) )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Render_ReconstructFailed" );
return;
}
// Store in the demo player's list
g_pReplayDemoPlayer->AddReplayToList( pReplay->GetHandle(), iPerformance );
}
void CReplayMovieManager::ClearRenderCancelledFlag()
{
m_bRenderingCancelled = false;
}
void CReplayMovieManager::RenderMovie( RenderMovieParams_t const& params )
{
// Save state
m_bIsRendering = true;
// Change video settings for recording
SetupVideo( params );
// Clear any old replays in the player
g_pReplayDemoPlayer->ClearReplayList();
// Render all unrendered replays
if ( params.m_hReplay == REPLAY_HANDLE_INVALID )
{
CRenderQueue *pRenderQueue = g_pClientReplayContextInternal->m_pRenderQueue;
const int nQueueCount = pRenderQueue->GetCount();
for ( int i = 0; i < nQueueCount; ++i )
{
ReplayHandle_t hCurReplay;
int iCurPerformance;
if ( !pRenderQueue->GetEntryData( i, &hCurReplay, &iCurPerformance ) )
continue;
CReplay *pReplay = CL_GetReplayManager()->GetReplay( hCurReplay );
if ( !pReplay )
continue;
AddReplayForRender( pReplay, iCurPerformance );
}
}
else
{
// Cache the title
CReplayMovieManager *pMovieManager = CL_GetMovieManager();
pMovieManager->CacheMovieTitle( params.m_wszTitle );
// Only render the specified replay
AddReplayForRender( CL_GetReplayManager()->GetReplay( params.m_hReplay ), params.m_iPerformance );
}
g_pReplayDemoPlayer->PlayNextReplay();
}
void CReplayMovieManager::RenderNextMovie()
{
m_bIsRendering = true;
g_pReplayDemoPlayer->PlayNextReplay();
}
void CReplayMovieManager::SetupHighDetailModels()
{
g_pEngine->Cbuf_AddText( "r_rootlod 0\n" );
}
void CReplayMovieManager::SetupHighDetailTextures()
{
g_pEngine->Cbuf_AddText( "mat_picmip -1\n" );
}
void CReplayMovieManager::SetupHighQualityAntialiasing()
{
int nNumSamples = 1;
int nQualityLevel = 0;
if ( materials->SupportsCSAAMode(8, 2) )
{
nNumSamples = 8;
nQualityLevel = 2;
}
else if ( materials->SupportsMSAAMode(8) )
{
nNumSamples = 8;
nQualityLevel = 0;
}
else if ( materials->SupportsCSAAMode(4, 4) )
{
nNumSamples = 4;
nQualityLevel = 4;
}
else if ( materials->SupportsCSAAMode(4, 2) )
{
nNumSamples = 4;
nQualityLevel = 2;
}
else if ( materials->SupportsMSAAMode(6) )
{
nNumSamples = 6;
nQualityLevel = 0;
}
else if ( materials->SupportsMSAAMode(4) )
{
nNumSamples = 4;
nQualityLevel = 0;
}
else if ( materials->SupportsMSAAMode(2) )
{
nNumSamples = 2;
nQualityLevel = 0;
}
g_pEngine->Cbuf_AddText( Replay_va( "mat_antialias %i\n", nNumSamples ) );
g_pEngine->Cbuf_AddText( Replay_va( "mat_aaquality %i\n", nQualityLevel ) );
}
void CReplayMovieManager::SetupHighQualityFiltering()
{
g_pEngine->Cbuf_AddText( "mat_forceaniso\n" );
}
void CReplayMovieManager::SetupHighQualityShadowDetail()
{
if ( materials->SupportsShadowDepthTextures() )
{
g_pEngine->Cbuf_AddText( "r_shadowrendertotexture 1\n" );
g_pEngine->Cbuf_AddText( "r_flashlightdepthtexture 1\n" );
}
else
{
g_pEngine->Cbuf_AddText( "r_shadowrendertotexture 1\n" );
g_pEngine->Cbuf_AddText( "r_flashlightdepthtexture 0\n" );
}
}
void CReplayMovieManager::SetupHighQualityHDR()
{
ConVarRef mat_dxlevel( "mat_dxlevel" );
if ( mat_dxlevel.GetInt() < 80 )
return;
g_pEngine->Cbuf_AddText( Replay_va( "mat_hdr_level %i\n", materials->SupportsHDRMode( HDR_TYPE_INTEGER ) ? 2 : 1 ) );
}
void CReplayMovieManager::SetupHighQualityWaterDetail()
{
#ifndef _X360
g_pEngine->Cbuf_AddText( "r_waterforceexpensive 1\n" );
#endif
g_pEngine->Cbuf_AddText( "r_waterforcereflectentities 1\n" );
}
void CReplayMovieManager::SetupMulticoreRender()
{
g_pEngine->Cbuf_AddText( "mat_queue_mode 0\n" );
}
void CReplayMovieManager::SetupHighQualityShaderDetail()
{
g_pEngine->Cbuf_AddText( "mat_reducefillrate 0\n" );
}
void CReplayMovieManager::SetupColorCorrection()
{
g_pEngine->Cbuf_AddText( "mat_colorcorrection 1\n" );
}
void CReplayMovieManager::SetupMotionBlur()
{
g_pEngine->Cbuf_AddText( "mat_motion_blur_enabled 1\n" );
}
void CReplayMovieManager::SetupVideo( RenderMovieParams_t const &params )
{
// Get current video config
const MaterialSystem_Config_t &config = materials->GetCurrentConfigForVideoCard();
// Cache config
V_memcpy( m_pVidModeSettings, &config, sizeof( config ) );
// Cache quit when done
V_memcpy( m_pRenderMovieSettings, &params, sizeof( params ) );
g_pEngine->Cbuf_Execute();
}
void CReplayMovieManager::CompleteRender( bool bSuccess, bool bShowBrowser )
{
// Store state
m_bIsRendering = false;
// Shutdown renderer
IReplayMovieRenderer *pRenderer = CL_GetMovieRenderer();
if ( pRenderer )
{
pRenderer->ShutdownRenderer();
}
if ( !bSuccess )
{
// Delete the movie from the manager
IReplayMovie *pMovie = CL_GetMovieManager()->GetPendingMovie();
if ( pMovie )
{
CL_GetMovieManager()->DeleteMovie( pMovie->GetMovieHandle() );
}
}
// Clear render queue if we're done
if ( bShowBrowser )
{
CL_GetRenderQueue()->Clear();
}
// Notify UI that rendering is complete
g_pClient->OnRenderComplete( *m_pRenderMovieSettings, m_bRenderingCancelled, bSuccess, bShowBrowser );
// Quit now?
if ( m_pRenderMovieSettings->m_bQuitWhenFinished )
{
g_pEngine->HostState_Shutdown();
return;
}
// Otherwise, play a sound.
g_pClient->PlaySound( "replay\\rendercomplete.wav" );
}
void CReplayMovieManager::CancelRender()
{
m_bRenderingCancelled = true;
CompleteRender( false, true );
g_pEngine->Host_Disconnect( false ); // CReplayDemoPlayer::StopPlayback() will be called
}
void CReplayMovieManager::GetMoviesAsQueryableItems( CUtlLinkedList< IQueryableReplayItem *, int > &lstMovies )
{
lstMovies.RemoveAll();
FOR_EACH_OBJ( this, i )
{
lstMovies.AddToHead( ToMovie( m_vecObjs[ i ] ) );
}
}
const char *CReplayMovieManager::GetRenderDir() const
{
return Replay_va( "%s%s%c", g_pClientReplayContextInternal->GetBaseDir(), SUBDIR_RENDERED, CORRECT_PATH_SEPARATOR );
}
const char *CReplayMovieManager::GetRawExportDir() const
{
static CFmtStr s_fmtExportDir;
CReplayTime time;
time.InitDateAndTimeToNow();
int nDay, nMonth, nYear;
time.GetDate( nDay, nMonth, nYear );
int nHour, nMin, nSec;
time.GetTime( nHour, nMin, nSec );
s_fmtExportDir.sprintf(
"%smovie_%02i%02i%04i_%02i%02i%02i%c",
GetRenderDir(),
nMonth, nDay, nYear,
nHour, nMin, nSec,
CORRECT_PATH_SEPARATOR
);
return s_fmtExportDir.Access();
}
//----------------------------------------------------------------------------------------
+114
View File
@@ -0,0 +1,114 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAYMOVIEMANAGER_H
#define REPLAYMOVIEMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplaymoviemanager.h"
#include "replay/shared_defs.h"
#include "genericpersistentmanager.h"
#include "cl_replaymovie.h"
#include "utlvector.h"
//----------------------------------------------------------------------------------------
class IClientReplayHistoryManager;
struct MaterialSystem_Config_t;
//----------------------------------------------------------------------------------------
class CReplayMovieManager : public CGenericPersistentManager< CReplayMovie >,
public IReplayMovieManager
{
typedef CGenericPersistentManager< CReplayMovie > BaseClass;
public:
CReplayMovieManager();
~CReplayMovieManager();
virtual bool Init();
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
//
// IReplayMovieManager
//
virtual int GetMovieCount();
virtual void GetMovieList( CUtlLinkedList< IReplayMovie * > &list );
virtual IReplayMovie *GetMovie( ReplayHandle_t hMovie );
virtual IReplayMovie *CreateAndAddMovie( ReplayHandle_t hReplay );
virtual void DeleteMovie( ReplayHandle_t hMovie );
virtual int GetNumMoviesDependentOnReplay( const CReplay *pReplay );
virtual void SetPendingMovie( IReplayMovie *pMovie );
virtual IReplayMovie *GetPendingMovie();
virtual void FlagMovieForFlush( IReplayMovie *pMovie, bool bImmediate );
virtual void GetMoviesAsQueryableItems( CUtlLinkedList< IQueryableReplayItem *, int > &lstMovies );
virtual const char *GetRenderDir() const;
virtual const char *GetRawExportDir() const;
virtual void RenderMovie( RenderMovieParams_t const& params );
virtual void RenderNextMovie();
virtual bool IsRendering() const { return m_bIsRendering; }
virtual bool RenderingCancelled() const { return m_bRenderingCancelled; }
virtual void CompleteRender( bool bSuccess, bool bShowBrowser );
virtual void ClearRenderCancelledFlag();
virtual void CancelRender();
void AddMovie( CReplayMovie *pNewMovie );
CReplayMovie *CastMovie( IReplayMovie *pMovie );
void CacheMovieTitle( const wchar_t *pTitle );
void GetCachedMovieTitleAndClear( wchar_t *pOut, int nBufLength );
RenderMovieParams_t &GetRenderMovieSettings() { return *m_pRenderMovieSettings; }
private:
//
// CGenericPersistentManager
//
virtual const char *GetDebugName() const { return "movie manager"; }
virtual const char *GetIndexFilename() const { return "movies." GENERIC_FILE_EXTENSION; }
virtual CReplayMovie *Create();
virtual const char *GetRelativeIndexPath() const;
virtual int GetVersion() const;
virtual int GetHandleBase() const { return MOVIE_HANDLE_BASE; }
virtual IReplayContext *GetReplayContext() const;
void AddReplayForRender( CReplay *pReplay, int iPerformance );
void SetupVideo( RenderMovieParams_t const &params );
void SetupHighDetailModels();
void SetupHighDetailTextures();
void SetupHighQualityAntialiasing();
void SetupHighQualityFiltering();
void SetupHighQualityShadowDetail();
void SetupHighQualityHDR();
void SetupHighQualityWaterDetail();
void SetupMulticoreRender();
void SetupHighQualityShaderDetail();
void SetupColorCorrection();
void SetupMotionBlur();
wchar_t m_wszCachedMovieTitle[MAX_REPLAY_TITLE_LENGTH];
IReplayMovie *m_pPendingMovie;
MaterialSystem_Config_t *m_pVidModeSettings; // Used to restore video mode settings after render completion
RenderMovieParams_t *m_pRenderMovieSettings;
bool m_bIsRendering;
bool m_bRenderingCancelled;
//
// TODO:
// - date rendered
//
};
//----------------------------------------------------------------------------------------
#endif // REPLAYMOVIEMANAGER_H
+290
View File
@@ -0,0 +1,290 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_screenshotmanager.h"
#include "replay/screenshot.h"
#include "replaysystem.h"
#include "cl_replaymanager.h"
#include "replay/replayutils.h"
#include "replay/ireplayscreenshotsystem.h"
#include "gametrace.h"
#include "icliententity.h"
#include "imageutils.h"
#include "filesystem.h"
#include "fmtstr.h"
#include "vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define SCREENSHOTS_SUBDIR "screenshots"
//----------------------------------------------------------------------------------------
CScreenshotManager::CScreenshotManager()
: m_flLastScreenshotTime( 0.0f ),
m_hScreenshotReplay( REPLAY_HANDLE_INVALID )
{
}
CScreenshotManager::~CScreenshotManager()
{
}
bool CScreenshotManager::Init()
{
// Create thumbnails directory
CFmtStr fmtThumbnailsPath(
"%s%cmaterials%cvgui%creplay%cthumbnails",
g_pEngine->GetGameDir(), CORRECT_PATH_SEPARATOR, CORRECT_PATH_SEPARATOR,
CORRECT_PATH_SEPARATOR, CORRECT_PATH_SEPARATOR
);
g_pFullFileSystem->CreateDirHierarchy( fmtThumbnailsPath.Access() );
// Compute all possible resolutions if first time we're running this function
for ( int iAspect = 0; iAspect < 3; ++iAspect )
{
for ( int iRes = 0; iRes < 2; ++iRes )
{
int nWidth = (int)FastPow2( 9 + iRes );
m_aScreenshotWidths[ iAspect ][ iRes ] = nWidth;
}
}
// Set current screenshot dims to 0 - screenshot cache will be created first time we see
// proper screen dimensions
m_nPrevScreenDims[0] = 0;
m_nPrevScreenDims[1] = 0;
// Initialize screenshot taker in client
g_pClient->GetReplayScreenshotSystem()->UpdateReplayScreenshotCache();
return true;
}
float CScreenshotManager::GetNextThinkTime() const
{
return 0.0f;
}
void CScreenshotManager::Think()
{
VPROF_BUDGET( "CScreenshotManager::Think", VPROF_BUDGETGROUP_REPLAY );
//
// NOTE:DoCaptureScreenshot() gets called from CReplaySystem::CL_Render()
//
IReplayScreenshotSystem *pScreenshotSystem = g_pClient->GetReplayScreenshotSystem(); Assert( pScreenshotSystem );
// Check to see if screen resolution changed, and if so, update the client-side screenshot cache.
int nScreenWidth = g_pEngineClient->GetScreenWidth();
int nScreenHeight = g_pEngineClient->GetScreenHeight();
if ( !CL_GetMovieManager()->IsRendering() && ( m_nPrevScreenDims[0] != nScreenWidth || m_nPrevScreenDims[1] != nScreenHeight ) )
{
if ( m_nPrevScreenDims[0] != 0 ) // If this is not the first update
{
pScreenshotSystem->UpdateReplayScreenshotCache();
}
m_nPrevScreenDims[0] = nScreenWidth;
m_nPrevScreenDims[1] = nScreenHeight;
}
}
bool CScreenshotManager::ShouldCaptureScreenshot()
{
// Record a screenshot if its been setup
return ( m_flScreenshotCaptureTime >= 0.0f && m_flScreenshotCaptureTime <= g_pEngine->GetHostTime() );
}
void CScreenshotManager::CaptureScreenshot( CaptureScreenshotParams_t& params )
{
extern ConVar replay_enableeventbasedscreenshots;
if ( !replay_enableeventbasedscreenshots.GetBool() && !params.m_bPrimary )
return;
// Schedule screenshot
m_flScreenshotCaptureTime = g_pEngine->GetHostTime() + params.m_flDelay;
// Cache parameters for when we take the screenshot
V_memcpy( &m_screenshotParams, &params, sizeof( params ) );
}
void CScreenshotManager::DoCaptureScreenshot()
{
// Reset screenshot capture schedule time, even if we don't end up actually taking the screenshot
m_flScreenshotCaptureTime = -1.0f;
// Make sure we're in-game
if ( !g_pEngineClient->IsConnected() )
return;
// Get a pointer to the replay
CReplay *pReplay = ::GetReplay( m_hScreenshotReplay );
if ( !pReplay )
{
AssertMsg( 0, "Failed to take screenshot!\n" );
return;
}
// Max # of screenshots already taken?
extern ConVar replay_maxscreenshotsperreplay;
int nScreenshotLimit = replay_maxscreenshotsperreplay.GetInt();
if ( nScreenshotLimit && pReplay->GetScreenshotCount() >= nScreenshotLimit )
return;
// If not enough time has passed since the last screenshot was taken, get out
extern ConVar replay_mintimebetweenscreenshots;
if ( !m_screenshotParams.m_bIgnoreMinTimeBetweenScreenshots &&
( g_pEngine->GetHostTime() - m_flLastScreenshotTime < replay_mintimebetweenscreenshots.GetInt() ) )
return;
// Update last screenshot taken time
m_flLastScreenshotTime = g_pEngine->GetHostTime();
// Setup screenshot base filename as <.dem base filename>_<N>
char szBaseFilename[ MAX_OSPATH ];
char szIdealBaseFilename[ MAX_OSPATH ];
V_strcpy_safe( szIdealBaseFilename, CL_GetRecordingSessionManager()->m_ServerRecordingState.m_strSessionName.Get() );
Replay_GetFirstAvailableFilename( szBaseFilename, sizeof( szBaseFilename ), szIdealBaseFilename, ".vtf",
"materials\\vgui\\replay\\thumbnails", pReplay->GetScreenshotCount() );
// Remove extension
int i = V_strlen( szBaseFilename ) - 1;
while ( i >= 0 )
{
if ( szBaseFilename[ i ] == '.' )
break;
--i;
}
szBaseFilename[ i ] = '\0';
// Get destination file
char szScreenshotPath[ MAX_OSPATH ];
V_snprintf( szScreenshotPath, sizeof( szScreenshotPath ), "materials\\vgui\\replay\\thumbnails\\%s.vtf", szBaseFilename );
// Make sure we're using the correct path separator
V_FixSlashes( szScreenshotPath );
// Setup screenshot dimensions
int nScreenshotDims[2];
GetUnpaddedScreenshotSize( nScreenshotDims[0], nScreenshotDims[1] );
// Setup parameters for screenshot
WriteReplayScreenshotParams_t params;
V_memset( &params, 0, sizeof( params ) );
// Setup the camera
Vector origin;
QAngle angles;
if ( m_screenshotParams.m_nEntity > 0 )
{
IClientEntity *pEntity = entitylist->GetClientEntity( m_screenshotParams.m_nEntity );
if ( pEntity )
{
// Clip the camera position if any world geometry is in the way
trace_t trace;
Ray_t ray;
CTraceFilterWorldAndPropsOnly traceFilter;
Vector vStartPos = pEntity->GetAbsOrigin();
ray.Init( vStartPos, pEntity->GetAbsOrigin() + m_screenshotParams.m_posCamera );
g_pEngineTraceClient->TraceRay( ray, MASK_PLAYERSOLID, &traceFilter, &trace );
// Setup world position and angles for camera
origin = trace.endpos;
if ( trace.DidHit() )
{
float d = 5; // The distance to push in if we
Vector dir = trace.endpos - vStartPos;
VectorNormalize( dir );
origin -= Vector( d * dir.x, d * dir.y, d * dir.z );
}
// Use the new camera origin
params.m_pOrigin = &origin;
// Use angles too if appropriate
if ( m_screenshotParams.m_bUseCameraAngles )
{
angles = m_screenshotParams.m_angCamera;
params.m_pAngles = &angles;
}
}
}
// Write the screenshot to disk
params.m_nWidth = nScreenshotDims[0];
params.m_nHeight = nScreenshotDims[1];
params.m_pFilename = szScreenshotPath;
g_pClient->GetReplayScreenshotSystem()->WriteReplayScreenshot( params );
// Write a generic VMT
char szVTFFullPath[ MAX_OSPATH ];
V_snprintf( szVTFFullPath, sizeof( szVTFFullPath ), "%s\\materials\\vgui\\replay\\thumbnails\\%s.vtf", g_pEngine->GetGameDir(), szBaseFilename );
V_FixSlashes( szVTFFullPath );
ImgUtl_WriteGenericVMT( szVTFFullPath, "vgui/replay/thumbnails" );
// Create the new screenshot info
pReplay->AddScreenshot( nScreenshotDims[0], nScreenshotDims[1], szBaseFilename );
}
void CScreenshotManager::DeleteScreenshotsForReplay( CReplay *pReplay )
{
char szFilename[ MAX_OSPATH ];
for ( int i = 0; i < pReplay->GetScreenshotCount(); ++i )
{
const CReplayScreenshot *pScreenshot = pReplay->GetScreenshot( i );
// Delete the VGUI thumbnail VTF
V_snprintf( szFilename, sizeof( szFilename ), "materials\\vgui\\replay\\thumbnails\\%s.vtf", pScreenshot->m_szBaseFilename );
V_FixSlashes( szFilename );
g_pFullFileSystem->RemoveFile( szFilename );
// Delete the VGUI thumbnail VMT
V_snprintf( szFilename, sizeof( szFilename ), "materials\\vgui\\replay\\thumbnails\\%s.vmt", pScreenshot->m_szBaseFilename );
V_FixSlashes( szFilename );
g_pFullFileSystem->RemoveFile( szFilename );
}
}
void CScreenshotManager::GetUnpaddedScreenshotSize( int &nOutWidth, int &nOutHeight )
{
// Figure out the proper screenshot size to use based on the aspect ratio
int nScreenWidth = g_pEngineClient->GetScreenWidth();
int nScreenHeight = g_pEngineClient->GetScreenHeight();
float flAspectRatio = (float)nScreenWidth / nScreenHeight;
// Get the screenshot res
extern ConVar replay_screenshotresolution;
int iRes = clamp( replay_screenshotresolution.GetInt(), 0, 1 );
int iAspect;
if ( flAspectRatio == 16.0f/9 )
{
iAspect = 0;
}
else if ( flAspectRatio == 16.0f/10 )
{
iAspect = 1;
}
else
{
iAspect = 2; // 4:3
}
static float s_flInvAspectRatios[3] = { 9.0f/16.0f, 10.0f/16, 3.0f/4 };
nOutWidth = min( nScreenWidth, m_aScreenshotWidths[ iAspect ][ iRes ] );
nOutHeight = m_aScreenshotWidths[ iAspect ][ iRes ] * s_flInvAspectRatios[ iAspect ];
}
void CScreenshotManager::SetScreenshotReplay( ReplayHandle_t hReplay )
{
m_hScreenshotReplay = hReplay;
}
//----------------------------------------------------------------------------------------
+66
View File
@@ -0,0 +1,66 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef CL_SCREENSHOTMANAGER_H
#define CL_SCREENSHOTMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "basethinker.h"
#include "replay/ireplayscreenshotmanager.h"
#include "replay/screenshot.h"
//----------------------------------------------------------------------------------------
class CReplay;
//----------------------------------------------------------------------------------------
class CScreenshotManager : public CBaseThinker,
public IReplayScreenshotManager
{
public:
CScreenshotManager();
~CScreenshotManager();
bool Init();
bool ShouldCaptureScreenshot();
void DoCaptureScreenshot();
void SetScreenshotReplay( ReplayHandle_t hReplay );
ReplayHandle_t GetScreenshotReplay() const { return m_hScreenshotReplay; }
//
// IReplayScreenshotManager
//
virtual void CaptureScreenshot( CaptureScreenshotParams_t& params );
virtual void GetUnpaddedScreenshotSize( int &nOutWidth, int &nOutHeight );
virtual void DeleteScreenshotsForReplay( CReplay *pReplay );
private:
//
// CBaseThinker
//
void Think();
float GetNextThinkTime() const;
float m_flScreenshotCaptureTime;
CaptureScreenshotParams_t m_screenshotParams; // Params for next scheduled screenshot
int m_aScreenshotWidths[3][2]; // [ 16:9, 16:10, 4:3 ][ lo res, hi res ]
ReplayHandle_t m_hScreenshotReplay; // Destination replay for any screenshots taken - we always write to the "pending"
// replay for the most part, but if we want to take a screenshot after the local
// player is dead, we need to use a handle rather than using m_pPendingReplay directly,
// since it becomes NULL when the player dies.
float m_flLastScreenshotTime;
int m_nPrevScreenDims[2]; // Screenshot dimensions, used to determine if we should update the screenshot cache on the client
};
//----------------------------------------------------------------------------------------
#endif // CL_SCREENSHOTMANAGER_H
+331
View File
@@ -0,0 +1,331 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_sessionblockdownloader.h"
#include "replay/ienginereplay.h"
#include "cl_recordingsessionblockmanager.h"
#include "cl_replaycontext.h"
#include "cl_recordingsession.h"
#include "cl_recordingsessionblock.h"
#include "errorsystem.h"
#include "convar.h"
#include "vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineReplay *g_pEngine;
extern ConVar replay_maxconcurrentdownloads;
//----------------------------------------------------------------------------------------
int CSessionBlockDownloader::sm_nNumCurrentDownloads = 0;
//----------------------------------------------------------------------------------------
CSessionBlockDownloader::CSessionBlockDownloader()
: m_nMaxBlock( -1 )
{
}
void CSessionBlockDownloader::Shutdown()
{
AbortDownloadsAndCleanup( NULL );
Assert( sm_nNumCurrentDownloads == 0 );
}
void CSessionBlockDownloader::AbortDownloadsAndCleanup( CClientRecordingSession *pSession )
{
// NOTE: sm_nNumCurrentDownloads will be decremented in OnDownloadComplete(), which is
// invoked by CHttpDownloader::AbortDownloadAndCleanup()
// Abort any remaining downloads - callbacks will be invoked, so this shutdown
// should be called before any of those objects are cleaned up.
FOR_EACH_LL( m_lstDownloaders, i )
{
CHttpDownloader *pCurDownloader = m_lstDownloaders[ i ];
// If a session was passed in, make sure it has the same handle as the block
CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pCurDownloader->GetUserData();
if ( pSession && ( !pBlock || pBlock->m_hSession != pSession->GetHandle() ) )
continue;
pCurDownloader->AbortDownloadAndCleanup();
delete pCurDownloader;
}
m_lstDownloaders.RemoveAll();
}
bool CSessionBlockDownloader::AtMaxConcurrentDownloads() const
{
return ( sm_nNumCurrentDownloads >= replay_maxconcurrentdownloads.GetInt() );
}
float CSessionBlockDownloader::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 0.5f;
}
void CSessionBlockDownloader::Think()
{
VPROF_BUDGET( "CSessionBlockDownloader::Think", VPROF_BUDGETGROUP_REPLAY );
CBaseThinker::Think();
// Hack to not think right away
if ( g_pEngine->GetHostTime() < 3 )
return;
// Don't go over the desired maximum # of concurrent downloads
if ( !AtMaxConcurrentDownloads() )
{
// Go through all blocks and begin downloading any that the server index downloader
// has determined are ready
CClientRecordingSessionBlockManager *pBlockManager = CL_GetRecordingSessionBlockManager();
FOR_EACH_OBJ( pBlockManager, i )
{
CClientRecordingSessionBlock *pBlock = CL_CastBlock( pBlockManager->m_vecObjs[ i ] );
// Checks to see if the remote status is marked as ready for download
if ( !pBlock->ShouldDownloadNow() )
continue;
// Lookup the session for the block
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->Find( pBlock->m_hSession ) );
if ( !pSession )
{
AssertMsg( 0, "Session for block not found! This should never happen!" );
continue;
}
// Do we need any blocks at all from this session? Is this block within range?
int iLastBlockToDownload = pSession->GetLastBlockToDownload();
if ( iLastBlockToDownload < 0 || pBlock->m_iReconstruction > iLastBlockToDownload )
{
continue;
}
// Begin the download
CHttpDownloader *pDownloader = new CHttpDownloader( this );
const char *pFilename = V_UnqualifiedFileName( pBlock->m_szFullFilename );
#ifdef _DEBUG
extern ConVar replay_forcedownloadurl;
const char *pForceURL = replay_forcedownloadurl.GetString();
const char *pURL = pForceURL[0] ? pForceURL : Replay_va( "%s%s", pSession->m_strBaseDownloadURL.Get(), pFilename );
#else
const char *pURL = Replay_va( "%s%s", pSession->m_strBaseDownloadURL.Get(), pFilename );
#endif
const char *pGamePath = Replay_va( "%s%s", CL_GetRecordingSessionBlockManager()->GetSavePath(), pFilename );
pDownloader->BeginDownload( pURL, pGamePath, (void *)pBlock, &pBlock->m_uBytesDownloaded );
IF_REPLAY_DBG(
Warning ( "%s block %i from %s to path %s...\n",
pBlock->GetNumDownloadAttempts() ? "RETRYING download for" : "Downloading" ,
pBlock->m_iReconstruction, pURL, pGamePath )
);
// Add the downloader
m_lstDownloaders.AddToTail( pDownloader );
// Update block's status
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADING;
// Mark as dirty
CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false );
// Update # of concurrent downloads
++sm_nNumCurrentDownloads;
// Get out if we're at max downloads now
if ( AtMaxConcurrentDownloads() )
break;
}
}
int it = m_lstDownloaders.Head();
while ( it != m_lstDownloaders.InvalidIndex() )
{
// Remove finished downloaders
CHttpDownloader *pCurDownloader = m_lstDownloaders[ it ];
if ( pCurDownloader->IsDone() && pCurDownloader->CanDelete() )
{
int itRemove = it;
// Next
it = m_lstDownloaders.Next( it );
// Remove the downloader from the list
m_lstDownloaders.Remove( itRemove );
// Free the downloader
delete pCurDownloader;
}
else
{
// Let the downloader think
pCurDownloader->Think();
// Next
it = m_lstDownloaders.Next( it );
}
}
}
void CSessionBlockDownloader::OnConnecting( CHttpDownloader *pDownloader )
{
CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pDownloader->GetUserData(); AssertValidReadPtr( pBlock );
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_CONNECTING;
}
void CSessionBlockDownloader::OnFetch( CHttpDownloader *pDownloader )
{
CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pDownloader->GetUserData(); AssertValidReadPtr( pBlock );
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADING;
}
void CSessionBlockDownloader::OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData )
{
// TODO: Compare downloaded byte size (pDownloader->GetBytesDownloaded()) to size in block
// Write block size into session info on server
int it = m_lstDownloaders.Find( pDownloader );
if ( it == m_lstDownloaders.InvalidIndex() )
{
AssertMsg( 0, "Downloader now found in session block downloader list! This should never happen!" );
return;
}
CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pDownloader->GetUserData(); AssertValidReadPtr( pBlock );
const int nSize = pDownloader->GetSize();
HTTPStatus_t nStatus = pDownloader->GetStatus();
#if _DEBUG
extern ConVar replay_simulatedownloadfailure;
if ( replay_simulatedownloadfailure.GetInt() == 3 )
{
nStatus = HTTP_ERROR;
}
#endif
switch ( nStatus )
{
case HTTP_ABORTED:
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_ABORTED;
break;
case HTTP_DONE:
{
unsigned char aLocalHash[16];
#if _DEBUG
extern ConVar replay_simulate_size_discrepancy;
extern ConVar replay_simulate_bad_hash;
const bool bSizesDiffer = replay_simulate_size_discrepancy.GetBool() || pBlock->m_uFileSize != pDownloader->GetBytesDownloaded();
const bool bHashFail = replay_simulate_bad_hash.GetBool() || !pBlock->ValidateData( pData, nSize, aLocalHash );
#else
const bool bSizesDiffer = pBlock->m_uFileSize != pDownloader->GetBytesDownloaded();
const bool bHashFail = !pBlock->ValidateData( pData, nSize );
#endif
bool bTryAgain = false;
if ( bSizesDiffer )
{
AssertMsg( 0, "Number of bytes downloaded differs from size specified in session info file." );
bTryAgain = true;
}
else if ( bHashFail )
{
DBG( "Download failed - either data validation failed\n" );
// Data validation failed
pBlock->m_bDataInvalid = true;
bTryAgain = true;
}
else
{
DBG( "Data validation successful.\n" );
// Data validation succeeded
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADED;
// Clear out any previous errors
pBlock->m_nHttpError = HTTP_ERROR_NONE;
pBlock->m_bDataInvalid = false;
}
// Failed?
if ( bTryAgain )
{
// Attempt to download again if necessary
pBlock->AttemptToResetForDownload();
// Report error to OGS.
CL_GetErrorSystem()->OGS_ReportSessionBlockDownloadError(
pDownloader, pBlock, pDownloader->GetBytesDownloaded(), m_nMaxBlock, &bSizesDiffer,
&bHashFail, aLocalHash
);
}
}
break;
case HTTP_ERROR:
// If we've attempted and failed to download the block 3 times, report the error and
// put the block in error state.
if ( pBlock->AttemptToResetForDownload() )
break;
// Otherwise, we've max'd out attempts - cache the error state
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_ERROR;
pBlock->m_nHttpError = pDownloader->GetError();
// Now that the block is in the error state, the replay's status will be updated to
// the error state as well (see pSession->UpdateReplayStatuses() below).
// Report the error to user & OGS
{
// Create a session block download error.
CL_GetErrorSystem()->OGS_ReportSessionBlockDownloadError(
pDownloader, pBlock, pDownloader->GetBytesDownloaded(), m_nMaxBlock, NULL, NULL, NULL
);
// Report error to user.
const char *pToken = CHttpDownloader::GetHttpErrorToken( pDownloader->GetError() );
CL_GetErrorSystem()->AddFormattedErrorFromTokenName(
"#Replay_DL_Err_HTTP_Prefix",
new KeyValues(
"args",
"err",
pToken
)
);
}
break;
default:
AssertMsg( 0, "Invalid download state in CSessionBlockDownloader::OnDownloadComplete()" );
}
// Flag block for flush
CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false );
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pBlock->m_hSession ) ); Assert( pSession );
// Update all replays that care about this block
pSession->UpdateReplayStatuses( pBlock );
// Decrement # of downloads
--sm_nNumCurrentDownloads;
}
//----------------------------------------------------------------------------------------
+64
View File
@@ -0,0 +1,64 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//----------------------------------------------------------------------------------------
#ifndef CL_SESSIONBLOCKDOWNLOADER_H
#define CL_SESSIONBLOCKDOWNLOADER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "utllinkedlist.h"
#include "cl_downloader.h"
#include "basethinker.h"
#include "replay/replayhandle.h"
//----------------------------------------------------------------------------------------
class CClientRecordingSession;
//----------------------------------------------------------------------------------------
//
// Downloads multiple session blocks concurrently and updates their status.
//
class CSessionBlockDownloader : public CBaseThinker,
public IDownloadHandler
{
public:
CSessionBlockDownloader();
void Shutdown();
void Think();
// If pSession is NULL, all remaining downloads will affected.
void AbortDownloadsAndCleanup( CClientRecordingSession *pSession );
private:
bool AtMaxConcurrentDownloads() const;
//
// CBaseThinker
//
virtual float GetNextThinkTime() const;
//
// IDownloadHandler
//
virtual void OnConnecting( CHttpDownloader *pDownloader );
virtual void OnFetch( CHttpDownloader *pDownloader );
virtual void OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData );
static int sm_nNumCurrentDownloads;
CUtlLinkedList< CHttpDownloader *, int > m_lstDownloaders;
int m_nMaxBlock;
friend class CErrorSystem;
};
//----------------------------------------------------------------------------------------
#endif // CL_SESSIONBLOCKDOWNLOADER_H
+444
View File
@@ -0,0 +1,444 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "cl_sessioninfodownloader.h"
#include "replay/ienginereplay.h"
#include "replay/shared_defs.h"
#include "cl_recordingsession.h"
#include "cl_recordingsessionblock.h"
#include "cl_replaycontext.h"
#include "cl_sessionblockdownloader.h"
#include "KeyValues.h"
#include "convar.h"
#include "dbg.h"
#include "vprof.h"
#include "sessioninfoheader.h"
#include "utlbuffer.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
CSessionInfoDownloader::CSessionInfoDownloader()
: m_pDownloader( NULL ),
m_pSession( NULL ),
m_flLastDownloadTime( 0.0f ),
m_nError( ERROR_NONE ),
m_nHttpError( HTTP_ERROR_NONE ),
m_bDone( false )
{
}
CSessionInfoDownloader::~CSessionInfoDownloader()
{
Assert( m_pDownloader == NULL ); // We should have deleted the downloader already
}
void CSessionInfoDownloader::CleanupDownloader()
{
if ( m_pDownloader )
{
m_pDownloader->AbortDownloadAndCleanup();
m_pDownloader = NULL;
}
}
void CSessionInfoDownloader::DownloadSessionInfoAndUpdateBlocks( CBaseRecordingSession *pSession )
{
Assert( m_pDownloader == NULL );
// Cache session
m_pSession = pSession;
// Download the session info now
m_pDownloader = new CHttpDownloader( this );
m_pDownloader->BeginDownload( pSession->GetSessionInfoURL(), NULL );
}
float CSessionInfoDownloader::GetNextThinkTime() const
{
extern ConVar replay_sessioninfo_updatefrequency;
return m_flLastDownloadTime + replay_sessioninfo_updatefrequency.GetFloat();
}
void CSessionInfoDownloader::Think()
{
VPROF_BUDGET( "CSessionInfoDownloader::Think", VPROF_BUDGETGROUP_REPLAY );
CBaseThinker::Think();
// If we're not downloading, no need to think
if ( !m_pDownloader )
return;
// If the download's complete
if ( m_pDownloader->IsDone() && m_pDownloader->CanDelete() )
{
// We're done - CanDelete() will now return true
delete m_pDownloader;
m_pDownloader = NULL;
}
else
{
// Otherwise, think...
m_pDownloader->Think();
}
}
void CSessionInfoDownloader::OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData )
{
Assert( pDownloader );
// Clear out any previous error
m_nError = ERROR_NONE;
m_nHttpError = HTTP_ERROR_NONE;
bool bUpdatedSomething = false;
#if _DEBUG
extern ConVar replay_simulatedownloadfailure;
const bool bForceError = replay_simulatedownloadfailure.GetInt() == 2;
#else
const bool bForceError = false;
#endif
if ( pDownloader->GetStatus() != HTTP_DONE || bForceError )
{
m_nError = ERROR_DOWNLOAD_FAILED;
m_nHttpError = pDownloader->GetError();
DBG( "Session info download FAILED.\n" );
}
else
{
Assert( pData );
// Read header
SessionInfoHeader_t header;
if ( !ReadSessionInfoHeader( pData, pDownloader->GetSize(), header ) )
{
m_nError = ERROR_NOT_ENOUGH_DATA;
}
else
{
IF_REPLAY_DBG( Warning( "Session info downloaded successfully for session %s\n", header.m_szSessionName ) );
// Get number of blocks for data validation
const int nNumBlocks = header.m_nNumBlocks;
if ( nNumBlocks <= 0 )
{
m_nError = ERROR_BAD_NUM_BLOCKS;
}
else
{
// The block has either been found or created - now, fill it with data
const char *pSessionName = header.m_szSessionName;
if ( !pSessionName || !pSessionName[0] )
{
m_nError = ERROR_NO_SESSION_NAME;
}
else
{
// Now that we have the session name, find it in the client session manager
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSessionByName( pSessionName ) ); Assert( pSession );
if ( !pSession )
{
AssertMsg( 0, "Session should always exist by this point" );
m_nError = ERROR_UNKNOWN_SESSION;
}
else
{
// Get recording state for session
pSession->m_bRecording = header.m_bRecording;
const CompressorType_t nHeaderCompressorType = header.m_nCompressorType;
uint8 *pPayload = (uint8 *)pData + sizeof( SessionInfoHeader_t );
uint8 *pUncompressedPayload = NULL;
unsigned int uUncompressedPayloadSize = header.m_uPayloadSizeUC;
// Validate the payload with the MD5 digest
bool bPayloadValid = g_pEngine->MD5_HashBuffer( header.m_aHash, (const uint8 *)pPayload, header.m_uPayloadSize, NULL );
if ( !bPayloadValid )
{
m_nError = ERROR_PAYLOAD_HASH_FAILED;
}
else
{
if ( nHeaderCompressorType == COMPRESSORTYPE_INVALID )
{
// Uncompressed payload is read to go
pUncompressedPayload = pPayload;
}
else
{
// Attempt to decompress the payload
ICompressor *pCompressor = CreateCompressor( header.m_nCompressorType );
if ( !pCompressor )
{
bPayloadValid = false;
m_nError = ERROR_COULD_NOT_CREATE_COMPRESSOR;
}
else
{
// Uncompressed size big enough to read at least one block?
if ( header.m_uPayloadSizeUC <= MIN_SESSION_INFO_PAYLOAD_SIZE )
{
bPayloadValid = false;
m_nError = ERROR_INVALID_UNCOMPRESSED_SIZE;
}
else
{
// Attempt to decompress payload now
pUncompressedPayload = new uint8[ uUncompressedPayloadSize ];
if ( !pCompressor->Decompress( (char *)pUncompressedPayload, &uUncompressedPayloadSize, (const char *)pPayload, header.m_uPayloadSize ) )
{
bPayloadValid = false;
m_nError = ERROR_PAYLOAD_DECOMPRESS_FAILED;
}
}
}
}
if ( bPayloadValid )
{
AssertMsg( pUncompressedPayload, "This should never be NULL here." );
AssertMsg( uUncompressedPayloadSize >= MIN_SESSION_INFO_PAYLOAD_SIZE, "This size should always be valid here." );
RecordingSessionBlockSpec_t DummyBlock;
CUtlBuffer buf( pUncompressedPayload, uUncompressedPayloadSize, CUtlBuffer::READ_ONLY );
// Optimization: start the read at the first block we care about, which is one block after the last consecutive, downloaded block.
// This optimization should come in handy on servers that run lengthy rounds, so we're not reiterating over inconsequential blocks
// (i.e. they were already downloaded).
int iStartBlock = pSession->GetGreatestConsecutiveBlockDownloaded() + 1;
if ( iStartBlock > 0 )
{
buf.SeekGet( CUtlBuffer::SEEK_HEAD, iStartBlock * sizeof( RecordingSessionBlockSpec_t ) );
}
// If for some reason the seek caused a 'get' overflow, try reading from the start of the buffer.
if ( !buf.IsValid() )
{
iStartBlock = 0;
buf.SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
}
// Read blocks, starting from the calculated start block.
for ( int i = iStartBlock; i < header.m_nNumBlocks; ++i )
{
// Attempt to read the current block from the buffer
buf.Get( &DummyBlock, sizeof( DummyBlock ) );
if ( !buf.IsValid() )
{
m_nError = ERROR_BLOCK_READ_FAILED;
break;
}
IF_REPLAY_DBG( Warning( "processing block with recon index: %i\n", DummyBlock.m_iReconstruction ) );
// Get reconstruction index
const int iBlockReconstruction = (ReplayHandle_t)DummyBlock.m_iReconstruction;
if ( iBlockReconstruction < 0 )
{
m_nError = ERROR_INVALID_ORDER;
continue;
}
// Check status
const int nRemoteStatus = (int)DummyBlock.m_uRemoteStatus;
if ( nRemoteStatus < 0 || nRemoteStatus >= CBaseRecordingSessionBlock::MAX_STATUS )
{
// Status not found or invalid status
m_nError = ERROR_INVALID_REPLAY_STATUS;
continue;
}
// Get the block file size
const uint32 uFileSize = (uint32)DummyBlock.m_uFileSize;
// Get the uncompressed block size
const uint32 uUncompressedSize = (uint32)DummyBlock.m_uUncompressedSize;
// Get the compressor type
const int nCompressorType = (uint32)DummyBlock.m_nCompressorType;
// Attempt to find the block in the session
CClientRecordingSessionBlock *pBlock = CL_CastBlock( CL_GetRecordingSessionBlockManager()->FindBlockForSession( m_pSession->GetHandle(), iBlockReconstruction ) );
// If the block exists and has already been downloaded, we have nothing more to update
if ( pBlock && !pBlock->NeedsUpdate() )
continue;
bool bBlockDataChanged = false;
// If the block doesn't exist in the session block manager, create it now
if ( !pBlock )
{
CClientRecordingSessionBlock *pNewBlock = CL_CastBlock( CL_GetRecordingSessionBlockManager()->CreateAndGenerateHandle() );
pNewBlock->m_iReconstruction = iBlockReconstruction;
// pNewBlock->m_strFullFilename = Replay_va(
const char *pFullFilename = Replay_va(
"%s%s_part_%i.%s",
pNewBlock->GetPath(),
pSession->m_strName.Get(), pNewBlock->m_iReconstruction,
BLOCK_FILE_EXTENSION
);
V_strcpy( pNewBlock->m_szFullFilename, pFullFilename );
pNewBlock->m_hSession = pSession->GetHandle();
// Add to session block manager
CL_GetRecordingSessionBlockManager()->Add( pNewBlock );
// Add the block to the session (marks session as dirty)
pSession->AddBlock( pNewBlock, false );
// Use the new block
pBlock = pNewBlock;
bBlockDataChanged = true;
}
IF_REPLAY_DBG2( Warning( " Block %i status=%s\n", pBlock->m_iReconstruction, pBlock->GetRemoteStatusStringSafe( pBlock->m_nRemoteStatus ) ) );
// Now that we've got a block, replicate the server data/fill it in
if ( pBlock->m_nRemoteStatus != nRemoteStatus )
{
pBlock->m_nRemoteStatus = (CBaseRecordingSessionBlock::RemoteStatus_t)nRemoteStatus;
bBlockDataChanged = true;
}
// Block file size needs to be set?
if ( pBlock->m_uFileSize != uFileSize )
{
pBlock->m_uFileSize = uFileSize;
bBlockDataChanged = true;
}
// Uncompressed block file size needs to be set?
if ( pBlock->m_uUncompressedSize != uUncompressedSize )
{
Assert( nCompressorType >= COMPRESSORTYPE_INVALID );
pBlock->m_uUncompressedSize = uUncompressedSize;
bBlockDataChanged = true;
}
// Compressor type needs to be set?
if ( pBlock->m_nCompressorType != (CompressorType_t)nCompressorType )
{
pBlock->m_nCompressorType = (CompressorType_t)nCompressorType;
bBlockDataChanged = true;
}
// Attempt to read the hash if we haven't already done so
if ( !pBlock->HasValidHash() )
{
V_memcpy( pBlock->m_aHash, DummyBlock.m_aHash, sizeof( pBlock->m_aHash ) );
bBlockDataChanged = true;
}
// Shift the block's state from waiting to ready-to-download if the block is ready on the server.
if ( pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_WAITING &&
nRemoteStatus == CBaseRecordingSessionBlock::STATUS_READYFORDOWNLOAD )
{
pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_READYTODOWNLOAD;
}
// Save
if ( bBlockDataChanged )
{
CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false );
bUpdatedSomething = true;
}
}
// If this session is not recording, make sure the max number of blocks in the session is in check.
if ( !pSession->m_bRecording && pSession->GetLastBlockToDownload() >= nNumBlocks )
{
// This will adjust all replay max blocks
pSession->AdjustLastBlockToDownload( nNumBlocks - 1 );
}
// If we've updated something, set cache the current time in the session
if ( bUpdatedSomething )
{
pSession->RefreshLastUpdateTime();
}
else if ( pSession->GetLastUpdateTime() >= 0.0f && ( g_pEngine->GetHostTime() - pSession->GetLastUpdateTime() > DOWNLOAD_TIMEOUT_THRESHOLD ) )
{
pSession->OnDownloadTimeout();
}
}
}
}
}
}
}
}
// Display a message for the given download error
if ( m_nError != ERROR_NONE )
{
// Report an error to the user
const char *pErrorToken = GetErrorString( m_nError, m_nHttpError );
if ( m_nError == ERROR_DOWNLOAD_FAILED )
{
KeyValues *pParams = new KeyValues( "args", "url", pDownloader->GetURL() );
CL_GetErrorSystem()->AddFormattedErrorFromTokenName( pErrorToken, pParams );
}
else
{
CL_GetErrorSystem()->AddErrorFromTokenName( pErrorToken );
}
// Report error to OGS
CL_GetErrorSystem()->OGS_ReportSessioInfoDownloadError( pDownloader, pErrorToken );
}
// Flag as done
m_bDone = true;
// Cache download time
m_flLastDownloadTime = g_pEngine->GetHostTime();
}
const char *CSessionInfoDownloader::GetErrorString( int nError, HTTPError_t nHttpError ) const
{
switch ( nError )
{
case ERROR_NO_SESSION_NAME: return "#Replay_DL_Err_SI_NoSessionName";
case ERROR_REPLAY_NOT_FOUND: return "#Replay_DL_Err_SI_ReplayNotFound";
case ERROR_INVALID_REPLAY_STATUS: return "#Replay_DL_Err_SI_InvalidReplayStatus";
case ERROR_INVALID_ORDER: return "#Replay_DL_Err_SI_InvalidOrder";
case ERROR_UNKNOWN_SESSION: return "#Replay_DL_Err_SI_Unknown_Session";
case ERROR_BLOCK_READ_FAILED: return "#Replay_DL_Err_SI_BlockReadFailed";
case ERROR_NOT_ENOUGH_DATA: return "#Replay_DL_Err_SI_NotEnoughData";
case ERROR_COULD_NOT_CREATE_COMPRESSOR: return "#Replay_DL_Err_SI_CouldNotCreateCompressor";
case ERROR_INVALID_UNCOMPRESSED_SIZE: return "#Replay_DL_Err_SI_InvalidUncompressedSize";
case ERROR_PAYLOAD_DECOMPRESS_FAILED: return "#Replay_DL_Err_SI_PayloadDecompressFailed";
case ERROR_PAYLOAD_HASH_FAILED: return "#Replay_DL_Err_SI_PayloadHashFailed";
case ERROR_DOWNLOAD_FAILED:
switch ( m_nHttpError )
{
case HTTP_ERROR_ZERO_LENGTH_FILE: return "#Replay_DL_Err_SI_DownloadFailed_ZeroLengthFile";
case HTTP_ERROR_CONNECTION_CLOSED: return "#Replay_DL_Err_SI_DownloadFailed_ConnectionClosed";
case HTTP_ERROR_INVALID_URL: return "#Replay_DL_Err_SI_DownloadFailed_InvalidURL";
case HTTP_ERROR_INVALID_PROTOCOL: return "#Replay_DL_Err_SI_DownloadFailed_InvalidProtocol";
case HTTP_ERROR_CANT_BIND_SOCKET: return "#Replay_DL_Err_SI_DownloadFailed_CantBindSocket";
case HTTP_ERROR_CANT_CONNECT: return "#Replay_DL_Err_SI_DownloadFailed_CantConnect";
case HTTP_ERROR_NO_HEADERS: return "#Replay_DL_Err_SI_DownloadFailed_NoHeaders";
case HTTP_ERROR_FILE_NONEXISTENT: return "#Replay_DL_Err_SI_DownloadFailed_FileNonExistent";
default: return "#Replay_DL_Err_SI_DownloadFailed_UnknownError";
}
}
return "#Replay_DL_Err_SI_Unknown";
}
//----------------------------------------------------------------------------------------
+82
View File
@@ -0,0 +1,82 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//----------------------------------------------------------------------------------------
#ifndef SESSIONINFODOWNLOADER_H
#define SESSIONINFODOWNLOADER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "basethinker.h"
#include "cl_downloader.h"
//----------------------------------------------------------------------------------------
class CHttpDownloader;
class CBaseRecordingSession;
//----------------------------------------------------------------------------------------
class CSessionInfoDownloader : public CBaseThinker,
public IDownloadHandler
{
public:
CSessionInfoDownloader();
~CSessionInfoDownloader();
void CleanupDownloader();
void DownloadSessionInfoAndUpdateBlocks( CBaseRecordingSession *pSession );
bool IsDone() const { return m_bDone; }
bool CanDelete() const { return m_pDownloader == NULL; }
enum ServerSessionInfoError_t
{
ERROR_NONE, // No error
ERROR_NOT_ENOUGH_DATA, // The session info file wasn't even big enough to read a header
ERROR_BAD_NUM_BLOCKS, // The "nb" field either didn't exist or was invalid - there should always been at least one block by the time we're downloading
ERROR_REPLAY_NOT_FOUND, // The server index was downloaded but the replay was not found
ERROR_INVALID_REPLAY_STATUS, // The server index was downloaded and the replay was found, but it had an invalid status
ERROR_INVALID_ORDER, // The server index was downloaded and the replay was found, but it had an invalid reconstruction order (-1)
ERROR_NO_SESSION_NAME, // No session name for entry
ERROR_UNKNOWN_SESSION, // The session info file points to a session (via its name) that the client doesn't know about
ERROR_DOWNLOAD_FAILED, // The session file failed to download
ERROR_BLOCK_READ_FAILED, // Failed to read a block - most likely an overflow
ERROR_COULD_NOT_CREATE_COMPRESSOR, // Could not create the ICompressor to decompress the payload
ERROR_INVALID_UNCOMPRESSED_SIZE, // Uncompressed size was not large enough to read at least one block
ERROR_PAYLOAD_DECOMPRESS_FAILED, // Decompression of the payload failed
ERROR_PAYLOAD_HASH_FAILED, // Used MD5 digest from header on payload and failed
};
ServerSessionInfoError_t m_nError;
HTTPError_t m_nHttpError;
private:
//
// CBaseThinker
//
float GetNextThinkTime() const;
void Think();
//
// IDownloadHandler
//
virtual void OnConnecting( CHttpDownloader *pDownloader ) {}
virtual void OnFetch( CHttpDownloader *pDownloader ) {}
virtual void OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData );
const char *GetErrorString( int nError, HTTPError_t nHttpError ) const;
const CBaseRecordingSession *m_pSession;
CHttpDownloader *m_pDownloader;
bool m_bDone;
float m_flLastDownloadTime;
};
//----------------------------------------------------------------------------------------
#endif // SESSIONINFODOWNLOADER_H
+87
View File
@@ -0,0 +1,87 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/basereplayserializeable.h"
#include "replay/replayutils.h"
#include "KeyValues.h"
#include "tier1/strtools.h"
#include "replay/shared_defs.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CBaseReplaySerializeable::CBaseReplaySerializeable()
: m_hThis( REPLAY_HANDLE_INVALID ),
m_bLocked( false )
{
}
void CBaseReplaySerializeable::SetHandle( ReplayHandle_t h )
{
m_hThis = h;
}
ReplayHandle_t CBaseReplaySerializeable::GetHandle() const
{
return m_hThis;
}
bool CBaseReplaySerializeable::Read( KeyValues *pIn )
{
m_hThis = (ReplayHandle_t)pIn->GetInt( "handle" );
return true;
}
void CBaseReplaySerializeable::Write( KeyValues *pOut )
{
pOut->SetInt( "handle", (int)m_hThis );
}
const char *CBaseReplaySerializeable::GetFullFilename() const
{
const char *pPath = GetPath();
const char *pFilename = GetFilename();
if ( !pPath || !pPath[0] || !pFilename || !pFilename[0] )
return NULL;
return Replay_va( "%s%s", pPath, pFilename );
}
const char *CBaseReplaySerializeable::GetFilename() const
{
return Replay_va( "%s.%s", GetSubKeyTitle(), GENERIC_FILE_EXTENSION );
}
const char *CBaseReplaySerializeable::GetDebugName() const
{
return GetSubKeyTitle();
}
void CBaseReplaySerializeable::SetLocked( bool bLocked )
{
m_bLocked = bLocked;
}
bool CBaseReplaySerializeable::IsLocked() const
{
return m_bLocked;
}
void CBaseReplaySerializeable::OnDelete()
{
}
void CBaseReplaySerializeable::OnUnload()
{
}
void CBaseReplaySerializeable::OnAddedToDirtyList()
{
}
//----------------------------------------------------------------------------------------
+15
View File
@@ -0,0 +1,15 @@
//-----------------------------------------------------------------------------
// HEADERS.VPC - Allows for sync'd inclusion of headers for replay_common.lib
// other vpc's.
//-----------------------------------------------------------------------------
$Folder $REPLAY_COMMON_HEADERS_TITLE
{
$File "$SRCDIR\common\replay\replay.h"
$File "$SRCDIR\common\replay\replaylib.h"
$File "$SRCDIR\common\replay\performance.h"
$File "$SRCDIR\common\replay\replaytime.h"
$File "$SRCDIR\common\replay\replayutils.h"
$File "$SRCDIR\common\replay\screenshot.h"
$File "$SRCDIR\common\replay\shared_defs.h"
}
+96
View File
@@ -0,0 +1,96 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/performance.h"
#include "replay/iclientreplaycontext.h"
#include "replay/ireplayperformancemanager.h"
#include "replay/replayutils.h"
#include "KeyValues.h"
#include "fmtstr.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IClientReplayContext *g_pClientReplayContext;
//----------------------------------------------------------------------------------------
CReplayPerformance::CReplayPerformance( CReplay *pReplay )
: m_pReplay( pReplay ),
m_nTickIn( -1 ),
m_nTickOut( -1 )
{
Assert( pReplay );
m_szBaseFilename[ 0 ] = '\0';
m_wszTitle[0] = L'\0';
}
CReplayPerformance::CReplayPerformance( const CReplayPerformance *pPerformance )
{
Copy( pPerformance );
}
void CReplayPerformance::Read( KeyValues *pIn )
{
SetFilename( pIn->GetString( "filename" ) );
m_nTickIn = pIn->GetInt( "tick_in", -1 );
m_nTickOut = pIn->GetInt( "tick_out", -1 );
V_wcsncpy( m_wszTitle, pIn->GetWString( "title" ), sizeof( m_wszTitle ) );
}
void CReplayPerformance::Write( KeyValues *pOut )
{
pOut->SetString( "filename", m_szBaseFilename );
pOut->SetInt( "tick_in", m_nTickIn );
pOut->SetInt( "tick_out", m_nTickOut );
pOut->SetWString( "title", m_wszTitle );
}
void CReplayPerformance::Copy( const CReplayPerformance *pSrc )
{
V_wcsncpy( m_wszTitle, pSrc->m_wszTitle, sizeof( m_wszTitle ) );
V_strcpy( m_szBaseFilename, pSrc->m_szBaseFilename );
m_pReplay = pSrc->m_pReplay;
CopyTicks( pSrc );
}
void CReplayPerformance::CopyTicks( const CReplayPerformance *pSrc )
{
m_nTickIn = pSrc->m_nTickIn;
m_nTickOut = pSrc->m_nTickOut;
}
void CReplayPerformance::SetFilename( const char *pFilename )
{
V_strcpy( m_szBaseFilename, pFilename );
}
const char *CReplayPerformance::GetFullPerformanceFilename()
{
return Replay_va( "%s%s", g_pClientReplayContext->GetPerformanceManager()->GetFullPath(), m_szBaseFilename );
}
void CReplayPerformance::AutoNameIfHasNoTitle( const char *pMapName )
{
if ( !m_wszTitle[ 0 ] )
{
Replay_GetAutoName( m_wszTitle, sizeof( m_wszTitle ), pMapName );
}
}
void CReplayPerformance::SetTitle( const wchar_t *pTitle )
{
V_wcsncpy( m_wszTitle, pTitle, sizeof( m_wszTitle ) );
}
CReplayPerformance *CReplayPerformance::MakeCopy() const
{
return new CReplayPerformance( this );
}
//----------------------------------------------------------------------------------------
+374
View File
@@ -0,0 +1,374 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/replay.h"
#include "replay/iclientreplaycontext.h"
#include "replay/ireplaymanager.h"
#include "replay/replayutils.h"
#include "replay/screenshot.h"
#include "replay/shared_defs.h"
#include "replay/ireplayscreenshotmanager.h"
#include "replay/ireplayperformancemanager.h"
#include "replay/performance.h"
#include "KeyValues.h"
#include "filesystem.h"
#include "vgui/ILocalize.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IClientReplayContext *g_pClientReplayContext;
extern vgui::ILocalize *g_pVGuiLocalize;
//----------------------------------------------------------------------------------------
CReplay::CReplay()
: m_pDownloadEventHandler( NULL ),
m_pUserData( NULL ),
m_bComplete( false ),
m_bRequestedByUser( false ),
m_bSaved( false ),
m_bRendered( false ),
m_bDirty( false ),
m_bSavedDuringThisSession( true ),
m_flLength( 0 ),
m_nPlayerSlot( -1 ),
m_nSpawnTick( -1 ),
m_nDeathTick( -1 ),
m_iMaxSessionBlockRequired( 0 ),
m_nStatus( REPLAYSTATUS_INVALID ),
m_hSession( REPLAY_HANDLE_INVALID ),
m_pFileURL( NULL ),
m_nPostDeathRecordTime( 0 ),
m_flStartTime( 0.0f ),
m_flNextUpdateTime( 0.0f )
{
m_wszTitle[0] = L'\0';
m_szMapName[0] = 0;
}
bool CReplay::IsDownloaded() const
{
return m_nStatus == REPLAYSTATUS_READYTOCONVERT;
}
const CReplayPerformance *CReplay::GetPerformance( int i ) const
{
return const_cast< CReplay * >( this )->GetPerformance( i );
}
CReplayPerformance *CReplay::GetPerformance( int i )
{
if ( i < 0 || i >= m_vecPerformances.Count() )
return NULL;
return m_vecPerformances[ i ];
}
bool CReplay::FindPerformance( CReplayPerformance *pPerformance, int &iResult )
{
const int it = m_vecPerformances.Find( pPerformance );
if ( it == m_vecPerformances.InvalidIndex() )
{
iResult = -1;
return false;
}
iResult = it;
return true;
}
CReplayPerformance *CReplay::GetPerformanceWithTitle( const wchar_t *pTitle )
{
FOR_EACH_VEC( m_vecPerformances, i )
{
CReplayPerformance *pCurPerformance = m_vecPerformances[ i ];
if ( !V_wcscmp( pTitle, pCurPerformance->m_wszTitle ) )
{
return pCurPerformance;
}
}
return NULL;
}
CReplayPerformance *CReplay::AddNewPerformance( bool bGenTitle/*=true*/, bool bGenFilename/*=true*/ )
{
// Create a performance
IReplayPerformanceManager *pPerformanceManager = g_pClientReplayContext->GetPerformanceManager();
CReplayPerformance *pPerformance = pPerformanceManager->CreatePerformance( this );
if ( bGenTitle )
{
// Give the performance a name
pPerformance->AutoNameIfHasNoTitle( m_szMapName );
}
if ( bGenFilename )
{
// Generate a filename for the new performance
pPerformance->SetFilename( pPerformanceManager->GeneratePerformanceFilename( this ) );
}
// Cache
m_vecPerformances.AddToTail( pPerformance );
return pPerformance;
}
void CReplay::AddPerformance( KeyValues *pIn )
{
// Create a performance
IReplayPerformanceManager *pPerformanceManager = g_pClientReplayContext->GetPerformanceManager();
CReplayPerformance *pPerformance = pPerformanceManager->CreatePerformance( this );
// Read
pPerformance->Read( pIn );
// Cache
m_vecPerformances.AddToTail( pPerformance );
}
void CReplay::AddPerformance( CReplayPerformance *pPerformance )
{
Assert( pPerformance );
m_vecPerformances.AddToTail( pPerformance );
}
const CReplayTime &CReplay::GetItemDate() const
{
return m_RecordTime;
}
bool CReplay::IsItemRendered() const
{
return m_bRendered;
}
CReplay *CReplay::GetItemReplay()
{
return this;
}
ReplayHandle_t CReplay::GetItemReplayHandle() const
{
return GetHandle();
}
QueryableReplayItemHandle_t CReplay::GetItemHandle() const
{
return GetHandle();
}
const wchar_t *CReplay::GetItemTitle() const
{
return m_wszTitle;
}
void CReplay::SetItemTitle( const wchar_t *pTitle )
{
V_wcsncpy( m_wszTitle, pTitle, sizeof( m_wszTitle ) );
}
float CReplay::GetItemLength() const
{
return m_flLength;
}
void *CReplay::GetUserData()
{
return m_pUserData;
}
void CReplay::SetUserData( void* pUserData )
{
m_pUserData = pUserData;
}
bool CReplay::IsItemAMovie() const
{
return false;
}
void CReplay::AddScreenshot( int nWidth, int nHeight, const char *pBaseFilename )
{
m_vecScreenshots.AddToTail( new CReplayScreenshot( nWidth, nHeight, pBaseFilename ) );
}
void CReplay::AutoNameTitleIfEmpty()
{
// Autoname it
if ( !m_wszTitle[0] )
{
Replay_GetAutoName( m_wszTitle, sizeof( m_wszTitle ), m_szMapName );
}
}
const char *CReplay::GetSubKeyTitle() const
{
return Replay_va( "replay_%i", GetHandle() );
}
const char *CReplay::GetPath() const
{
return Replay_va( "%s%s%c", g_pClientReplayContext->GetBaseDir(), SUBDIR_REPLAYS, CORRECT_PATH_SEPARATOR );
}
void CReplay::OnDelete()
{
BaseClass::OnDelete();
// Delete reconstructed replay if one exists
if ( HasReconstructedReplay() )
{
g_pFullFileSystem->RemoveFile( m_strReconstructedFilename.Get() );
}
// Delete screenshots
g_pClientReplayContext->GetScreenshotManager()->DeleteScreenshotsForReplay( this );
// TODO: Delete performance(s)
}
bool CReplay::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_hSession = (ReplayHandle_t)pIn->GetInt( "session", REPLAY_HANDLE_INVALID );
V_strcpy_safe( m_szMapName, pIn->GetString( "map", "" ) );
m_nSpawnTick = pIn->GetInt( "spawn_tick", -1 );
m_nDeathTick = pIn->GetInt( "death_tick", -1 );
m_nStatus = static_cast< CReplay::ReplayStatus_t >( pIn->GetInt( "status", (int)CReplay::REPLAYSTATUS_INVALID ) );
m_bComplete = pIn->GetInt( "complete" ) != 0;
m_flLength = pIn->GetFloat( "length" );
m_nPostDeathRecordTime = pIn->GetInt( "postdeathrecordtime" );
m_bRendered = pIn->GetInt( "rendered" ) != 0;
m_nPlayerSlot = pIn->GetInt( "player_slot", -1 );
m_iMaxSessionBlockRequired = pIn->GetInt( "max_block", 0 ); Assert( m_iMaxSessionBlockRequired >= 0 );
m_flStartTime = pIn->GetFloat( "start_time", -1.0f ); Assert( m_flStartTime >= 0.0f );
V_wcsncpy( m_wszTitle, pIn->GetWString( "title" ), sizeof( m_wszTitle ) );
// Read reconstructed filename and infer path
const char *pReplaysDir = g_pClientReplayContext->GetReplayManager()->GetReplaysDir();
const char *pReconFilename = pIn->GetString( "recon_filename" );
if ( pReconFilename[0] != 0 )
{
m_strReconstructedFilename = Replay_va( "%s%s", pReplaysDir, pReconFilename );
}
// Read screenshots
KeyValues *pScreenshots = pIn->FindKey( "screenshots" );
if ( pScreenshots )
{
FOR_EACH_TRUE_SUBKEY( pScreenshots, pScreenshot )
{
int nWidth = pScreenshot->GetInt( "width" );
int nHeight = pScreenshot->GetInt( "height" );
const char *pBaseFilename = pScreenshot->GetString( "base_filename" );
AddScreenshot( nWidth, nHeight, pBaseFilename );
}
}
// Read performances
KeyValues *pPerformances = pIn->FindKey( "edits" );
if ( pPerformances )
{
FOR_EACH_TRUE_SUBKEY( pPerformances, pPerformance )
{
AddPerformance( pPerformance );
}
}
// Record time
KeyValues *pRecordTimeSubKey = pIn->FindKey( "record_time" );
if ( pRecordTimeSubKey )
{
m_RecordTime.Read( pRecordTimeSubKey );
}
// Mark replay as saved, since it was just loaded from disk
m_bSaved = true;
return true;
}
void CReplay::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetString( "map", m_szMapName );
pOut->SetInt( "session", m_hSession );
pOut->SetInt( "spawn_tick", m_nSpawnTick );
pOut->SetInt( "death_tick", m_nDeathTick );
pOut->SetInt( "status", static_cast< int >( m_nStatus ) );
pOut->SetInt( "complete", static_cast< int >( m_bComplete ) );
pOut->SetFloat( "length", m_flLength );
pOut->SetInt( "postdeathrecordtime", m_nPostDeathRecordTime );
pOut->SetInt( "rendered", m_bRendered );
pOut->SetInt( "player_slot", m_nPlayerSlot );
pOut->SetInt( "max_block", m_iMaxSessionBlockRequired );
pOut->SetFloat( "start_time", m_flStartTime );
pOut->SetWString( "title", m_wszTitle );
// Store only filename for reconstructed .dem
if ( !m_strReconstructedFilename.IsEmpty() )
{
const char *pReconFilename = V_UnqualifiedFileName( m_strReconstructedFilename.Get() );
if ( pReconFilename[0] )
{
pOut->SetString( "recon_filename", pReconFilename );
}
}
// Write screenshots
KeyValues *pScreenshots = new KeyValues( "screenshots" );
pOut->AddSubKey( pScreenshots );
for ( int i = 0; i < m_vecScreenshots.Count(); ++i )
{
KeyValues *pScreenshotOut = new KeyValues( "screenshot" );
CReplayScreenshot *pScreenshot = m_vecScreenshots[ i ];
pScreenshotOut->SetInt( "width", pScreenshot->m_nWidth );
pScreenshotOut->SetInt( "height", pScreenshot->m_nHeight );
pScreenshotOut->SetString( "base_filename", pScreenshot->m_szBaseFilename );
pScreenshots->AddSubKey( pScreenshotOut );
}
// Write performances
KeyValues *pPerformances = new KeyValues( "edits" );
pOut->AddSubKey( pPerformances );
for ( int i = 0; i < m_vecPerformances.Count(); ++i )
{
KeyValues *pPerfOut = new KeyValues( "edit" );
CReplayPerformance *pPerformance = m_vecPerformances[ i ];
pPerformance->Write( pPerfOut );
pPerformances->AddSubKey( pPerfOut );
}
KeyValues *pRecordTime = new KeyValues( "record_time" );
pOut->AddSubKey( pRecordTime );
m_RecordTime.Write( pRecordTime );
// Mark as saved
m_bSaved = true;
}
bool CReplay::HasReconstructedReplay() const
{
return !m_strReconstructedFilename.IsEmpty() &&
g_pFullFileSystem->FileExists( m_strReconstructedFilename.Get() );
}
bool CReplay::IsSignificantBlock( int iBlockReconstruction ) const
{
return iBlockReconstruction <= m_iMaxSessionBlockRequired;
}
void CReplay::OnComplete()
{
AutoNameTitleIfEmpty();
}
//----------------------------------------------------------------------------------------
+36
View File
@@ -0,0 +1,36 @@
//-----------------------------------------------------------------------------
// REPLAY_COMMON.VPC
//
// Project Script
//-----------------------------------------------------------------------------
$Macro SRCDIR "..\.."
$Macro REPLAY_COMMON_HEADERS_TITLE "Headers"
$Macro OUTLIBDIR "$LIBCOMMON"
$include "$SRCDIR\vpc_scripts\source_lib_base.vpc"
$Configuration
{
$Compiler
{
$AdditionalIncludeDirectories "$BASE;$SRCDIR\dx9sdk\include" [$WINDOWS]
$AdditionalIncludeDirectories "$BASE;$SRCDIR\x360xdk\include\win32\vs2005" [$WINDOWS]
}
}
$Project "replay_common"
{
$Folder "Source Files"
{
$File "replay.cpp"
$File "replaylib.cpp"
$File "basereplayserializeable.cpp"
$File "performance.cpp"
$File "replaytime.cpp"
$File "replayutils.cpp"
$File "screenshot.cpp"
}
$Include "$SRCDIR\replay\common\headers.vpc"
}
+27
View File
@@ -0,0 +1,27 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/replaylib.h"
#include "replay/replayutils.h"
#include "replay/iclientreplaycontext.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
IClientReplayContext *g_pClientReplayContext = NULL;
//----------------------------------------------------------------------------------------
bool ReplayLib_Init( const char *pGameDir, IClientReplayContext *pClientReplayContext )
{
Replay_SetGameDir( pGameDir );
g_pClientReplayContext = pClientReplayContext;
return true;
}
//----------------------------------------------------------------------------------------
+274
View File
@@ -0,0 +1,274 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/replaytime.h"
#include "KeyValues.h"
#include <time.h>
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CReplayTime::CReplayTime()
: m_fDate( 0 ),
m_fTime( 0 )
{
}
void CReplayTime::InitDateAndTimeToNow()
{
tm now;
VCRHook_LocalTime( &now );
SetDate( now.tm_mday, now.tm_mon + 1, now.tm_year + 1900 );
SetTime( now.tm_hour, now.tm_min, now.tm_sec );
}
void CReplayTime::SetDate( int nDay, int nMonth, int nYear )
{
Assert( nDay >= 1 && nDay <= 31 );
Assert( nMonth >= 1 && nMonth <= 12 );
Assert( nYear >= 2009 && nYear <= 2136 );
m_fDate = nDay - 1;
m_fDate |= ( ( nMonth - 1 ) << 5 );
m_fDate |= ( ( nYear - 2009 ) << 9 );
#ifdef _DEBUG
int nDbgDay, nDbgMonth, nDbgYear;
GetDate( nDbgDay, nDbgMonth, nDbgYear );
Assert( nDay == nDbgDay );
Assert( nMonth == nDbgMonth );
Assert( nYear == nDbgYear );
#endif
}
void CReplayTime::GetDate( int &nDay, int &nMonth, int &nYear ) const
{
nDay = 1 + ( m_fDate & 0x1F ); // Bits 0-4 for day
nMonth = 1 + ( ( m_fDate >> 5 ) & 0x0F ); // Bits 5-8 for month
nYear = 2009 + ( ( m_fDate >> 9 ) & 0x7F ); // Bits 9-15 for year
Assert( nDay >= 1 && nDay <= 31 );
Assert( nMonth >= 1 && nMonth <= 12 );
Assert( nYear >= 2009 && nYear <= 2136 );
}
void CReplayTime::SetTime( int nHour, int nMin, int nSec )
{
Assert( nHour >= 0 && nHour <= 23 );
Assert( nMin >= 0 && nMin <= 59 );
Assert( nSec >= 0 && nSec <= 59 );
m_fTime = nHour;
m_fTime |= ( ( nMin ) << 5 );
m_fTime |= ( ( nSec ) << 11 );
#ifdef _DEBUG
int nDbgHour, nDbgMin, nDbgSec;
GetTime( nDbgHour, nDbgMin, nDbgSec );
Assert( nHour == nDbgHour );
Assert( nMin == nDbgMin );
Assert( nSec == nDbgSec );
#endif
}
void CReplayTime::GetTime( int &nHour, int &nMin, int &nSec ) const
{
nHour = m_fTime & 0x1F; // Bits 0-4 for hour
nMin = ( m_fTime >> 5 ) & 0x3F; // Bits 5-10 for min
nSec = ( m_fTime >> 11 ) & 0x3F; // Bits 11-16 for sec
Assert( nHour >= 0 && nHour <= 23 );
Assert( nMin >= 0 && nMin <= 59 );
Assert( nSec >= 0 && nSec <= 59 );
}
void CReplayTime::Read( KeyValues *pIn )
{
m_fDate = pIn->GetInt( "date" );
m_fTime = pIn->GetInt( "time" );
}
void CReplayTime::Write( KeyValues *pOut )
{
pOut->SetInt( "date", m_fDate );
pOut->SetInt( "time", m_fTime );
}
/*static*/ const wchar_t *CReplayTime::GetLocalizedMonth( vgui::ILocalize *pLocalize, int nMonth )
{
char szMonthKey[32]; // Get localized month
V_snprintf( szMonthKey, sizeof( szMonthKey ), "#Month_%i", nMonth );
wchar_t *pResult = pLocalize->Find( szMonthKey );
return pResult ? pResult : L"";
}
/*static*/ const wchar_t *CReplayTime::GetLocalizedDay( vgui::ILocalize *pLocalize, int nDay )
{
char szDay[8]; // Convert day to wide
static wchar_t s_wDay[8];
V_snprintf( szDay, sizeof( szDay ), "%i", nDay );
pLocalize->ConvertANSIToUnicode( szDay, s_wDay, sizeof( s_wDay ) );
return s_wDay;
}
/*static*/ const wchar_t *CReplayTime::GetLocalizedYear( vgui::ILocalize *pLocalize, int nYear )
{
char szYear[8]; // Convert year to wide
static wchar_t s_wYear[8];
V_snprintf( szYear, sizeof( szYear ), "%i", nYear );
pLocalize->ConvertANSIToUnicode( szYear, s_wYear, sizeof( s_wYear ) );
return s_wYear;
}
/*static*/ const wchar_t *CReplayTime::GetLocalizedTime( vgui::ILocalize *pLocalize, int nHour, int nMin, int nSec )
{
char szTime[16]; // Convert time to wide
static wchar_t s_wTime[16];
V_snprintf( szTime, sizeof( szTime ), "%i:%02i %s", nHour % 12, nMin, nHour < 12 ? "AM" : "PM" );
pLocalize->ConvertANSIToUnicode( szTime, s_wTime, sizeof( s_wTime ) );
return s_wTime;
}
/*static*/ const wchar_t *CReplayTime::GetLocalizedDate( vgui::ILocalize *pLocalize, const CReplayTime &t,
bool bForceFullFormat/*=false*/ )
{
int nHour, nMin, nSec;
int nDay, nMonth, nYear;
t.GetTime( nHour, nMin, nSec );
t.GetDate( nDay, nMonth, nYear );
return GetLocalizedDate( pLocalize, nDay, nMonth, nYear, &nHour, &nMin, &nSec, bForceFullFormat );
}
/*static*/ const wchar_t *CReplayTime::GetLocalizedDate( vgui::ILocalize *pLocalize, int nDay, int nMonth, int nYear,
int *pHour/*=NULL*/, int *pMin/*=NULL*/, int *pSec/*=NULL*/,
bool bForceFullFormat/*=false*/ )
{
static wchar_t s_wBuf[256];
// Is this collection for replays from today?
time_t today;
time( &today );
tm *pNowTime = localtime( &today );
bool bToday = ( pNowTime->tm_mday == nDay ) && ( pNowTime->tm_mon + 1 == nMonth ) && ( 1900 + pNowTime->tm_year == nYear );
// Yesterday?
time_t yesterday = today - time_t( 86400 );
tm *pYesterdayTime = localtime( &yesterday );
bool bYesterday = ( pYesterdayTime->tm_mday == nDay ) && ( pYesterdayTime->tm_mon + 1 == nMonth ) && ( 1900 + pYesterdayTime->tm_year == nYear );
const wchar_t *pMonth = GetLocalizedMonth( pLocalize, nMonth );
const wchar_t *pDay = GetLocalizedDay( pLocalize, nDay );
const wchar_t *pYear = GetLocalizedYear( pLocalize, nYear );
const wchar_t *pToday = pLocalize->Find( "#Replay_Today" );
const wchar_t *pYesterday = pLocalize->Find( "#Replay_Yesterday" );
bool bTime = pHour && pMin && pSec;
// Include time in formatted string?
if ( bTime )
{
const wchar_t *pTime = GetLocalizedTime( pLocalize, *pHour, *pMin, *pSec );
if ( bForceFullFormat || ( !bToday && !bYesterday ) )
{
pLocalize->ConstructString(
s_wBuf,
sizeof( s_wBuf ),
pLocalize->Find( "#Replay_DateAndTime" ),
4,
pMonth, pDay, pYear, pTime
);
}
else
{
pLocalize->ConstructString(
s_wBuf,
sizeof( s_wBuf ),
pLocalize->Find( "#Replay_SingleWordDateAndTime" ),
2,
bToday ? pToday : pYesterday,
pTime
);
}
}
else
{
if ( !bToday && !bYesterday )
{
pLocalize->ConstructString(
s_wBuf,
sizeof( s_wBuf ),
pLocalize->Find( "#Replay_Date" ),
3,
pMonth, pDay, pYear
);
}
else
{
V_wcsncpy( s_wBuf, bToday ? pToday : pYesterday, sizeof( s_wBuf ) );
}
}
return s_wBuf;
}
/*static*/ const char *CReplayTime::FormatTimeString( int nSecs )
{
static int nWhichStr = 0;
static const int nNumStrings = 2;
static const int nStrLen = 32;
static char s_szResult[nNumStrings][nStrLen];
char *pResult = s_szResult[ nWhichStr ];
int nSeconds = nSecs % 60;
int nMins = nSecs / 60;
int nHours = nMins / 60;
nMins %= 60;
if ( nHours > 0 )
{
V_snprintf( pResult, nStrLen, "%i:%02i:%02i", nHours, nMins, nSeconds );
}
else
{
V_snprintf( pResult, nStrLen, "%02i:%02i", nMins, nSeconds );
}
nWhichStr = ( nWhichStr + 1 ) % nNumStrings;
return pResult;
}
/*static*/ const char *CReplayTime::FormatPreciseTimeString( float flSecs )
{
static int nWhichStr = 0;
static const int nNumStrings = 2;
static const int nStrLen = 32;
static char s_szResult[nNumStrings][nStrLen];
char *pResult = s_szResult[ nWhichStr ];
int nSecs = (int)flSecs;
int nMins = ( nSecs % 3600 ) / 60;
int nSeconds = nSecs % 60;
int nMilliseconds = (flSecs - (float)nSecs) * 10.0f;
V_snprintf( pResult, nStrLen, "%02i:%02i:%02i", nMins, nSeconds, nMilliseconds );
nWhichStr = ( nWhichStr + 1 ) % nNumStrings;
return pResult;
}
//----------------------------------------------------------------------------------------
+130
View File
@@ -0,0 +1,130 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/replayutils.h"
#include "dbg.h"
#include "strtools.h"
#include "qlimits.h"
#include "filesystem.h"
#include "replay/replaytime.h"
#include "fmtstr.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
static char gs_szGameDir[MAX_OSPATH];
//----------------------------------------------------------------------------------------
void Replay_GetFirstAvailableFilename( char *pDst, int nDstLen, const char *pIdealFilename, const char *pExt,
const char *pFilePath, int nStartIndex )
{
// Strip extension from ideal filename
char szIdealFilename[ MAX_OSPATH ];
V_StripExtension( pIdealFilename, szIdealFilename, sizeof( szIdealFilename ) );
int i = nStartIndex;
while ( 1 )
{
V_strncpy( pDst, szIdealFilename, nDstLen );
V_strcat( pDst, Replay_va( "_%i%s", i, pExt ), nDstLen );
// Get a potential working path/filename
CFmtStr fmtTestFilename(
"%s%c%s",
pFilePath,
CORRECT_PATH_SEPARATOR,
pDst
);
// Make sure slashes are correct for platform
V_FixSlashes( fmtTestFilename.Access() );
// Fix up double slashes
V_FixDoubleSlashes( fmtTestFilename.Access() );
if ( !g_pFullFileSystem->FileExists( fmtTestFilename ) )
break;
++i;
}
}
//----------------------------------------------------------------------------------------
void Replay_ConstructReplayFilenameString( CUtlString &strOut, const char *pReplaySubDir, const char *pFilename, const char *pGameDir )
{
// Construct full filename
strOut.Format( "%s%creplays%c%s%c%s", pGameDir,
CORRECT_PATH_SEPARATOR, CORRECT_PATH_SEPARATOR, pReplaySubDir,
CORRECT_PATH_SEPARATOR, pFilename
);
}
//----------------------------------------------------------------------------------------
char *Replay_va( const char *format, ... )
{
va_list argptr;
static char string[8][512];
static int curstring = 0;
curstring = ( curstring + 1 ) % 8;
va_start (argptr, format);
Q_vsnprintf( string[curstring], sizeof( string[curstring] ), format, argptr );
va_end (argptr);
return string[curstring];
}
//----------------------------------------------------------------------------------------
void Replay_SetGameDir( const char *pGameDir )
{
V_strcpy( gs_szGameDir, pGameDir );
}
//----------------------------------------------------------------------------------------
const char *Replay_GetGameDir()
{
return gs_szGameDir;
}
//----------------------------------------------------------------------------------------
const char *Replay_GetBaseDir()
{
return Replay_va(
"%s%creplays%c",
Replay_GetGameDir(),
CORRECT_PATH_SEPARATOR,
CORRECT_PATH_SEPARATOR
);
}
//----------------------------------------------------------------------------------------
void Replay_GetAutoName( wchar_t *pDest, int nDestSize, const char *pMapName )
{
// Get date/time
CReplayTime now;
now.InitDateAndTimeToNow();
// Convert map name to unicode
wchar_t wszMapName[256];
extern vgui::ILocalize *g_pVGuiLocalize;
g_pVGuiLocalize->ConvertANSIToUnicode( pMapName, wszMapName, sizeof( wszMapName ) );
// Get localized date as string
const wchar_t *pLocalizedDate = CReplayTime::GetLocalizedDate( g_pVGuiLocalize, now, true );
// Create title
g_pVGuiLocalize->ConstructString( pDest, nDestSize, L"%s1: %s2", 2, wszMapName, pLocalizedDate );
}
//----------------------------------------------------------------------------------------
+42
View File
@@ -0,0 +1,42 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay/screenshot.h"
#include "replay/replayutils.h"
#include "replay/iclientreplaycontext.h"
#include "KeyValues.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
bool CReplayScreenshot::Read( KeyValues *pIn )
{
m_nWidth = pIn->GetInt( "w" );
m_nHeight = pIn->GetInt( "h" );
V_strcpy_safe( m_szBaseFilename, pIn->GetString( "file", "" ) );
return true;
}
void CReplayScreenshot::Write( KeyValues *pOut )
{
pOut->SetInt( "w", m_nWidth );
pOut->SetInt( "h", m_nHeight );
pOut->SetString( "file", m_szBaseFilename );
}
const char *CReplayScreenshot::GetSubKeyTitle() const
{
return m_szBaseFilename;
}
const char *CReplayScreenshot::GetPath() const
{
extern IClientReplayContext *g_pClientReplayContext;
return Replay_va( "%s%s%c", g_pClientReplayContext->GetBaseDir(), SUBDIR_SCREENSHOTS, CORRECT_PATH_SEPARATOR );
}
//----------------------------------------------------------------------------------------
+229
View File
@@ -0,0 +1,229 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "compression.h"
#include "replay/ienginereplay.h"
#include "replay/replayutils.h"
#include "convar.h"
#include "filesystem.h"
#include "fmtstr.h"
#include "../utils/bzip2/bzlib.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
const char *g_pCompressorTypes[ NUM_COMPRESSOR_TYPES ] =
{
"lzss",
"bz2",
};
//----------------------------------------------------------------------------------------
class CCompressor_Lzss : public ICompressor
{
public:
virtual bool Compress( char *pDest, unsigned int *pDestLen, const char *pSource, unsigned int nSourceLen )
{
return g_pEngine->LZSS_Compress( pDest, pDestLen, pSource, nSourceLen );
}
virtual bool Decompress( char *pDest, unsigned int *pDestLen, const char *pSource, unsigned int nSourceLen )
{
return g_pEngine->LZSS_Decompress( pDest, pDestLen, pSource, nSourceLen );
}
virtual int GetEstimatedCompressionSize( unsigned int nSourceLen )
{
return nSourceLen;
}
};
//----------------------------------------------------------------------------------------
#define BZ2_DEFAULT_BLOCKSIZE100k 9 // Highest compression rate, but uses the most memory
#define BZ2_DEFAULT_WORKFACTOR 0 // Default work factor - same as using 30
//----------------------------------------------------------------------------------------
class CCompressor_Bz2 : public ICompressor
{
public:
CCompressor_Bz2(
int nBlockSize100k = BZ2_DEFAULT_BLOCKSIZE100k,
int nWorkFactor = BZ2_DEFAULT_WORKFACTOR
)
: m_nBlockSize100k( nBlockSize100k ),
m_nWorkFactor( nWorkFactor )
{
}
virtual bool Compress( char *pDest, unsigned int *pDestLen, const char *pSource, unsigned int nSourceLen )
{
return BZ_OK == BZ2_bzBuffToBuffCompress(
pDest,
pDestLen,
const_cast< char *>( pSource ),
nSourceLen,
m_nBlockSize100k,
0, // Silent verbosity
m_nWorkFactor
);
}
virtual bool Decompress( char *pDest, unsigned int *pDestLen, const char *pSource, unsigned int nSourceLen )
{
return BZ_OK == BZ2_bzBuffToBuffDecompress(
pDest,
pDestLen,
const_cast< char * >( pSource ),
nSourceLen,
0, // Don't use smaller decompressor (half as fast)
0 // Quiet
);
}
virtual int GetEstimatedCompressionSize( unsigned int nSourceLen )
{
return (int)( 1.1f * nSourceLen ) + 600;
}
private:
int m_nBlockSize100k;
int m_nWorkFactor;
};
//----------------------------------------------------------------------------------------
ICompressor *CreateCompressor( CompressorType_t nType )
{
switch ( nType )
{
case COMPRESSORTYPE_BZ2: return new CCompressor_Bz2();
case COMPRESSORTYPE_LZSS: return new CCompressor_Lzss();
}
return NULL;
}
const char *GetCompressorNameSafe( CompressorType_t nType )
{
if ( nType < 0 || nType >= NUM_COMPRESSOR_TYPES )
return "Unknown compressor type";
return g_pCompressorTypes[ nType ];
}
//----------------------------------------------------------------------------------------
#ifdef _DEBUG
CON_COMMAND( replay_testcompress, "Test compression" )
{
if ( args.ArgC() < 3 )
{
Warning( "replay_testcompress <lzss|bz2> <file to compress>" );
return;
}
const char *pInFilename = args[ 2 ];
const char *pCompressionTypeName = args[ 1 ];
CompressorType_t nCompressorType = COMPRESSORTYPE_INVALID;
for ( int i = 0; i < (int)NUM_COMPRESSOR_TYPES; ++i )
{
if ( !V_stricmp( pCompressionTypeName, g_pCompressorTypes[ i ] ) )
{
nCompressorType = (CompressorType_t)i;
break;
}
}
if ( nCompressorType == COMPRESSORTYPE_INVALID )
{
Warning( "Invalid compression type specified. Use \"bz2\" or \"lzss\"\n" );
return;
}
const unsigned int nInFileSize = g_pFullFileSystem->Size( pInFilename );
if ( !nInFileSize )
{
Warning( "Zero length file.\n" );
return;
}
FileHandle_t hInFile = g_pFullFileSystem->Open( pInFilename, "rb" );
if ( !hInFile )
{
Warning( "Failed to open file, %s\n", pInFilename );
return;
}
char *pUncompressed = new char[ nInFileSize ];
if ( !pUncompressed )
{
Warning( "Failed to alloc %u bytes\n", nInFileSize );
return;
}
if ( g_pFullFileSystem->Read( pUncompressed, nInFileSize, hInFile ) != (int)nInFileSize )
{
Warning( "Failed to read file %s\n", pInFilename );
}
else
{
ICompressor *pCompressor = CreateCompressor( nCompressorType );
unsigned int nCompressedSize = pCompressor->GetEstimatedCompressionSize( nInFileSize );
char *pCompressed = new char[ nCompressedSize ];
if ( !pCompressed )
{
Warning( "Failed to allocate %u bytes for compressed buffer.\n", nCompressedSize );
return;
}
// Compress
if ( !pCompressor->Compress( pCompressed, &nCompressedSize, pUncompressed, nInFileSize ) )
{
Warning( "Compression failed.\n" );
}
else
{
CFmtStr fmtOutFilename( "%s.%s", pInFilename, pCompressionTypeName );
FileHandle_t hOutFile = g_pFullFileSystem->Open( fmtOutFilename.Access(), "wb+" );
if ( !hOutFile )
{
Warning( "Failed to open out file, %s\n", fmtOutFilename.Access() );
}
else
{
if ( g_pFullFileSystem->Write( pCompressed, nCompressedSize, hOutFile ) != (int)nCompressedSize )
{
Warning( "Failed to write compressed data to %s\n", fmtOutFilename.Access() );
}
else
{
const float flRatio = (float)nInFileSize / nCompressedSize;
Warning( "Wrote compressed file to successfully (%s) - ratio: %.2f:1\n", fmtOutFilename.Access(), flRatio );
}
g_pFullFileSystem->Close( hOutFile );
}
}
delete [] pCompressed;
}
g_pFullFileSystem->Close( hInFile );
}
#endif // _DEBUG
//----------------------------------------------------------------------------------------
+47
View File
@@ -0,0 +1,47 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef COMPRESSION_H
#define COMPRESSION_H
//----------------------------------------------------------------------------------------
#include "platform.h"
//----------------------------------------------------------------------------------------
class ICompressor
{
public:
virtual ~ICompressor() {}
virtual bool Compress( char *pDest, unsigned int *pDestLen, const char *pSource, unsigned int nSourceLen ) = 0;
virtual bool Decompress( char *pDest, unsigned int *pDestLen, const char *pSource, unsigned int nSourceLen ) = 0;
virtual int GetEstimatedCompressionSize( unsigned int nSourceLen ) = 0;
};
//----------------------------------------------------------------------------------------
enum CompressorType_t
{
COMPRESSORTYPE_INVALID = -1,
COMPRESSORTYPE_LZSS,
COMPRESSORTYPE_BZ2,
NUM_COMPRESSOR_TYPES
};
//----------------------------------------------------------------------------------------
extern const char *g_pCompressorTypes[ NUM_COMPRESSOR_TYPES ];
//----------------------------------------------------------------------------------------
ICompressor *CreateCompressor( CompressorType_t nType );
const char *GetCompressorNameSafe( CompressorType_t nType );
//----------------------------------------------------------------------------------------
#endif // COMPRESSION_H
+230
View File
@@ -0,0 +1,230 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "errorsystem.h"
#include "replay/ienginereplay.h"
#include "vgui/ILocalize.h"
#include "shared_replaycontext.h"
#if !defined( DEDICATED )
#include "cl_downloader.h"
#include "cl_sessionblockdownloader.h"
#include "cl_recordingsessionblock.h"
#include "replay/iclientreplay.h"
extern IClientReplay *g_pClient;
#endif // !defined( DEDICATED )
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineReplay *g_pEngine;
extern vgui::ILocalize *g_pVGuiLocalize;
//----------------------------------------------------------------------------------------
CErrorSystem::CErrorSystem( IErrorReporter *pErrorReporter )
: m_pErrorReporter( pErrorReporter )
{
}
CErrorSystem::~CErrorSystem()
{
Clear();
}
void CErrorSystem::Clear()
{
FOR_EACH_LL( m_lstErrors, i )
{
wchar_t *pText = m_lstErrors[ i ];
delete [] pText;
}
m_lstErrors.RemoveAll();
}
void CErrorSystem::AddError( const wchar_t *pError )
{
if ( !pError || !pError[0] )
return;
// Cache a copied version of the string
const int nLen = wcslen( pError ) + 1;
wchar_t *pNewError = new wchar_t[ nLen ];
const int nSize = nLen * sizeof( wchar_t );
V_wcsncpy( pNewError, pError, nSize );
m_lstErrors.AddToTail( pNewError );
}
void CErrorSystem::AddError( const char *pError )
{
if ( !pError || !pError[0] )
return;
wchar_t wszError[1024];
V_UTF8ToUnicode( pError, wszError, sizeof( wszError ) );
AddError( wszError );
}
void CErrorSystem::AddErrorFromTokenName( const char *pToken )
{
if ( g_pVGuiLocalize )
{
AddError( g_pVGuiLocalize->Find( pToken ) );
}
else
{
AddError( pToken );
}
}
void CErrorSystem::AddFormattedErrorFromTokenName( const char *pFormatToken/*=NULL*/, KeyValues *pFormatArgs/*=NULL*/ )
{
if ( !pFormatToken )
{
AssertMsg( 0, "Error token should always be valid." );
return;
}
wchar_t wszErrorStr[1024];
if ( g_pVGuiLocalize )
{
g_pVGuiLocalize->ConstructString( wszErrorStr, sizeof( wszErrorStr ), pFormatToken, pFormatArgs );
}
else
{
V_UTF8ToUnicode( pFormatToken, wszErrorStr, sizeof( wszErrorStr ) );
}
// Add the error
AddError( wszErrorStr );
// Delete args
pFormatArgs->deleteThis();
}
#if !defined( DEDICATED )
int g_nGenericErrorCounter = 0;
void CErrorSystem::OGS_ReportSessionBlockDownloadError( const CHttpDownloader *pDownloader, const CClientRecordingSessionBlock *pBlock,
int nLocalFileSize, int nMaxBlock, const bool *pSizesDiffer,
const bool *pHashFail, uint8 *pLocalHash )
{
// Create a download error and queue for upload
KeyValues *pDownloadError = pDownloader->GetOgsRow( g_nGenericErrorCounter );
g_pClient->UploadOgsData( pDownloadError, false );
// Create block download error
KeyValues *pBlockDownloadError = new KeyValues( "TF2ReplayBlockDownloadErrors" );
pBlockDownloadError->SetInt( "ErrorCounter", g_nGenericErrorCounter );
pBlockDownloadError->SetInt( "NumCurrentDownloads", CSessionBlockDownloader::sm_nNumCurrentDownloads );
pBlockDownloadError->SetInt( "MaxBlock", nMaxBlock );
pBlockDownloadError->SetInt( "RemoteStatus", (int)pBlock->m_nRemoteStatus );
pBlockDownloadError->SetInt( "ReconstructionIndex", pBlock->m_iReconstruction );
pBlockDownloadError->SetInt( "RemoteFileSize", (int)pBlock->m_uFileSize );
pBlockDownloadError->SetInt( "LocalFileSize", nLocalFileSize );
pBlockDownloadError->SetInt( "NumDownloadAttempts", pBlock->GetNumDownloadAttempts() );
// Only include these if appropriate - otherwise, let them be NULL for the given row
if ( pSizesDiffer )
{
pBlockDownloadError->SetInt( "SizesDiffer", (int)*pSizesDiffer );
}
if ( pHashFail )
{
pBlockDownloadError->SetInt( "HashFail", (int)*pHashFail );
// Include hashes
char szRemoteHash[64], szLocalHash[64];
V_binarytohex( pBlock->m_aHash, sizeof( pBlock->m_aHash ), szRemoteHash, sizeof( szRemoteHash ) );
V_binarytohex( pLocalHash, sizeof( pBlock->m_aHash ), szLocalHash, sizeof( szLocalHash ) );
pBlockDownloadError->SetString( "RemoteHash", szRemoteHash );
pBlockDownloadError->SetString( "LocalHash", szLocalHash );
}
// Upload block download error
g_pClient->UploadOgsData( pBlockDownloadError, false );
// Upload generic error and link to this specific block error.
OGS_ReportGenericError( "Block download failed" );
}
void CErrorSystem::OGS_ReportSessioInfoDownloadError( const CHttpDownloader *pDownloader, const char *pErrorToken )
{
// Create a download error and queue for upload
KeyValues *pDownloadError = pDownloader->GetOgsRow( g_nGenericErrorCounter );
g_pClient->UploadOgsData( pDownloadError, false );
// Create session info download error
KeyValues *pSessionInfoDownloadError = new KeyValues( "TF2ReplaySessionInfoDownloadErrors" );
pSessionInfoDownloadError->SetInt( "ErrorCounter", g_nGenericErrorCounter++ );
pSessionInfoDownloadError->SetString( "SessionInfoDownloadErrorID", pErrorToken );
g_pClient->UploadOgsData( pSessionInfoDownloadError, false );
// Upload generic error and link to this specific block error.
OGS_ReportGenericError( "Session info download failed" );
}
// Note: we use the ErrorCounter as part of the key and so it must be unique. This means that all
// special error tables (ie., session info download errors) must call back into this base function
// to write out the base error and increment the counter.
void CErrorSystem::OGS_ReportGenericError( const char *pGenericErrorToken )
{
KeyValues *pGenericError = new KeyValues( "TF2ReplayErrors" );
pGenericError->SetInt( "ErrorCounter", g_nGenericErrorCounter++ );
pGenericError->SetString( "ReplayErrorID", pGenericErrorToken );
// Upload the generic error row now
g_pClient->UploadOgsData( pGenericError, true );
// Next error!
++g_nGenericErrorCounter;
}
#endif // !defined( DEDICATED )
float CErrorSystem::GetNextThinkTime() const
{
return g_pEngine->GetHostTime() + 5.0f;
}
void CErrorSystem::Think()
{
CBaseThinker::Think();
if ( m_lstErrors.Count() == 0 )
return;
const int nMaxLen = 4096;
wchar_t wszErrorText[ nMaxLen ] = L"";
FOR_EACH_LL( m_lstErrors, i )
{
const wchar_t *pError = m_lstErrors[ i ];
if ( wcslen( wszErrorText ) + wcslen( pError ) + 1 >= nMaxLen )
break;
wcscat( wszErrorText, pError );
wcscat( wszErrorText, L"\n" );
}
// Report now
m_pErrorReporter->ReportErrorsToUser( wszErrorText );
// Clear
Clear();
}
//----------------------------------------------------------------------------------------
+67
View File
@@ -0,0 +1,67 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef ERRORSYSTEM_H
#define ERRORSYSTEM_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplayerrorsystem.h"
#include "basethinker.h"
#include "utllinkedlist.h"
//----------------------------------------------------------------------------------------
class KeyValues;
class CClientRecordingSessionBlock;
class CHttpDownloader;
//----------------------------------------------------------------------------------------
class IErrorReporter
{
public:
virtual void ReportErrorsToUser( wchar_t *pErrorText ) = 0;
};
//----------------------------------------------------------------------------------------
class CErrorSystem : public CBaseThinker,
public IReplayErrorSystem
{
public:
CErrorSystem( IErrorReporter *pErrorReporter );
~CErrorSystem();
virtual void AddErrorFromTokenName( const char *pToken );
virtual void AddFormattedErrorFromTokenName( const char *pFormatToken, KeyValues *pFormatArgs );
#if !defined( DEDICATED )
void OGS_ReportSessionBlockDownloadError( const CHttpDownloader *pDownloader, const CClientRecordingSessionBlock *pBlock,
int nLocalFileSize, int nMaxBlock, const bool *pSizesDiffer,
const bool *pHashFail, uint8 *pLocalHash );
void OGS_ReportSessioInfoDownloadError( const CHttpDownloader *pDownloader, const char *pErrorToken );
void OGS_ReportGenericError( const char *pGenericErrorToken );
#endif
private:
void AddError( const wchar_t *pError );
void AddError( const char *pError );
float GetNextThinkTime() const;
void Think();
void Clear();
IErrorReporter *m_pErrorReporter;
CUtlLinkedList< wchar_t *, int > m_lstErrors;
};
//----------------------------------------------------------------------------------------
#endif // ERRORSYSTEM_H
+712
View File
@@ -0,0 +1,712 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef GENERICPERSISTENTMANAGER_H
#define GENERICPERSISTENTMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/replayhandle.h"
#include "replay/ienginereplay.h"
#include "replay/replayutils.h"
#include "basethinker.h"
#include "utllinkedlist.h"
#include "utlstring.h"
#include "KeyValues.h"
#include "filesystem.h"
#include "convar.h"
#include "replay/ireplayserializeable.h"
#include "replay/ireplaycontext.h"
#include "replay/shared_defs.h"
#include "replay_dbg.h"
#include "vprof.h"
#include "fmtstr.h"
#include "UtlSortVector.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
template< class T >
class CGenericPersistentManager : public CBaseThinker
{
public:
CGenericPersistentManager();
virtual ~CGenericPersistentManager();
virtual bool Init( bool bLoad = true );
virtual void Shutdown();
virtual T *Create() = 0; // Create an object
T *CreateAndGenerateHandle(); // Creates a new object and generates a unique handle
void Add( T *pNewObj ); // Commit the object - NOTE: The Create*() functions don't call Add() for you
void Remove( ReplayHandle_t hObj ); // Remove() will remove the object, remove any .dmx associated with the object on disk, and usually delete attached files (like .dems or movies, etc, depending on what the manager implementation is)
void Remove( T *pObj );
void RemoveFromIndex( int it );
void Clear(); // Remove all objects - NOTE: Doesn't save right away
bool WriteObjToFile( T *pObj, const char *pFilename ); // Write object data to an arbitrary file
bool Save(); // Saves any unsaved data immediately
void FlagIndexForFlush(); // Mark index as dirty
void FlagForFlush( T *pObj, bool bForceImmediate ); // Mark an object as dirty
void FlagForUnload( T *pObj ); // Unload as soon as possible
T *Find( ReplayHandle_t hHandle );
int FindIteratorFromHandle( ReplayHandle_t hHandle );
int Count() const;
bool IsDirty( T *pNewObj );
virtual void Think(); // IReplayThinker implementation - NOTE: not meant to be called directly - called from think manager
virtual const char *GetIndexPath() const; // Should return path where index file lives
private:
class CLessFunctor
{
public:
bool Less( const T *pSrc1, const T *pSrc2, void *pContext )
{
return pSrc1->GetHandle() < pSrc2->GetHandle();
}
};
public:
typedef CUtlSortVector< T *, CLessFunctor > ObjContainer_t;
ObjContainer_t m_vecObjs;
protected:
// For derived classes to implement:
virtual IReplayContext *GetReplayContext() const = 0;
virtual const char *GetRelativeIndexPath() const = 0; // Should return relative (to replay/client or replay/server) path where index file lives - NOTE: Last char should be a slash
virtual const char *GetIndexFilename() const = 0; // Should return just the name of the file, e.g. "replays.dmx"
virtual const char *GetDebugName() const = 0;
virtual bool ShouldDeleteObjects() const { return true; } // TODO: Used by Clear() - I'm not convinced this is needed yet though.
virtual int GetVersion() const = 0;
virtual bool ShouldSerializeToIndividualFiles() const { return true; }
virtual bool ShouldSerializeIndexWithFullPath() const { return false; }
virtual bool ShouldLoadObj( const T *pObj ) const { return true; }
virtual void OnObjLoaded( T *pObj ) {}
virtual int GetHandleBase() const { return 0; } // Subclass can implement this to provide a base/minimum for handles
virtual void PreLoad() {}
const char *GetIndexFullFilename() const; // Should return the full path to the main .dmx file
bool HaveDirtyObjects() const;
bool HaveObjsToUnload() const;
bool ReadObjFromFile( const char *pFile, T *&pOut, bool bForceLoad );
bool Load();
virtual float GetNextThinkTime() const; // IReplayThinker implementation
void FlushThink();
void UnloadThink();
void CreateIndexDir();
void ReadObjFromKeyValues( KeyValues *pObjData );
T* ReadObjFromKeyValues( KeyValues *pObjData, bool bForceLoad );
bool ReadObjFromFile( const char *pFile );
void UpdateHandleSeed( ReplayHandle_t hNewHandle );
typedef CUtlLinkedList< T *, int > ListContainer_t;
ReplayHandle_t m_nHandleSeed;
int m_nVersion;
bool m_bIndexDirty;
ListContainer_t m_lstDirtyObjs;
ListContainer_t m_lstObjsToUnload;
float m_flNextFlushTime;
float m_flNextUnloadTime;
};
//----------------------------------------------------------------------------------------
template< class T >
bool CGenericPersistentManager< T >::Init( bool bLoad/*=true*/ )
{
// Make directory structure is in place
CreateIndexDir();
// Initialize handle seed to start at base
m_nHandleSeed = GetHandleBase();
return bLoad ? Load() : true;
}
template< class T >
void CGenericPersistentManager< T >::Shutdown()
{
Save();
}
template< class T >
CGenericPersistentManager< T >::CGenericPersistentManager()
: m_nHandleSeed( 0 ),
m_nVersion( -1 ),
m_bIndexDirty( false ),
m_flNextFlushTime( 0.0f ),
m_flNextUnloadTime( 0.0f )
{
}
template< class T >
CGenericPersistentManager< T >::~CGenericPersistentManager()
{
Clear();
}
template< class T >
void CGenericPersistentManager< T >::Clear()
{
if ( ShouldDeleteObjects() )
{
m_vecObjs.PurgeAndDeleteElements();
}
else
{
m_vecObjs.RemoveAll();
}
// NOTE: This list contains pointers to objects in m_vecObjs, so no destruction of elements here
m_lstDirtyObjs.RemoveAll();
m_lstObjsToUnload.RemoveAll();
}
template< class T >
int CGenericPersistentManager< T >::Count() const
{
return m_vecObjs.Count();
}
template< class T >
void CGenericPersistentManager< T >::FlagIndexForFlush()
{
m_bIndexDirty = true;
IF_REPLAY_DBG2( Warning( "%f %s: Index flagged\n", g_pEngine->GetHostTime(), GetDebugName() ) );
}
template< class T >
void CGenericPersistentManager< T >::FlagForFlush( T *pObj, bool bForceImmediate )
{
if ( !pObj )
{
AssertMsg( 0, "Trying to flag a NULL object for flush." );
return;
}
// Add to dirty list if it's not already there
if ( m_lstDirtyObjs.Find( pObj ) == m_lstDirtyObjs.InvalidIndex() )
{
m_lstDirtyObjs.AddToTail( pObj );
}
IF_REPLAY_DBG2( Warning( "%f %s: Obj %s flagged for flush\n", g_pEngine->GetHostTime(), GetDebugName(), pObj->GetDebugName() ) );
// Force write now?
if ( bForceImmediate )
{
Save();
}
}
template< class T >
void CGenericPersistentManager< T >::FlagForUnload( T *pObj )
{
AssertMsg(
ShouldSerializeToIndividualFiles(),
"This functionality should only be used for managers that write to individual files, i.e. NOT managers that maintain one monolithic index."
);
if ( !pObj )
{
AssertMsg( 0, "Trying to flag a NULL object for unload." );
return;
}
if ( m_lstObjsToUnload.Find( pObj ) == m_lstObjsToUnload.InvalidIndex() )
{
m_lstObjsToUnload.AddToTail( pObj );
}
IF_REPLAY_DBG2( Warning( "%f %s: Obj %s flagged for unload\n", g_pEngine->GetHostTime(), GetDebugName(), pObj->GetDebugName() ) );
}
template< class T >
bool CGenericPersistentManager< T >::IsDirty( T *pNewObj )
{
return m_lstDirtyObjs.Find( pNewObj ) != m_lstDirtyObjs.InvalidIndex();
}
template< class T >
void CGenericPersistentManager< T >::Add( T *pNewObj )
{
IF_REPLAY_DBG2( Warning( "Adding object with handle %i\n", pNewObj->GetHandle() ) );
Assert( m_vecObjs.Find( pNewObj ) == m_vecObjs.InvalidIndex() );
m_vecObjs.Insert( pNewObj );
FlagIndexForFlush();
FlagForFlush( pNewObj, false );
}
template< class T >
void CGenericPersistentManager< T >::Remove( ReplayHandle_t hObj )
{
int itObj = FindIteratorFromHandle( hObj );
if ( itObj == m_vecObjs.InvalidIndex() )
{
AssertMsg( 0, "Attemting to remove an object which does not exist." );
return;
}
RemoveFromIndex( itObj );
}
template< class T >
void CGenericPersistentManager< T >::Remove( T *pObj )
{
const int it = m_vecObjs.Find( pObj );
if ( it != m_vecObjs.InvalidIndex() )
{
RemoveFromIndex( it );
}
}
template< class T >
void CGenericPersistentManager< T >::RemoveFromIndex( int it )
{
T *pObj = m_vecObjs[ it ]; // NOTE: Constant speed since the implementation of
// CUtlLinkedList indexes into an array
// Remove file associated w/ this object if necessary
if ( ShouldSerializeToIndividualFiles() )
{
CUtlString strFullFilename = pObj->GetFullFilename();
bool bSimulateDelete = false;
#if _DEBUG
extern ConVar replay_fileserver_simulate_delete;
bSimulateDelete = replay_fileserver_simulate_delete.GetBool();
#endif
if ( g_pFullFileSystem->FileExists( strFullFilename.Get() ) && !bSimulateDelete )
{
g_pFullFileSystem->RemoveFile( strFullFilename.Get() );
}
}
Assert( !pObj->IsLocked() );
// Let the object do stuff before it gets deleted
pObj->OnDelete();
// If the object is in the dirty list, remove it - NOTE: this is safe
m_lstDirtyObjs.FindAndRemove( pObj );
// The object should not be in the 'objects-to-unload' list
AssertMsg( m_lstObjsToUnload.Find( pObj ) == m_lstObjsToUnload.InvalidIndex(), "The object being removed was also in the unload list - is this OK? If so, code should be added to remove from that list as well." );
// Remove the object
m_vecObjs.Remove( it );
// Free the object
delete pObj;
FlagIndexForFlush();
}
template< class T >
T *CGenericPersistentManager< T >::Find( ReplayHandle_t hHandle )
{
FOR_EACH_VEC( m_vecObjs, i )
{
T *pCurObj = m_vecObjs[ i ];
if ( hHandle == pCurObj->GetHandle() )
{
return pCurObj;
}
}
return NULL;
}
template< class T >
int CGenericPersistentManager< T >::FindIteratorFromHandle( ReplayHandle_t hHandle )
{
FOR_EACH_VEC( m_vecObjs, i )
{
T *pCurObj = m_vecObjs[ i ];
if ( hHandle == pCurObj->GetHandle() )
{
return i;
}
}
return m_vecObjs.InvalidIndex();
}
template< class T >
bool CGenericPersistentManager< T >::Load()
{
bool bResult = true;
Clear();
PreLoad();
const char *pFullFilename = GetIndexFullFilename();
// Attempt to load from disk
KeyValuesAD pRoot( pFullFilename );
if ( pRoot->LoadFromFile( g_pFullFileSystem, pFullFilename ) )
{
// Get file format version
m_nVersion = pRoot->GetInt( "version", -1 );
if ( m_nVersion != GetVersion() )
{
Warning( "File (%s) has old format (%i).\n", pFullFilename, m_nVersion );
}
// Read from individual files?
if ( ShouldSerializeToIndividualFiles() )
{
KeyValues *pFileIndex = pRoot->FindKey( "files" );
if ( pFileIndex )
{
FOR_EACH_VALUE( pFileIndex, pValue )
{
const char *pName = pValue->GetName();
if ( !ReadObjFromFile( pName ) )
{
Warning( "Failed to load data from file, \"%s\"\n", pName );
}
}
}
else
{
// Peek in directory and load files based on what's there
CFmtStr fmtPath( "%s*.%s", GetIndexPath(), GENERIC_FILE_EXTENSION );
FileFindHandle_t hFind;
const char *pFilename = g_pFullFileSystem->FindFirst( fmtPath.Access(), &hFind );
while ( pFilename )
{
// Ignore index file
if ( V_stricmp( pFilename, GetIndexFilename() ) )
{
if ( !ReadObjFromFile( pFilename ) )
{
Warning( "Failed to load data from file, \"%s\"\n", pFilename );
}
}
pFilename = g_pFullFileSystem->FindNext( hFind );
}
}
}
else
{
FOR_EACH_TRUE_SUBKEY( pRoot, pObjSubKey )
{
// Read data
m_vecObjs.Insert( ReadObjFromKeyValues( pObjSubKey, false ) );
}
}
// Let derived class do any per-object processing.
FOR_EACH_VEC( m_vecObjs, i )
{
OnObjLoaded( m_vecObjs[ i ] );
}
}
return bResult;
}
template< class T >
bool CGenericPersistentManager< T >::WriteObjToFile( T *pObj, const char *pFilename )
{
// Create a keyvalues for the object
KeyValuesAD pObjData( pObj->GetSubKeyTitle() );
// Fill the keyvalues w/ data
pObj->Write( pObjData );
// Attempt to save the current object data to a separate file
if ( !pObjData->SaveToFile( g_pFullFileSystem, pFilename ) )
{
Warning( "Failed to write file %s\n", pFilename );
return false;
}
return true;
}
template< class T >
bool CGenericPersistentManager< T >::Save()
{
IF_REPLAY_DBG2( Warning( "%f %s: Saving now...\n", g_pEngine->GetHostTime(), GetDebugName() ) );
bool bResult = true;
// Add subkey for movies
KeyValuesAD pRoot( "root" );
// Write format version
pRoot->SetInt( "version", GetVersion() );
// Write a file index instead of adding subkeys to the root?
if ( ShouldSerializeToIndividualFiles() )
{
// Go through each object in the dirty list and write to a separate file
FOR_EACH_LL( m_lstDirtyObjs, i )
{
T *pCurObj = m_lstDirtyObjs[ i ];
// Write to the file
bResult = bResult && WriteObjToFile( pCurObj, pCurObj->GetFullFilename() );
}
}
// Write all objects to one monolithic file - writes all objects (ignores "dirtyness")
else
{
FOR_EACH_VEC( m_vecObjs, i )
{
T *pCurObj = m_vecObjs[ i ];
// Create a keyvalues for the object
KeyValues *pCurObjData = new KeyValues( pCurObj->GetSubKeyTitle() );
// Fill the keyvalues w/ data
pCurObj->Write( pCurObjData );
// Add as a subkey to the root keyvalues
pRoot->AddSubKey( pCurObjData );
}
}
// Clear the dirty list
m_lstDirtyObjs.RemoveAll();
// Write the index file if dirty
if ( m_bIndexDirty )
{
return bResult && pRoot->SaveToFile( g_pFullFileSystem, GetIndexFullFilename() );
}
return bResult;
}
template< class T >
T *CGenericPersistentManager< T >::CreateAndGenerateHandle()
{
T *pNewObj = Create();
pNewObj->SetHandle( m_nHandleSeed++ ); Assert( Find( pNewObj->GetHandle() ) == NULL );
FlagIndexForFlush();
return pNewObj;
}
template< class T >
float CGenericPersistentManager< T >::GetNextThinkTime() const
{
// Always think
return 0.0f;
}
template< class T >
void CGenericPersistentManager< T >::Think()
{
VPROF_BUDGET( "CGenericPersistentManager::Think", VPROF_BUDGETGROUP_REPLAY );
CBaseThinker::Think();
FlushThink();
UnloadThink();
}
template< class T >
void CGenericPersistentManager< T >::FlushThink()
{
const float flHostTime = g_pEngine->GetHostTime();
bool bTimeToFlush = flHostTime >= m_flNextFlushTime;
if ( !bTimeToFlush || ( !m_bIndexDirty && !HaveDirtyObjects() ) )
return;
// Flush now and clear dirty objects
Save();
// Reset
m_bIndexDirty = false;
// Setup next flush think
extern ConVar replay_flushinterval;
m_flNextFlushTime = flHostTime + replay_flushinterval.GetInt();
}
template< class T >
void CGenericPersistentManager< T >::UnloadThink()
{
const float flHostTime = g_pEngine->GetHostTime();
bool bTimeToUnload = flHostTime >= m_flNextUnloadTime;
if ( !bTimeToUnload || !HaveObjsToUnload() )
return;
// Unload objects now
FOR_EACH_LL( m_lstObjsToUnload, i )
{
T *pObj = m_lstObjsToUnload[ i ];
// If the object has been marked as locked, don't unload it.
if ( pObj->IsLocked() )
continue;
// If we're waiting to flush the file, don't unload it yet
if ( IsDirty( pObj ) )
continue;
// Let the object do stuff before it gets deleted
pObj->OnUnload();
// Remove the object
m_vecObjs.FindAndRemove( pObj );
IF_REPLAY_DBG( Warning( "Unloading object %s\n", pObj->GetDebugName() ) );
// Free the object
delete pObj;
}
// Clear the list
m_lstObjsToUnload.RemoveAll();
// Think once a second
m_flNextUnloadTime = flHostTime + 1.0f;
}
template< class T >
const char *CGenericPersistentManager< T >::GetIndexPath() const
{
return Replay_va( "%s%s", GetReplayContext()->GetBaseDir(), GetRelativeIndexPath() );
}
template< class T >
const char *CGenericPersistentManager< T >::GetIndexFullFilename() const // Should return the full path to the main .dmx file
{
return Replay_va( "%s%s", GetIndexPath(), GetIndexFilename() );
}
template< class T >
bool CGenericPersistentManager< T >::HaveDirtyObjects() const
{
return m_lstDirtyObjs.Count() > 0;
}
template< class T >
bool CGenericPersistentManager< T >::HaveObjsToUnload() const
{
return m_lstObjsToUnload.Count() > 0;
}
template< class T >
void CGenericPersistentManager< T >::CreateIndexDir()
{
g_pFullFileSystem->CreateDirHierarchy( GetIndexPath(), "DEFAULT_WRITE_PATH" );
}
template< class T >
bool CGenericPersistentManager< T >::ReadObjFromFile( const char *pFile, T *&pOut, bool bForceLoad )
{
// Use the full path and filename specified, or construct it if necessary
CUtlString strFullFilename;
if ( ShouldSerializeIndexWithFullPath() )
{
strFullFilename = pFile;
}
else
{
strFullFilename.Format( "%s%s", GetIndexPath(), pFile );
}
// Attempt to load the file
KeyValuesAD pObjData( pFile );
if ( !pObjData->LoadFromFile( g_pFullFileSystem, strFullFilename.Get() ) )
{
Warning( "Failed to load from file %s\n", strFullFilename.Get() );
AssertMsg( 0, "Manager failed to load something..." );
return false;
}
// Create and read a new object
pOut = ReadObjFromKeyValues( pObjData, bForceLoad );
if ( !pOut )
return NULL;
// Add the object to the manager
m_vecObjs.Insert( pOut );
return true;
}
template< class T >
bool CGenericPersistentManager< T >::ReadObjFromFile( const char *pFile )
{
T *pNewObj;
if ( !ReadObjFromFile( pFile, pNewObj, false ) )
return false;
return true;
}
template< class T >
T* CGenericPersistentManager< T >::ReadObjFromKeyValues( KeyValues *pObjData, bool bForceLoad )
{
T *pNewObj = Create(); Assert( pNewObj );
if ( !pNewObj )
return NULL;
// Attempt to read data for the object, and fail to load this particular object if the reader
// says we should.
if ( !pNewObj->Read( pObjData ) )
{
delete pNewObj;
return NULL;
}
// This object OK to load? Only check if bForceLoad is false.
if ( !bForceLoad && !ShouldLoadObj( pNewObj ) )
{
delete pNewObj;
return NULL;
}
// Sync up handle seed
UpdateHandleSeed( pNewObj->GetHandle() );
return pNewObj;
}
template< class T >
void CGenericPersistentManager< T >::UpdateHandleSeed( ReplayHandle_t hNewHandle )
{
m_nHandleSeed = (ReplayHandle_t)( GetHandleBase() + MAX( (uint32)m_nHandleSeed, (uint32)hNewHandle ) + 1 );
#ifdef _DEBUG
FOR_EACH_VEC( m_vecObjs, i )
{
AssertMsg( m_nHandleSeed != m_vecObjs[ i ]->GetHandle(), "Handle seed collision!" );
}
#endif
}
//----------------------------------------------------------------------------------------
#define FOR_EACH_OBJ( _manager, _i ) FOR_EACH_VEC( _manager->m_vecObjs, _i )
//----------------------------------------------------------------------------------------
#endif // GENERICPERSISTENTMANAGER_H
+27
View File
@@ -0,0 +1,27 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef ITHINKER_H
#define ITHINKER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "interface.h"
//----------------------------------------------------------------------------------------
class IThinker : public IBaseInterface
{
public:
virtual void Think() = 0;
virtual void PostThink() = 0;
virtual bool ShouldThink() const = 0;
};
//----------------------------------------------------------------------------------------
#endif // ITHINKER_H
+32
View File
@@ -0,0 +1,32 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef ITHINKMANAGER_H
#define ITHINKMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "interface.h"
//----------------------------------------------------------------------------------------
class IThinker;
//----------------------------------------------------------------------------------------
class IThinkManager : public IBaseInterface
{
public:
virtual void AddThinker( IThinker *pThinker ) = 0;
virtual void RemoveThinker( IThinker *pThinker ) = 0;
virtual void Think() = 0;
};
//----------------------------------------------------------------------------------------
#endif // ITHINKMANAGER_H
+117
View File
@@ -0,0 +1,117 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "managertest.h"
#include "replay/replayutils.h"
#include "cl_replaycontext.h"
#include "KeyValues.h"
#include "replay/shared_defs.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#if _DEBUG
//----------------------------------------------------------------------------------------
#define TESTMANAGER_VERSION 0
#define SUBDIR_TEST "test"
//----------------------------------------------------------------------------------------
const char *Test_GetPath()
{
return Replay_va( "%s%c%s%c%s%c", g_pEngine->GetGameDir(), CORRECT_PATH_SEPARATOR, SUBDIR_REPLAY, CORRECT_PATH_SEPARATOR, SUBDIR_TEST, CORRECT_PATH_SEPARATOR );
}
//----------------------------------------------------------------------------------------
CTestObj::CTestObj()
: m_nTest( -1 )
{
m_strTest = "";
m_pTest = new int;
}
CTestObj::~CTestObj()
{
delete m_pTest;
}
const char *CTestObj::GetSubKeyTitle() const
{
return Replay_va( "test_%i", GetHandle() );
}
const char *CTestObj::GetPath() const
{
return Test_GetPath();
}
void CTestObj::OnDelete()
{
}
bool CTestObj::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_nTest = pIn->GetInt( "int_test", -1 );
m_strTest = pIn->GetString( "int_test" );
return true;
}
void CTestObj::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "int_test", m_nTest );
pOut->SetString( "str_test", m_strTest.Get() );
}
//----------------------------------------------------------------------------------------
/*static*/ void CTestManager::Test()
{
#if 0
CUtlLinkedList< CTestObj *, int > lstTest;
CTestObj test;
lstTest.AddToTail( &test );
lstTest.RemoveAll();
CTestManager m;
m.Init();
for ( int i = 0; i < 3; ++i )
{
CTestObj *pNewObj = m.CreateAndGenerateHandle();
m.Add( pNewObj );
}
m.Shutdown();
#endif
}
CTestManager::CTestManager()
{
}
const char *CTestManager::GetIndexPath() const
{
return Test_GetPath();
}
int CTestManager::GetVersion() const
{
return TESTMANAGER_VERSION;
}
CTestObj *CTestManager::Create()
{
return new CTestObj();
}
#endif
//----------------------------------------------------------------------------------------
+81
View File
@@ -0,0 +1,81 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef MANAGERTEST_H
#define MANAGERTEST_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "genericpersistentmanager.h"
#include "replay/replayhandle.h"
#include "replay/irecordingsessionblockmanager.h"
#include "utlstring.h"
#include "baserecordingsession.h"
#include "replay/basereplayserializeable.h"
#include "baserecordingsessionblock.h"
//----------------------------------------------------------------------------------------
class CTestObj : public CBaseReplaySerializeable
{
typedef CBaseReplaySerializeable BaseClass;
public:
CTestObj();
~CTestObj();
virtual const char *GetSubKeyTitle() const;
virtual const char *GetPath() const;
virtual void OnDelete();
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
CUtlString m_strTest;
int m_nTest;
int *m_pTest;
};
//----------------------------------------------------------------------------------------
class ITestManager : public IBaseInterface
{
public:
virtual void SomeTest() = 0;
};
//----------------------------------------------------------------------------------------
class CTestManager : public CGenericPersistentManager< CTestObj >,
public ITestManager
{
typedef CGenericPersistentManager< CTestObj > BaseClass;
public:
CTestManager();
static void Test();
//
// CGenericPersistentManager
//
virtual CTestObj *Create();
virtual bool ShouldSerializeToIndividualFiles() const { return true; }
virtual const char *GetIndexPath() const;
virtual const char *GetDebugName() const { return "test manager"; }
virtual int GetVersion() const;
virtual const char *GetIndexFilename() const { return "test_index." GENERIC_FILE_EXTENSION; }
//
// ITestManager
//
virtual void SomeTest() {}
};
//----------------------------------------------------------------------------------------
#endif // MANAGERTEST_H
+215
View File
@@ -0,0 +1,215 @@
//-----------------------------------------------------------------------------
// REPLAY.VPC
//
// Project Script
//
// Files in the project that begin with "cl_" should not be built if the
// DEDICATED macro is defined.
//
//-----------------------------------------------------------------------------
$macro SRCDIR ".."
$Macro OUTBINDIR "$SRCDIR\..\game\bin"
// NOTE: We don't want to include source_replay.vpc here. The only purpose
// REPLAY_ENABLED // serves in this project is for includes like netmessages.h
// which need REPLAY_ENABLED to be defined in order to build properly.
$include "$SRCDIR\vpc_scripts\source_dll_base.vpc"
$Configuration
{
$Compiler
{
// REPLAY_ENABLED is set here for netmessages.h
$PreprocessorDefinitions "$BASE;REPLAY_DLL;REPLAY_ENABLED"
$PreprocessorDefinitions "$BASE;DEDICATED;SWDS" [$DEDICATED]
$PreprocessorDefinitions "$BASE;CURL_STATICLIB" [$WIN32]
}
$Linker
{
// 360 will auto generate a def file for this import library
$ModuleDefinitionFile " " [$X360]
$AdditionalOptions "$BASE /AUTODEF:xbox\xbox.def" [$X360]
$SystemLibraries "iconv;z" [$OSXALL]
$SystemLibraries "rt;" [$LINUXALL]
$SystemLibraries "$BASE;curl-gnutls" [$LINUXALL]
$AdditionalDependencies "$BASE ws2_32.lib" [$WINDOWS]
}
}
$Macro REPLAY_COMMON_HEADERS_TITLE "replay_common.lib headers"
$Project "replay"
{
$Include "$SRCDIR\replay\common\headers.vpc"
$Folder "Source Files"
{
$File "baserecordingsession.cpp"
$File "baserecordingsessionblock.cpp"
$File "baserecordingsessionblockmanager.cpp"
$File "baserecordingsessionmanager.cpp"
$File "basethinker.cpp"
$File "compression.cpp"
$File "cl_commands.cpp" [!$DEDICATED]
$File "cl_cvars.cpp" [!$DEDICATED]
$File "cl_downloader.cpp" [!$DEDICATED]
$File "cl_recordingsession.cpp" [!$DEDICATED]
$File "cl_recordingsessionblock.cpp" [!$DEDICATED]
$File "cl_recordingsessionblockmanager.cpp" [!$DEDICATED]
$File "cl_recordingsessionmanager.cpp" [!$DEDICATED]
$File "cl_renderqueue.cpp" [!$DEDICATED]
$File "cl_replaycontext.cpp" [!$DEDICATED]
$File "cl_replaymanager.cpp" [!$DEDICATED]
$File "cl_replaymovie.cpp" [!$DEDICATED]
$File "cl_replaymoviemanager.cpp" [!$DEDICATED]
$File "cl_performancecontroller.cpp" [!$DEDICATED]
$File "cl_performancemanager.cpp" [!$DEDICATED]
$File "cl_screenshotmanager.cpp" [!$DEDICATED]
$File "cl_sessionblockdownloader.cpp" [!$DEDICATED]
$File "cl_sessioninfodownloader.cpp" [!$DEDICATED]
$File "errorsystem.cpp"
$File "$SRCDIR\common\imageutils.cpp" [!$DEDICATED]
$File "managertest.cpp" [!$DEDICATED]
$File "$SRCDIR\common\netmessages.cpp"
$File "replay_dbg.cpp"
$File "replay_reconstructor.cpp" [!$DEDICATED]
$File "replaysystem.cpp"
$File "sessioninfoheader.cpp"
$File "shared_cvars.cpp"
$File "shared_replaycontext.cpp"
$File "spew.cpp"
$File "sv_basejob.cpp"
$File "sv_commands.cpp"
$File "sv_filepublish.cpp"
$File "sv_fileservercleanup.cpp"
$File "sv_cvars.cpp"
$File "sv_publishtest.cpp"
$File "sv_recordingsession.cpp"
$File "sv_recordingsessionblock.cpp"
$File "sv_recordingsessionmanager.cpp"
$File "sv_recordingsessionblockmanager.cpp"
$File "sv_replaycontext.cpp"
$File "sv_sessionblockpublisher.cpp"
$File "sv_sessioninfopublisher.cpp"
$File "sv_sessionpublishmanager.cpp"
$File "sv_sessionrecorder.cpp"
$File "thinkmanager.cpp"
}
$Folder "Header Files"
{
$File "baserecordingsession.h"
$File "baserecordingsessionblock.h"
$File "baserecordingsessionblockmanager.h"
$File "baserecordingsessionmanager.h"
$File "basethinker.h"
$File "cl_downloader.h" [!$DEDICATED]
$File "cl_recordingsession.h" [!$DEDICATED]
$File "cl_recordingsessionblock.h" [!$DEDICATED]
$File "cl_recordingsessionblockmanager.h" [!$DEDICATED]
$File "cl_recordingsessionmanager.h" [!$DEDICATED]
$File "cl_renderqueue.h" [!$DEDICATED]
$File "cl_replaycontext.h" [!$DEDICATED]
$File "cl_replaymanager.h" [!$DEDICATED]
$File "cl_replaymovie.h" [!$DEDICATED]
$File "cl_replaymoviemanager.h" [!$DEDICATED]
$File "cl_performance_common.h" [!$DEDICATED]
$File "cl_performancecontroller.h" [!$DEDICATED]
$File "cl_performancemanager.h" [!$DEDICATED]
$File "cl_screenshotmanager.h" [!$DEDICATED]
$File "cl_sessionblockdownloader.h" [!$DEDICATED]
$File "cl_sessioninfodownloader.h" [!$DEDICATED]
$File "compression.h"
$File "errorsystem.h"
$File "genericpersistentmanager.h"
$File "$SRCDIR\common\engine\idownloadsystem.h"
$File "$SRCDIR\common\replay\ienginereplay.h"
$File "$SRCDIR\common\imageutils.h" [!$DEDICATED]
$File "ithinker.h"
$File "ithinkmanager.h"
$File "managertest.h" [!$DEDICATED]
$File "$SRCDIR\common\netmessages.h"
$File "$SRCDIR\common\replay\rendermovieparams.h" [!$DEDICATED]
$File "replay_dbg.h"
$File "replay_reconstructor.h" [!$DEDICATED]
$File "replaysystem.h"
$File "sessioninfoheader.h"
$File "shared_replaycontext.h"
$File "spew.h"
$File "sv_basejob.h"
$File "sv_filepublish.h"
$File "sv_fileservercleanup.h"
$File "sv_publishtest.h"
$File "sv_recordingsession.h"
$File "sv_recordingsessionblock.h"
$File "sv_recordingsessionblockmanager.h"
$File "sv_recordingsessionmanager.h"
$File "sv_replaycontext.h"
$File "sv_sessionblockpublisher.h"
$File "sv_sessioninfopublisher.h"
$File "sv_sessionpublishmanager.h"
$File "sv_sessionrecorder.h"
$File "thinkmanager.h"
}
$Folder "Public Header Files"
{
$File "$SRCDIR\common\replay\iclientreplay.h"
$File "$SRCDIR\common\replay\iclientreplaycontext.h"
$File "$SRCDIR\common\replay\iqueryablereplayitem.h"
$File "$SRCDIR\common\replay\irecordingsession.h"
$File "$SRCDIR\common\replay\irecordingsessionblockmanager.h"
$File "$SRCDIR\common\replay\ireplayrenderqueue.h"
$File "$SRCDIR\common\replay\ireplaycamera.h"
$File "$SRCDIR\common\replay\ireplayerrorsystem.h"
$File "$SRCDIR\common\replay\ireplaysystem.h"
$File "$SRCDIR\common\replay\ireplayfactory.h"
$File "$SRCDIR\common\replay\ireplaycontext.h"
$File "$SRCDIR\common\replay\ireplaymanager.h"
$File "$SRCDIR\common\replay\ireplaymovie.h"
$File "$SRCDIR\common\replay\ireplaymoviemanager.h"
$File "$SRCDIR\common\replay\ireplaymovierenderer.h"
$File "$SRCDIR\common\replay\irecordingsessionmanager.h"
$File "$SRCDIR\common\replay\ireplayperformancecontroller.h"
$File "$SRCDIR\common\replay\ireplayperformancemanager.h"
$File "$SRCDIR\common\replay\ireplayperformanceplaybackhandler.h"
$File "$SRCDIR\common\replay\ireplayscreenshotmanager.h"
$File "$SRCDIR\common\replay\ireplayscreenshotsystem.h"
$File "$SRCDIR\common\replay\ireplayserializeable.h"
$File "$SRCDIR\common\replay\ireplaysessionrecorder.h"
$File "$SRCDIR\common\replay\iserverengine.h"
$File "$SRCDIR\common\replay\iserverreplaycontext.h"
}
$Folder "Link Libraries"
{
$Lib tier2
$Lib mathlib
$Lib bitmap
$Lib $LIBCOMMON\replay_common
$Lib $LIBCOMMON\lzma
$Lib vtf
$Lib "$LIBCOMMON/bzip2"
$Lib "$LIBCOMMON/libjpeg" [!$DEDICATED]
$Libexternal $LIBCOMMON/libcrypto [$OSXALL]
$Libexternal "$SRCDIR\lib\common\$(CRYPTOPPDIR)\libcrypto" [$LINUXALL]
$Libexternal libpng [!$VS2015]
$Libexternal $LIBCOMMON/libpng [$VS2015]
$ImpLib "$LIBCOMMON\curl" [$OSXALL]
$Lib "$LIBCOMMON\libcurl" [$WIN32&&!$VS2015]
$Lib "libz" [$WIN32]
$Libexternal libz [$LINUXALL]
}
}
+14
View File
@@ -0,0 +1,14 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay_dbg.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
ConVar replay_debug( "replay_debug", "0", FCVAR_DONTRECORD, "Show Replay debug info." );
//----------------------------------------------------------------------------------------
+40
View File
@@ -0,0 +1,40 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAY_DBG_H
#define REPLAY_DBG_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ienginereplay.h"
#include "convar.h"
//----------------------------------------------------------------------------------------
extern ConVar replay_debug;
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
#define IF_REPLAY_DBG( x_ ) if ( replay_debug.GetBool() ) { x_; }
#define IF_REPLAY_DBGN( x_, n_ ) if ( replay_debug.GetInt() >= n_ ) { x_; }
#define IF_REPLAY_DBG2( x_ ) IF_REPLAY_DBGN( x_, 2 )
#define IF_REPLAY_DBG3( x_ ) IF_REPLAY_DBGN( x_, 3 )
#ifndef DEDICATED
#define DBG( x_ ) IF_REPLAY_DBG( Msg( "%f: " x_, g_pEngine->GetHostTime() ) )
#define DBGN( x_, n_ ) IF_REPLAY_DBGN( Msg( "%f: " x_, g_pEngine->GetHostTime() ), n_ )
#define DBG2( x_ ) DBGN( x_, 2 )
#define DBG3( x_ ) DBGN( x_, 3 )
#else
#define DBG( x_ )
#define DBG2( x_ )
#endif
//----------------------------------------------------------------------------------------
#endif // REPLAY_DBG_H
+253
View File
@@ -0,0 +1,253 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replay_reconstructor.h"
#include "replay/replay.h"
#include "cl_recordingsessionblock.h"
#include "cl_recordingsession.h"
#include "cl_replaycontext.h"
#include "cl_replaymanager.h"
#include "UtlSortVector.h"
#include "demofile/demoformat.h"
#include "lzss.h"
#include "compression.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
class CReplay_LessFunc
{
public:
bool Less( const CClientRecordingSessionBlock *pBlock1, const CClientRecordingSessionBlock *pBlock2, void *pCtx )
{
return ( pBlock1->m_iReconstruction < pBlock2->m_iReconstruction );
}
};
//----------------------------------------------------------------------------------------
bool Replay_Reconstruct( CReplay *pReplay, bool bDeleteBlocks/*=true*/ )
{
// Get the session for the given replay
CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pReplay->m_hSession ) );
if ( !pSession )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_BadSession" );
return false;
}
// Dynamically load blocks for the session
pSession->LoadBlocksForSession();
// How many blocks needed
const int nNumBlocksNeeded = pReplay->m_iMaxSessionBlockRequired + 1;
// Enough blocks to proceed?
const CBaseRecordingSession::BlockContainer_t &vecAllBlocks = pSession->GetBlocks();
if ( vecAllBlocks.Count() < nNumBlocksNeeded )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_NotEnoughBlocksForReconstruction" );
return false;
}
// Add needed blocks in sorted order
CUtlSortVector< const CClientRecordingSessionBlock *, CReplay_LessFunc > vecReplayBlocks;
FOR_EACH_VEC( vecAllBlocks, i )
{
const CClientRecordingSessionBlock *pCurBlock = CL_CastBlock( vecAllBlocks[ i ] );
// Don't add more blocks than are needed
if ( pCurBlock->m_iReconstruction >= nNumBlocksNeeded )
continue;
// Sorted insert
vecReplayBlocks.Insert( pCurBlock );
}
// Now we need to do an integrity check on all blocks
int iLastReconstructionIndex = 0; // All replay reconstruction will start with block 0
FOR_EACH_VEC( vecReplayBlocks, i )
{
const CClientRecordingSessionBlock *pCurBlock = vecReplayBlocks[ i ];
// Haven't downloaded yet or failed to download for some reason?
if ( !pCurBlock->DownloadedSuccessfully() )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_BlocksNotDLd" );
return false;
}
// Check against reconstruction indices and make sure the list is continuous
if ( pCurBlock->m_iReconstruction - iLastReconstructionIndex > 1 )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_NonContinuous" );
return false;
}
// Cache for next iteration
iLastReconstructionIndex = pCurBlock->m_iReconstruction;
}
// Open the target, reconstruction file - "<session_name>_<replay handle>.dem"
CUtlString strReconstructedFileFilename;
strReconstructedFileFilename.Format( "%s%s_%i.dem", CL_GetReplayManager()->GetIndexPath(), pSession->m_strName.Get(), pReplay->GetHandle() );
FileHandle_t hReconstructedFile = g_pFullFileSystem->Open( strReconstructedFileFilename.Get(), "wb" );
if ( hReconstructedFile == FILESYSTEM_INVALID_HANDLE )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_OpenOutFile" );
return false;
}
// Now that we have an ordered list of replays to reconstruct, create the mother file
bool bFailed = false;
FOR_EACH_VEC( vecReplayBlocks, i )
{
const CClientRecordingSessionBlock *pCurBlock = vecReplayBlocks[ i ];
// Open the partial file for the current replay
const char *pFilename = pCurBlock->m_szFullFilename;
FileHandle_t hBlockFile = g_pFullFileSystem->Open( pFilename, "rb" );
if ( hBlockFile == FILESYSTEM_INVALID_HANDLE )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_BlockDNE" );
bFailed = true;
break;
}
int nSize = g_pFullFileSystem->Size( hBlockFile );
if ( nSize == 0 )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_ZeroLengthBlock" );
bFailed = true;
break;
}
char *pBuffer = (char *)new char[ nSize ];
if ( !pBuffer )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_OutOfMemory" );
bFailed = true;
}
else
{
// Read the file
if ( nSize != g_pFullFileSystem->Read( pBuffer, nSize, hBlockFile ) )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_FailedToRead" );
bFailed = true;
}
else
{
// Decompress if necessary
CompressorType_t nCompressorType = (CompressorType_t)pCurBlock->m_nCompressorType;
if ( nCompressorType != COMPRESSORTYPE_INVALID )
{
ICompressor *pCompressor = CreateCompressor( nCompressorType );
if ( !pCompressor )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_DecompressorCreate" );
bFailed = true;
}
else
{
const unsigned int nCompressedSize = nSize;
unsigned int nUncompressedSize = pCurBlock->m_uUncompressedSize;
char *pUncompressedBuffer = new char[ nUncompressedSize ];
if ( !pUncompressedBuffer )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_Alloc" );
bFailed = true;
}
else
{
if ( !nUncompressedSize )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_UncompressedSizeIsZero" );
bFailed = true;
}
else if ( !pCompressor->Decompress( pUncompressedBuffer, &nUncompressedSize, pBuffer, nCompressedSize ) )
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_Decompression" );
bFailed = true;
}
}
if ( bFailed )
{
delete [] pUncompressedBuffer;
}
else
{
// Overwrite buffer pointer and buffer size
pBuffer = pUncompressedBuffer;
nSize = nUncompressedSize;
}
// Free compressor
delete pCompressor;
}
}
// Append the read data to the mother file
if ( g_pFullFileSystem->Write( pBuffer, nSize, hReconstructedFile ) == nSize )
{
g_pFullFileSystem->Close( hBlockFile );
}
else
{
CL_GetErrorSystem()->AddErrorFromTokenName( "#Replay_Err_Recon_FailedToWrite" );
bFailed = true;
}
}
}
// Free
delete [] pBuffer;
}
if ( !bFailed )
{
// Add dem_stop - embed the calculated end tick
const int nLengthInTicks = g_pEngine->TimeToTicks( pReplay->m_flLength );
int nLastTick = 0;
if ( nLengthInTicks > 0 )
{
nLastTick = pReplay->m_nSpawnTick + nLengthInTicks;
}
unsigned char szEndTickBuf[4];
*( (int32 *)szEndTickBuf ) = nLastTick;
unsigned char szStopBuf[] = { dem_stop, szEndTickBuf[0], szEndTickBuf[1], szEndTickBuf[2], szEndTickBuf[3], 0 };
int nStopSize = sizeof( szStopBuf );
if ( g_pFullFileSystem->Write( szStopBuf, nStopSize, hReconstructedFile ) != nStopSize )
{
Warning( "Replay: Failed to write stop bits to reconstructed replay file \"%s\"\n", strReconstructedFileFilename.Get() );
// Should still run fine
}
// Save reconstructed filename, which will serve to indicate whether the
// replay's been successfully reconstructed or not.
pReplay->m_strReconstructedFilename = strReconstructedFileFilename;
// Mark the replay for flush
CL_GetReplayManager()->FlagForFlush( pReplay, true );
// Delete blocks - removes from session, session block manager, and from disk if no other replays are depending on them.
if ( bDeleteBlocks )
{
pSession->DeleteBlocks();
}
}
// Close reconstructed file
g_pFullFileSystem->Close( hReconstructedFile );
return true;
}
//----------------------------------------------------------------------------------------
+21
View File
@@ -0,0 +1,21 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAY_RECONSTRUCTOR_H
#define REPLAY_RECONSTRUCTOR_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
class CReplay;
//----------------------------------------------------------------------------------------
bool Replay_Reconstruct( CReplay *pReplay, bool bDeleteBlocks = true );
//----------------------------------------------------------------------------------------
#endif // REPLAY_RECONSTRUCTOR_H
+522
View File
@@ -0,0 +1,522 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replaysystem.h"
#include "tier2/tier2.h"
#include "iserver.h"
#include "iclient.h"
#include "icliententitylist.h"
#include "igameevents.h"
#include "replay/ireplaymovierenderer.h"
#include "replay/ireplayscreenshotsystem.h"
#include "replay/replayutils.h"
#include "replay/replaylib.h"
#include "sv_sessionrecorder.h"
#include "sv_recordingsession.h"
#include "cl_screenshotmanager.h"
#include "netmessages.h"
#include "thinkmanager.h"
#include "managertest.h"
#include "vprof.h"
#include "sv_fileservercleanup.h"
#if !defined( _X360 )
#include "winlite.h"
#include "xbox/xboxstubs.h"
#endif
// TODO: Deal with linux build includes
#ifdef IS_WINDOWS_PC
#include <winsock.h>
#endif
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#undef CreateEvent // Can't call IGameEventManager2::CreateEvent() without this
//----------------------------------------------------------------------------------------
#if !defined( DEDICATED )
IEngineClientReplay *g_pEngineClient = NULL;
CClientReplayContext *g_pClientReplayContextInternal = NULL;
IVDebugOverlay *g_pDebugOverlay = NULL;
IDownloadSystem *g_pDownloadSystem = NULL;
#endif
vgui::ILocalize *g_pVGuiLocalize = NULL;
CServerReplayContext *g_pServerReplayContext = NULL;
IClientReplay *g_pClient = NULL;
IServerReplay *g_pServer = NULL;
IEngineReplay *g_pEngine = NULL;
IGameEventManager2 *g_pGameEventManager = NULL;
IEngineTrace *g_pEngineTraceClient = NULL;
IReplayDemoPlayer *g_pReplayDemoPlayer = NULL;
IClientEntityList *entitylist = NULL; // icliententitylist.h forces the use of this name by externing in the header
//----------------------------------------------------------------------------------------
#define REPLAY_INIT( exp_ ) \
if ( !( exp_ ) ) \
{ \
Warning( "CReplaySystem::Connect() failed on: \"%s\"!\n", #exp_ ); \
return false; \
}
//----------------------------------------------------------------------------------------
class CReplaySystem : public CTier2AppSystem< IReplaySystem >
{
typedef CTier2AppSystem< IReplaySystem > BaseClass;
public:
virtual bool Connect( CreateInterfaceFn fnFactory )
{
REPLAY_INIT( fnFactory );
REPLAY_INIT( BaseClass::Connect( fnFactory ) );
ConVar_Register( FCVAR_CLIENTDLL );
REPLAY_INIT( g_pFullFileSystem );
g_pEngine = (IEngineReplay *)fnFactory( ENGINE_REPLAY_INTERFACE_VERSION, NULL );
REPLAY_INIT( g_pEngine );
REPLAY_INIT( g_pEngine->IsSupportedModAndPlatform() );
#if !defined( DEDICATED )
g_pEngineClient = (IEngineClientReplay *)fnFactory( ENGINE_REPLAY_CLIENT_INTERFACE_VERSION, NULL );
REPLAY_INIT( g_pEngineClient );
g_pEngineTraceClient = (IEngineTrace *)fnFactory( INTERFACEVERSION_ENGINETRACE_CLIENT, NULL );
REPLAY_INIT( g_pEngineTraceClient );
g_pReplayDemoPlayer = (IReplayDemoPlayer *)fnFactory( INTERFACEVERSION_REPLAYDEMOPLAYER, NULL );
REPLAY_INIT( g_pReplayDemoPlayer );
g_pVGuiLocalize = (vgui::ILocalize *)fnFactory( VGUI_LOCALIZE_INTERFACE_VERSION, NULL );
REPLAY_INIT( g_pVGuiLocalize );
g_pDebugOverlay = ( IVDebugOverlay * )fnFactory( VDEBUG_OVERLAY_INTERFACE_VERSION, NULL );
REPLAY_INIT( g_pDebugOverlay );
#endif
g_pGameEventManager = (IGameEventManager2 *)fnFactory( INTERFACEVERSION_GAMEEVENTSMANAGER2, NULL );
REPLAY_INIT( g_pGameEventManager );
#if !defined( DEDICATED )
g_pDownloadSystem = (IDownloadSystem *)fnFactory( INTERFACEVERSION_DOWNLOADSYSTEM, NULL );
REPLAY_INIT( g_pDownloadSystem );
// Create client context now if not running a dedicated server
if ( !g_pEngine->IsDedicated() )
{
g_pClientReplayContextInternal = new CClientReplayContext();
}
// ...and create server replay context if we are
else
#endif
{
g_pServerReplayContext = new CServerReplayContext();
}
#if defined( DEDICATED )
REPLAY_INIT( ReplayLib_Init( g_pEngine->GetGameDir(), NULL ) ) // Init without the client replay context
#else
REPLAY_INIT( ReplayLib_Init( g_pEngine->GetGameDir(), g_pClientReplayContextInternal ) );
#endif
Test();
return true;
}
virtual void Disconnect()
{
BaseClass::Disconnect();
}
virtual InitReturnVal_t Init()
{
InitReturnVal_t nRetVal = BaseClass::Init();
if ( nRetVal != INIT_OK )
return nRetVal;
return INIT_OK;
}
virtual void Shutdown()
{
BaseClass::Shutdown();
#if !defined( DEDICATED )
delete g_pClientReplayContextInternal;
g_pClientReplayContextInternal = NULL;
#endif
delete g_pServerReplayContext;
g_pServerReplayContext = NULL;
}
virtual void Think()
{
VPROF_BUDGET( "CReplaySystem::Think", VPROF_BUDGETGROUP_REPLAY );
g_pThinkManager->Think();
}
virtual bool IsReplayEnabled()
{
extern ConVar replay_enable;
return replay_enable.GetInt() != 0;
}
virtual bool IsRecording()
{
// NOTE: demoplayer->IsPlayingBack() needs to be checked here, as "replay_enable" and
// "replay_recording" will inevitably get stored with signon data in any playing demo.
// If the !demoplayer->IsPlayingBack() line is omitted below, Replay_IsRecording()
// becomes useless during demo playback and will always return true.
extern ConVar replay_recording;
#if !defined( DEDICATED )
return IsReplayEnabled() &&
replay_recording.GetInt() &&
!g_pEngineClient->IsDemoPlayingBack();
#else
return IsReplayEnabled() &&
replay_recording.GetInt();
#endif
}
//----------------------------------------------------------------------------------------
// Client-specific implementation:
//----------------------------------------------------------------------------------------
virtual bool CL_Init( CreateInterfaceFn fnClientFactory )
{
#if !defined( DEDICATED )
g_pClient = (IClientReplay *)fnClientFactory( CLIENT_REPLAY_INTERFACE_VERSION, NULL );
if ( !g_pClient )
return false;
entitylist = (IClientEntityList *)fnClientFactory( VCLIENTENTITYLIST_INTERFACE_VERSION, NULL );
if ( !entitylist )
return false;
if ( !g_pClientReplayContextInternal->Init( fnClientFactory ) )
return false;
return true;
#else
return false;
#endif
}
virtual void CL_Shutdown()
{
#if !defined( DEDICATED )
if ( g_pClientReplayContextInternal && g_pClientReplayContextInternal->IsInitialized() )
{
g_pClientReplayContextInternal->Shutdown();
}
#endif
}
virtual void CL_Render()
{
#if !defined( DEDICATED )
// If the replay system wants to take a screenshot, do it now
if ( g_pClientReplayContextInternal->m_pScreenshotManager->ShouldCaptureScreenshot() )
{
g_pClientReplayContextInternal->m_pScreenshotManager->DoCaptureScreenshot();
return;
}
// Currently rendering? NOTE: GetMovieRenderer() only returns a valid ptr during rendering
IReplayMovieRenderer *pReplayMovieRenderer = g_pClientReplayContextInternal->GetMovieRenderer();
if ( !pReplayMovieRenderer )
return;
pReplayMovieRenderer->RenderVideo();
#endif
}
virtual IClientReplayContext *CL_GetContext()
{
#if !defined( DEDICATED )
return g_pClientReplayContextInternal;
#else
return NULL;
#endif
}
//----------------------------------------------------------------------------------------
// Server-specific implementation:
//----------------------------------------------------------------------------------------
virtual bool SV_Init( CreateInterfaceFn fnServerFactory )
{
if ( !g_pEngine->IsDedicated() || !g_pServerReplayContext )
return false;
g_pServer = (IServerReplay *)fnServerFactory( SERVER_REPLAY_INTERFACE_VERSION, NULL );
if ( !g_pServer )
return false;
Assert( !ReplayServer() );
return g_pServerReplayContext->Init( fnServerFactory );
}
virtual void SV_Shutdown()
{
if ( g_pServerReplayContext && g_pServerReplayContext->IsInitialized() )
{
g_pServerReplayContext->Shutdown();
}
}
virtual IServerReplayContext *SV_GetContext()
{
return g_pServerReplayContext;
}
virtual bool SV_ShouldBeginRecording( bool bIsInWaitingForPlayers )
{
extern ConVar replay_enable;
return !bIsInWaitingForPlayers &&
#if !defined( DEDICATED )
!g_pEngineClient->IsPlayingReplayDemo() &&
#endif
replay_enable.GetBool();
}
virtual void SV_NotifyReplayRequested()
{
if ( !g_pEngine->IsSupportedModAndPlatform() )
return;
CServerRecordingSession *pSession = SV_GetRecordingSessionInProgress();
if ( !pSession )
return;
// A replay was requested - notify the session so we don't throw it away at the end of the round
pSession->NotifyReplayRequested();
}
virtual void SV_SendReplayEvent( const char *pEventName, int nClientSlot )
{
// Attempt to create the event
IGameEvent *pEvent = g_pGameEventManager->CreateEvent( pEventName, true );
if ( !pEvent )
return;
SV_SendReplayEvent( pEvent, nClientSlot );
}
virtual void SV_SendReplayEvent( IGameEvent *pEvent, int nClientSlot/*=-1*/ )
{
IServer *pGameServer = g_pEngine->GetGameServer();
if ( !pEvent )
return;
// Write event info to SVC_GameEvent msg
char buffer_data[MAX_EVENT_BYTES];
SVC_GameEvent msg;
msg.SetReliable( false );
msg.m_DataOut.StartWriting( buffer_data, sizeof( buffer_data ) );
if ( !g_pGameEventManager->SerializeEvent( pEvent, &msg.m_DataOut ) )
{
DevMsg( "Replay_SendReplayEvent(): failed to serialize event '%s'.\n", pEvent->GetName() );
goto free_event;
}
// Send to all clients?
if ( nClientSlot == -1 )
{
for ( int i = 0; i < pGameServer->GetClientCount(); ++i )
{
IClient *pClient = pGameServer->GetClient( i );
if ( pClient )
{
// Send the message
pClient->SendNetMsg( msg );
}
}
}
else // Send to just the one client?
{
IClient *pClient = pGameServer->GetClient( nClientSlot );
if ( pClient )
{
// Send the message
pClient->SendNetMsg( msg );
}
}
free_event:
g_pGameEventManager->FreeEvent( pEvent );
}
virtual void SV_EndRecordingSession( bool bForceSynchronousPublish/*=false*/ )
{
if ( !g_pEngine->IsSupportedModAndPlatform() )
return;
if ( !ReplayServer() )
return;
SV_GetSessionRecorder()->StopRecording( false );
if ( bForceSynchronousPublish )
{
// Publish all files
SV_GetSessionRecorder()->PublishAllSynchronous();
// This should unlock all sessions
SV_GetSessionRecorder()->UpdateSessionLocks();
// Let the session manager do any cleanup - this will remove files associated with a ditched
// session. For example, if the server was shut down in the middle of a round where no one
// saved a replay, the files will be published synchronously above and then cleaned up
// synchronously here.
SV_GetRecordingSessionManager()->DoSessionCleanup();
// Since the recording session manager will kick off a cleanup job, we need to wait for it
// here since we're shutting down.
SV_GetFileserverCleaner()->BlockForCompletion();
}
}
void Test()
{
#if !defined( DEDICATED ) && _DEBUG
// This gets called after interfaces are hooked up, and before any of the
// internal replay systems get init'd.
CTestManager::Test();
#endif
}
};
//----------------------------------------------------------------------------------------
static CReplaySystem s_Replay;
IReplaySystem *g_pReplay = &s_Replay;
//----------------------------------------------------------------------------------------
EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CReplaySystem, IReplaySystem, REPLAY_INTERFACE_VERSION,
s_Replay );
//----------------------------------------------------------------------------------------
void Replay_MsgBox( const char *pText )
{
g_pClient->DisplayReplayMessage( pText, false, true, NULL );
}
void Replay_MsgBox( const wchar_t *pText )
{
g_pClient->DisplayReplayMessage( pText, false, true, NULL );
}
const char *Replay_GetDownloadURLPath()
{
static char s_szDownloadURLPath[MAX_OSPATH];
extern ConVar replay_fileserver_path; // NOTE: replicated
V_strcpy_safe( s_szDownloadURLPath, replay_fileserver_path.GetString() );
V_StripTrailingSlash( s_szDownloadURLPath );
V_FixSlashes( s_szDownloadURLPath, '/' );
// Get rid of starting slash
if ( s_szDownloadURLPath[0] == '/' )
return &s_szDownloadURLPath[1];
return s_szDownloadURLPath;
}
const char *Replay_GetDownloadURL()
{
#if 0
// Get the local host name
char szHostname[MAX_OSPATH];
if ( gethostname( szHostname, sizeof( szHostname ) ) == -1 )
{
Error( "Failed to send to Replay to client - couldn't get local IP.\n" );
return "";
}
#endif
// Construct the URL based on replicated cvars
static char s_szFileURL[ MAX_OSPATH ];
extern ConVar replay_fileserver_protocol;
extern ConVar replay_fileserver_host;
extern ConVar replay_fileserver_port;
V_snprintf(
s_szFileURL, sizeof( s_szFileURL ),
"%s://%s:%i/%s/",
replay_fileserver_protocol.GetString(),
replay_fileserver_host.GetString(),
replay_fileserver_port.GetInt(),
Replay_GetDownloadURLPath()
);
// Cleanup
V_FixDoubleSlashes( s_szFileURL + V_strlen("http://") );
return s_szFileURL;
}
//----------------------------------------------------------------------------------------
// Purpose: (client/server) Crack a URL into a base and a path
// NOTE: the URL *must contain a port* !
//
// Example: http://some.base.url:8080/a/path/here.txt cracks into:
// pBaseURL = "http://some.base.url:8080"
// pURLPath = "/a/path/here.txt"
//----------------------------------------------------------------------------------------
void Replay_CrackURL( const char *pURL, char *pBaseURLOut, char *pURLPathOut )
{
const char *pColon;
const char *pURLPath;
// Must at least have "http://"
if ( V_strlen(pURL) < 6 )
goto fail;
// Skip protocol ':' (eg http://)
pColon = V_strstr( pURL, ":" );
if ( !pColon )
goto fail;
// Find next colon
pColon = V_strstr( pColon + 1, ":" );
if ( !pColon )
goto fail;
// Copies "http[s]://<address>:<port>
pURLPath = V_strstr( pColon, "/" );
V_strncpy( pBaseURLOut, pURL, pURLPath - pURL + 1 );
V_strcpy( pURLPathOut, pURLPath );
return;
fail:
AssertMsg( 0, "Replay_CrackURL() was passed an invalid URL and has failed. This should never happen." );
}
#ifndef DEDICATED
void Replay_HudMsg( const char *pText, const char *pSound, bool bUrgent )
{
g_pClient->DisplayReplayMessage( pText, bUrgent, false, pSound );
}
#endif
//----------------------------------------------------------------------------------------
+74
View File
@@ -0,0 +1,74 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef REPLAYDLL_H
#define REPLAYDLL_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/ireplaysystem.h"
#include "replay/ienginereplay.h"
#include "replay/iclientreplay.h"
#include "replay/iserverreplay.h"
#include "replay/ireplaydemoplayer.h"
#include "replay/ireplayserver.h"
#include "igameevents.h"
#include "engine/IEngineTrace.h"
#include "engine/idownloadsystem.h"
#include "icliententitylist.h"
#if !defined( DEDICATED )
#include "cl_replaycontext.h"
#include "engine/ivdebugoverlay.h"
#endif
#include "vgui/ILocalize.h"
#include "sv_replaycontext.h"
#include "convar.h"
//----------------------------------------------------------------------------------------
extern IReplaySystem *g_pReplay;
extern IClientReplay *g_pClient;
extern IServerReplay *g_pServer;
extern IGameEventManager2 *g_pGameEventManager;
extern IEngineTrace *g_pEngineTraceClient;
extern IReplayDemoPlayer *g_pReplayDemoPlayer;
extern IEngineReplay *g_pEngine;
extern vgui::ILocalize *g_pVGuiLocalize;
#if !defined( DEDICATED )
extern IEngineClientReplay *g_pEngineClient;
extern IVDebugOverlay *g_pDebugOverlay;
extern IDownloadSystem *g_pDownloadSystem;
#endif
//----------------------------------------------------------------------------------------
inline IReplayServer *ReplayServer()
{
return g_pEngine->GetReplayServer();
}
inline IServer *ReplayServerAsIServer()
{
return g_pEngine->GetReplayServerAsIServer();
}
//----------------------------------------------------------------------------------------
void Replay_MsgBox( const char *pText ); // Display a message box
void Replay_MsgBox( const wchar_t *pText );
const char *Replay_GetBaseDir(); // Returns the replays base dir - eg, "/home/user/<...>/replays/"
const char *Replay_GetDownloadURLPath();
const char *Replay_GetDownloadURL();
void Replay_CrackURL( const char *pURL, char *pBaseURLOut, char *pURLPathOut );
#ifndef DEDICATED
void Replay_HudMsg( const char *pText, const char *pSound = NULL, bool bUrgent = false );
#endif
//----------------------------------------------------------------------------------------
#endif // REPLAYDLL_H
+27
View File
@@ -0,0 +1,27 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sessioninfoheader.h"
#include "dbg.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
bool ReadSessionInfoHeader( const void *pBuf, int nBufSize, SessionInfoHeader_t &outHeader )
{
if ( nBufSize < sizeof( SessionInfoHeader_t ) )
{
AssertMsg( 0, "Buffer size too small to read session info header" );
return false;
}
// Read the header
V_memcpy( &outHeader, pBuf, sizeof( SessionInfoHeader_t ) );
return true;
}
//----------------------------------------------------------------------------------------
+56
View File
@@ -0,0 +1,56 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SESSIONINFOHEADER_H
#define SESSIONINFOHEADER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "replay/shared_defs.h"
#include "compression.h"
#include "strtools.h"
//----------------------------------------------------------------------------------------
#define SESSION_INFO_VERSION 1
//----------------------------------------------------------------------------------------
struct SessionInfoHeader_t
{
inline SessionInfoHeader_t()
{
V_memset( this, 0, sizeof( SessionInfoHeader_t ) );
m_nCompressorType = COMPRESSORTYPE_INVALID;
m_uVersion = SESSION_INFO_VERSION;
}
//
// Session info files may be around for days, during which this format may change - so
// we need to be careful not to break it.
//
// Therefore, any changes to data here should be reflected in the size of m_aUnused.
//
uint8 m_uVersion;
char m_szSessionName[MAX_SESSIONNAME_LENGTH]; // Name of session
bool m_bRecording; // Is this session currenty recording?
int32 m_nNumBlocks; // # blocks in the session so far if recording, or total if not recording
CompressorType_t m_nCompressorType; // COMPRESSORTYPE_INVALID if header is not compressed
uint8 m_aHash[16]; // MD5 digest on payload
uint32 m_uPayloadSize; // Size of the payload - the compressed payload if it's compressed
uint32 m_uPayloadSizeUC; // Size of the uncompressed payload, if its compressed, otherwise 0
uint8 m_aUnused[128];
};
//----------------------------------------------------------------------------------------
bool ReadSessionInfoHeader( const void *pBuf, int nBufSize, SessionInfoHeader_t &outHeader );
//----------------------------------------------------------------------------------------
#endif // SESSIONINFOHEADER_H
+99
View File
@@ -0,0 +1,99 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replaysystem.h"
#include "cl_replaymanager.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
void OnReplayEnableChanged( IConVar *pVar, const char *pOldValue, float flOldValue );
void OnReplayRecordingChanged( IConVar *pVar, const char *pOldValue, float flOldValue );
//----------------------------------------------------------------------------------------
// Replicated
ConVar replay_enable( "replay_enable", "0", FCVAR_REPLICATED | FCVAR_DONTRECORD, "Enable Replay recording on server", true, 0, true, 1, OnReplayEnableChanged );
ConVar replay_recording( "replay_recording", "0", FCVAR_REPLICATED | FCVAR_DONTRECORD | FCVAR_HIDDEN, "", true, 0, true, 1, OnReplayRecordingChanged );
ConVar replay_flushinterval( "replay_flushinterval", "15", FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Replay system will flush to disk a maximum of every replay_flushinterval seconds.", true, 1.0f, true, 60.0f );
//----------------------------------------------------------------------------------------
//
// A little class to keep OnReplayEnableChanged() from recursing unnecessarily
//
class CSimpleCounter
{
public:
CSimpleCounter() { ++m_nCounter; }
~CSimpleCounter() { --m_nCounter; }
int GetCounter() const { return m_nCounter; }
private:
static int m_nCounter;
};
int CSimpleCounter::m_nCounter = 0;
//----------------------------------------------------------------------------------------
void OnReplayEnableChanged( IConVar *pVar, const char *pOldValue, float flOldValue )
{
// We want to avoid recursing when we SetValue() on replay_enable (ie 'var')
CSimpleCounter counter;
if ( counter.GetCounter() != 1 )
return;
if ( !g_pEngine->IsDedicated() )
return;
ConVarRef var( pVar );
if ( (int)flOldValue == var.GetInt() )
return;
/*
ConVarRef tv_enable( "tv_enable" );
if ( var.GetBool() && tv_enable.IsValid() && tv_enable.GetBool() )
{
var.SetValue( 0 );
Warning( "Error: SourceTV is enabled. Please disable SourceTV if you wish to enable Replay.\n" );
return;
}
*/
const int nNewValue = var.GetInt();
if ( nNewValue )
{
g_pServerReplayContext->FlagForConVarSanityCheck();
}
else
{
// Reset value - note that the recursion depth counter will keep this from being dumb.
var.SetValue( 0 );
// End recording, which will clear the value again.
g_pReplay->SV_EndRecordingSession( false );
}
g_pEngine->RecalculateTags();
}
void OnReplayRecordingChanged( IConVar *pVar, const char *pOldValue, float flOldValue )
{
if ( g_pEngine->IsDedicated() )
return;
#if !defined( DEDICATED )
// If we're playing back a replay, we don't care
if ( g_pEngineClient->IsPlayingReplayDemo() )
return;
// Client-only
CL_GetReplayManager()->OnReplayRecordingCvarChanged();
#endif
}
+152
View File
@@ -0,0 +1,152 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "shared_replaycontext.h"
#include "replay/shared_defs.h"
#include "replay/replayutils.h"
#include "baserecordingsession.h"
#include "baserecordingsessionblock.h"
#include "baserecordingsessionmanager.h"
#include "baserecordingsessionblockmanager.h"
#include "thinkmanager.h"
#include "filesystem.h"
#include "errorsystem.h"
#undef Yield
#include "vstdlib/jobthread.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CSharedReplayContext::CSharedReplayContext( IReplayContext *pOwnerContext )
: m_pOwnerContext( pOwnerContext ),
m_pRecordingSessionManager( NULL ),
m_pRecordingSessionBlockManager( NULL ),
m_pErrorSystem( NULL ),
m_pThreadPool( NULL ),
m_bInit( false )
{
}
CSharedReplayContext::~CSharedReplayContext()
{
delete m_pRecordingSessionManager;
delete m_pRecordingSessionBlockManager;
delete m_pErrorSystem;
delete m_pThreadPool;
}
bool CSharedReplayContext::Init( CreateInterfaceFn fnFactory )
{
m_strRelativeBasePath.Format(
"%s%c%s%c",
SUBDIR_REPLAY,
CORRECT_PATH_SEPARATOR,
m_strSubDir.Get(),
CORRECT_PATH_SEPARATOR
);
m_strBasePath.Format(
"%s%c%s",
g_pEngine->GetGameDir(),
CORRECT_PATH_SEPARATOR,
m_strRelativeBasePath.Get()
);
// Owning context should have initialized these by now
// NOTE: Session manager init must come after block manager init since session manager
// assumes all blocks have been loaded.
//
m_pRecordingSessionBlockManager->Init();
m_pRecordingSessionManager->Init();
if ( !InitThreadPool() )
return false;
m_bInit = true;
return true;
}
bool CSharedReplayContext::InitThreadPool()
{
// Create thread pool
Log( "Replay: Creating thread pool..." );
IThreadPool *pThreadPool = CreateThreadPool();
if ( !pThreadPool )
{
Log( "failed!\n" );
return false;
}
Log( "succeeded.\n" );
// Jon says: The client only really needs a single "ReplayContext" thread, so that the replay editor can write
// data asynchronously. The game server does in fact require 4 threads, and can be configured to use more
// via the replay_max_publish_threads convar.
int nMaxThreads = 1;
if ( g_pEngine->IsDedicated() )
{
// Use the convar for max threads on servers
extern ConVar replay_max_publish_threads;
nMaxThreads = replay_max_publish_threads.GetInt();
}
// Start thread pool
Log( "Replay: Starting thread pool with %i threads...", nMaxThreads );
if ( !pThreadPool->Start( ThreadPoolStartParams_t( true, nMaxThreads ), "ReplayContext" ) )
{
Log( "failed!\n" );
return false;
}
Log( "succeeded.\n" );
m_pThreadPool = pThreadPool;
return true;
}
void CSharedReplayContext::Shutdown()
{
m_pRecordingSessionBlockManager->Shutdown();
m_pRecordingSessionManager->Shutdown();
m_pThreadPool->Stop();
}
void CSharedReplayContext::Think()
{
}
const char *CSharedReplayContext::GetRelativeBaseDir() const
{
return m_strRelativeBasePath.Get();
}
const char *CSharedReplayContext::GetBaseDir() const
{
return m_strBasePath.Get();
}
const char *CSharedReplayContext::GetReplaySubDir() const
{
return m_strSubDir.Get();
}
void CSharedReplayContext::EnsureDirHierarchy()
{
g_pFullFileSystem->CreateDirHierarchy( m_strBasePath.Get() );
}
//----------------------------------------------------------------------------------------
bool RunJobToCompletion( IThreadPool *pThreadPool, CJob *pJob )
{
pThreadPool->AddJob( pJob );
pJob->WaitForFinish();
return pJob->Executed();
}
//----------------------------------------------------------------------------------------
+94
View File
@@ -0,0 +1,94 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SHARED_REPLAYCONTEXT_H
#define SHARED_REPLAYCONTEXT_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionmanager.h"
#include "baserecordingsessionblockmanager.h"
#include "errorsystem.h"
//----------------------------------------------------------------------------------------
class CBaseRecordingSessionManager;
class CBaseRecordingSessionBlockManager;
class CErrorSystem;
class IThreadPool;
//----------------------------------------------------------------------------------------
class CSharedReplayContext
{
public:
CSharedReplayContext( IReplayContext *pOwnerContext );
virtual ~CSharedReplayContext();
// Sets up public data members and such
virtual bool Init( CreateInterfaceFn fnFactory );
virtual void Shutdown();
virtual void Think();
virtual bool IsInitialized() const { return m_bInit; }
virtual const char *GetRelativeBaseDir() const;
virtual const char *GetBaseDir() const;
virtual const char *GetReplaySubDir() const;
IThreadPool *m_pThreadPool;
CBaseRecordingSessionManager *m_pRecordingSessionManager;
CBaseRecordingSessionBlockManager *m_pRecordingSessionBlockManager;
CErrorSystem *m_pErrorSystem;
CUtlString m_strRelativeBasePath; // eg: "/replay/server/"
CUtlString m_strBasePath; // eg: "/user/home/tfadmin/tf/replay/server/"
CUtlString m_strSubDir; // "client" or "server"
bool m_bInit; // Initialized yet? Set by outer class.
private:
bool InitThreadPool();
void EnsureDirHierarchy();
IReplayContext *m_pOwnerContext;
};
//----------------------------------------------------------------------------------------
#define LINK_TO_SHARED_REPLAYCONTEXT_IMP() \
CSharedReplayContext *m_pShared; \
virtual bool IsInitialized() const { return m_pShared && m_pShared->IsInitialized(); } \
virtual const char *GetRelativeBaseDir() const { return m_pShared->GetRelativeBaseDir(); } \
virtual const char *GetBaseDir() const { return m_pShared->GetBaseDir(); } \
virtual const char *GetReplaySubDir() const { return m_pShared->GetReplaySubDir(); } \
virtual IReplayErrorSystem *GetErrorSystem() { return m_pShared->m_pErrorSystem; } \
virtual IRecordingSessionManager *GetRecordingSessionManager() \
{ \
return m_pShared->m_pRecordingSessionManager; \
} \
virtual CBaseRecordingSession *GetRecordingSession( ReplayHandle_t hSession ) \
{ \
return static_cast< CBaseRecordingSession * >( m_pShared->m_pRecordingSessionManager->Find( hSession ) ); \
} \
virtual CBaseRecordingSessionBlockManager *GetRecordingSessionBlockManager() \
{ \
return m_pShared->m_pRecordingSessionBlockManager; \
}
//----------------------------------------------------------------------------------------
class CJob;
bool RunJobToCompletion( IThreadPool *pThreadPool, CJob *pJob );
//----------------------------------------------------------------------------------------
#endif // SHARED_REPLAYCONTEXT_H
+125
View File
@@ -0,0 +1,125 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "spew.h"
#include "dbg.h"
#include "strtools.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
class CBlockSpewer : public ISpewer
{
virtual void PrintBlockStart() const
{
Log( "\n********************************************************************************\n*\n" );
}
virtual void PrintBlockEnd() const
{
Log( "*\n********************************************************************************\n\n" );
}
virtual void PrintEmptyLine() const
{
Log( "*\n" );
}
virtual void PrintEventStartMsg( const char *pMsg ) const
{
char pDots[] = { "............................................." };
const int nNumDots = MAX( 3, V_strlen( pDots ) - V_strlen( pMsg ) );
pDots[ nNumDots ] = '\0';
Log( "* %s%s", pMsg, pDots );
}
virtual void PrintEventResult( bool bSuccess ) const
{
Log( "%s\n", bSuccess ? "OK" : "FAILED" );
}
virtual void PrintEventError( const char *pError ) const
{
Log( "*\n*\n* ** ERROR: %s\n*\n", pError );
}
virtual void PrintTestHeader( const char *pHeader ) const
{
Log( "*\n*\n* %s...\n*\n", pHeader );
}
virtual void PrintValue( const char *pWhat, const char *pValue ) const
{
char pSpaces[] = { " " };
const int nNumSpaces = MAX( 3, V_strlen( pSpaces ) - V_strlen( pWhat ) );
pSpaces[ nNumSpaces ] = '\0';
Log( "* %s: %s%s\n", pWhat, pSpaces, pValue );
}
virtual void PrintMsg( const char *pMsg ) const
{
Log( "* %s\n", pMsg );
}
};
//----------------------------------------------------------------------------------------
static CBlockSpewer s_BlockSpewer;
ISpewer *g_pBlockSpewer = &s_BlockSpewer;
//----------------------------------------------------------------------------------------
class CNullSpewer : public ISpewer
{
public:
virtual void PrintBlockStart() const {}
virtual void PrintBlockEnd() const {}
virtual void PrintEmptyLine() const {}
virtual void PrintEventStartMsg( const char *pMsg ) const {}
virtual void PrintEventResult( bool bSuccess ) const {}
virtual void PrintTestHeader( const char *pHeader ) const {}
virtual void PrintMsg( const char *pMsg ) const {}
virtual void PrintValue( const char *pWhat, const char *pValue ) const {}
virtual void PrintEventError( const char *pError ) const
{
Log( "\n\nERROR: %s\n\n", pError );
}
};
//----------------------------------------------------------------------------------------
static CNullSpewer s_NullSpewer;
ISpewer *g_pNullSpewer = &s_NullSpewer;
//----------------------------------------------------------------------------------------
class CSimpleSpewer : public CNullSpewer
{
public:
virtual void PrintMsg( const char *pMsg ) const
{
Log( "%s", pMsg );
}
};
//----------------------------------------------------------------------------------------
static CSimpleSpewer s_SimpleSpewer;
ISpewer *g_pSimpleSpewer = &s_SimpleSpewer;
//----------------------------------------------------------------------------------------
ISpewer *g_pDefaultSpewer = g_pNullSpewer;
//----------------------------------------------------------------------------------------
CBaseSpewer::CBaseSpewer( ISpewer *pSpewer/*=g_pDefaultSpewer*/ )
: m_pSpewer( pSpewer )
{
}
//----------------------------------------------------------------------------------------
+82
View File
@@ -0,0 +1,82 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SPEW_H
#define SPEW_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
class ISpewer
{
public:
virtual ~ISpewer() {}
virtual void PrintBlockStart() const = 0;
virtual void PrintBlockEnd() const = 0;
virtual void PrintEmptyLine() const = 0;
virtual void PrintEventStartMsg( const char *pMsg ) const = 0;
virtual void PrintEventResult( bool bSuccess ) const = 0;
virtual void PrintEventError( const char *pError ) const = 0;
virtual void PrintTestHeader( const char *pHeader ) const = 0;
virtual void PrintMsg( const char *pMsg ) const = 0;
virtual void PrintValue( const char *pWhat, const char *pValue ) const = 0;
};
//----------------------------------------------------------------------------------------
extern ISpewer *g_pDefaultSpewer;
extern ISpewer *g_pBlockSpewer;
extern ISpewer *g_pSimpleSpewer;
extern ISpewer *g_pNullSpewer;
//----------------------------------------------------------------------------------------
class CSpewScope
{
public:
CSpewScope( ISpewer *pSpewer )
{
m_pOldSpewer = g_pDefaultSpewer;
g_pDefaultSpewer = pSpewer;
}
~CSpewScope()
{
g_pDefaultSpewer = m_pOldSpewer;
}
private:
ISpewer *m_pOldSpewer;
};
//----------------------------------------------------------------------------------------
class CBaseSpewer : public ISpewer
{
public:
CBaseSpewer( ISpewer *pSpewer = g_pDefaultSpewer );
//
// ISpewer implementation for shorthand.
//
virtual void PrintBlockStart() const { m_pSpewer->PrintBlockStart(); }
virtual void PrintBlockEnd() const { m_pSpewer->PrintBlockEnd(); }
virtual void PrintEmptyLine() const { m_pSpewer->PrintEmptyLine(); }
virtual void PrintEventStartMsg( const char *pMsg ) const { m_pSpewer->PrintEventStartMsg( pMsg ); }
virtual void PrintEventResult( bool bSuccess ) const { m_pSpewer->PrintEventResult( bSuccess ); }
virtual void PrintEventError( const char *pError ) const { m_pSpewer->PrintEventError( pError ); }
virtual void PrintTestHeader( const char *pHeader ) const { m_pSpewer->PrintTestHeader( pHeader ); }
virtual void PrintMsg( const char *pMsg ) const { m_pSpewer->PrintMsg( pMsg ); }
virtual void PrintValue( const char *pWhat, const char *pValue ) const { m_pSpewer->PrintValue( pWhat, pValue ); }
private:
ISpewer *m_pSpewer;
};
//----------------------------------------------------------------------------------------
#endif // SPEW_H
+31
View File
@@ -0,0 +1,31 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_basejob.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CBaseJob::CBaseJob( JobPriority_t priority/*=JP_NORMAL*/,
ISpewer *pSpewer/*=g_pDefaultSpewer*/ )
: CJob( priority ),
CBaseSpewer( pSpewer ),
m_nError( ERROR_NONE )
{
m_szError[ 0 ] = '\0';
}
void CBaseJob::SetError( int nError, const char *pError )
{
m_nError = nError;
if ( pError )
{
V_strcpy( m_szError, pError );
}
}
//----------------------------------------------------------------------------------------
+40
View File
@@ -0,0 +1,40 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_BASEJOB_H
#define SV_BASEJOB_H
//----------------------------------------------------------------------------------------
#undef Yield
#include "spew.h"
#include "vstdlib/jobthread.h"
//----------------------------------------------------------------------------------------
class CBaseJob : public CJob,
public CBaseSpewer
{
public:
CBaseJob( JobPriority_t priority = JP_NORMAL, ISpewer *pSpewer = g_pDefaultSpewer );
enum Error_t
{
ERROR_NONE = -1,
};
const char *GetErrorStr() const { return m_szError; }
protected:
void SetError( int nError, const char *pError = NULL );
private:
int m_nError;
char m_szError[256];
};
//----------------------------------------------------------------------------------------
#endif // SV_BASEJOB_H
+208
View File
@@ -0,0 +1,208 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "replaysystem.h"
#include "sv_sessionrecorder.h"
#include "utlbuffer.h"
#include "sessioninfoheader.h"
#include "sv_fileservercleanup.h"
#include "sv_publishtest.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define ENSURE_DEDICATED() if ( !g_pEngine->IsDedicated() ) return;
//----------------------------------------------------------------------------------------
CON_COMMAND( replay_record, "Starts Replay demo recording." )
{
ENSURE_DEDICATED();
SV_GetSessionRecorder()->StartRecording();
}
//----------------------------------------------------------------------------------------
CON_COMMAND( replay_stoprecord, "Stop Replay demo recording." )
{
ENSURE_DEDICATED();
g_pReplay->SV_EndRecordingSession();
}
//----------------------------------------------------------------------------------------
CON_COMMAND( replay_docleanup, "Deletes stale session data from the fileserver. \"replay_docleanup force\" will remove all replay session data." )
{
ENSURE_DEDICATED();
bool bForceCleanAll = false;
if ( args.ArgC() == 2 )
{
if ( !V_stricmp( args[1], "force" ) )
{
bForceCleanAll = true;
}
else
{
ConMsg( "\n ** ERROR: '%s' is not a valid paramter - use 'force' to force clean all replay session data.\n\n", args[1] );
return;
}
}
if ( !SV_DoFileserverCleanup( bForceCleanAll, g_pBlockSpewer ) )
{
Msg( "No demos were deleted.\n" );
}
}
//----------------------------------------------------------------------------------------
CON_COMMAND_F( replay_dopublishtest, "Do a replay publish test using the current setup.", FCVAR_DONTRECORD )
{
ENSURE_DEDICATED();
g_pBlockSpewer->PrintBlockStart();
SV_DoTestPublish();
g_pBlockSpewer->PrintBlockEnd();
}
//----------------------------------------------------------------------------------------
void PrintSessionInfo( const char *pFilename )
{
CUtlBuffer buf;
if ( !g_pFullFileSystem->ReadFile( pFilename, NULL, buf ) )
{
Msg( "Failed to read file, \"%s\"\n", pFilename );
return;
}
int nFileSize = buf.TellPut();
SessionInfoHeader_t header;
if ( !ReadSessionInfoHeader( buf.Base(), nFileSize, header ) )
{
Msg( "Failed to read header information.\n" );
return;
}
char szDigestStr[33];
V_binarytohex( header.m_aHash, sizeof( header.m_aHash ), szDigestStr, sizeof( szDigestStr ) );
Msg( "\n\theader:\n" );
Msg( "\n" );
Msg( "\t%27s: %u\n", "version", header.m_uVersion );
Msg( "\t%27s: %s\n", "session name", header.m_szSessionName );
Msg( "\t%27s: %s\n", "currently recording?", header.m_bRecording ? "yes" : "no" );
Msg( "\t%27s: %i\n", "# blocks", header.m_nNumBlocks );
Msg( "\t%27s: %s\n", "compressor", GetCompressorNameSafe( header.m_nCompressorType ) );
Msg( "\t%27s: %s\n", "md5 digest", szDigestStr );
Msg( "\t%27s: %u bytes\n", "payload size (compressed)", header.m_uPayloadSize );
Msg( "\t%27s: %u bytes\n", "payload size (uncompressed)", header.m_uPayloadSizeUC );
Msg( "\n" );
const uint8 *pPayload = (uint8 *)buf.Base() + sizeof( SessionInfoHeader_t );
uint32 uUncompressedPayloadSize = header.m_uPayloadSizeUC;
if ( !g_pEngine->MD5_HashBuffer( header.m_aHash, (const uint8 *)pPayload, header.m_uPayloadSize, NULL ) )
{
Msg( "Data validation failed.\n" );
return;
}
const uint8 *pUncompressedPayload;
bool bFreeUncompressedPayload = true;
if ( header.m_nCompressorType == COMPRESSORTYPE_INVALID )
{
// The payload is already uncompressed - don't free, since this buffer was allocated by the CUtlBuffer "buf"
pUncompressedPayload = pPayload;
bFreeUncompressedPayload = false;
}
else
{
if ( uUncompressedPayloadSize != header.m_uPayloadSizeUC )
{
Msg( "Decompressed to a different size (%u) than specified by header (%u)\n", uUncompressedPayloadSize, header.m_uPayloadSizeUC );
return;
}
ICompressor *pCompressor = CreateCompressor( header.m_nCompressorType );
if ( !pCompressor )
{
Msg( "Failed to create compressor.\n" );
return;
}
pUncompressedPayload = new uint8[ uUncompressedPayloadSize ];
if ( !pUncompressedPayload )
{
Msg( "Failed to allocate uncompressed payload.\n" );
delete [] pCompressor;
return;
}
pCompressor->Decompress( (char *)pUncompressedPayload, &uUncompressedPayloadSize, (const char *)pPayload, header.m_uPayloadSize );
delete pCompressor;
}
if ( uUncompressedPayloadSize <= MIN_SESSION_INFO_PAYLOAD_SIZE )
{
Msg( "Uncompressed payload not large enough to read a single block.\n" );
}
else
{
RecordingSessionBlockSpec_t DummyBlock;
CUtlBuffer bufPayload( pUncompressedPayload, uUncompressedPayloadSize, CUtlBuffer::READ_ONLY );
Msg( "\n\tblocks:\n\n" );
Msg( "\t index status MD5 compressor size (uncompressed) size (compressed)\n" );
bool bBlockReadFailed = false;
for ( int i = 0; i < header.m_nNumBlocks; ++i )
{
// Attempt to read the current block from the buffer
bufPayload.Get( &DummyBlock, sizeof( DummyBlock ) );
if ( !bufPayload.IsValid() )
{
bBlockReadFailed = true;
break;
}
V_binarytohex( DummyBlock.m_aHash, sizeof( DummyBlock.m_aHash ), szDigestStr, sizeof( szDigestStr ) );
Msg( "\t %5i", DummyBlock.m_iReconstruction );
Msg( "%20s", CBaseRecordingSessionBlock::GetRemoteStatusStringSafe( (CBaseRecordingSessionBlock::RemoteStatus_t)DummyBlock.m_uRemoteStatus ) );
Msg( "%35s", szDigestStr );
Msg( "%8s", GetCompressorNameSafe( (CompressorType_t)DummyBlock.m_nCompressorType ) );
Msg( "%20u", DummyBlock.m_uFileSize );
Msg( "%20u", DummyBlock.m_uUncompressedSize );
Msg( "\n" );
}
}
Msg( "\n" );
if ( bFreeUncompressedPayload )
{
delete [] pUncompressedPayload;
}
}
CON_COMMAND_F( replay_printsessioninfo, "Print session info", FCVAR_DONTRECORD )
{
ENSURE_DEDICATED();
if ( args.ArgC() != 2 )
{
Msg( "Usage: replay_printsessioninfo <full path and filename>\n" );
return;
}
PrintSessionInfo( args[1] );
}
//----------------------------------------------------------------------------------------
+76
View File
@@ -0,0 +1,76 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "convar.h"
#include "replay/shared_defs.h"
#include "sv_replaycontext.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
void OnFileserverHostnameChanged( IConVar *pVar, const char *pOldValue, float flOldValue )
{
ConVarRef var( pVar );
if ( !var.IsValid() )
return;
if ( g_pServerReplayContext )
{
g_pServerReplayContext->UpdateFileserverIPFromHostname( var.GetString() );
}
else
{
Warning ( "Cannot set ConVar %s yet. Replay is not initialized.", var.GetName() );
}
}
void OnFileserverProxyHostnameChanged( IConVar *pVar, const char *pOldValue, float flOldValue )
{
ConVarRef var( pVar );
if ( !var.IsValid() )
return;
if ( g_pServerReplayContext )
{
g_pServerReplayContext->UpdateFileserverProxyIPFromHostname( var.GetString() );
}
else
{
Warning ( "Cannot set ConVar %s yet. Replay is not initialized.", var.GetName() );
}
}
//----------------------------------------------------------------------------------------
ConVar replay_name( "replay_name", "Replay", FCVAR_GAMEDLL, "Replay bot name" );
ConVar replay_dofileserver_cleanup_on_start( "replay_dofileserver_cleanup_on_start", "1", FCVAR_GAMEDLL, "Cleanup any stale replay data (both locally and on fileserver) at startup." );
//
// FTP offloading
//
ConVar replay_fileserver_autocleanup( "replay_fileserver_autocleanup", "0", FCVAR_GAMEDLL, "Automatically do fileserver cleanup in between rounds? This is the same as explicitly calling replay_docleanup." );
ConVar replay_fileserver_offload_aborttime( "replay_fileserver_offload_aborttime", "60", FCVAR_GAMEDLL, "The time after which publishing will be aborted for a session block or session info file.", true, 30.0f, true, 60.0f );
//
// For URL construction
//
ConVar replay_fileserver_protocol( "replay_fileserver_protocol", "http", FCVAR_REPLICATED | FCVAR_DONTRECORD, "Can be \"http\" or \"https\"" );
ConVar replay_fileserver_host( "replay_fileserver_host", "", FCVAR_REPLICATED | FCVAR_DONTRECORD, "The hostname of the Web server hosting replays. This can be an IP or a hostname, e.g. \"1.2.3.4\" or \"www.myserver.com\"" );
ConVar replay_fileserver_port( "replay_fileserver_port", "80", FCVAR_REPLICATED | FCVAR_DONTRECORD, "The port for the Web server hosting replays. For example, if your replays are stored at \"http://123.123.123.123:4567/tf/replays\", replay_fileserver_port should be 4567." );
ConVar replay_fileserver_path( "replay_fileserver_path", "", FCVAR_REPLICATED | FCVAR_DONTRECORD, "If your replays are stored at \"http://123.123.123.123:4567/tf/replays\", replay_fileserver_path should be set to \"/tf/replays\"" );
ConVar replay_max_publish_threads( "replay_max_publish_threads", "4", FCVAR_GAMEDLL, "The max number of threads allowed for publishing replay data, e.g. FTP threads.", true, 4, true, 8 );
ConVar replay_block_dump_interval( "replay_block_dump_interval", "10", FCVAR_DONTRECORD, "The server will write partial replay files at this interval when recording.", true, MIN_SERVER_DUMP_INTERVAL, true, MAX_SERVER_DUMP_INTERVAL );
ConVar replay_data_lifespan( "replay_data_lifespan", "1", FCVAR_REPLICATED | FCVAR_DONTRECORD, "The number of days before replay data will be removed from the server. Server operators can expect that any data written more than replay_data_lifespan days will be considered stale, and any subsequent execution of replay_docleanup (or automatic cleanup, which can be enabled with replay_fileserver_autocleanup) will remove that data.", true, 1, true, 30 );
ConVar replay_local_fileserver_path( "replay_local_fileserver_path", "", FCVAR_DONTRECORD, "The file server local path. For example, \"c:\\MyWebServer\\htdocs\\replays\" or \"/MyWebServer/htdocs/replays\"." );
ConVar replay_buffersize( "replay_buffersize", "32", FCVAR_DONTRECORD, "Maximum size for the replay memory buffer.", true, 16, false, 0 );
ConVar replay_record_voice( "replay_record_voice", "1", FCVAR_GAMEDLL, "If enabled, voice data is recorded into the replay files." );
//----------------------------------------------------------------------------------------
+782
View File
@@ -0,0 +1,782 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "../utils/bzip2/bzlib.h"
#include "sv_filepublish.h"
#include "utlstring.h"
#include "strtools.h"
#include "sv_replaycontext.h"
#include "convar.h"
#include "fmtstr.h"
#include "compression.h"
#include "replay/shared_defs.h"
#include "spew.h"
#include "utlqueue.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
ConVar replay_publish_simulate_delay_local_http( "replay_publish_simulate_delay_local_http", "0", FCVAR_DONTRECORD,
"Simulate a delay (in seconds) when publishing replay data via local HTTP.", true, 0.0f, true, 60.0f );
ConVar replay_publish_simulate_rename_fail( "replay_publish_simulate_rename_fail", "0", FCVAR_DONTRECORD,
"Simulate a rename failure during local HTTP publishing, which will force a manual copy & delete.", true, 0.0f, true, 1.0f );
//----------------------------------------------------------------------------------------
CBasePublishJob::CBasePublishJob( JobPriority_t nPriority/*=JP_NORMAL*/,
ISpewer *pSpewer/*=g_pDefaultSpewer*/ )
: CBaseJob( nPriority, pSpewer )
{
}
void CBasePublishJob::SimulateDelay( int nDelay, const char *pThreadName )
{
if ( nDelay > 0 )
{
Log( "%s thread: Simulating %i sec delay.\n", pThreadName, nDelay );
ThreadSleep( nDelay * 1000 );
Log( "%s thread: simulation done.\n", pThreadName );
}
}
//----------------------------------------------------------------------------------------
CLocalPublishJob::CLocalPublishJob( const char *pLocalFilename )
{
V_strcpy( m_szLocalFilename, pLocalFilename );
}
JobStatus_t CLocalPublishJob::DoExecute()
{
DBG( "Attempting to rename file to local fileserver path..." );
PrintEventStartMsg( "Source file exists?" );
if ( !g_pFullFileSystem->FileExists( m_szLocalFilename ) )
{
PrintEventResult( false );
CFmtStr fmtError( "Source file '%s' does not exist", m_szLocalFilename );
SetError( ERROR_SOURCE_FILE_DOES_NOT_EXIST, fmtError.Access() );
return JOB_FAILED;
}
PrintEventResult( true );
// Make sure the publish path exists
const char *pFileserverPath = g_pServerReplayContext->GetLocalFileServerPath();
PrintEventStartMsg( "Checking fileserver path" );
if ( !g_pFullFileSystem->IsDirectory( pFileserverPath ) )
{
PrintEventResult( false );
CFmtStr fmtError( "Fileserver path '%s' invalid (see replay_local_fileserver_path)",
pFileserverPath );
SetError( ERROR_INVALID_FILESERVER_PATH, fmtError.Access() );
return JOB_FAILED;
}
PrintEventResult( true );
// Format a path & filename that points to the fileserver's download directory, with <session name>.dmx on the end
const char *pFilename = V_UnqualifiedFileName( m_szLocalFilename );
CFmtStr fmtPublishFilename( "%s%s", pFileserverPath, pFilename );
const char *pTargetFilename = fmtPublishFilename.Access();
// Delete the destination file if it exists already
if ( g_pFullFileSystem->FileExists( pTargetFilename ) )
{
PrintEventStartMsg( "Target file exists - deleting" );
g_pFullFileSystem->RemoveFile( pTargetFilename );
// Give the system a bit of time before another check
ThreadSleep( 500 );
if ( g_pFullFileSystem->FileExists( pTargetFilename ) )
{
#ifdef WIN32
LPVOID pMsgBuf;
if ( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &pMsgBuf,
0,
NULL ))
{
Log( "\n\nError: %s\n", (const char *)pMsgBuf );
LocalFree( pMsgBuf );
}
#endif
PrintEventResult( false );
CFmtStr fmtError( "Target already existed and could not be removed: '%s'", pTargetFilename );
SetError( ERROR_COULD_NOT_DELETE_TARGET_FILE, fmtError.Access() );
return JOB_FAILED;
}
PrintEventResult( true );
}
// Simulate a delay if necessary
SimulateDelay( replay_publish_simulate_delay_local_http.GetInt(), "Local HTTP" );
// Rename the file - RenameFile() still returns true, even if the destination pathname
// is nonsense. If the *source* is invalid, it fails as expected, though. Adding a FileExists()
// does not help.
PrintEventStartMsg( "Renaming to target" );
const bool bSimulateRenameFail = replay_publish_simulate_rename_fail.GetBool();
if ( bSimulateRenameFail || !g_pFullFileSystem->RenameFile( m_szLocalFilename, pTargetFilename ) )
{
// Try to explicitly copy to target
if ( g_pEngine->CopyFile( m_szLocalFilename, pTargetFilename ) )
{
// ...and deletion of source.
g_pFullFileSystem->RemoveFile( m_szLocalFilename );
}
else
{
PrintEventResult( false );
CFmtStr fmtError( "Failed to rename '%s' -> '%s'\n", m_szLocalFilename, pTargetFilename );
SetError( ERROR_RENAME_FAILED, fmtError.Access() );
return JOB_FAILED;
}
}
PrintEventResult( true );
DBG( "Rename succeeded.\n" );
return JOB_OK;
}
//----------------------------------------------------------------------------------------
CLocalPublishJob *SV_CreateLocalPublishJob( const char *pLocalFilename )
{
return new CLocalPublishJob( pLocalFilename );
}
//----------------------------------------------------------------------------------------
CCompressionJob::CCompressionJob( const uint8 *pSrcData, uint32 nSrcSize, CompressorType_t nType,
bool *pOutResult, uint32 *pResultSize )
: m_pSrcData( pSrcData ),
m_nSrcSize( nSrcSize ),
m_pCompressionResult( pOutResult ),
m_pResultSize( pResultSize )
{
*m_pCompressionResult = false;
*m_pResultSize = 0;
m_pCompressor = CreateCompressor( nType );
}
JobStatus_t CCompressionJob::DoExecute()
{
IF_REPLAY_DBG2( Warning( "Attempting to compress...\n" ) );
if ( m_nSrcSize == 0 )
{
SetError( ERROR_FAILED_ZERO_LENGTH_DATA, "Compression failed. Zero length data." );
return JOB_FAILED;
}
int nResult = JOB_FAILED;
// Attempt to compress the file
const int nMaxCompressedSize = ceil( m_nSrcSize * 1.1f ) + 600; // see "destLen" - http://www.bzip.org/1.0.3/html/util-fns.html
uint8 *pCompressed = new uint8[ nMaxCompressedSize ];
// Compress
unsigned int nCompressedSize;
PrintEventStartMsg( "Compressing" );
if ( !m_pCompressor->Compress( (char *)pCompressed, &nCompressedSize, (const char *)m_pSrcData, m_nSrcSize ) )
{
// Compression failed?
IF_REPLAY_DBG2( Warning( "Could not compress stream.\n" ) );
PrintEventResult( false );
SetError( ERROR_OK_COULDNOTCOMPRESS );
// Set result to uncompressed buffer and free compressed
m_pResult = (uint8 *)m_pSrcData;
delete [] pCompressed;
*m_pCompressionResult = false;
*m_pResultSize = m_nSrcSize;
}
else
{
PrintEventResult( true );
// Success!
DBG( "Compression succeeded.\n" );
nResult = JOB_OK;
// Set result to compressed buffer
m_pResult = pCompressed;
*m_pResultSize = nCompressedSize;
*m_pCompressionResult = true;
}
// Compression would have been worse than not compressing at all
return nResult;
}
void CCompressionJob::GetOutputData( uint8 **ppData, uint32 *pDataSize ) const
{
*ppData = m_pResult;
*pDataSize = *m_pResultSize;
}
//----------------------------------------------------------------------------------------
CMd5Job::CMd5Job( const void *pSrcData, int nSrcSize, bool *pOutHashed, uint8 *pOutHash,
unsigned int pSeed[4]/*=NULL*/ )
: m_pSrcData( pSrcData ),
m_nSrcSize( nSrcSize ),
m_pHashed( pOutHashed ),
m_pHash( pOutHash ),
m_pSeed( pSeed )
{
*m_pHashed = false;
V_memset( pOutHash, 0, 16 );
}
JobStatus_t CMd5Job::DoExecute()
{
IF_REPLAY_DBG2( Warning( "Attempting to hash...\n" ) );
PrintEventStartMsg( "Running" );
bool bResult = g_pEngine->MD5_HashBuffer( m_pHash, (const uint8 *)m_pSrcData, m_nSrcSize, m_pSeed );
PrintEventResult( bResult );
*m_pHashed = bResult;
if ( !bResult )
return JOB_FAILED;
IF_REPLAY_DBG2( Warning( "Hash succeeded\n" ) );
return JOB_OK;
}
//----------------------------------------------------------------------------------------
CDeleteLocalFileJob::CDeleteLocalFileJob( const char *pFilename )
{
V_strncpy( m_szFilename, pFilename, sizeof( m_szFilename ) - 1 );
}
JobStatus_t CDeleteLocalFileJob::DoExecute()
{
// File exists?
if ( !g_pFullFileSystem->FileExists( m_szFilename ) )
{
SetError( ERROR_FILE_DOES_NOT_EXISTS );
return JOB_FAILED;
}
// Attempt to remove the file now
g_pFullFileSystem->RemoveFile( m_szFilename );
// Delete succeeded?
if ( g_pFullFileSystem->FileExists( m_szFilename ) )
{
SetError( ERROR_COULD_NOT_DELETE );
return JOB_FAILED;
}
return JOB_OK;
}
//----------------------------------------------------------------------------------------
class CBaseFilePublisher : public IFilePublisher
{
public:
enum Phase_t
{
PHASE_INVALID = -1,
PHASE_COMPRESSION,
PHASE_HASH,
PHASE_ADJUSTHEADER,
PHASE_WRITETODISK,
PHASE_PUBLISH,
PHASE_DELETEFILE,
NUM_PHASES
};
CBaseFilePublisher()
: m_pCallbackHandler( NULL ),
m_pUserData( NULL ),
m_pCurrentJob( NULL ),
m_pInData( NULL ),
m_pHeaderData( NULL ),
m_nStatus( PUBLISHSTATUS_INVALID ),
m_nPhase( PHASE_INVALID ),
m_bCompressedOk( false ),
m_bHashedOk( false ),
m_nHeaderSize( 0 ),
m_nCompressedSize( 0 ),
m_nInSize( 0 ),
m_nInType( IO_INVALID )
{
m_szOutFilename[ 0 ] = 0;
V_memset( m_aHash, 0, sizeof( m_aHash ) );
}
virtual PublishStatus_t GetStatus() const
{
return m_nStatus;
}
void SetStatus( PublishStatus_t nStatus )
{
m_nStatus = nStatus;
}
virtual bool IsDone() const
{
return m_nStatus != PUBLISHSTATUS_INVALID;
}
virtual bool Compressed() const
{
return m_bCompressedOk;
}
virtual bool Hashed() const
{
return m_bHashedOk;
}
virtual void GetHash( uint8 *pOut ) const
{
V_memcpy( pOut, m_aHash, sizeof( m_aHash ) );
}
virtual CompressorType_t GetCompressorType() const
{
return m_bCompressedOk ? m_nCompressorType : COMPRESSORTYPE_INVALID;
}
virtual int GetCompressedSize() const
{
return m_nCompressedSize;
}
virtual void AbortAndCleanup()
{
if ( m_pCurrentJob )
{
m_pCurrentJob->Abort( true );
m_pCurrentJob = NULL;
}
}
virtual void FinishSynchronouslyAndCleanup()
{
if ( m_pCurrentJob )
{
m_pCurrentJob->WaitForFinishAndRelease();
m_pCurrentJob = NULL;
}
SetStatus( PUBLISHSTATUS_ABORTED );
}
virtual void Publish( const PublishFileParams_t &params )
{
V_strcpy( m_szOutFilename, params.m_pOutFilename );
m_pInData = params.m_pSrcData;
m_nInSize = params.m_nSrcSize;
m_pCallbackHandler = params.m_pCallbackHandler;
m_pUserData = params.m_pUserData;
m_bFreeSrcData = params.m_bFreeSrcData;
m_pSrcData = params.m_pSrcData; // Cache src data so we can determine whether free'ing is OK
m_pHeaderData = params.m_pHeaderData;
m_nHeaderSize = params.m_nHeaderSize;
m_flStartTime = g_pEngine->GetHostTime();
if ( params.m_nCompressorType != COMPRESSORTYPE_INVALID )
{
m_PhaseQueue.Insert( PHASE_COMPRESSION );
m_nCompressorType = params.m_nCompressorType; // Cache compressor type
}
if ( params.m_bHash )
{
m_PhaseQueue.Insert( PHASE_HASH );
}
if ( params.m_pHeaderData )
{
Assert( params.m_nHeaderSize );
m_PhaseQueue.Insert( PHASE_ADJUSTHEADER );
}
m_PhaseQueue.Insert( PHASE_WRITETODISK );
m_PhaseQueue.Insert( PHASE_PUBLISH );
if ( params.m_bDeleteFile )
{
m_PhaseQueue.Insert( PHASE_DELETEFILE );
}
// Start off first job
SetupNextJob( true );
}
void PrintErrors()
{
// If we don't print out any error now, it'll be lost once the job is released. Kind of a hack.
if ( m_pCurrentJob->GetStatus() == JOB_FAILED && !IsFailureOkForPhase() )
{
CBasePublishJob *pCurrentJob = dynamic_cast< CBasePublishJob * >( m_pCurrentJob );
if ( pCurrentJob )
{
g_pBlockSpewer->PrintBlockStart();
g_pBlockSpewer->PrintEventError( pCurrentJob->GetErrorStr() );
g_pBlockSpewer->PrintBlockEnd();
}
}
}
void Abort()
{
// Abort the job
if ( m_pCurrentJob )
{
m_pCurrentJob->Abort( true );
m_pCurrentJob = NULL;
}
// Update status
SetStatus( PUBLISHSTATUS_ABORTED );
// Let owner know we've aborted
if ( m_pCallbackHandler )
{
m_pCallbackHandler->OnPublishAborted( this );
}
}
virtual void Think()
{
const float flCurTime = g_pEngine->GetHostTime();
extern ConVar replay_fileserver_offload_aborttime;
if ( flCurTime > m_flStartTime + replay_fileserver_offload_aborttime.GetFloat() )
{
g_pBlockSpewer->PrintMsg( Replay_va( "ERROR: Publish timed out after %i seconds.", replay_fileserver_offload_aborttime.GetInt() ) );
Abort();
return;
}
if ( !m_pCurrentJob )
return;
const int nJobStatus = m_pCurrentJob->GetStatus();
if ( nJobStatus <= JOB_OK )
{
PrintErrors();
// What it says
CacheOutputsOfCurrentJobForInputsOfNextJob();
// Job's done - clean up
m_pCurrentJob->Release();
m_pCurrentJob = NULL;
// Did the current job fail?
bool bPublishDone = false;
if ( nJobStatus < JOB_OK && !IsFailureOkForPhase() )
{
// Don't process the next job
SetStatus( PUBLISHSTATUS_FAILED );
bPublishDone = true;
}
else if ( IsLastPhase() )
{
// nJobStatus is JOB_OK and we are in publish phase.
SetStatus( PUBLISHSTATUS_OK );
bPublishDone = true;
}
if ( bPublishDone )
{
InvokeCallback();
return;
}
// Otherwise, publish isn't complete yet - go to next phase and spawn job thread
SetupNextJob( false );
}
}
protected:
virtual CBasePublishJob *GetPublishJob() const = 0;
char m_szOutFilename[MAX_OSPATH]; // Filename only
IPublishCallbackHandler *m_pCallbackHandler;
void *m_pUserData;
private:
enum IO_t
{
IO_INVALID = -1,
IO_BUFFER,
IO_FILE,
IO_DONTCARE, // As an input, this means the job doesn't care about the main pipeline stream
// (e.g. adjust header gets its inputs elsewhere) phase. As an output, this
// should only be used for the final phase (publish).
};
void CacheOutputsOfCurrentJobForInputsOfNextJob()
{
bool bFreeOldInData = false;
uint8 *pOldInData = m_pInData;
IO_t nOutputType = GetCurrentPhaseOutputType();
// Write phase is a special case
if ( m_nPhase == PHASE_WRITETODISK )
{
// Clear the in buffer
m_pInData = NULL;
m_nInSize = 0;
bFreeOldInData = true;
}
else if ( nOutputType == IO_BUFFER )
{
// This should always be a CBasePublishJob
CBasePublishJob *pCurrentJob = dynamic_cast< CBasePublishJob * >( m_pCurrentJob );
Assert( pCurrentJob );
// Get job output buffer
uint8 *pJobOutData;
uint32 nJobOutDataSize;
pCurrentJob->GetOutputData( &pJobOutData, &nJobOutDataSize );
// Compare output data against input data - if different, free input and replace
// with output. In the case of hashing, for example, the input buffer is used
// to do some computation, but the buffer itself goes untouched.
if ( pJobOutData && ( m_pInData != pJobOutData || m_nInSize != nJobOutDataSize ) )
{
m_pInData = pJobOutData;
m_nInSize = nJobOutDataSize;
bFreeOldInData = true;
}
}
else if ( nOutputType == IO_DONTCARE )
{
// This should have been cleaned up in write-to-disk phase if we're in publish phase
Assert( m_nPhase != PHASE_PUBLISH || m_pInData == NULL );
}
#ifdef _DEBUG
else
{
AssertMsg( 0, "Shouldn't reach here" );
}
#endif
// Free old input data?
if ( bFreeOldInData && ( m_bFreeSrcData || pOldInData != m_pSrcData ) )
{
delete [] pOldInData;
}
// Cache output of current job for input of next job
if ( m_nPhase != PHASE_PUBLISH )
{
m_nInType = nOutputType;
}
}
// NOTE: This needs to return a CJob ptr (i.e. and not a CBaseJob) since the job may be an AsyncWrite
CJob *GetJobForPhase( Phase_t nPhase )
{
CJob *pResult = NULL;
switch ( nPhase )
{
case PHASE_COMPRESSION:
pResult = new CCompressionJob( m_pInData, m_nInSize, m_nCompressorType, &m_bCompressedOk, &m_nCompressedSize );
break;
case PHASE_HASH:
pResult = new CMd5Job( m_pInData, m_nInSize, &m_bHashedOk, m_aHash );
break;
case PHASE_ADJUSTHEADER:
{
// Let the callback handler make any adjustments to the header (add md5 digest, etc.)
m_pCallbackHandler->AdjustHeader( this, m_pHeaderData );
if ( m_pHeaderData && m_nHeaderSize )
{
// Write the header to the target file
FSAsyncControl_t hFileJob;
const bool bFreeMemory = false;
g_pFullFileSystem->AsyncWrite( m_szOutFilename, m_pHeaderData, m_nHeaderSize, bFreeMemory, false, &hFileJob );
pResult = (CJob *)hFileJob;
}
}
break;
case PHASE_WRITETODISK:
if ( m_pInData && m_nInSize )
{
// Create an asynchronous write job - if a header already exists in the file, append.
FSAsyncControl_t hFileJob;
const bool bAppend = m_pHeaderData != NULL;
g_pFullFileSystem->AsyncWrite( m_szOutFilename, m_pInData, m_nInSize, false, bAppend, &hFileJob );
pResult = (CJob *)hFileJob;
}
break;
case PHASE_PUBLISH:
pResult = GetPublishJob();
break;
case PHASE_DELETEFILE:
pResult = new CDeleteLocalFileJob( m_szOutFilename );
break;
default:
AssertMsg( 0, "File publish phase is bad." );
}
// Sanity check input type with output type of previous job
Assert(
GetCurrentPhaseInputType() == IO_DONTCARE ||
m_nInType == IO_DONTCARE ||
GetCurrentPhaseInputType() == m_nInType
);
return pResult;
}
bool IsFailureOkForPhase() const
{
// Compression will fail (e.g. due to small buffer size), which shouldn't bring down the house.
return m_nPhase == PHASE_COMPRESSION || m_nPhase == PHASE_DELETEFILE;
}
bool IsLastPhase() const
{
return m_PhaseQueue.Count() == 0;
}
IO_t GetCurrentPhaseInputType() const
{
return sm_aPhaseIOTypes[ m_nPhase ].m_nInputType;
}
IO_t GetCurrentPhaseOutputType() const
{
return sm_aPhaseIOTypes[ m_nPhase ].m_nOutputType;
}
void SetupNextJob( bool bFirstJob )
{
// Get next phase from queue
Assert( m_PhaseQueue.Count() > 0 );
m_nPhase = ( Phase_t )m_PhaseQueue.RemoveAtHead();
// Set the input type if this is the first job
if ( bFirstJob )
{
m_nInType = GetCurrentPhaseInputType();
}
// Create the job
m_pCurrentJob = GetJobForPhase( m_nPhase );
// Kick off the job now
SV_GetThreadPool()->AddJob( m_pCurrentJob );
}
void InvokeCallback()
{
if ( m_pCallbackHandler )
{
m_pCallbackHandler->OnPublishComplete( this, m_pUserData );
}
}
CUtlQueue< uint8 > m_PhaseQueue;
bool m_bCompressedOk;
bool m_bHashedOk;
CompressorType_t m_nCompressorType;
uint8 m_aHash[16];
Phase_t m_nPhase;
PublishStatus_t m_nStatus;
CJob *m_pCurrentJob;
uint32 m_nCompressedSize;
IO_t m_nInType;
uint8 *m_pInData;
uint32 m_nInSize;
bool m_bFreeSrcData;
void *m_pSrcData;
void *m_pHeaderData;
int m_nHeaderSize;
float m_flStartTime;
struct IoInfo_t
{
IO_t m_nInputType;
IO_t m_nOutputType;
};
static IoInfo_t sm_aPhaseIOTypes[ NUM_PHASES ];
};
CBaseFilePublisher::IoInfo_t CBaseFilePublisher::sm_aPhaseIOTypes[ NUM_PHASES ] =
{
// Input Output
{ IO_BUFFER, IO_BUFFER }, // PHASE_COMPRESSION
{ IO_BUFFER, IO_BUFFER }, // PHASE_HASH
{ IO_DONTCARE, IO_DONTCARE }, // PHASE_ADJUSTHEADER - this phase can operate independent of the pipeline, so
// long as any compression/hashing is taken care of.
{ IO_BUFFER, IO_FILE }, // PHASE_WRITETODISK
{ IO_FILE, IO_DONTCARE }, // PHASE_PUBLISH
{ IO_DONTCARE, IO_DONTCARE } // PHASE_DELETEFILE
};
//----------------------------------------------------------------------------------------
class CLocalFileserverPublisher : public CBaseFilePublisher
{
typedef CBaseFilePublisher BaseClass;
public:
virtual CBasePublishJob *GetPublishJob() const
{
DBG( "Attempting to publish a file locally...\n" );
// Destination filename is implied
return new CLocalPublishJob( m_szOutFilename );
}
};
//----------------------------------------------------------------------------------------
IFilePublisher *SV_PublishFile( const PublishFileParams_t &params )
{
Assert( !params.m_pHeaderData || ( params.m_pHeaderData && params.m_pCallbackHandler ) );
IFilePublisher *pResult;
pResult = new CLocalFileserverPublisher();
pResult->Publish( params );
return pResult;
}
//----------------------------------------------------------------------------------------
+207
View File
@@ -0,0 +1,207 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_FILEPUBLISH_H
#define SV_FILEPUBLISH_H
//----------------------------------------------------------------------------------------
#include "platform.h"
#include "qlimits.h"
#include "spew.h"
#include "sv_basejob.h"
#include "compression.h"
//----------------------------------------------------------------------------------------
class IFilePublisher;
//----------------------------------------------------------------------------------------
class IPublishCallbackHandler
{
public:
virtual ~IPublishCallbackHandler() {}
virtual void OnPublishComplete( const IFilePublisher *pPublisher, void *pUserData ) = 0;
virtual void OnPublishAborted( const IFilePublisher *pPublisher ) = 0;
virtual void AdjustHeader( const IFilePublisher *pPublisher, void *pHeaderData ) = 0;
};
//----------------------------------------------------------------------------------------
struct PublishFileParams_t
{
inline PublishFileParams_t()
{
V_memset( this, 0, sizeof( PublishFileParams_t ) );
m_nCompressorType = COMPRESSORTYPE_BZ2;
}
IPublishCallbackHandler *m_pCallbackHandler;
const char *m_pOutFilename;
uint8 *m_pSrcData;
int m_nSrcSize;
bool m_bHash;
bool m_bFreeSrcData;
bool m_bDeleteFile;
void *m_pHeaderData;
int m_nHeaderSize;
void *m_pUserData;
CompressorType_t m_nCompressorType;
};
//----------------------------------------------------------------------------------------
//
// Interface for publishing files to fileserver.
//
// NOTE: You can force an IFilePublisher to delete itself by calling EnableAutoDelete().
//
class IFilePublisher
{
public:
virtual ~IFilePublisher() {}
enum PublishStatus_t
{
PUBLISHSTATUS_INVALID,
PUBLISHSTATUS_OK,
PUBLISHSTATUS_FAILED,
PUBLISHSTATUS_ABORTED,
};
//
// NOTE: Call Compressed() and Hashed() when IsDone() is true or in OnPublishComplete() to find
// out if compression/hashing succeeded.
//
// Setting bFreeSrcData to false will keep the publisher from deleting the initial source data.
// Otherwise, the process is that buffers will be cleaned up as they are no longer needed - for
// example, if compression is enabled, the initial buffer will be free'd if bFreeSrcData is true.
//
virtual void Publish( const PublishFileParams_t &params ) = 0;
virtual void AbortAndCleanup() = 0;
virtual void FinishSynchronouslyAndCleanup() = 0;
virtual void Think() = 0;
virtual PublishStatus_t GetStatus() const = 0;
virtual bool IsDone() const = 0;
virtual bool Compressed() const = 0; // If compression was requested, did it succeed?
virtual bool Hashed() const = 0;
virtual void GetHash( uint8 *pOut ) const = 0; // Writes 16 bytes to pOut
virtual CompressorType_t GetCompressorType() const = 0;
virtual int GetCompressedSize() const = 0;
};
//----------------------------------------------------------------------------------------
IFilePublisher *SV_PublishFile( const PublishFileParams_t &params );
//----------------------------------------------------------------------------------------
class CBasePublishJob : public CBaseJob
{
public:
CBasePublishJob( JobPriority_t nPriority = JP_NORMAL, ISpewer *pSpewer = g_pDefaultSpewer );
virtual void GetOutputData( uint8 **ppData, uint32 *pDataSize ) const { *ppData = NULL; *pDataSize = 0; }
virtual const char *GetOutputFilename() const { return NULL; }
protected:
void SimulateDelay( int nDelay, const char *pThreadName ); // Seconds
};
//----------------------------------------------------------------------------------------
class CLocalPublishJob : public CBasePublishJob
{
public:
CLocalPublishJob( const char *pLocalFilename );
enum LocalPublishError_t
{
ERROR_SOURCE_FILE_DOES_NOT_EXIST,
ERROR_INVALID_FILESERVER_PATH,
ERROR_COULD_NOT_DELETE_TARGET_FILE,
ERROR_RENAME_FAILED,
};
private:
virtual JobStatus_t DoExecute();
char m_szLocalFilename[MAX_OSPATH];
};
CLocalPublishJob *SV_CreateLocalPublishJob( const char *pLocalFilename );
//----------------------------------------------------------------------------------------
class ICompressor;
class CCompressionJob : public CBasePublishJob
{
public:
CCompressionJob( const uint8 *pSrcData, uint32 nSrcSize, CompressorType_t nType,
bool *pOutCompressed, uint32 *pCompressedSize );
enum CompressionError_t
{
ERROR_FAILED_ZERO_LENGTH_DATA,
ERROR_OK_COULDNOTCOMPRESS,
ERROR_FAILED_OPENOUTFILE,
};
private:
virtual JobStatus_t DoExecute();
virtual void GetOutputData( uint8 **ppData, uint32 *pDataSize ) const;
const uint8 *m_pSrcData;
uint32 m_nSrcSize;
char m_szOutFilename[ MAX_OSPATH ];
bool *m_pCompressionResult;
uint8 *m_pResult;
unsigned int *m_pResultSize;
ICompressor *m_pCompressor;
};
//----------------------------------------------------------------------------------------
class CMd5Job : public CBasePublishJob
{
public:
CMd5Job( const void *pSrcData, int nSrcSize, bool *pOutHashed, uint8 pOutHash[16],
unsigned int pSeed[4] = NULL );
private:
virtual JobStatus_t DoExecute();
const void *m_pSrcData;
int m_nSrcSize;
bool *m_pHashed;
uint8 *m_pHash;
unsigned int *m_pSeed;
};
//----------------------------------------------------------------------------------------
class CDeleteLocalFileJob : public CBasePublishJob
{
public:
CDeleteLocalFileJob( const char *pFilename );
enum CompressionError_t
{
ERROR_FILE_DOES_NOT_EXISTS,
ERROR_COULD_NOT_DELETE,
};
private:
virtual JobStatus_t DoExecute();
char m_szFilename[ MAX_OSPATH ];
};
//----------------------------------------------------------------------------------------
#endif // SV_FILEPUBLISH_H
+260
View File
@@ -0,0 +1,260 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_fileservercleanup.h"
#include "sv_replaycontext.h"
#include "sv_recordingsession.h"
#include "spew.h"
#if BUILD_CURL
#include "curl/curl.h"
#endif
#undef AddJob
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#if _DEBUG
ConVar replay_fileserver_simulate_delete( "replay_fileserver_simulate_delete", "0", FCVAR_GAMEDLL, "Don't delete any actual files during replay cleanup." );
#endif
//----------------------------------------------------------------------------------------
IFileserverCleanerJob *SV_CastJobToIFileserverCleanerJob( CBaseJob *pJob )
{
IFileserverCleanerJob *pResult = dynamic_cast< IFileserverCleanerJob * >( pJob );
AssertMsg( pResult != NULL, "Cast failed! Are you sure this job is an IFileserverCleanerJob?" );
return pResult;
}
//----------------------------------------------------------------------------------------
CFileserverCleaner::CFileserverCleaner()
: m_bRunning( false ),
m_bPrintResult( false ),
m_pCleanerJob( NULL ),
m_pSpewer( NULL ),
m_nNumFilesDeleted( 0 )
{
}
void CFileserverCleaner::MarkFileForDelete( const char *pFilename )
{
if ( m_bRunning )
return;
// Create cleaner job now if need be
if ( !m_pCleanerJob )
{
m_pCleanerJob = SV_CreateDeleteFileJob();
m_nNumFilesDeleted = 0;
}
IFileserverCleanerJob *pCleanerJobImp = SV_CastJobToIFileserverCleanerJob( m_pCleanerJob );
AssertMsg( pCleanerJobImp != NULL, "This cast should always work!" );
if ( pCleanerJobImp )
{
pCleanerJobImp->AddFileForDelete( pFilename );
}
}
void CFileserverCleaner::BlockForCompletion()
{
if ( !m_bRunning )
return;
if ( !m_pCleanerJob )
return;
m_pCleanerJob->WaitForFinish();
Clear();
}
void CFileserverCleaner::DoCleanAsynchronous( bool bPrintResult/*=false*/, ISpewer *pSpewer/*=g_pDefaultSpewer*/ )
{
if ( m_bRunning )
return;
if ( !m_pCleanerJob )
return;
m_pSpewer = pSpewer;
m_bPrintResult = bPrintResult;
m_bRunning = true;
SV_GetThreadPool()->AddJob( m_pCleanerJob );
}
void CFileserverCleaner::Clear()
{
m_pCleanerJob->Release();
m_pCleanerJob = NULL;
m_bPrintResult = false;
m_bRunning = false;
}
void CFileserverCleaner::Think()
{
CBaseThinker::Think();
if ( !m_bRunning )
return;
if ( !m_pCleanerJob->IsFinished() )
return;
IFileserverCleanerJob *pCleanerJobImp = SV_CastJobToIFileserverCleanerJob( m_pCleanerJob );
if ( pCleanerJobImp )
{
m_nNumFilesDeleted += pCleanerJobImp->GetNumFilesDeleted();
}
PrintResult();
Clear();
}
void CFileserverCleaner::PrintResult()
{
if ( !m_bPrintResult || !m_pSpewer )
return;
m_pSpewer->PrintEmptyLine();
const int nNumFilesRemoved = SV_GetFileserverCleaner()->GetNumFilesDeleted();
m_pSpewer->PrintValue( "Number of files removed", Replay_va( "%i", nNumFilesRemoved ) );
m_pSpewer->PrintBlockEnd();
}
float CFileserverCleaner::GetNextThinkTime() const
{
return 0.0f;
}
//----------------------------------------------------------------------------------------
CLocalFileDeleterJob::CLocalFileDeleterJob()
: m_nNumDeleted( 0 )
{
}
void CLocalFileDeleterJob::AddFileForDelete( const char *pFilename )
{
CFmtStr fmtFullFilename( "%s%s", g_pServerReplayContext->GetLocalFileServerPath(), pFilename );
m_vecFiles.CopyAndAddToTail( fmtFullFilename.Access() );
}
JobStatus_t CLocalFileDeleterJob::DoExecute()
{
bool bResult = true;
FOR_EACH_VEC( m_vecFiles, i )
{
const char *pCurFilename = m_vecFiles[ i ];
// File exists?
PrintEventStartMsg( "File exists?" );
if ( !g_pFullFileSystem->FileExists( pCurFilename ) )
{
CFmtStr fmtError( "File '%s' does not exist", pCurFilename );
SetError( ERROR_FILE_DOES_NOT_EXIST, fmtError.Access() ); // TODO: This will only catch the last filename
PrintEventResult( false );
bResult = false;
continue;
}
PrintEventResult( true );
// Delete the file
PrintEventStartMsg( "Deleting file" );
g_pFullFileSystem->RemoveFile( pCurFilename );
// File gone?
const bool bDeleted = !g_pFullFileSystem->FileExists( pCurFilename );
PrintEventResult( bDeleted );
// Increment # deleted if appropriate
if ( bDeleted )
{
++m_nNumDeleted;
}
bResult = bResult && bDeleted;
}
return bResult ? JOB_OK : JOB_FAILED;
}
//----------------------------------------------------------------------------------------
CLocalFileDeleterJob *SV_CreateLocalFileDeleterJob()
{
return new CLocalFileDeleterJob();
}
//----------------------------------------------------------------------------------------
bool SV_DoFileserverCleanup( bool bForceCleanAll, ISpewer *pSpewer )
{
CServerRecordingSessionManager *pSessionManager = SV_GetRecordingSessionManager();
CBaseRecordingSession *pRecordingSession = SV_GetRecordingSessionInProgress();
for ( int i = 0; i < pSessionManager->Count(); )
{
CServerRecordingSession *pCurSession = SV_CastSession( SV_GetRecordingSessionManager()->m_vecObjs[ i ] );
// Skip session in progress
bool bRemoved = false;
if ( pCurSession != NULL && pCurSession != pRecordingSession )
{
// Session expired?
if ( bForceCleanAll || pCurSession->SessionExpired() )
{
// The session's OnDelete() will add the session file to the cleanup system,
// and also delete all associated blocks, whose OnDelete() will also add their
// associated .block files to the cleanup system.
pSessionManager->RemoveFromIndex( i );
bRemoved = true;
}
}
if ( !bRemoved )
{
++i;
}
}
pSpewer->PrintBlockStart();
pSpewer->PrintMsg( "Attempting to clean up stale replay data..." );
pSpewer->PrintEmptyLine();
// NOTE: There may be files queued up in addition to those marked above
if ( !SV_GetFileserverCleaner()->HasFilesQueuedForDelete() )
{
pSpewer->PrintMsg( "No replay data to clean up." );
pSpewer->PrintBlockEnd();
}
else
{
// Asynchronously delete all collected files
SV_GetFileserverCleaner()->DoCleanAsynchronous( true, g_pBlockSpewer );
}
return true;
}
CBaseJob *SV_CreateDeleteFileJob()
{
return SV_CreateLocalFileDeleterJob();
}
//----------------------------------------------------------------------------------------
+91
View File
@@ -0,0 +1,91 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_FILESERVERCLEANUP_H
#define SV_FILESERVERCLEANUP_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "basethinker.h"
#include "spew.h"
#include "sv_basejob.h"
//----------------------------------------------------------------------------------------
bool SV_DoFileserverCleanup( bool bForceCleanAll, ISpewer *pSpewer/*=g_pDefaultSpewer*/ );
CBaseJob *SV_CreateDeleteFileJob();
//----------------------------------------------------------------------------------------
class IFileserverCleanerJob
{
public:
virtual ~IFileserverCleanerJob() {}
virtual void AddFileForDelete( const char *pFilename ) = 0;
virtual int GetNumFilesDeleted() const = 0;
};
IFileserverCleanerJob *SV_CastJobToIFileserverCleanerJob( CBaseJob *pJob );
//----------------------------------------------------------------------------------------
class CFileserverCleaner : public CBaseThinker
{
public:
CFileserverCleaner();
void MarkFileForDelete( const char *pFilename );
int GetNumFilesDeleted() const { return m_nNumFilesDeleted; }
bool HasFilesQueuedForDelete() const { return m_pCleanerJob != NULL; }
void BlockForCompletion();
void DoCleanAsynchronous( bool bPrintResult = false, ISpewer *pSpewer = g_pDefaultSpewer );
private:
void Clear();
void PrintResult();
virtual void Think();
virtual float GetNextThinkTime() const;
CBaseJob *m_pCleanerJob;
bool m_bRunning;
bool m_bPrintResult;
int m_nNumFilesDeleted;
ISpewer *m_pSpewer;
};
//----------------------------------------------------------------------------------------
class CLocalFileDeleterJob : public CBaseJob,
public IFileserverCleanerJob
{
public:
CLocalFileDeleterJob();
virtual void AddFileForDelete( const char *pFilename );
virtual int GetNumFilesDeleted() const { return m_nNumDeleted; }
enum DeleteError_t
{
ERROR_FILE_DOES_NOT_EXIST,
};
private:
virtual JobStatus_t DoExecute();
CUtlStringList m_vecFiles;
int m_nNumDeleted;
};
CLocalFileDeleterJob *SV_CreateLocalFileDeleterJob();
//----------------------------------------------------------------------------------------
#endif // SV_FILESERVERCLEANUP_H
+406
View File
@@ -0,0 +1,406 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_publishtest.h"
#include "spew.h"
#include "replay/replayutils.h"
#include "replaysystem.h"
#include "sv_basejob.h"
#include "sv_fileservercleanup.h"
#include "tier1/convar.h"
#include "sv_filepublish.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
const char *g_pAcceptableFileserverProtocols[] = { "http", "https", NULL };
const char *g_pAcceptableOffloadProtocols[] = { "ftp", NULL };
//----------------------------------------------------------------------------------------
class CPublishTester
{
public:
CPublishTester();
~CPublishTester();
bool Go();
private:
bool Test_Emptyness( const char *pDescription, const char *pStr, bool bPrintResult );
bool Test_Hostname( const char *pHostname, const char *pProtocolExample );
bool Test_Protocol( const char *pDescription, const char *pProtocol, const char **pAcceptableProtocols );
bool Test_Port( int nPort );
bool Test_Path( const char *pDescription, const char *pPath, bool bForwardSlashesAllowed, bool bBackslashesAllowed );
bool Test_LocalWebServerCVars();
bool Test_IO( const char *pFilename );
bool Test_FilePublish( const char *pFilename, bool bOffload );
bool Test_PublishedFileDelete( const char *pFullFilename, const char *pFilename, bool bOffload );
bool Test_WaitingForPlayersCVar();
void PrintBaseUrlWarning();
char *m_pGarbageBuffer;
CBaseJob *m_pJob;
CBaseJob *m_pCleanerJob;
};
//----------------------------------------------------------------------------------------
#define GARBAGE_BUFFER_SIZE ( 1024 * 1000 )
CPublishTester::CPublishTester()
: m_pGarbageBuffer( NULL ),
m_pJob( NULL ),
m_pCleanerJob( NULL )
{
m_pGarbageBuffer = new char[ GARBAGE_BUFFER_SIZE ];
}
CPublishTester::~CPublishTester()
{
delete [] m_pGarbageBuffer;
if ( m_pJob )
{
m_pJob->Release();
}
if ( m_pCleanerJob )
{
m_pCleanerJob->Release();
}
}
bool CPublishTester::Test_Hostname( const char *pHostname, const char *pProtocolExample )
{
if ( !Test_Emptyness( "Hostname", pHostname, false ) )
return false;
if ( V_strstr( pHostname, "://" ) )
{
g_pBlockSpewer->PrintEventResult( false );
CFmtStr fmtError( "Should not contain a protocol (e.g: %s)!", pProtocolExample );
g_pBlockSpewer->PrintEventError( fmtError.Access() );
return false;
}
// Test IP lookup
char szIP[16];
if ( !g_pEngine->NET_GetHostnameAsIP( pHostname, szIP, sizeof( szIP ) ) )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "DNS lookup failed!" );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
g_pBlockSpewer->PrintEmptyLine();
return true;
}
bool CPublishTester::Test_Emptyness( const char *pDescription, const char *pStr, bool bPrintResult )
{
g_pBlockSpewer->PrintValue( pDescription, pStr );
g_pBlockSpewer->PrintEventStartMsg( "Validating" );
if ( V_strlen( pStr ) == 0 )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "Empty!" );
return false;
}
if ( bPrintResult )
{
g_pBlockSpewer->PrintEventResult( true );
g_pBlockSpewer->PrintEmptyLine();
}
return true;
}
bool CPublishTester::Test_Port( int nPort )
{
g_pBlockSpewer->PrintValue( "Port", Replay_va( "%i", nPort ) );
g_pBlockSpewer->PrintEventStartMsg( "Validating" );
if ( nPort < 0 || nPort > 65535 )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "Port must be between 0 and 65535." );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
g_pBlockSpewer->PrintEmptyLine();
return true;
}
bool CPublishTester::Test_Path( const char *pDescription, const char *pPath, bool bForwardSlashesAllowed, bool bBackslashesAllowed )
{
g_pBlockSpewer->PrintValue( pDescription, pPath );
g_pBlockSpewer->PrintEventStartMsg( "Validating" );
if ( V_strlen( pPath ) == 0 )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "Empty path not allowed." );
return false;
}
if ( !bBackslashesAllowed && V_strstr( pPath, "\\" ) )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "Backslashes not allowed!" );
return false;
}
if ( !bForwardSlashesAllowed && V_strstr( pPath, "/" ) )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "Forward slashes not allowed!" );
return false;
}
if ( V_strstr( pPath, "//" ) || V_strstr( pPath, "\\\\" ) )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "Double slash detected!" );
return false;
}
if ( V_strstr( pPath, ".." ) )
{
g_pBlockSpewer->PrintEventResult( false );
g_pBlockSpewer->PrintEventError( "\"..\" not allowed!" );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
g_pBlockSpewer->PrintEmptyLine();
return true;
}
bool CPublishTester::Test_Protocol( const char *pDescription, const char *pProtocol, const char **pAcceptableProtocols )
{
g_pBlockSpewer->PrintValue( pDescription, pProtocol );
g_pBlockSpewer->PrintEventStartMsg( "Validating" );
// Test to see if the input protocol is acceptable
bool bProtocolOK = false;
int i = 0;
while ( pAcceptableProtocols[ i ] )
{
if ( V_strcmp( pAcceptableProtocols[ i++ ], pProtocol ) == 0 )
{
bProtocolOK = true;
break;
}
}
// Protocol allowed?
if ( !bProtocolOK )
{
g_pBlockSpewer->PrintEventResult( false );
CFmtStr fmtError( "Must be one of the following (case-sensitive): " );
i = 0;
while ( pAcceptableProtocols[ i ] )
fmtError.AppendFormat( "\"%s\" ", pAcceptableProtocols[ i++ ] );
g_pBlockSpewer->PrintEventError( fmtError.Access() );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
g_pBlockSpewer->PrintEmptyLine();
return true;
}
bool CPublishTester::Test_LocalWebServerCVars()
{
// NOTE: We use the raw cvar here as opposed to CServerReplayContext::GetLocalFileServerPath(),
// which actually fixes slashes. If the cvar is using incorrect slashes here for the given OS,
// this test will fail with a specific error message.
extern ConVar replay_local_fileserver_path;
if ( !Test_Path( "Path", replay_local_fileserver_path.GetString(), IsPosix(), IsWindows() ) )
return false;
return true;
}
bool CPublishTester::Test_IO( const char *pFilename )
{
g_pBlockSpewer->PrintTestHeader( "Testing File I/O" );
// Print out temp directory so the context for this section is clear
g_pBlockSpewer->PrintValue( "Temp path", SV_GetTmpDir() );
g_pBlockSpewer->PrintEmptyLine();
// Open the file
FileHandle_t hTmpFile = g_pFullFileSystem->Open( pFilename, "wb+" );
g_pBlockSpewer->PrintEventStartMsg( "Opening temp file" );
if ( !hTmpFile )
{
g_pBlockSpewer->PrintEventResult( false );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
// Write the file
g_pBlockSpewer->PrintEventStartMsg( "Allocating test buffer" ); // Lie.
if ( !m_pGarbageBuffer )
{
g_pBlockSpewer->PrintEventResult( false );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
g_pBlockSpewer->PrintEventStartMsg( "Writing temp file" );
if ( g_pFullFileSystem->Write( m_pGarbageBuffer, GARBAGE_BUFFER_SIZE, hTmpFile ) != GARBAGE_BUFFER_SIZE )
{
g_pBlockSpewer->PrintEventResult( false );
return false;
}
g_pBlockSpewer->PrintEventResult( true );
// Close the file
g_pFullFileSystem->Close( hTmpFile );
return true;
}
bool CPublishTester::Test_FilePublish( const char *pFilename, bool bOffload )
{
g_pBlockSpewer->PrintTestHeader( "Testing file publisher" );
g_pBlockSpewer->PrintValue( "Fileserver type", "Local Web server" );
if ( !Test_LocalWebServerCVars() )
return false;
m_pJob = SV_CreateLocalPublishJob( pFilename );
g_pBlockSpewer->PrintEmptyLine();
// Run publish test
if ( !m_pJob || !SV_RunJobToCompletion( m_pJob ) )
{
g_pBlockSpewer->PrintEventError( m_pJob->GetErrorStr() );
return false;
}
return true;
}
bool CPublishTester::Test_PublishedFileDelete( const char *pFullFilename, const char *pFilename, bool bOffload )
{
g_pBlockSpewer->PrintTestHeader( "Testing fileserver delete" );
if ( bOffload )
{
// Delete the file from the tmp dir
g_pFullFileSystem->RemoveFile( pFullFilename );
}
m_pCleanerJob = SV_CreateDeleteFileJob();
IFileserverCleanerJob *pCleanerJobImp = SV_CastJobToIFileserverCleanerJob( m_pCleanerJob );
pCleanerJobImp->AddFileForDelete( pFilename );
if ( !m_pCleanerJob || !SV_RunJobToCompletion( m_pCleanerJob ) )
{
g_pBlockSpewer->PrintEventError( m_pCleanerJob->GetErrorStr() );
return false;
}
return true;
}
bool CPublishTester::Test_WaitingForPlayersCVar()
{
ConVarRef mp_waitingforplayers_cancel( "mp_waitingforplayers_cancel" );
if ( mp_waitingforplayers_cancel.IsValid() && mp_waitingforplayers_cancel.GetBool() )
{
g_pBlockSpewer->PrintEventError( "mp_waitingforplayers_cancel must be 0 in order for replay to work!" );
return false;
}
return true;
}
void CPublishTester::PrintBaseUrlWarning()
{
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintMsg( "If clients can't access the following URL via a Web" );
g_pBlockSpewer->PrintMsg( "browser, they will not be able to download Replays." );
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintValue( "URL", Replay_GetDownloadURL() );
}
bool CPublishTester::Go()
{
const bool bOffload = false;
// Force anyone outside of Go() to use the block spewer until we're done.
CSpewScope SpewScope( g_pBlockSpewer );
g_pBlockSpewer->PrintMsg( "TESTING REPLAY SYSTEM CONFIGURATION..." );
// Fileserver convars
g_pBlockSpewer->PrintTestHeader( "Testing Fileserver ConVars (replay_fileserver_*)" );
// Test replay_fileserver_protocol
extern ConVar replay_fileserver_protocol;
if ( !Test_Protocol( "Protocol", replay_fileserver_protocol.GetString(), g_pAcceptableFileserverProtocols ) )
return false;
extern ConVar replay_fileserver_host;
if ( !Test_Hostname( replay_fileserver_host.GetString(), "\"http\" or \"https\"" ) )
return false;
extern ConVar replay_fileserver_port;
if ( !Test_Port( replay_fileserver_port.GetInt() ) )
return false;
extern ConVar replay_fileserver_path;
if ( !Test_Path( "Path", replay_fileserver_path.GetString(), true, false ) )
return false;
// Print out the base URL / warning
PrintBaseUrlWarning();
CFmtStr fmtTmpFilename( "testpublish_%i.tmp", (int)abs( rand() % 10000 ) );
CFmtStr fmtTmpFilenameFullPath( "%s%s", SV_GetTmpDir(), fmtTmpFilename.Access() );
const char *pFilename = fmtTmpFilenameFullPath.Access();
// Test file I/O
if ( !Test_IO( pFilename ) )
return false;
// Get out if necessary
if ( !Test_FilePublish( pFilename, bOffload ) )
return false;
// Test delete from fileserver
if ( !Test_PublishedFileDelete( pFilename, fmtTmpFilename.Access(), bOffload ) )
return false;
// Make sure mp_waitingforplayers_cancel isn't on or replay will be fucked.
if ( !Test_WaitingForPlayersCVar() )
return false;
return true;
}
//----------------------------------------------------------------------------------------
bool SV_DoTestPublish()
{
CPublishTester tester;
return tester.Go();
}
//----------------------------------------------------------------------------------------
+17
View File
@@ -0,0 +1,17 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_PUBLISHTEST_H
#define SV_PUBLISHTEST_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
bool SV_DoTestPublish();
//----------------------------------------------------------------------------------------
#endif // SV_PUBLISHTEST_H
+160
View File
@@ -0,0 +1,160 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_recordingsession.h"
#include "sv_recordingsessionmanager.h"
#include "sv_replaycontext.h"
#include "sv_filepublish.h"
#include "sv_recordingsessionblock.h"
#include "vstdlib/jobthread.h"
#include "fmtstr.h"
#include "sv_fileservercleanup.h"
#include <time.h>
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#ifdef _DEBUG
ConVar replay_simulate_expired_sessions( "replay_simulate_expired_sessions", "0", FCVAR_DONTRECORD,
"Simulate expired replay session data - the value of this cvar should be between 0 and 100 and is a probability - any cleanup done (via end of round cleanup or explicit replay_docleanup) will use this value to determine whether data is expired. E.g, use a value of 100 to delete all sessions, or 50 for a 50 chance of a given session being considered expired.",
true, 0.0f, true, 100.0f );
#endif
//----------------------------------------------------------------------------------------
CServerRecordingSession::CServerRecordingSession( IReplayContext *pContext )
: CBaseRecordingSession( pContext ),
m_bReplaysRequested( false ),
m_nLifeSpan( 0 )
{
}
CServerRecordingSession::~CServerRecordingSession()
{
}
bool CServerRecordingSession::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_nLifeSpan = pIn->GetInt( "lifespan", 0 );
KeyValues *pRecordTimeSubKey = pIn->FindKey( "record_time" );
if ( pRecordTimeSubKey )
{
m_RecordTime.Read( pRecordTimeSubKey );
}
return true;
}
void CServerRecordingSession::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "lifespan", m_nLifeSpan );
KeyValues *pRecordTime = new KeyValues( "record_time" );
pOut->AddSubKey( pRecordTime );
m_RecordTime.Write( pRecordTime );
}
void CServerRecordingSession::OnDelete()
{
BaseClass::OnDelete();
SV_GetFileserverCleaner()->MarkFileForDelete( GetFilename() );
}
void CServerRecordingSession::SetLocked( bool bLocked )
{
BaseClass::SetLocked( bLocked );
// Propagate to contained blocks
FOR_EACH_VEC( m_vecBlocks, i )
{
m_vecBlocks[ i ]->SetLocked( bLocked );
}
}
void CServerRecordingSession::PopulateWithRecordingData( int nCurrentRecordingStartTick )
{
BaseClass::PopulateWithRecordingData( nCurrentRecordingStartTick );
// Create a new session name
m_strName = SV_GetRecordingSessionManager()->GetNewSessionName();
// Cache current date/time and life-span
extern ConVar replay_data_lifespan;
m_nLifeSpan = replay_data_lifespan.GetInt() * 24 * 3600;
m_RecordTime.InitDateAndTimeToNow();
}
bool CServerRecordingSession::ShouldDitchSession() const
{
return BaseClass::ShouldDitchSession() || !m_bReplaysRequested;
}
#ifdef _DEBUG
void CServerRecordingSession::VerifyLocks()
{
const bool bLocked = IsLocked();
FOR_EACH_VEC( m_vecBlocks, i )
{
AssertMsg( m_vecBlocks[ i ]->IsLocked() == bLocked, "Parent/child locks out of sync. The block probably needs to inherit the parent's lock value on creation." );
}
}
#endif
double CServerRecordingSession::GetSecondsToExpiration() const
{
tm recordtime_tm;
V_memset( &recordtime_tm, 0, sizeof( recordtime_tm ) );
int nDay, nMonth, nYear;
m_RecordTime.GetDate( nDay, nMonth, nYear );
recordtime_tm.tm_mday = nDay;
recordtime_tm.tm_mon = nMonth - 1;
recordtime_tm.tm_year = nYear - 1900;
int nHour, nMin, nSec;
m_RecordTime.GetTime( nHour, nMin, nSec );
recordtime_tm.tm_hour = nHour;
recordtime_tm.tm_min = nMin;
recordtime_tm.tm_sec = nSec;
time_t recordtime = mktime( &recordtime_tm );
time_t nowtime;
time( &nowtime );
double delta = m_nLifeSpan - difftime( nowtime, recordtime );
#ifdef DBGFLAG_ASSERT
tm *pTest = localtime( &recordtime );
Assert( recordtime_tm.tm_mday == pTest->tm_mday );
Assert( recordtime_tm.tm_mon == pTest->tm_mon );
Assert( recordtime_tm.tm_year == pTest->tm_year );
Assert( recordtime_tm.tm_hour == pTest->tm_hour );
Assert( recordtime_tm.tm_min == pTest->tm_min );
Assert( recordtime_tm.tm_sec == pTest->tm_sec );
#endif
return delta;
}
bool CServerRecordingSession::SessionExpired() const
{
#ifdef _DEBUG
if ( ( 1+rand()%100 ) <= replay_simulate_expired_sessions.GetInt() )
return true;
#endif
return GetSecondsToExpiration() <= 0.0;
}
//----------------------------------------------------------------------------------------
+66
View File
@@ -0,0 +1,66 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_RECORDINGSESSION_H
#define SV_RECORDINGSESSION_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsession.h"
#include "basethinker.h"
#include "utlbuffer.h"
#include "sv_filepublish.h"
#include "replay/replaytime.h"
#include "sessioninfoheader.h"
//----------------------------------------------------------------------------------------
class IFilePublisher;
//----------------------------------------------------------------------------------------
class CServerRecordingSession : public CBaseRecordingSession
{
typedef CBaseRecordingSession BaseClass;
public:
CServerRecordingSession( IReplayContext *pContext );
~CServerRecordingSession();
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
virtual void OnDelete();
virtual void SetLocked( bool bLocked );
virtual void PopulateWithRecordingData( int nCurrentRecordingStartTick );
void NotifyReplayRequested() { m_bReplaysRequested = true; }
double GetSecondsToExpiration() const;
bool SessionExpired() const;
#ifdef _DEBUG
void VerifyLocks();
#endif
private:
virtual bool ShouldDitchSession() const;
bool m_bReplaysRequested;
int m_nLifeSpan;
CReplayTime m_RecordTime;
};
//----------------------------------------------------------------------------------------
inline CServerRecordingSession *SV_CastSession( CBaseRecordingSession *pSession )
{
return static_cast< CServerRecordingSession * >( pSession );
}
//----------------------------------------------------------------------------------------
#endif // SV_RECORDINGSESSION_H
+48
View File
@@ -0,0 +1,48 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_recordingsessionblock.h"
#include "qlimits.h"
#include "sv_fileservercleanup.h"
#include "sv_replaycontext.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CServerRecordingSessionBlock::CServerRecordingSessionBlock( IReplayContext *pContext )
: CBaseRecordingSessionBlock( pContext ),
m_nWriteStatus( WRITESTATUS_INVALID ),
m_pPublisher( NULL )
{
}
bool CServerRecordingSessionBlock::Read( KeyValues *pIn )
{
if ( !BaseClass::Read( pIn ) )
return false;
m_nWriteStatus = (WriteStatus_t)pIn->GetInt( "write_status", (int)WRITESTATUS_INVALID ); Assert( m_nWriteStatus != WRITESTATUS_INVALID );
V_strcpy_safe( m_szFullFilename, pIn->GetString( "filename" ) ); Assert( V_strlen( m_szFullFilename ) > 0 );
return true;
}
void CServerRecordingSessionBlock::Write( KeyValues *pOut )
{
BaseClass::Write( pOut );
pOut->SetInt( "write_status", (int)m_nWriteStatus ); Assert( m_nWriteStatus != WRITESTATUS_INVALID );
pOut->SetString( "filename", m_szFullFilename );
}
void CServerRecordingSessionBlock::OnDelete()
{
BaseClass::OnDelete();
SV_GetFileserverCleaner()->MarkFileForDelete( V_UnqualifiedFileName( m_szFullFilename ) );
}
//----------------------------------------------------------------------------------------
+63
View File
@@ -0,0 +1,63 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_RECORDINGSESSIONBLOCK_H
#define SV_RECORDINGSESSIONBLOCK_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionblock.h"
//----------------------------------------------------------------------------------------
class IFilePublisher;
//----------------------------------------------------------------------------------------
class CServerRecordingSessionBlock : public CBaseRecordingSessionBlock
{
typedef CBaseRecordingSessionBlock BaseClass;
public:
CServerRecordingSessionBlock( IReplayContext *pContext );
virtual bool Read( KeyValues *pIn );
virtual void Write( KeyValues *pOut );
double GetSecondsToExpiration();
enum WriteStatus_t
{
WRITESTATUS_INVALID = -1,
WRITESTATUS_WORKING,
WRITESTATUS_SUCCESS,
WRITESTATUS_FAILED
};
WriteStatus_t m_nWriteStatus; // SERVER: initially set to WRITESTATUS_INVALID, then set to STATUS_WORKING, STATUS_SUCCESS,
// or STATUS_FAILED, depending on the state of the write process (which runs on a separate thread
IFilePublisher *m_pPublisher; // Managed by session recorder
private:
virtual void OnDelete();
};
//----------------------------------------------------------------------------------------
inline CServerRecordingSessionBlock *SV_CastBlock( IReplaySerializeable *pBlock )
{
return static_cast< CServerRecordingSessionBlock * >( pBlock );
}
inline const CServerRecordingSessionBlock *SV_CastBlock( const IReplaySerializeable *pBlock )
{
return static_cast< const CServerRecordingSessionBlock * >( pBlock );
}
//----------------------------------------------------------------------------------------
#endif // SV_RECORDINGSESSIONBLOCK_H
@@ -0,0 +1,35 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_recordingsessionblockmanager.h"
#include "sv_recordingsessionblock.h"
#include "sv_replaycontext.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CServerRecordingSessionBlockManager::CServerRecordingSessionBlockManager( IReplayContext *pContext )
: CBaseRecordingSessionBlockManager( pContext )
{
}
CBaseRecordingSessionBlock *CServerRecordingSessionBlockManager::Create()
{
return new CServerRecordingSessionBlock( m_pContext );
}
IReplayContext *CServerRecordingSessionBlockManager::GetReplayContext() const
{
extern CServerReplayContext *g_pServerReplayContext;
return g_pServerReplayContext;
}
void CServerRecordingSessionBlockManager::PreLoad()
{
ConMsg( "Loading recording session blocks - this may take a minute...\n" );
}
//----------------------------------------------------------------------------------------
+37
View File
@@ -0,0 +1,37 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_RECORDINGSESSIONBLOCKMANAGER_H
#define SV_RECORDINGSESSIONBLOCKMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionblockmanager.h"
//----------------------------------------------------------------------------------------
//
// Maintains a persistent list of session blocks in a keyvalues file
//
class CServerRecordingSessionBlockManager : public CBaseRecordingSessionBlockManager
{
typedef CBaseRecordingSessionBlockManager BaseClass;
public:
CServerRecordingSessionBlockManager( IReplayContext *pContext );
virtual CBaseRecordingSessionBlock *Create();
virtual IReplayContext *GetReplayContext() const;
private:
virtual bool ShouldLoadBlocks() const { return false; }
virtual void PreLoad();
};
//----------------------------------------------------------------------------------------
#endif // SV_RECORDINGSESSIONBLOCKMANAGER_H
+107
View File
@@ -0,0 +1,107 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_recordingsessionmanager.h"
#include "baserecordingsessionblock.h"
#include "sv_replaycontext.h"
#include "sv_recordingsession.h"
#include "replaysystem.h"
#include "KeyValues.h"
#include "replay/replayutils.h"
#include "filesystem.h"
#include "iserver.h"
#include "sv_filepublish.h"
#include <time.h>
#include "vprof.h"
#include "sv_fileservercleanup.h"
#include "sv_sessionrecorder.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#define VERSION_SERVERRECORDINGSESSIONMANAGER 0
//----------------------------------------------------------------------------------------
CServerRecordingSessionManager::CServerRecordingSessionManager( IReplayContext *pContext )
: CBaseRecordingSessionManager( pContext ),
m_flNextScheduledCleanup( 0.0f ),
m_bOffload( false )
{
}
CServerRecordingSessionManager::~CServerRecordingSessionManager()
{
}
const char *CServerRecordingSessionManager::GetNewSessionName() const
{
// Setup filename for the session
tm today; VCRHook_LocalTime( &today );
return Replay_va(
"%04i%02i%02i-%02i%02i%02i-%s",
1900 + today.tm_year, today.tm_mon+1, today.tm_mday,
today.tm_hour, today.tm_min, today.tm_sec,
g_pEngine->GetGameServer()->GetMapName()
);
}
void CServerRecordingSessionManager::Think()
{
VPROF_BUDGET( "CServerRecordingSessionManager::Think", VPROF_BUDGETGROUP_REPLAY );
BaseClass::Think();
}
CBaseRecordingSession *CServerRecordingSessionManager::Create()
{
return new CServerRecordingSession( m_pContext );
}
int CServerRecordingSessionManager::GetVersion() const
{
return VERSION_SERVERRECORDINGSESSIONMANAGER;
}
IReplayContext *CServerRecordingSessionManager::GetReplayContext() const
{
return g_pServerReplayContext;
}
bool CServerRecordingSessionManager::CanDeleteSession( ReplayHandle_t hSession ) const
{
const CBaseRecordingSession *pSession = FindSession( hSession ); AssertMsg( pSession, "The session should always be valid here!" );
return !pSession->IsLocked();
}
void CServerRecordingSessionManager::OnAllSessionsDeleted()
{
SV_GetFileserverCleaner()->DoCleanAsynchronous();
}
CBaseRecordingSession *CServerRecordingSessionManager::OnSessionStart( int nCurrentRecordingStartTick, const char *pSessionName )
{
CBaseRecordingSession *pResult = BaseClass::OnSessionStart( nCurrentRecordingStartTick, pSessionName );
// Cache offload state
m_bOffload = false;
return pResult;
}
void CServerRecordingSessionManager::OnSessionEnd()
{
BaseClass::OnSessionEnd();
extern ConVar replay_fileserver_autocleanup;
if ( replay_fileserver_autocleanup.GetBool() )
{
// Cleanup expired sessions/blocks now
SV_DoFileserverCleanup( false, g_pBlockSpewer );
}
}
//----------------------------------------------------------------------------------------
+64
View File
@@ -0,0 +1,64 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_PUBLISHMANAGER_H
#define SV_PUBLISHMANAGER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "baserecordingsessionmanager.h"
//----------------------------------------------------------------------------------------
class CServerRecordingSession;
//----------------------------------------------------------------------------------------
//
// Manages and serializes all replay recording session data on the server
//
class CServerRecordingSessionManager : public CBaseRecordingSessionManager
{
typedef CBaseRecordingSessionManager BaseClass;
public:
CServerRecordingSessionManager( IReplayContext *pContext );
~CServerRecordingSessionManager();
void Think();
const char *GetNewSessionName() const;
virtual CBaseRecordingSession *OnSessionStart( int nCurrentRecordingStartTick, const char *pSessionName );
virtual void OnSessionEnd();
void EnableCleanupOnSessionEnd( bool bState );
// Offload session data to external fileserver? Cached once per session based on replay_fileserver_offload_enable.
bool ShouldOffload() const { return m_bOffload; }
protected:
//
// CGenericPersistentManager
//
virtual CBaseRecordingSession *Create();
virtual int GetVersion() const;
virtual bool ShouldSerializeIndexWithFullPath() { return false; } // On the server, write one file per session
virtual IReplayContext *GetReplayContext() const;
virtual bool CanDeleteSession( ReplayHandle_t hSession ) const;
virtual bool ShouldUnloadSessions() const { return true; }
virtual void OnAllSessionsDeleted();
private:
float m_flNextScheduledCleanup;
bool m_bOffload;
};
//----------------------------------------------------------------------------------------
#endif // SV_PUBLISHMANAGER_H
+326
View File
@@ -0,0 +1,326 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_replaycontext.h"
#include "replay/shared_defs.h" // BUILD_CURL defined here
#include "sv_sessionrecorder.h"
#include "sv_fileservercleanup.h"
#include "sv_recordingsession.h"
#include "sv_publishtest.h"
#include "replaysystem.h"
#include "icommandline.h"
#if BUILD_CURL
#include "curl/curl.h"
#endif
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
#undef CreateEvent
//----------------------------------------------------------------------------------------
CServerReplayContext::CServerReplayContext()
: m_pSessionRecorder( NULL ),
m_pFileserverCleaner( NULL ),
m_bShouldAbortRecording( false ),
m_flConVarSanityCheckTime( 0.0f )
{
}
CServerReplayContext::~CServerReplayContext()
{
delete m_pSessionRecorder;
delete m_pFileserverCleaner;
}
bool CServerReplayContext::Init( CreateInterfaceFn fnFactory )
{
#if BUILD_CURL
// Initialize cURL - in windows, this will init the winsock stuff.
curl_global_init( CURL_GLOBAL_ALL );
#endif
m_pShared = new CSharedReplayContext( this );
m_pShared->m_strSubDir = GetServerSubDirName();
m_pShared->m_pRecordingSessionManager = new CServerRecordingSessionManager( this );
m_pShared->m_pRecordingSessionBlockManager = new CServerRecordingSessionBlockManager( this );
m_pShared->m_pErrorSystem = new CErrorSystem( this );
m_pShared->Init( fnFactory );
// Create directory for temp files
CFmtStr fmtTmpDir( "%s%s", SV_GetBasePath(), SUBDIR_TMP );
g_pFullFileSystem->CreateDirHierarchy( fmtTmpDir.Access() );
// Remove any extraneous files from the temp directory
CleanTmpDir();
m_pSessionRecorder = new CSessionRecorder();
m_pSessionRecorder->Init();
m_pFileserverCleaner = new CFileserverCleaner();
return true;
}
void CServerReplayContext::CleanTmpDir()
{
int nFilesRemoved = 0;
Log( "Cleaning files from temp dir, \"%s\" ...", SV_GetTmpDir() );
FileFindHandle_t hFind;
CFmtStr fmtPath( "%s*", SV_GetTmpDir() );
const char *pFilename = g_pFullFileSystem->FindFirst( fmtPath.Access(), &hFind );
while ( pFilename )
{
if ( pFilename[0] != '.' )
{
// Remove the file
CFmtStr fmtFullFilename( "%s%s", SV_GetTmpDir(), pFilename );
g_pFullFileSystem->RemoveFile( fmtFullFilename.Access() );
++nFilesRemoved;
}
// Get next file
pFilename = g_pFullFileSystem->FindNext( hFind );
}
if ( nFilesRemoved )
{
Log( "%i %s removed.\n", nFilesRemoved, nFilesRemoved == 1 ? "file" : "files" );
}
else
{
Log( "no files removed.\n" );
}
}
void CServerReplayContext::Shutdown()
{
m_pShared->Shutdown();
#if BUILD_CURL
// Shutdown cURL
curl_global_cleanup();
#endif
}
void CServerReplayContext::Think()
{
ConVarSanityThink();
if ( !g_pReplay->IsReplayEnabled() )
return;
if ( m_bShouldAbortRecording )
{
g_pBlockSpewer->PrintBlockStart();
g_pBlockSpewer->PrintMsg( "Replay recording shutting down due to publishing error! Recording will begin" );
g_pBlockSpewer->PrintMsg( "at the beginning of the next round, but may fail again." );
g_pBlockSpewer->PrintBlockEnd();
// Shutdown recording
m_pSessionRecorder->AbortCurrentSessionRecording();
m_bShouldAbortRecording = false;
}
m_pShared->Think();
}
void CServerReplayContext::ConVarSanityThink()
{
if ( m_flConVarSanityCheckTime == 0.0f )
return;
DoSanityCheckNow();
}
void CServerReplayContext::UpdateFileserverIPFromHostname( const char *pHostname )
{
if ( !g_pEngine->NET_GetHostnameAsIP( pHostname, m_szFileserverIP, sizeof( m_szFileserverIP ) ) )
{
V_strcpy( m_szFileserverIP, "0.0.0.0" );
Log( "ERROR: Could not resolve fileserver hostname \"%s\" !\n", pHostname );
return;
}
Log( "Cached resolved fileserver hostname to IP address: \"%s\" -> \"%s\"\n", pHostname, m_szFileserverIP );
}
void CServerReplayContext::UpdateFileserverProxyIPFromHostname( const char *pHostname )
{
if ( !g_pEngine->NET_GetHostnameAsIP( pHostname, m_szFileserverProxyIP, sizeof( m_szFileserverProxyIP ) ) )
{
V_strcpy( m_szFileserverProxyIP, "0.0.0.0" );
Log( "ERROR: Could not resolve fileserver proxy hostname \"%s\" !\n", pHostname );
return;
}
Log( "Cached resolved fileserver proxy hostname to IP address: \"%s\" -> \"%s\"\n", pHostname, m_szFileserverProxyIP );
}
void CServerReplayContext::DoSanityCheckNow()
{
// Check now?
if ( m_flConVarSanityCheckTime <= g_pEngine->GetHostTime() )
{
// Reset
m_flConVarSanityCheckTime = 0.0f;
g_pBlockSpewer->PrintBlockStart();
extern ConVar replay_enable;
if ( replay_enable.GetBool() )
{
// Test publish
const bool bPublishResult = SV_DoTestPublish();
g_pBlockSpewer->PrintEmptyLine();
if ( bPublishResult )
{
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintMsg( "SUCCESS - REPLAY IS ENABLED!" );
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintMsg( "A 'changelevel' or 'map' is required - recording will" );
g_pBlockSpewer->PrintMsg( "begin at the start of the next round." );
g_pBlockSpewer->PrintEmptyLine();
}
else
{
replay_enable.SetValue( 0 );
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintMsg( "FAILURE - REPLAY DISABLED! \"replay_enable\" is now 0." );
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintEmptyLine();
g_pBlockSpewer->PrintMsg( "Address any failures above and re-exec replay.cfg." );
}
}
g_pBlockSpewer->PrintBlockEnd();
}
}
void CServerReplayContext::FlagForConVarSanityCheck()
{
m_flConVarSanityCheckTime = g_pEngine->GetHostTime() + 0.2f;
}
IGameEvent *CServerReplayContext::CreateReplaySessionInfoEvent()
{
IGameEvent *pEvent = g_pGameEventManager->CreateEvent( "replay_sessioninfo", true );
if ( !pEvent )
return NULL;
extern ConVar replay_block_dump_interval;
// Fill event
pEvent->SetString( "sn", m_pShared->m_pRecordingSessionManager->GetCurrentSessionName() );
pEvent->SetInt( "di", replay_block_dump_interval.GetInt() );
pEvent->SetInt( "cb", m_pShared->m_pRecordingSessionManager->GetCurrentSessionBlockIndex() );
pEvent->SetInt( "st", m_pSessionRecorder->GetCurrentRecordingStartTick() );
return pEvent;
}
IReplaySessionRecorder *CServerReplayContext::GetSessionRecorder()
{
return g_pServerReplayContext->m_pSessionRecorder;
}
const char *CServerReplayContext::GetLocalFileServerPath() const
{
static char s_szBuf[MAX_OSPATH];
extern ConVar replay_local_fileserver_path;
// Fix up the path name - NOTE: We intentionally avoid calling V_FixupPathName(), which
// pushes the entire output string to lower case.
V_strncpy( s_szBuf, replay_local_fileserver_path.GetString(), sizeof( s_szBuf ) );
V_FixSlashes( s_szBuf );
V_RemoveDotSlashes( s_szBuf );
V_FixDoubleSlashes( s_szBuf );
V_StripTrailingSlash( s_szBuf );
V_AppendSlash( s_szBuf, sizeof( s_szBuf ) );
return s_szBuf;
}
void CServerReplayContext::CreateSessionOnClient( int nClientSlot )
{
// If we have a session (i.e. if we're recording)
if ( SV_GetRecordingSessionInProgress() )
{
// Create the session on the client
IGameEvent *pSessionInfoEvent = CreateReplaySessionInfoEvent();
g_pReplay->SV_SendReplayEvent( pSessionInfoEvent, nClientSlot );
}
}
const char *CServerReplayContext::GetServerSubDirName() const
{
const char *pSubDirName = CommandLine()->ParmValue( "-replayserverdir" );
if ( !pSubDirName || !pSubDirName[0] )
{
Msg( "No '-replayserverdir' parameter found - using default replay folder.\n" );
return SUBDIR_SERVER;
}
Msg( "\n** Using custom replay dir name: \"%s%c%s\"\n\n", SUBDIR_REPLAY, CORRECT_PATH_SEPARATOR, pSubDirName );
return pSubDirName;
}
void CServerReplayContext::ReportErrorsToUser( wchar_t *pErrorText )
{
char szErrorText[4096];
g_pVGuiLocalize->ConvertUnicodeToANSI( pErrorText, szErrorText, sizeof( szErrorText ) );
static Color s_clrRed( 255, 0, 0 );
Warning( "\n-----------------------------------------------\n" );
Warning( "%s", szErrorText );
Warning( "-----------------------------------------------\n\n" );
}
void CServerReplayContext::OnPublishFailed()
{
// Don't report publish failure and shutdown publishing more than once per session.
if ( !m_pSessionRecorder->RecordingAborted() )
{
m_bShouldAbortRecording = true;
}
}
//----------------------------------------------------------------------------------------
CServerRecordingSession *SV_GetRecordingSessionInProgress()
{
return SV_CastSession( SV_GetRecordingSessionManager()->GetRecordingSessionInProgress() );
}
const char *SV_GetTmpDir()
{
return Replay_va( "%s%s%c", SV_GetBasePath(), SUBDIR_TMP, CORRECT_PATH_SEPARATOR );
}
bool SV_IsOffloadingEnabled()
{
return false;
}
bool SV_RunJobToCompletion( CJob *pJob )
{
return RunJobToCompletion( SV_GetThreadPool(), pJob );
}
//----------------------------------------------------------------------------------------
+134
View File
@@ -0,0 +1,134 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_REPLAYCONTEXT_H
#define SV_REPLAYCONTEXT_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "shared_replaycontext.h"
#include "replay/iserverreplaycontext.h"
#include "sv_recordingsessionmanager.h"
#include "sv_recordingsessionblockmanager.h"
#include "errorsystem.h"
//----------------------------------------------------------------------------------------
class CSessionRecorder;
class CBaseRecordingSessionBlock;
class IRecordingSessionManager;
class IThreadPool;
class CFileserverCleaner;
//----------------------------------------------------------------------------------------
class CServerReplayContext : public IServerReplayContext,
public IErrorReporter
{
public:
LINK_TO_SHARED_REPLAYCONTEXT_IMP();
CServerReplayContext();
~CServerReplayContext();
virtual bool Init( CreateInterfaceFn fnFactory );
virtual void Shutdown();
virtual void Think(); // Called by engine
virtual void OnPublishFailed();
void DoSanityCheckNow();
void UpdateFileserverIPFromHostname( const char *pHostname );
void UpdateFileserverProxyIPFromHostname( const char *pHostname );
//
// IErrorReporter
//
virtual void ReportErrorsToUser( wchar_t *pErrorText );
//
// IServerReplayContext
//
virtual void FlagForConVarSanityCheck();
virtual IGameEvent *CreateReplaySessionInfoEvent();
virtual IReplaySessionRecorder *GetSessionRecorder();
virtual const char *GetLocalFileServerPath() const;
virtual void CreateSessionOnClient( int nClientSlot );
const char *GetServerSubDirName() const;
CSessionRecorder *m_pSessionRecorder;
CFileserverCleaner *m_pFileserverCleaner;
char m_szFileserverIP[16]; // Fileserver's IP, cached any time "replay_fileserver_offload_hostname" is modified.
char m_szFileserverProxyIP[16]; // Proxy's IP, cached any time "replay_fileserver_offload_proxy_host" is modified.
private:
void CleanTmpDir();
void ConVarSanityThink();
float m_flConVarSanityCheckTime;
bool m_bShouldAbortRecording;
};
//----------------------------------------------------------------------------------------
extern CServerReplayContext *g_pServerReplayContext;
//----------------------------------------------------------------------------------------
inline CServerRecordingSessionManager *SV_GetRecordingSessionManager()
{
return static_cast< CServerRecordingSessionManager * >( g_pServerReplayContext->GetRecordingSessionManager() );
}
inline CServerRecordingSessionBlockManager *SV_GetRecordingSessionBlockManager()
{
return static_cast< CServerRecordingSessionBlockManager * >( g_pServerReplayContext->GetRecordingSessionBlockManager() );
}
inline CSessionRecorder *SV_GetSessionRecorder()
{
return g_pServerReplayContext->m_pSessionRecorder;
}
inline CFileserverCleaner *SV_GetFileserverCleaner()
{
return g_pServerReplayContext->m_pFileserverCleaner;
}
inline const char *SV_GetBasePath()
{
return g_pServerReplayContext->m_pShared->m_strBasePath;
}
inline IThreadPool *SV_GetThreadPool()
{
return g_pServerReplayContext->m_pShared->m_pThreadPool;
}
inline char const *SV_GetFileserverIP()
{
return g_pServerReplayContext->m_szFileserverIP;
}
inline char const *SV_GetFileserverProxyIP()
{
return g_pServerReplayContext->m_szFileserverProxyIP;
}
CServerRecordingSession *SV_GetRecordingSessionInProgress();
const char *SV_GetTmpDir(); // Get "replay/server/tmp/"
bool SV_IsOffloadingEnabled();
class CJob;
bool SV_RunJobToCompletion( CJob *pJob ); // NOTE: Adds to thread pool first
//----------------------------------------------------------------------------------------
#endif // SV_REPLAYCONTEXT_H
+455
View File
@@ -0,0 +1,455 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_sessionblockpublisher.h"
#include "sv_replaycontext.h"
#include "demofile/demoformat.h"
#include "sv_recordingsession.h"
#include "sv_recordingsessionblock.h"
#include "sv_sessioninfopublisher.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CSessionBlockPublisher::CSessionBlockPublisher( CServerRecordingSession *pSession,
CSessionInfoPublisher *pSessionInfoPublisher )
: m_pSession( pSession ),
m_pSessionInfoPublisher( pSessionInfoPublisher )
{
// Cache the dump interval so it can't be modified during a round - doing so would require
// an update on all clients.
extern ConVar replay_block_dump_interval;
m_nDumpInterval = MAX( MIN_SERVER_DUMP_INTERVAL, replay_block_dump_interval.GetInt() );
// Write the first block 15 or so seconds from now
m_flLastBlockWriteTime = g_pEngine->GetHostTime();
}
CSessionBlockPublisher::~CSessionBlockPublisher()
{
}
void CSessionBlockPublisher::PublishAllSynchronous()
{
while ( !IsDone() )
{
Think();
}
}
void CSessionBlockPublisher::AbortPublish()
{
FOR_EACH_LL( m_lstPublishingBlocks, it )
{
CServerRecordingSessionBlock *pCurBlock = m_lstPublishingBlocks[ it ];
IFilePublisher *&pPublisher = pCurBlock->m_pPublisher; // Shorthand
if ( !pPublisher )
continue;
// Already done?
if ( pPublisher->IsDone() )
continue;
pPublisher->AbortAndCleanup();
}
// Remove all blocks
m_lstPublishingBlocks.RemoveAll();
}
void CSessionBlockPublisher::OnStartRecording()
{
}
void CSessionBlockPublisher::OnStopRecord( bool bAborting )
{
if ( !bAborting )
{
// Write one final session block.
WriteAndPublishSessionBlock();
}
}
ReplayHandle_t CSessionBlockPublisher::GetSessionHandle() const
{
return m_pSession->GetHandle();
}
void CSessionBlockPublisher::WriteAndPublishSessionBlock()
{
// Make sure there is data to write
uint8 *pSessionBuffer;
int nSessionBufferSize;
g_pEngine->GetSessionRecordBuffer( &pSessionBuffer, &nSessionBufferSize ); // This will get called the last client disconnects from the server - but in waiting for players state we won't have a demo buffer
if ( !pSessionBuffer || nSessionBufferSize == 0 )
return;
// Create a new block
CServerRecordingSessionBlock *pNewBlock = SV_CastBlock( SV_GetRecordingSessionBlockManager()->CreateAndGenerateHandle() );
if ( !pNewBlock )
{
Warning( "Failed to create replay \"%s\"\n", pNewBlock->m_szFullFilename );
delete pNewBlock;
return;
}
if ( m_pSession->m_nServerStartRecordTick < 0 )
{
Warning( "Error: Current recording start tick was not properly setup. Aborting block write.\n" );
delete pNewBlock;
return;
}
// Figure out what the current block is
const int iCurrentSessionBlock = m_pSession->GetNumBlocks();
// Add an entry to the server index with the "writing" status set
const char *pFullFilename = Replay_va(
"%s%s_part_%u.%s", SV_GetTmpDir(),
SV_GetRecordingSessionManager()->GetCurrentSessionName(), iCurrentSessionBlock, BLOCK_FILE_EXTENSION
);
V_strcpy( pNewBlock->m_szFullFilename, pFullFilename );
pNewBlock->m_nWriteStatus = CServerRecordingSessionBlock::WRITESTATUS_INVALID; // Must be set here to trigger write
pNewBlock->m_nRemoteStatus = CBaseRecordingSessionBlock::STATUS_WRITING;
pNewBlock->m_iReconstruction = iCurrentSessionBlock;
pNewBlock->m_hSession = m_pSession->GetHandle();
// Match the session's lock - the block will be unlocked once recording has stopped and all publishing is complete.
pNewBlock->SetLocked( true );
// Commit the replay to the history manager's list
SV_GetRecordingSessionBlockManager()->Add( pNewBlock );
// Also store a pointer to the block in the session - NOTE: session will not attempt to free this pointer
m_pSession->AddBlock( pNewBlock, false );
// Cache the block temporarily while the binary block itself writes to disk - NOTE: will not attempt to free
m_lstPublishingBlocks.AddToTail( pNewBlock );
// Write the block now
PublishBlock( pNewBlock ); // pNewBlock->m_nWriteStatus modified here
IF_REPLAY_DBG( Warning( "%f: (%i) Publishing new block, %s\n", g_pEngine->GetHostTime(), iCurrentSessionBlock, pNewBlock->GetFilename() ) );
}
void CSessionBlockPublisher::GatherBlockData( uint8 *pSessionBuffer, int nSessionBufferSize, CServerRecordingSessionBlock *pBlock, unsigned char **ppSafeBlockData, int *pBlockSize )
{
const int nHeaderSize = sizeof( demoheader_t );
int nBlockOffset = 0;
const int nBlockSize = nSessionBufferSize;
int nTotalSize = nBlockSize;
demoheader_t *pHeader = NULL;
// If this is the first block, pass in a header to be written. Otherwise, just write the block.
if ( !pBlock->m_iReconstruction )
{
// Setup start tick in the header
pHeader = g_pEngine->GetReplayDemoHeader();
// Add header size
nBlockOffset = nHeaderSize;
nTotalSize += nHeaderSize;
}
// Make a copy of the block
unsigned char *pBuffer = new unsigned char[ nTotalSize ];
unsigned char *pBlockCopy = pBuffer + nBlockOffset;
// Only write the header if necessary
if ( pHeader )
{
demoheader_t littleEndianHeader = *pHeader;
littleEndianHeader.playback_time = FLT_MAX;
littleEndianHeader.playback_ticks = INT_MAX;
littleEndianHeader.playback_frames = INT_MAX;
// Byteswap
ByteSwap_demoheader_t( littleEndianHeader );
// Write header
V_memcpy( pBuffer, &littleEndianHeader, sizeof( littleEndianHeader ) );
}
// Note that pBlockCopy is based on pBuffer, which was allocated with nBlockSize PLUS
// header size - this will not overflow.
V_memcpy( pBlockCopy, pSessionBuffer, nBlockSize );
// Copy to "out" parameters
*pBlockSize = nTotalSize;
*ppSafeBlockData = pBuffer;
}
void CSessionBlockPublisher::PublishBlock( CServerRecordingSessionBlock *pBlock )
{
uint8 *pSessionBuffer;
int nSessionBufferSize;
if ( !g_pEngine->GetSessionRecordBuffer( &pSessionBuffer, &nSessionBufferSize ) )
{
Warning( "Block publish failed!\n" );
return;
}
unsigned char *pSafeBlockData;
int nBlockSize;
GatherBlockData( pSessionBuffer, nSessionBufferSize, pBlock, &pSafeBlockData, &nBlockSize );
// We've got what we need and can reset the put ptr
g_pEngine->ResetReplayRecordBuffer();
AssertMsg( !pBlock->m_pPublisher, "No publisher should exist for this block yet!" );
// Set status to working
pBlock->m_nWriteStatus = CServerRecordingSessionBlock::WRITESTATUS_WORKING;
// Get the number of bytes written
pBlock->m_uFileSize = nBlockSize;
// Make sure the main thread doesn't unload the block while it's being published
pBlock->SetLocked( true );
// Asynchronously publish to fileserver
PublishFileParams_t params;
params.m_pOutFilename = pBlock->m_szFullFilename;
params.m_pSrcData = pSafeBlockData;
params.m_nSrcSize = nBlockSize;
params.m_pCallbackHandler = this;
params.m_nCompressorType = COMPRESSORTYPE_BZ2;
params.m_bHash = true;
params.m_bFreeSrcData = true;
params.m_bDeleteFile = false;
params.m_pUserData = pBlock;
pBlock->m_pPublisher = SV_PublishFile( params );
}
void CSessionBlockPublisher::OnPublishComplete( const IFilePublisher *pPublisher, void *pUserData )
{
CServerRecordingSessionBlock *pBlock = (CServerRecordingSessionBlock *)pUserData;
Assert( pBlock );
// Set block status
if ( pPublisher->GetStatus() == IFilePublisher::PUBLISHSTATUS_OK )
{
pBlock->m_nWriteStatus = CServerRecordingSessionBlock::WRITESTATUS_SUCCESS;
}
else
{
pBlock->m_nWriteStatus = CServerRecordingSessionBlock::WRITESTATUS_FAILED;
// Publish failed - handle as needed
g_pServerReplayContext->OnPublishFailed();
}
// Did the block compress OK?
if ( pPublisher->Compressed() )
{
// Cache compressor type
pBlock->m_nCompressorType = pPublisher->GetCompressorType();
const int nCompressedSize = pPublisher->GetCompressedSize();
const float flRatio = (float)pBlock->m_uFileSize / nCompressedSize;
IF_REPLAY_DBG( Warning( "Block compression ratio: %.3f:1\n", flRatio ) );
// Update size
pBlock->m_uUncompressedSize = pBlock->m_uFileSize;
pBlock->m_uFileSize = nCompressedSize;
}
// Get the MD5
if ( pPublisher->Hashed() )
{
pPublisher->GetHash( pBlock->m_aHash );
}
// Now that m_nWriteStatus has been set in the block, the session info will be updated
// accordingly the next time PublishThink() is run.
// Mark the block as dirty since it was modified
Assert( pBlock->m_nWriteStatus != CServerRecordingSessionBlock::WRITESTATUS_INVALID );
SV_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false );
IF_REPLAY_DBG( Warning( "Publish complete for block %s\n", pBlock->GetDebugName() ) );
}
void CSessionBlockPublisher::OnPublishAborted( const IFilePublisher *pPublisher )
{
CServerRecordingSessionBlock *pBlock = FindBlockFromPublisher( pPublisher );
// Update the block's status
if ( pBlock )
{
pBlock->m_nWriteStatus = CServerRecordingSessionBlock::WRITESTATUS_FAILED;
}
g_pServerReplayContext->OnPublishFailed();
}
CServerRecordingSessionBlock *CSessionBlockPublisher::FindBlockFromPublisher( const IFilePublisher *pPublisher )
{
FOR_EACH_LL( m_lstPublishingBlocks, i )
{
CServerRecordingSessionBlock *pCurBlock = m_lstPublishingBlocks[ i ];
if ( pCurBlock->m_pPublisher == pPublisher )
{
return pCurBlock;
}
}
AssertMsg( 0, "Could not find block with the given publisher!" );
return NULL;
}
void CSessionBlockPublisher::Think()
{
// NOTE: This member function gets called even if replay is disabled. This is intentional.
VPROF_BUDGET( "CSessionBlockPublisher::Think", VPROF_BUDGETGROUP_REPLAY );
PublishThink();
}
void CSessionBlockPublisher::PublishThink()
{
AssertMsg( m_pSession->IsLocked(), "The session isn't locked, which means blocks can be being deleted and will probably cause a crash." );
// Go through all currently publishing blocks and free/think
FOR_EACH_LL( m_lstPublishingBlocks, it )
{
CServerRecordingSessionBlock *pCurBlock = m_lstPublishingBlocks[ it ];
IFilePublisher *&pPublisher = pCurBlock->m_pPublisher; // Shorthand
if ( !pPublisher )
continue;
// If the publisher's done, free it
if ( pPublisher->IsDone() )
{
delete pPublisher;
pPublisher = NULL;
}
else
{
// Let the publisher think
pPublisher->Think();
}
}
// Write a new session block out right now?
float flHostTime = g_pEngine->GetHostTime();
if ( m_flLastBlockWriteTime != 0.0f &&
flHostTime - m_flLastBlockWriteTime >= m_nDumpInterval &&
m_pSession->m_bRecording )
{
Assert( m_nDumpInterval > 0 );
// Write it
WriteAndPublishSessionBlock();
// Update the time
m_flLastBlockWriteTime = flHostTime;
}
// Check status of any replays that are being written
bool bUpdateSessionInfo = false;
for( int it = m_lstPublishingBlocks.Head(); it != m_lstPublishingBlocks.InvalidIndex(); )
{
CServerRecordingSessionBlock *pCurBlock = m_lstPublishingBlocks[ it ];
// Updated when write status is set to success or failure
int nPendingRequestStatus = CBaseRecordingSessionBlock::STATUS_INVALID;
// If set to anything besides InvalidIndex(), it will be removed from the list
int itRemove = m_lstPublishingBlocks.InvalidIndex();
bool bWriteBlockInfoToDisk = false;
switch ( pCurBlock->m_nWriteStatus )
{
case CServerRecordingSessionBlock::WRITESTATUS_INVALID:
AssertMsg( 0, "Why is m_nWriteStatus WRITESTATUS_INVALID here?" );
break;
case CServerRecordingSessionBlock::WRITESTATUS_WORKING: // Do nothing if still writing
break;
case CServerRecordingSessionBlock::WRITESTATUS_SUCCESS:
IF_REPLAY_DBG2( Warning( " Block %i marked as succeeded.\n", pCurBlock->m_iReconstruction ) );
pCurBlock->m_nRemoteStatus = CBaseRecordingSessionBlock::STATUS_READYFORDOWNLOAD;
nPendingRequestStatus = pCurBlock->m_nRemoteStatus;
bWriteBlockInfoToDisk = true;
itRemove = it;
break;
case CServerRecordingSessionBlock::WRITESTATUS_FAILED:
default: // Error?
IF_REPLAY_DBG2( Warning( " Block %i marked as failed.\n", pCurBlock->m_iReconstruction ) );
pCurBlock->m_nRemoteStatus = CBaseRecordingSessionBlock::STATUS_ERROR;
pCurBlock->m_nHttpError = CBaseRecordingSessionBlock::ERROR_WRITEFAILED;
nPendingRequestStatus = pCurBlock->m_nRemoteStatus;
bWriteBlockInfoToDisk = true;
itRemove = it;
// TODO: Retry
}
if ( bWriteBlockInfoToDisk )
{
// Save the master index file
Assert( pCurBlock->m_nWriteStatus != CServerRecordingSessionBlock::WRITESTATUS_INVALID );
SV_GetRecordingSessionBlockManager()->FlagForFlush( pCurBlock, false );
}
// Find the owning session
Assert( pCurBlock->m_hSession == m_pSession->GetHandle() );
// Refresh session info file
if ( nPendingRequestStatus != CBaseRecordingSessionBlock::STATUS_INVALID )
{
// Update it after this loop
bUpdateSessionInfo = true;
}
// Update iterator
it = m_lstPublishingBlocks.Next( it );
// Remove?
if ( itRemove != m_lstPublishingBlocks.InvalidIndex() )
{
IF_REPLAY_DBG( Warning( "Removing block %i from publisher\n", pCurBlock->m_iReconstruction ) );
// Free/clear publisher
delete pCurBlock->m_pPublisher;
pCurBlock->m_pPublisher = NULL;
// Removes from the list but doesn't free, since any pointer here points to a block somewhere
m_lstPublishingBlocks.Unlink( itRemove );
}
}
// Publish session info file now if it isn't already publishing
if ( bUpdateSessionInfo )
{
m_pSessionInfoPublisher->Publish();
}
}
bool CSessionBlockPublisher::IsDone() const
{
return m_lstPublishingBlocks.Count() == 0;
}
#ifdef _DEBUG
void CSessionBlockPublisher::Validate()
{
FOR_EACH_LL( m_lstPublishingBlocks, i )
{
CServerRecordingSessionBlock *pCurBlock = m_lstPublishingBlocks[ i ];
Assert( pCurBlock->m_nRemoteStatus == CBaseRecordingSessionBlock::STATUS_READYFORDOWNLOAD );
}
}
#endif
//----------------------------------------------------------------------------------------
+83
View File
@@ -0,0 +1,83 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_SESSIONBLOCKPUBLISHER_H
#define SV_SESSIONBLOCKPUBLISHER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "sv_filepublish.h"
#include "replay/replayhandle.h"
//----------------------------------------------------------------------------------------
class CServerRecordingSession;
class CServerRecordingSessionBlock;
class CSessionInfoPublisher;
class IDemoBuffer;
//----------------------------------------------------------------------------------------
class CSessionBlockPublisher : public IPublishCallbackHandler
{
public:
CSessionBlockPublisher( CServerRecordingSession *pSession, CSessionInfoPublisher *pSessionInfoPublisher );
~CSessionBlockPublisher();
void Think(); // Called explicitly
// Finish any publish jobs synchronously
void PublishAllSynchronous();
// Abort any publishing
void AbortPublish();
// Have all publish job completed?
bool IsDone() const;
// This will flag this publish manager as recording
void OnStartRecording();
// This will write out and publish any final session block
void OnStopRecord( bool bAborting );
// Get the handle for the associated session
ReplayHandle_t GetSessionHandle() const;
#ifdef _DEBUG
void Validate();
#endif
private:
//
// IPublishCallback
//
virtual void OnPublishComplete( const IFilePublisher *pPublisher, void *pUserData );
virtual void OnPublishAborted( const IFilePublisher *pPublisher );
virtual void AdjustHeader( const IFilePublisher *pPublisher, void *pHeaderData ) {}
void PublishBlock( CServerRecordingSessionBlock *pBlock );
void WriteAndPublishSessionBlock();
void PublishThink();
void WriteSessionBlockThink();
void SessionLockThink();
void GatherBlockData( uint8 *pSessionBuffer, int nSessionBufferSize, CServerRecordingSessionBlock *pBlock,
unsigned char **ppBlockData, int *pBlockSize );
CServerRecordingSessionBlock *FindBlockFromPublisher( const IFilePublisher *pPublisher );
float m_flLastBlockWriteTime;
int m_nDumpInterval;
CUtlLinkedList< CServerRecordingSessionBlock *, int > m_lstPublishingBlocks;
CServerRecordingSession *m_pSession;
CSessionInfoPublisher *m_pSessionInfoPublisher;
};
//----------------------------------------------------------------------------------------
#endif // SV_SESSIONBLOCKPUBLISHER_H
+182
View File
@@ -0,0 +1,182 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_sessioninfopublisher.h"
#include "sv_replaycontext.h"
#include "sv_recordingsessionblock.h"
#include "sv_recordingsession.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CSessionInfoPublisher::CSessionInfoPublisher( CServerRecordingSession *pSession )
: m_pSession( pSession ),
m_flSessionInfoPublishTime( 0.0f ),
m_itLastCompletedBlockWrittenToBuffer( ~0 ),
m_pFilePublisher( NULL ),
m_bShouldPublish( false )
{
}
CSessionInfoPublisher::~CSessionInfoPublisher()
{
}
void CSessionInfoPublisher::Publish()
{
m_bShouldPublish = true;
}
bool CSessionInfoPublisher::IsDone() const
{
return !m_bShouldPublish && m_pFilePublisher == NULL;
}
void CSessionInfoPublisher::OnStopRecord( bool bAborting )
{
}
void CSessionInfoPublisher::AbortPublish()
{
m_bShouldPublish = false;
if ( m_pFilePublisher && !m_pFilePublisher->IsDone() )
{
m_pFilePublisher->AbortAndCleanup();
}
}
void CSessionInfoPublisher::RefreshSessionInfoBlockData( CUtlBuffer &buf )
{
const CBaseRecordingSession::BlockContainer_t &vecBlocks = m_pSession->GetBlocks();
// Start from head if this is the first time we're writing, otherwise start
// from block after the last one written.
const int itStart = m_itLastCompletedBlockWrittenToBuffer == vecBlocks.InvalidIndex() ?
0 : m_itLastCompletedBlockWrittenToBuffer + 1;
for( int i = itStart; i < vecBlocks.Count(); ++i )
{
const CServerRecordingSessionBlock *pBlock = SV_CastBlock( vecBlocks[ i ] );
// NOTE: This will SeekPut() on buf, based on the block's reconstruction index.
pBlock->WriteSessionInfoDataToBuffer( buf );
// Cache the last block written whose state isn't going to change
if ( pBlock->m_nRemoteStatus == CBaseRecordingSessionBlock::STATUS_READYFORDOWNLOAD &&
i > m_itLastCompletedBlockWrittenToBuffer )
{
m_itLastCompletedBlockWrittenToBuffer = i;
}
IF_REPLAY_DBG( Warning( "Writing block w/ recon index %i to session info buffer\n", pBlock->m_iReconstruction ) );
}
}
void CSessionInfoPublisher::Think()
{
// Existing publisher?
if ( m_pFilePublisher )
{
// Finished?
if ( m_pFilePublisher->IsDone() )
{
// Free/clear
delete m_pFilePublisher;
m_pFilePublisher = NULL;
}
else
{
// Let the publisher think
m_pFilePublisher->Think();
}
}
// Publish needed?
if ( !m_bShouldPublish )
return;
// Already publishing?
if ( m_pFilePublisher )
return;
DBG( "Publishing session info...\n" );
// Write outstanding blocks to the buffer
RefreshSessionInfoBlockData( m_bufSessionInfo );
// We now know the uncompressed payload size
const int nPayloadSize = m_bufSessionInfo.TellPut();
// Create as much of the header as possible now - the rest will be written in AdjustHeader()
// once the publisher knows the md5 digest and the compression result.
Assert( m_pSession->m_strName.Length() < MAX_SESSIONNAME_LENGTH ); // The only way this name is going to get very long is if
V_strcpy_safe( m_SessionInfoHeader.m_szSessionName, m_pSession->m_strName.Get() );
m_SessionInfoHeader.m_nNumBlocks = m_pSession->GetNumBlocks();
m_SessionInfoHeader.m_bRecording = m_pSession->m_bRecording;
m_SessionInfoHeader.m_uPayloadSizeUC = nPayloadSize;
// Format a path & filename that points to the tmp dir, with <session name>.dmx on the end
CFmtStr fmtTmpSessionInfoFile( "%s%s.%s", SV_GetTmpDir(), m_pSession->m_strName.Get(), GENERIC_FILE_EXTENSION );
// Publish the file now (asynchronous)
PublishFileParams_t params;
params.m_pOutFilename = fmtTmpSessionInfoFile.Access(),
params.m_pSrcData = (uint8 *)m_bufSessionInfo.Base();
params.m_nSrcSize = nPayloadSize;
params.m_pCallbackHandler = this;
params.m_nCompressorType = COMPRESSORTYPE_LZSS;
params.m_bHash = true;
params.m_bFreeSrcData = false;
params.m_bDeleteFile = false;
params.m_pHeaderData = &m_SessionInfoHeader;
params.m_nHeaderSize = sizeof( SessionInfoHeader_t );
params.m_pUserData = NULL;
m_pFilePublisher = SV_PublishFile( params );
// Reset flag
m_bShouldPublish = false;
}
void CSessionInfoPublisher::OnPublishComplete( const IFilePublisher *pPublisher, void *pUserData )
{
Assert( !pUserData );
// Handle publish failure
if ( pPublisher->GetStatus() != IFilePublisher::PUBLISHSTATUS_OK )
{
g_pServerReplayContext->OnPublishFailed();
}
}
void CSessionInfoPublisher::OnPublishAborted( const IFilePublisher *pPublisher )
{
Assert( pPublisher == m_pFilePublisher );
g_pServerReplayContext->OnPublishFailed();
}
void CSessionInfoPublisher::AdjustHeader( const IFilePublisher *pPublisher, void *pHeaderData )
{
SessionInfoHeader_t *pHeader = static_cast< SessionInfoHeader_t * >( pHeaderData );
// Set compressor type - will return COMPRESSORTYPE_INVALID if compression failed.
pHeader->m_nCompressorType = pPublisher->GetCompressorType();
pHeader->m_uPayloadSize = pPublisher->GetCompressedSize();
// Get MD5 digest
pPublisher->GetHash( pHeader->m_aHash );
}
void CSessionInfoPublisher::PublishAllSynchronous()
{
while ( !IsDone() )
{
Think();
}
}
//----------------------------------------------------------------------------------------
+65
View File
@@ -0,0 +1,65 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#ifndef SV_SESSIONINFOPUBLISHER_H
#define SV_SESSIONINFOPUBLISHER_H
#ifdef _WIN32
#pragma once
#endif
//----------------------------------------------------------------------------------------
#include "utlbuffer.h"
#include "sv_filepublish.h"
#include "replay/replaytime.h"
#include "sessioninfoheader.h"
//----------------------------------------------------------------------------------------
class CServerRecordingSession;
class IFilePublisher;
//----------------------------------------------------------------------------------------
class CSessionInfoPublisher : public IPublishCallbackHandler
{
public:
CSessionInfoPublisher( CServerRecordingSession *pSession );
~CSessionInfoPublisher();
void Publish();
void PublishAllSynchronous();
bool IsPublishingSessionInfo() const { return m_flSessionInfoPublishTime != 0.0f || m_pFilePublisher; }
bool IsDone() const;
void OnStopRecord( bool bAborting );
void AbortPublish();
void Think(); // Called explicitly
private:
//
// IPublishCallback
//
virtual void OnPublishComplete( const IFilePublisher *pPublisher, void *pUserData );
virtual void OnPublishAborted( const IFilePublisher *pPublisher );
virtual void AdjustHeader( const IFilePublisher *pPublisher, void *pHeaderData );
void RefreshSessionInfoBlockData( CUtlBuffer &buf );
IFilePublisher *m_pFilePublisher;
float m_flSessionInfoPublishTime;
SessionInfoHeader_t m_SessionInfoHeader;
int m_itLastCompletedBlockWrittenToBuffer; // The last block written to the buffer that isn't going to change state
CUtlBuffer m_bufSessionInfo; // Growable buffer for the session info block data (does not include header)
CServerRecordingSession *m_pSession;
bool m_bShouldPublish; // True if we should publish - can be true while already publishing. Means another publish is needed.
};
//----------------------------------------------------------------------------------------
#endif // SV_SESSIONINFOPUBLISHER_H
+107
View File
@@ -0,0 +1,107 @@
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#include "sv_sessionpublishmanager.h"
#include "sv_recordingsession.h"
#include "sv_sessionblockpublisher.h"
#include "sv_sessioninfopublisher.h"
#include "replay_dbg.h"
#include "vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
CSessionPublishManager::CSessionPublishManager( CServerRecordingSession *pSession )
: m_pSession( pSession ),
m_pBlockPublisher( NULL ),
m_pSessionInfoPublisher( NULL )
{
m_pSessionInfoPublisher = new CSessionInfoPublisher( pSession );
m_pBlockPublisher = new CSessionBlockPublisher( pSession, m_pSessionInfoPublisher );
}
CSessionPublishManager::~CSessionPublishManager()
{
delete m_pBlockPublisher;
delete m_pSessionInfoPublisher;
}
void CSessionPublishManager::PublishAllSynchronous()
{
Msg( "Finishing up replay publish...\n" );
m_pBlockPublisher->PublishAllSynchronous();
m_pSessionInfoPublisher->PublishAllSynchronous();
}
void CSessionPublishManager::OnStartRecording()
{
// Lock the session (which will propagate the lock to all contained blocks)
m_pSession->SetLocked( true );
Assert( m_pSession->m_bRecording );
}
void CSessionPublishManager::OnStopRecord( bool bAborting )
{
// Recording should be turned off on the session by this point
Assert( !m_pSession->m_bRecording );
m_pBlockPublisher->OnStopRecord( bAborting );
m_pSessionInfoPublisher->OnStopRecord( bAborting );
}
ReplayHandle_t CSessionPublishManager::GetSessionHandle() const
{
return m_pSession->GetHandle();
}
bool CSessionPublishManager::IsDone() const
{
return !m_pSession->m_bRecording &&
m_pBlockPublisher->IsDone() &&
m_pSessionInfoPublisher->IsDone();
}
void CSessionPublishManager::Think()
{
// NOTE: This gets called even if replay is disabled. This is intentional.
VPROF_BUDGET( "CSessionPublishManager::Think", VPROF_BUDGETGROUP_REPLAY );
// Call publishers
m_pBlockPublisher->Think();
m_pSessionInfoPublisher->Think();
#ifdef _DEBUG
m_pSession->VerifyLocks();
#endif
}
void CSessionPublishManager::UnlockSession()
{
Assert( !m_pSession->m_bRecording );
Assert( m_pBlockPublisher->IsDone() );
Assert( m_pSessionInfoPublisher->IsDone() );
IF_REPLAY_DBG( Warning( "Unlocking session %s\n", m_pSession->GetDebugName() ) );
m_pSession->SetLocked( false );
}
void CSessionPublishManager::AbortPublish()
{
m_pBlockPublisher->AbortPublish();
m_pSessionInfoPublisher->AbortPublish();
}
#ifdef _DEBUG
void CSessionPublishManager::Validate()
{
m_pBlockPublisher->Validate();
}
#endif
//----------------------------------------------------------------------------------------

Some files were not shown because too many files have changed in this diff Show More