diff options
Diffstat (limited to 'replay')
105 files changed, 17278 insertions, 0 deletions
diff --git a/replay/baserecordingsession.cpp b/replay/baserecordingsession.cpp new file mode 100644 index 0000000..62f302d --- /dev/null +++ b/replay/baserecordingsession.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/baserecordingsession.h b/replay/baserecordingsession.h new file mode 100644 index 0000000..0471d62 --- /dev/null +++ b/replay/baserecordingsession.h @@ -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 diff --git a/replay/baserecordingsessionblock.cpp b/replay/baserecordingsessionblock.cpp new file mode 100644 index 0000000..dbc5fdc --- /dev/null +++ b/replay/baserecordingsessionblock.cpp @@ -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"; + } +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/baserecordingsessionblock.h b/replay/baserecordingsessionblock.h new file mode 100644 index 0000000..0b3bda4 --- /dev/null +++ b/replay/baserecordingsessionblock.h @@ -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 diff --git a/replay/baserecordingsessionblockmanager.cpp b/replay/baserecordingsessionblockmanager.cpp new file mode 100644 index 0000000..1d8970c --- /dev/null +++ b/replay/baserecordingsessionblockmanager.cpp @@ -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 ); + } +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/baserecordingsessionblockmanager.h b/replay/baserecordingsessionblockmanager.h new file mode 100644 index 0000000..0cc22b0 --- /dev/null +++ b/replay/baserecordingsessionblockmanager.h @@ -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
\ No newline at end of file diff --git a/replay/baserecordingsessionmanager.cpp b/replay/baserecordingsessionmanager.cpp new file mode 100644 index 0000000..06a1d26 --- /dev/null +++ b/replay/baserecordingsessionmanager.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/baserecordingsessionmanager.h b/replay/baserecordingsessionmanager.h new file mode 100644 index 0000000..fa86e40 --- /dev/null +++ b/replay/baserecordingsessionmanager.h @@ -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 diff --git a/replay/basethinker.cpp b/replay/basethinker.cpp new file mode 100644 index 0000000..108a345 --- /dev/null +++ b/replay/basethinker.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/basethinker.h b/replay/basethinker.h new file mode 100644 index 0000000..e6a53b8 --- /dev/null +++ b/replay/basethinker.h @@ -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 diff --git a/replay/cl_commands.cpp b/replay/cl_commands.cpp new file mode 100644 index 0000000..cd7975d --- /dev/null +++ b/replay/cl_commands.cpp @@ -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( ¶ms, 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" ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_cvars.cpp b/replay/cl_cvars.cpp new file mode 100644 index 0000000..11a75cf --- /dev/null +++ b/replay/cl_cvars.cpp @@ -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" ); + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/cl_downloader.cpp b/replay/cl_downloader.cpp new file mode 100644 index 0000000..9e375da --- /dev/null +++ b/replay/cl_downloader.cpp @@ -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
\ No newline at end of file diff --git a/replay/cl_downloader.h b/replay/cl_downloader.h new file mode 100644 index 0000000..f484cc6 --- /dev/null +++ b/replay/cl_downloader.h @@ -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 diff --git a/replay/cl_performance_common.h b/replay/cl_performance_common.h new file mode 100644 index 0000000..5dfcf66 --- /dev/null +++ b/replay/cl_performance_common.h @@ -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 diff --git a/replay/cl_performancecontroller.cpp b/replay/cl_performancecontroller.cpp new file mode 100644 index 0000000..f662585 --- /dev/null +++ b/replay/cl_performancecontroller.cpp @@ -0,0 +1,1026 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#include "cl_performancecontroller.h" +#include "cl_replaycontext.h" +#include "globalvars_base.h" +#include "cl_replaycontext.h" +#include "replay/replay.h" +#include "replay/ireplaycamera.h" +#include "replay/replayutils.h" +#include "replay/ireplayperformanceplaybackhandler.h" +#include "replay/ireplayperformanceeditor.h" +#include "filesystem.h" +#include "KeyValues.h" +#include "replaysystem.h" +#include "cl_replaymanager.h" +#include "vprof.h" +#include "cl_performance_common.h" +#include "engine/ivdebugoverlay.h" +#include "utlbuffer.h" + +#undef Yield +#include "vstdlib/jobthread.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//---------------------------------------------------------------------------------------- + +#ifdef _DEBUG +ConVar replay_simulate_long_save( "replay_simulate_long_save", "0", FCVAR_DONTRECORD, "Simulate a long save. Seconds." ); +#endif + +//---------------------------------------------------------------------------------------- + +class CSaveJob : public CJob +{ +public: + CSaveJob( KeyValues *pInData, const char *pFullFilename ) + : m_pInData( pInData ) + { + SetFlags( GetFlags() | JF_IO ); + + V_strncpy( m_szFullFilename, pFullFilename, sizeof( m_szFullFilename ) ); + } + + virtual JobStatus_t DoExecute() + { + if ( !m_pInData ) + return JOB_FAILED; + + if ( !V_strlen( m_szFullFilename ) ) + return JOB_FAILED; + +#ifdef _DEBUG + const int nDelay = replay_simulate_long_save.GetInt(); + if ( nDelay ) + { + ThreadSleep( nDelay * 1000 ); + } +#endif + + return m_pInData->SaveToFile( g_pFullFileSystem, m_szFullFilename, "MOD" ) ? JOB_OK : JOB_FAILED; + } + +private: + KeyValues *m_pInData; + char m_szFullFilename[MAX_OSPATH]; +}; + +//---------------------------------------------------------------------------------------- + +CPerformanceController::CPerformanceController() +: m_pRoot( NULL ), + m_pCurEvent( NULL ), + m_pDbgRoot( NULL ), + m_pPlaybackHandler( NULL ), + m_pSetViewEvent( NULL ), + m_pSaveJob( NULL ), + m_bViewOverrideMode( false ), + m_bDirty( false ), + m_bLastSaveStatus( false ), + m_bRewinding( false ), + m_pSavedPerformance( NULL ), + m_pScratchPerformance( NULL ), + m_hReplay( REPLAY_HANDLE_INVALID ), + m_nState( STATE_DORMANT ), + m_pEditor( NULL ), + m_flLastCamSetViewTime( 0.0f ), + m_flTimeScale( 1.0f ) +{ +} + +CPerformanceController::~CPerformanceController() +{ + Cleanup(); +} + +void CPerformanceController::Cleanup() +{ + AssertMsg( + !m_pScratchPerformance || m_pScratchPerformance != m_pSavedPerformance, + "Sanity check failed. We should never be assigning saved to scratch or vice versa." + ); + + m_pSavedPerformance = NULL; + + if ( m_pScratchPerformance ) + { + delete m_pScratchPerformance; + m_pScratchPerformance = NULL; + } + + CleanupStream(); + CleanupDbgStream(); + + ClearDirtyFlag(); + + m_pCurEvent = NULL; + m_pEditor = NULL; + m_pPlaybackHandler = NULL; + m_hReplay = REPLAY_HANDLE_INVALID; + m_nState = STATE_DORMANT; + m_flLastCamSetViewTime = 0.0f; + m_flTimeScale = 1.0f; + + // Remove all queued events + FOR_EACH_LL( m_EventQueue, i ) + { + m_EventQueue[ i ]->deleteThis(); + } + m_EventQueue.RemoveAll(); +} + +void CPerformanceController::CleanupStream() +{ + if ( m_pRoot ) + { + m_pRoot->deleteThis(); + m_pRoot = NULL; + } +} + +void CPerformanceController::CleanupDbgStream() +{ + if ( m_pDbgRoot ) + { + m_pDbgRoot->deleteThis(); + m_pDbgRoot = NULL; + } +} + +float CPerformanceController::GetTime() const +{ + Assert( m_pCurEvent ); + return atof( m_pCurEvent->GetName() ); +} + +void CPerformanceController::SetEditor( IReplayPerformanceEditor *pEditor ) +{ + AssertMsg( pEditor, "This is bad. You must supply a valid editor pointer." ); + + // Cache editor + m_pEditor = pEditor; +} + +void CPerformanceController::StartRecording( CReplay *pReplay, bool bSnip ) +{ + Assert( !IsRecording() ); + + AssertMsg( + m_nState == STATE_PLAYING || !m_pRoot, + "Unless we're playing, root should be NULL here" + ); + + if ( m_nState == STATE_DORMANT ) + { + Assert( !m_pSavedPerformance ); + + // Create the performance KeyValues + m_pRoot = new KeyValues( "performance" ); + } + else if ( m_nState == STATE_PLAYING ) + { + // Nuke everything after the current event, or does nothing if we've past the end of the playback stream + if ( bSnip ) + { + Snip(); + } + + // When we go from playback to recording, we need to reset override view + g_pClient->GetReplayCamera()->ClearOverrideView(); + } + + // Update the state + m_nState = STATE_RECORDING; + + // Mark as dirty + m_bDirty = true; +} + +void CPerformanceController::Stop() +{ + Assert( !m_bRewinding ); + + ClearRewinding(); + Cleanup(); +} + +bool CPerformanceController::DumpStreamToFileAsync( const char *pFullFilename ) +{ + // m_pRoot can be NULL if the user only set an in and/or out point, and wants to save. + if ( !m_pRoot ) + return true; + + // Save the file + m_pSaveJob = new CSaveJob( m_pRoot, pFullFilename ); + if ( !m_pSaveJob ) + return false; + + IThreadPool *pThreadPool = CL_GetThreadPool(); + if ( !pThreadPool ) + return false; + + pThreadPool->AddJob( m_pSaveJob ); + + return true; +} + +bool CPerformanceController::FlushReplay() +{ + // Get the replay + CReplay *pReplay = GetReplay( m_hReplay ); + if ( !pReplay ) + return false; + + // Add the performance to the replay and save + Assert( !m_pSavedPerformance || pReplay->HasPerformance( m_pSavedPerformance ) ); + CL_GetReplayManager()->FlagReplayForFlush( pReplay, true ); + + return true; +} + +bool CPerformanceController::SaveAsync() +{ + if ( !m_pRoot ) + return false; + + if ( !m_pScratchPerformance ) + { + AssertMsg( 0, "Scratch performance should always be valid at this point." ); + return false; + } + + // NOTE: m_pSavedPerformance should always be valid here, as 'save' is disabled until it + // has an actual performance to save to. + + // Copy the relevant data from scratch -> saved - we want to preserve the filename + // the saved performance, and have no reason to copy over duplicate data (eg the replay + // handle). + m_pSavedPerformance->CopyTicks( m_pScratchPerformance ); + + // Copy title + V_wcsncpy( m_pSavedPerformance->m_wszTitle, m_pScratchPerformance->m_wszTitle, sizeof( m_pSavedPerformance->m_wszTitle ) ); + + // Use the saved performance's filename + DumpStreamToFileAsync( m_pSavedPerformance->GetFullPerformanceFilename() ); + + // Save the replay file + FlushReplay(); + + // Clear dirty flag + ClearDirtyFlag(); + + return true; +} + +bool CPerformanceController::SaveAsAsync( const wchar_t *pTitle ) +{ + // + // NOTE: This function assumes the following: + // + // * We've already dealt with checking the given title versus existing performances + // in the replay and that the user has selected to overwrite. + // + + CReplay *pReplay = m_pEditor->GetReplay(); + if ( !pReplay ) + { + AssertMsg( 0, "Replay must exist!" ); + return false; + } + + // Find existing performance in replay, if it exists. + CReplayPerformance *pExistingPerformance = pReplay->GetPerformanceWithTitle( pTitle ); + if ( !pExistingPerformance ) + { + // Create and add a new performance to the replay with a unique filename - do not generate a title since we will + // use the incoming title. + CReplayPerformance *pCopy = pReplay->AddNewPerformance( false, true ); + + // Copy the ticks, which is all we care about + pCopy->CopyTicks( m_pScratchPerformance ); + + // Set the title + pCopy->SetTitle( pTitle ); + + // Dump to the new file and save the replay + if ( !DumpStreamToFileAsync( pCopy->GetFullPerformanceFilename() ) || + !FlushReplay() ) + { + return false; + } + + // If we didn't spawn a thread, we want this to be true here, since the replay flushed + // and DumpStreamToFileAsync() succeeded. + m_bLastSaveStatus = true; + + // Saved performance is now replaced with the newly created performance + m_pSavedPerformance = pCopy; + + // Clear dirty flag + ClearDirtyFlag(); + + return true; + } + + // Overwriting an existing performance? + else + { + // Performance with the given name already exists - overwrite it (again, this function + // assumes that any UI around asking the user if they're sure they want to replace has + // already been navigated, and the user has selected to overwrite). + m_pSavedPerformance = pExistingPerformance; + } + + // Copy the title to the scratch + V_wcsncpy( m_pScratchPerformance->m_wszTitle, pTitle, MAX_TAKE_TITLE_LENGTH * sizeof( wchar_t ) ); + + // Attempt to save + if ( !SaveAsync() ) + return false; + + // Clear dirty flag + ClearDirtyFlag(); + + return true; +} + +bool CPerformanceController::IsSaving() const +{ + return m_pSaveJob != NULL; +} + +void CPerformanceController::SaveThink() +{ + if ( !m_pSaveJob ) + return; + + if ( m_pSaveJob->IsFinished() ) + { + // Cache save status + m_bLastSaveStatus = m_pSaveJob->GetStatus() == JOB_OK; + + m_pSaveJob->Release(); + m_pSaveJob = NULL; + } +} + +bool CPerformanceController::GetLastSaveStatus() const +{ + return m_bLastSaveStatus; +} + +void CPerformanceController::ClearDirtyFlag() +{ + m_bDirty = false; +} + +bool CPerformanceController::IsRecording() const +{ + return m_nState == STATE_RECORDING; +} + +bool CPerformanceController::IsPlaying() const +{ + return m_nState == STATE_PLAYING; +} + +bool CPerformanceController::IsPlaybackDataLeft() +{ + return m_pCurEvent && m_pCurEvent->GetNextTrueSubKey(); +} + +bool CPerformanceController::IsDirty() const +{ + return m_bDirty; +} + +void CPerformanceController::NotifyDirty() +{ + AssertMsg( GetPerformance() != NULL, "Can't mark empty performance as dirty." ); + m_bDirty = true; +} + +void CPerformanceController::OnSignonStateFull() +{ + if ( !g_pEngineClient->IsDemoPlayingBack() ) + return; + + // User hit rewind button (which reloads the map)? + if ( m_bRewinding ) + { + // Setup controller for playback from existing data. + SetupPlaybackExistingStream(); + + // Clear rewinding + ClearRewinding(); + + // Let the editor know the rewind has completed. + m_pEditor->OnRewindComplete(); + } + else + { + AssertMsg( !m_pScratchPerformance, "Scratch replay should not be valid yet." ); + + // If we've gotten this far and the replay is invalid, we're likely playing back a + // regular demo and didn't early out somewhere up the chain. + CReplay *pReplay = g_pReplayDemoPlayer->GetCurrentReplay(); + if ( !pReplay ) + return; + + // Cache replay + m_hReplay = pReplay->GetHandle(); + + // Play a performance from the beginning. + CReplayPerformance *pPerformance = g_pReplayDemoPlayer->GetCurrentPerformance(); + if ( pPerformance ) + { + SetupPlaybackFromPerformance( pPerformance ); + + // Make a copy of the performance we're playing back so the user can make changes + // w/o fucking up the original. + m_pScratchPerformance = pPerformance->MakeCopy(); + } + else + { + CreateNewScratchPerformance( pReplay ); + } + } +} + +float CPerformanceController::GetPlaybackTimeScale() const +{ + return m_flTimeScale; +} + +void CPerformanceController::CreateNewScratchPerformance( CReplay *pReplay ) +{ + // Create a new performance, but don't add it to the replay yet + m_pScratchPerformance = CL_GetPerformanceManager()->CreatePerformance( pReplay ); + + // Give it a default name + m_pScratchPerformance->AutoNameIfHasNoTitle( pReplay->m_szMapName ); + + // Generate a filename for the new performance + m_pScratchPerformance->SetFilename( CL_GetPerformanceManager()->GeneratePerformanceFilename( pReplay ) ); +} + +//---------------------------------------------------------------------------------------- + +void CPerformanceController::NotifyPauseState( bool bPaused ) +{ + if ( m_bPaused == bPaused ) + return; + + m_bPaused = bPaused; + + // Unpause? + if ( !bPaused ) + { + // Add queued events + for( int i = m_EventQueue.Tail(); i != m_EventQueue.InvalidIndex(); i = m_EventQueue.Previous( i ) ) + { + KeyValues *pCurEvent = m_EventQueue[ i ]; + AddEvent( pCurEvent ); + } + + m_EventQueue.RemoveAll(); + } +} + +CReplayPerformance *CPerformanceController::GetPerformance() +{ + return m_pScratchPerformance; +} + +CReplayPerformance *CPerformanceController::GetSavedPerformance() +{ + return m_pSavedPerformance; +} + +bool CPerformanceController::HasSavedPerformance() +{ + return m_pSavedPerformance != NULL; +} + +void CPerformanceController::Snip() +{ + if ( !m_pCurEvent ) + return; + + const float flTime = GetTime(); + + // Go through all events and delete anything on or after flSnipTime + for ( KeyValues *pCurEvent = m_pRoot->GetFirstTrueSubKey(); pCurEvent != NULL; ) + { + // Get next first, in case we delete + KeyValues *pNext = pCurEvent->GetNextTrueSubKey(); + + const float flCurEventTime = atof( pCurEvent->GetName() ); + if ( flCurEventTime >= flTime ) + { + // Delete the key + m_pRoot->RemoveSubKey( pCurEvent ); + pCurEvent->deleteThis(); + } + + pCurEvent = pNext; + } +} + +bool CPerformanceController::IsCameraChangeEvent( int nType ) const +{ + return nType >= EVENTTYPE_CAMERA_CHANGE_BEGIN && nType <= EVENTTYPE_CAMERA_CHANGE_END; +} + +void CPerformanceController::NotifyRewinding() +{ + m_bRewinding = true; + m_flLastCamSetViewTime = 0.0f; +} + +void CPerformanceController::ClearRewinding() +{ + m_bRewinding = false; +} + +//---------------------------------------------------------------------------------------- + +#define CREATE_EVENT( time_, type_ ) \ + new KeyValues( Replay_va( "%f", time_ ), "type", type_ ) + +#define RECORD_EVENT_( event_, time_, type_ ) \ + event_ = CREATE_EVENT( time_, type_ ); \ + AddEvent( event_ ) + +#define RECORD_EVENT( event_, time_, type_ ) \ + KeyValues *event_ = RECORD_EVENT_( event_, time_, type_ ) + +#define QUEUE_OR_RECORD_EVENT( event_, time_, type_ ) \ + if ( !m_pRoot ) \ + return; \ + \ + KeyValues *event_; \ + if ( m_bPaused ) \ + { \ + KeyValues *pQueuedEvent = CREATE_EVENT( time_, type_ ); \ + event_ = pQueuedEvent; \ + m_EventQueue.AddToHead( pQueuedEvent ); \ + RemoveDuplicateEventsFromQueue(); \ + } \ + else \ + { \ + RECORD_EVENT_( event_, time_, type_ ); \ + } + +void CPerformanceController::RemoveDuplicateEventsFromQueue() +{ + // Add queued events - only add the most recent camera change event, and the most recent + // player change event. + bool bFoundCameraChange = false; + bool bFoundPlayerChange = false; + bool bFoundSetView = false; + bool bFoundTimeScale = false; + + for( int i = m_EventQueue.Head(); i != m_EventQueue.InvalidIndex(); ) + { + KeyValues *pCurEvent = m_EventQueue[ i ]; + const int nType = pCurEvent->GetInt( "type" ); + + bool bDitchEvent = false; + bool bSetupCut = false; + + // Determine whether we should record the event or not + if ( nType == EVENTTYPE_CHANGEPLAYER ) + { + bDitchEvent = bFoundPlayerChange; + bFoundPlayerChange = true; + } + else if ( IsCameraChangeEvent( nType ) ) + { + bDitchEvent = bFoundCameraChange; + bFoundCameraChange = true; + } + else if ( nType == EVENTTYPE_CAMERA_SETVIEW ) + { + bDitchEvent = bFoundSetView; + bFoundSetView = true; + bSetupCut = true; // If we end up keeping this event, it should be a cut. + } + else if ( nType == EVENTTYPE_TIMESCALE ) + { + bDitchEvent = bFoundTimeScale; + bFoundTimeScale = true; + } + + // Setup as cut + if ( bSetupCut ) + { + pCurEvent->SetInt( "cut", 1 ); + } + + int itNext = m_EventQueue.Next( i ); + + if ( bDitchEvent ) + { +#if _DEBUG + CUtlBuffer buf; + pCurEvent->RecursiveSaveToFile( buf, 1 ); + IF_REPLAY_DBG( Warning( "Ditching event of type %s\n...", ( const char * )buf.Base() ) ); +#endif + + // Free the event + pCurEvent->deleteThis(); + m_EventQueue.Remove( i ); + } + + i = itNext; + } +} + +void CPerformanceController::AddEvent( KeyValues *pEvent ) +{ + IF_REPLAY_DBG2( + CUtlBuffer buf; + pEvent->RecursiveSaveToFile( buf, 1 ); + Warning( "Recording event:\n%s\n", ( const char * )buf.Base() ); + ); + m_pRoot->AddSubKey( pEvent ); +} + +void CPerformanceController::AddEvent_Camera_Change_FirstPerson( float flTime, int nEntityIndex ) +{ + QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CAMERA_CHANGE_FIRSTPERSON ); + pEvent->SetInt( "ent", nEntityIndex ); +} + +void CPerformanceController::AddEvent_Camera_Change_ThirdPerson( float flTime, int nEntityIndex ) +{ + QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CAMERA_CHANGE_THIRDPERSON ); + pEvent->SetInt( "ent", nEntityIndex ); +} + +void CPerformanceController::AddEvent_Camera_Change_Free( float flTime ) +{ + QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CAMERA_CHANGE_FREE ); +} + +void CPerformanceController::AddEvent_Camera_ChangePlayer( float flTime, int nEntIndex ) +{ + QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CHANGEPLAYER ); + pEvent->SetInt( "ent", nEntIndex ); +} + +void CPerformanceController::AddEvent_Camera_SetView( const SetViewParams_t ¶ms ) +{ + QUEUE_OR_RECORD_EVENT( pEvent, params.m_flTime, EVENTTYPE_CAMERA_SETVIEW ); + pEvent->SetString( "pos", Replay_va( "%f %f %f", params.m_pOrigin->x, params.m_pOrigin->y, params.m_pOrigin->z ) ); + pEvent->SetString( "ang", Replay_va( "%f %f %f", params.m_pAngles->x, params.m_pAngles->y, params.m_pAngles->z ) ); + pEvent->SetFloat( "fov", params.m_flFov ); + pEvent->SetFloat( "a", params.m_flAccel ); + pEvent->SetFloat( "s", params.m_flSpeed ); + pEvent->SetFloat( "rf", params.m_flRotationFilter ); +} + +void CPerformanceController::AddEvent_TimeScale( float flTime, float flScale ) +{ + QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_TIMESCALE ); + pEvent->SetFloat( "scale", flScale ); + + m_flTimeScale = flScale; +} + +//---------------------------------------------------------------------------------------- + +bool CPerformanceController::SetupPlaybackHandler() +{ + IReplayPerformancePlaybackHandler *pHandler = g_pClient->GetPerformancePlaybackHandler(); + if ( !pHandler ) + return false; + + // Cache + m_pPlaybackHandler = pHandler; + + return true; +} + +void CPerformanceController::FinishBeginPerformancePlayback() +{ + // Root should be setup by now + Assert( m_pRoot ); + + // Make sure the camera isn't setup for camera override + // TODO: Definitely need this here? + g_pClient->GetReplayCamera()->ClearOverrideView(); + + // Set to initial event + m_pCurEvent = m_pRoot->GetFirstTrueSubKey(); + + IF_REPLAY_DBG( + m_pDbgRoot = m_pRoot->MakeCopy(); + ); + + m_nState = STATE_PLAYING; + m_bViewOverrideMode = false; +} + +void CPerformanceController::SetupPlaybackExistingStream() +{ + // m_pRoot can be NULL here if the user is watching the original replay and has rewound + // without changing anything. + if ( !m_pRoot ) + return; + + if ( !SetupPlaybackHandler() ) + return; + + FinishBeginPerformancePlayback(); +} + +void CPerformanceController::SetupPlaybackFromPerformance( CReplayPerformance *pPerformance ) +{ + AssertMsg( !m_pSavedPerformance, "This probably hit because either SaveNow() or Discard() were not called. One of those should always be called on disconnect after watching or editing a replay." ); + AssertMsg( !m_pScratchPerformance, "Scratch performance should be NULL here." ); + + if ( !pPerformance ) + return; + + if ( !pPerformance->m_pReplay ) + { + AssertMsg( 0, "Performance passed in with an invalid replay pointer! This bad!" ); + return; + } + + if ( !SetupPlaybackHandler() ) + return; + + // Cache off the performance and replay for playback + m_pSavedPerformance = pPerformance; + m_pScratchPerformance = NULL; + m_hReplay = pPerformance->m_pReplay->GetHandle(); + + // Read the file + Assert( !m_pRoot ); + const char *pFilename = pPerformance->GetFullPerformanceFilename(); + m_pRoot = new KeyValues( pFilename ); + if ( !m_pRoot->LoadFromFile( g_pFullFileSystem, pFilename ) ) + { + Warning( "Failed to load replay file, \"%s\"!\n", pFilename ); + return; + } + + FinishBeginPerformancePlayback(); +} + +void CPerformanceController::ReadSetViewEvent( KeyValues *pEventSubKey, Vector &origin, QAngle &angles, float &fov, + float *pAccel, float *pSpeed, float *pRotFilter ) +{ + const char *pViewStr[2]; + + pViewStr[0] = pEventSubKey->GetString( "pos" ); + pViewStr[1] = pEventSubKey->GetString( "ang" ); + + sscanf( pViewStr[0], "%f %f %f", &origin.x, &origin.y, &origin.z ); + sscanf( pViewStr[1], "%f %f %f", &angles.x, &angles.y, &angles.z ); + fov = pEventSubKey->GetFloat( "fov", 90 ); + + if ( pAccel && pSpeed && pRotFilter ) + { + *pAccel = pEventSubKey->GetFloat( "a" ); + *pSpeed = pEventSubKey->GetFloat( "s" ); + *pRotFilter = pEventSubKey->GetFloat( "rf" ); + } +} + +void CPerformanceController::PlaybackThink() +{ + static Vector aOrigin[3]; + static QAngle aAngles[3]; + static float aFov[3]; + float flAccel = 0.0f, flSpeed = 0.0f, flRotFilter = 0.0f; + + KeyValues *pSearch = NULL; + float t; + + if ( !IsPlaying() ) + return; + + if ( !m_pCurEvent ) + return; + + if ( !m_pPlaybackHandler ) + return; + + CReplay *pReplay = GetReplay( m_hReplay ); + if ( !pReplay ) + return; + + const CGlobalVarsBase *g_pClientGlobalVariables = g_pEngineClient->GetClientGlobalVars(); + + const int nReplaySpawnTick = pReplay->m_nSpawnTick; + Assert( nReplaySpawnTick >= 0 ); + const float flCurTime = g_pClientGlobalVariables->curtime - g_pEngine->TicksToTime( nReplaySpawnTick ); + + float flEventTime = 0; + bool bShouldCut = false; + + while ( 1 ) + { + // Get event time + flEventTime = GetTime(); + + // Get out if this event shouldn't fire yet + if ( flEventTime > flCurTime ) + break; + + IF_REPLAY_DBG2( + CUtlBuffer buf; + m_pCurEvent->RecursiveSaveToFile( buf, 1 ); + Warning( "%s\n", ( const char * )buf.Base() ); + ); + + switch ( m_pCurEvent->GetInt( "type", EVENTTYPE_INVALID ) ) + { + case EVENTTYPE_CAMERA_CHANGE_FIRSTPERSON: + m_bViewOverrideMode = false; + m_pPlaybackHandler->OnEvent_Camera_Change_FirstPerson( flEventTime, m_pCurEvent->GetInt( "ent" ) ); + break; + + case EVENTTYPE_CAMERA_CHANGE_THIRDPERSON: + m_bViewOverrideMode = true; + m_pPlaybackHandler->OnEvent_Camera_Change_ThirdPerson( flEventTime, m_pCurEvent->GetInt( "ent" ) ); + break; + + case EVENTTYPE_CAMERA_CHANGE_FREE: + m_bViewOverrideMode = true; + m_pPlaybackHandler->OnEvent_Camera_Change_Free( flEventTime ); + break; + + case EVENTTYPE_CHANGEPLAYER: + m_pPlaybackHandler->OnEvent_Camera_ChangePlayer( flEventTime, m_pCurEvent->GetInt( "ent" ) ); + break; + + case EVENTTYPE_CAMERA_SETVIEW: + AssertMsg( m_bViewOverrideMode, "Camera mode needs to be set before a setview can take effect." ); + + if ( m_bViewOverrideMode ) + { + // Get sample for current time + ReadSetViewEvent( m_pCurEvent, aOrigin[0], aAngles[0], aFov[0], &flAccel, &flSpeed, &flRotFilter ); +// g_pEngineClient->Con_NPrintf( 0, "sample 0 time: %f", flEventTime ); + m_flLastCamSetViewTime = flEventTime; + m_pSetViewEvent = m_pCurEvent; // Stomp any previous set view - we want the last one + bShouldCut = bShouldCut || m_pCurEvent->GetBool( "cut" ); // We cut if any set-view event cuts, otherwise we will interpolate + } + break; + + case EVENTTYPE_TIMESCALE: + m_flTimeScale = m_pCurEvent->GetFloat( "scale" ); + m_pPlaybackHandler->OnEvent_TimeScale( flEventTime, m_flTimeScale ); + break; + + default: + AssertMsg( 0, "Unknown event in performance playback!\n" ); + Warning( "Unknown event in performance playback!\n" ); + } + + // Get next event (or NULL if there isn't one) + m_pCurEvent = m_pCurEvent->GetNextTrueSubKey(); + + // Get out if no more events + if ( !m_pCurEvent ) + break; + } + + // If in override mode, interpolate and setup camera + if ( m_bViewOverrideMode && m_pSetViewEvent ) + { + if ( bShouldCut ) + { + DBG2( "CUT\n" ); + aOrigin[2] = aOrigin[0]; + aAngles[2] = aAngles[0]; + aFov[2] = aFov[0]; + } + else + { + // Default second sample to first, in case we don't find a sample to interpolate with + aOrigin[1] = aOrigin[0]; + aAngles[1] = aAngles[0]; + aFov[1] = aFov[0]; + + // Parameter for interpolation + t = 0.0f; + + // Seek forward to half a second from current event time and see if there + // are any other set view events. + pSearch = m_pSetViewEvent->GetNextTrueSubKey(); + while ( pSearch ) + { + // Another sample not available + float flSearchTime = atof( pSearch->GetName() ); + if ( flSearchTime > m_flLastCamSetViewTime + 0.5f ) + break; + + if ( pSearch->GetInt( "type", EVENTTYPE_INVALID ) == EVENTTYPE_CAMERA_SETVIEW ) + { + // Found next sample within half a second - calc interpolation parameter & get data + float flDiff = flSearchTime - m_flLastCamSetViewTime; + Assert( flDiff > 0.0f ); + if ( flDiff > 0.0f ) + { + t = clamp ( ( flCurTime - m_flLastCamSetViewTime ) / flDiff, 0.0f, 1.0f ); + + // If the next set-view is a cut, we don't want to interpolate + if ( pSearch->GetBool( "cut" ) ) + { + const int iSrc = clamp( (int)( .5f + t ), 0, 1 ); // Round t to 0 or 1, so we set the camera to the current frame if t < 0.5f, and we set the camera to the 'cut'/next frame if t >= 0.5. + aOrigin[2] = aOrigin[ iSrc ]; + aAngles[2] = aAngles[ iSrc ]; + aFov[2] = aFov[ iSrc ]; + } + else + { + ReadSetViewEvent( pSearch, aOrigin[1], aAngles[1], aFov[1], &flAccel, &flSpeed, &flRotFilter ); + } + } + break; + } + + pSearch = pSearch->GetNextTrueSubKey(); + } + + // Interpolate + aOrigin[2] = Lerp( t, aOrigin[0], aOrigin[1] ); + aAngles[2] = Lerp( t, aAngles[0], aAngles[1] ); // NOTE: Calls QuaternionSlerp() internally + aFov[2] = Lerp( t, aFov[0], aFov[1] ); + } + + // Setup current view + SetViewParams_t params( flEventTime, &aOrigin[2], &aAngles[2], aFov[2], flAccel, flSpeed, flRotFilter ); + m_pPlaybackHandler->OnEvent_Camera_SetView( params ); + } + + IF_REPLAY_DBG( DebugRender() ); +} + +void CPerformanceController::DebugRender() +{ + KeyValues *pIt = m_pDbgRoot->GetFirstTrueSubKey(); + + Vector prevpos, pos; + QAngle angles; + float fov; + bool bPrev = false; + + g_pDebugOverlay->ClearDeadOverlays(); + + while ( pIt ) + { + if ( pIt->GetInt( "type", EVENTTYPE_INVALID ) == EVENTTYPE_CAMERA_SETVIEW ) + { + ReadSetViewEvent( pIt, pos, angles, fov, NULL, NULL, NULL ); + + // Skip first view event since no previous + if ( !bPrev ) + { + bPrev = true; + } + else + { + const bool bCut = pIt->GetBool( "cut" ); + const int r = bCut ? 0 : 255; + const int g = bCut ? 255 : 0; + const int b = 0; + Vector tickpos = pos + Vector(10,0,0); + g_pDebugOverlay->AddLineOverlay( prevpos, pos, r, g, b, true, 0.0f ); + g_pDebugOverlay->AddLineOverlay( pos, tickpos, 0, 255, 255, true, 0.0f ); + } + + prevpos = pos; + } + + pIt = pIt->GetNextTrueSubKey(); + } +} + +//---------------------------------------------------------------------------------------- + +void CPerformanceController::Think() +{ + VPROF_BUDGET( "CReplayPerformancePlayer::Think", VPROF_BUDGETGROUP_REPLAY ); + + CBaseThinker::Think(); + + PlaybackThink(); +} + +float CPerformanceController::GetNextThinkTime() const +{ + return 0.0f; +} + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/cl_performancecontroller.h b/replay/cl_performancecontroller.h new file mode 100644 index 0000000..abdcff2 --- /dev/null +++ b/replay/cl_performancecontroller.h @@ -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 ¶ms ); + 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 diff --git a/replay/cl_performancemanager.cpp b/replay/cl_performancemanager.cpp new file mode 100644 index 0000000..6b867ca --- /dev/null +++ b/replay/cl_performancemanager.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_performancemanager.h b/replay/cl_performancemanager.h new file mode 100644 index 0000000..7e79878 --- /dev/null +++ b/replay/cl_performancemanager.h @@ -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 diff --git a/replay/cl_recordingsession.cpp b/replay/cl_recordingsession.cpp new file mode 100644 index 0000000..59969b4 --- /dev/null +++ b/replay/cl_recordingsession.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_recordingsession.h b/replay/cl_recordingsession.h new file mode 100644 index 0000000..b43830b --- /dev/null +++ b/replay/cl_recordingsession.h @@ -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 diff --git a/replay/cl_recordingsessionblock.cpp b/replay/cl_recordingsessionblock.cpp new file mode 100644 index 0000000..1519e7c --- /dev/null +++ b/replay/cl_recordingsessionblock.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_recordingsessionblock.h b/replay/cl_recordingsessionblock.h new file mode 100644 index 0000000..1f60330 --- /dev/null +++ b/replay/cl_recordingsessionblock.h @@ -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
\ No newline at end of file diff --git a/replay/cl_recordingsessionblockmanager.cpp b/replay/cl_recordingsessionblockmanager.cpp new file mode 100644 index 0000000..6397a55 --- /dev/null +++ b/replay/cl_recordingsessionblockmanager.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_recordingsessionblockmanager.h b/replay/cl_recordingsessionblockmanager.h new file mode 100644 index 0000000..03c3d09 --- /dev/null +++ b/replay/cl_recordingsessionblockmanager.h @@ -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
\ No newline at end of file diff --git a/replay/cl_recordingsessionmanager.cpp b/replay/cl_recordingsessionmanager.cpp new file mode 100644 index 0000000..cda5ece --- /dev/null +++ b/replay/cl_recordingsessionmanager.cpp @@ -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 ); + } +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_recordingsessionmanager.h b/replay/cl_recordingsessionmanager.h new file mode 100644 index 0000000..b38429b --- /dev/null +++ b/replay/cl_recordingsessionmanager.h @@ -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 diff --git a/replay/cl_renderqueue.cpp b/replay/cl_renderqueue.cpp new file mode 100644 index 0000000..454d3a6 --- /dev/null +++ b/replay/cl_renderqueue.cpp @@ -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 ); +} + +//---------------------------------------------------------------------------------------- + diff --git a/replay/cl_renderqueue.h b/replay/cl_renderqueue.h new file mode 100644 index 0000000..adf2279 --- /dev/null +++ b/replay/cl_renderqueue.h @@ -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 diff --git a/replay/cl_replaycontext.cpp b/replay/cl_replaycontext.cpp new file mode 100644 index 0000000..441d5ea --- /dev/null +++ b/replay/cl_replaycontext.cpp @@ -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() ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_replaycontext.h b/replay/cl_replaycontext.h new file mode 100644 index 0000000..3032b83 --- /dev/null +++ b/replay/cl_replaycontext.h @@ -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 diff --git a/replay/cl_replaymanager.cpp b/replay/cl_replaymanager.cpp new file mode 100644 index 0000000..834cdce --- /dev/null +++ b/replay/cl_replaymanager.cpp @@ -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 ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_replaymanager.h b/replay/cl_replaymanager.h new file mode 100644 index 0000000..6c83e99 --- /dev/null +++ b/replay/cl_replaymanager.h @@ -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 diff --git a/replay/cl_replaymovie.cpp b/replay/cl_replaymovie.cpp new file mode 100644 index 0000000..79dbf3d --- /dev/null +++ b/replay/cl_replaymovie.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_replaymovie.h b/replay/cl_replaymovie.h new file mode 100644 index 0000000..28a817a --- /dev/null +++ b/replay/cl_replaymovie.h @@ -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 diff --git a/replay/cl_replaymoviemanager.cpp b/replay/cl_replaymoviemanager.cpp new file mode 100644 index 0000000..b84e563 --- /dev/null +++ b/replay/cl_replaymoviemanager.cpp @@ -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 ¶ms ) +{ + // 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, ¶ms, 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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_replaymoviemanager.h b/replay/cl_replaymoviemanager.h new file mode 100644 index 0000000..e7f6f05 --- /dev/null +++ b/replay/cl_replaymoviemanager.h @@ -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 ¶ms ); + 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 diff --git a/replay/cl_screenshotmanager.cpp b/replay/cl_screenshotmanager.cpp new file mode 100644 index 0000000..8c4eae1 --- /dev/null +++ b/replay/cl_screenshotmanager.cpp @@ -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, ¶ms, 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( ¶ms, 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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_screenshotmanager.h b/replay/cl_screenshotmanager.h new file mode 100644 index 0000000..536a3a8 --- /dev/null +++ b/replay/cl_screenshotmanager.h @@ -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 diff --git a/replay/cl_sessionblockdownloader.cpp b/replay/cl_sessionblockdownloader.cpp new file mode 100644 index 0000000..398a67c --- /dev/null +++ b/replay/cl_sessionblockdownloader.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_sessionblockdownloader.h b/replay/cl_sessionblockdownloader.h new file mode 100644 index 0000000..354ef3f --- /dev/null +++ b/replay/cl_sessionblockdownloader.h @@ -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 diff --git a/replay/cl_sessioninfodownloader.cpp b/replay/cl_sessioninfodownloader.cpp new file mode 100644 index 0000000..4cd586d --- /dev/null +++ b/replay/cl_sessioninfodownloader.cpp @@ -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"; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/cl_sessioninfodownloader.h b/replay/cl_sessioninfodownloader.h new file mode 100644 index 0000000..be754bc --- /dev/null +++ b/replay/cl_sessioninfodownloader.h @@ -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 diff --git a/replay/common/basereplayserializeable.cpp b/replay/common/basereplayserializeable.cpp new file mode 100644 index 0000000..a0a7bd4 --- /dev/null +++ b/replay/common/basereplayserializeable.cpp @@ -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() +{ +} + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/common/headers.vpc b/replay/common/headers.vpc new file mode 100644 index 0000000..5979400 --- /dev/null +++ b/replay/common/headers.vpc @@ -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" +}
\ No newline at end of file diff --git a/replay/common/performance.cpp b/replay/common/performance.cpp new file mode 100644 index 0000000..1888071 --- /dev/null +++ b/replay/common/performance.cpp @@ -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 ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/common/replay.cpp b/replay/common/replay.cpp new file mode 100644 index 0000000..1d56def --- /dev/null +++ b/replay/common/replay.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/common/replay_common.vpc b/replay/common/replay_common.vpc new file mode 100644 index 0000000..16b5b3f --- /dev/null +++ b/replay/common/replay_common.vpc @@ -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" +} diff --git a/replay/common/replaylib.cpp b/replay/common/replaylib.cpp new file mode 100644 index 0000000..87a87b0 --- /dev/null +++ b/replay/common/replaylib.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/common/replaytime.cpp b/replay/common/replaytime.cpp new file mode 100644 index 0000000..feb2405 --- /dev/null +++ b/replay/common/replaytime.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/common/replayutils.cpp b/replay/common/replayutils.cpp new file mode 100644 index 0000000..e8d63d5 --- /dev/null +++ b/replay/common/replayutils.cpp @@ -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 ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/common/screenshot.cpp b/replay/common/screenshot.cpp new file mode 100644 index 0000000..8a0004b --- /dev/null +++ b/replay/common/screenshot.cpp @@ -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 ); +} + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/compression.cpp b/replay/compression.cpp new file mode 100644 index 0000000..1acb420 --- /dev/null +++ b/replay/compression.cpp @@ -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 + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/compression.h b/replay/compression.h new file mode 100644 index 0000000..4807f23 --- /dev/null +++ b/replay/compression.h @@ -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 diff --git a/replay/errorsystem.cpp b/replay/errorsystem.cpp new file mode 100644 index 0000000..5f0ab96 --- /dev/null +++ b/replay/errorsystem.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- + diff --git a/replay/errorsystem.h b/replay/errorsystem.h new file mode 100644 index 0000000..967133d --- /dev/null +++ b/replay/errorsystem.h @@ -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 diff --git a/replay/genericpersistentmanager.h b/replay/genericpersistentmanager.h new file mode 100644 index 0000000..dbab0ca --- /dev/null +++ b/replay/genericpersistentmanager.h @@ -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 diff --git a/replay/ithinker.h b/replay/ithinker.h new file mode 100644 index 0000000..e52f152 --- /dev/null +++ b/replay/ithinker.h @@ -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 diff --git a/replay/ithinkmanager.h b/replay/ithinkmanager.h new file mode 100644 index 0000000..733c1da --- /dev/null +++ b/replay/ithinkmanager.h @@ -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 diff --git a/replay/managertest.cpp b/replay/managertest.cpp new file mode 100644 index 0000000..7829bb5 --- /dev/null +++ b/replay/managertest.cpp @@ -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 + +//---------------------------------------------------------------------------------------- diff --git a/replay/managertest.h b/replay/managertest.h new file mode 100644 index 0000000..ee21f2d --- /dev/null +++ b/replay/managertest.h @@ -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
\ No newline at end of file diff --git a/replay/replay.vpc b/replay/replay.vpc new file mode 100644 index 0000000..711cec7 --- /dev/null +++ b/replay/replay.vpc @@ -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] + } + +} diff --git a/replay/replay_dbg.cpp b/replay/replay_dbg.cpp new file mode 100644 index 0000000..f783035 --- /dev/null +++ b/replay/replay_dbg.cpp @@ -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." ); + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/replay_dbg.h b/replay/replay_dbg.h new file mode 100644 index 0000000..f6765bc --- /dev/null +++ b/replay/replay_dbg.h @@ -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 diff --git a/replay/replay_reconstructor.cpp b/replay/replay_reconstructor.cpp new file mode 100644 index 0000000..dee7385 --- /dev/null +++ b/replay/replay_reconstructor.cpp @@ -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; +} + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/replay_reconstructor.h b/replay/replay_reconstructor.h new file mode 100644 index 0000000..4c16883 --- /dev/null +++ b/replay/replay_reconstructor.h @@ -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 diff --git a/replay/replaysystem.cpp b/replay/replaysystem.cpp new file mode 100644 index 0000000..c268773 --- /dev/null +++ b/replay/replaysystem.cpp @@ -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 + +//---------------------------------------------------------------------------------------- diff --git a/replay/replaysystem.h b/replay/replaysystem.h new file mode 100644 index 0000000..be932d2 --- /dev/null +++ b/replay/replaysystem.h @@ -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 diff --git a/replay/sessioninfoheader.cpp b/replay/sessioninfoheader.cpp new file mode 100644 index 0000000..2249d61 --- /dev/null +++ b/replay/sessioninfoheader.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sessioninfoheader.h b/replay/sessioninfoheader.h new file mode 100644 index 0000000..9359e66 --- /dev/null +++ b/replay/sessioninfoheader.h @@ -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 diff --git a/replay/shared_cvars.cpp b/replay/shared_cvars.cpp new file mode 100644 index 0000000..3cddeec --- /dev/null +++ b/replay/shared_cvars.cpp @@ -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 +} diff --git a/replay/shared_replaycontext.cpp b/replay/shared_replaycontext.cpp new file mode 100644 index 0000000..3199c3d --- /dev/null +++ b/replay/shared_replaycontext.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/shared_replaycontext.h b/replay/shared_replaycontext.h new file mode 100644 index 0000000..85fc04d --- /dev/null +++ b/replay/shared_replaycontext.h @@ -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 diff --git a/replay/spew.cpp b/replay/spew.cpp new file mode 100644 index 0000000..475f60f --- /dev/null +++ b/replay/spew.cpp @@ -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 ) +{ +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/spew.h b/replay/spew.h new file mode 100644 index 0000000..a567ad3 --- /dev/null +++ b/replay/spew.h @@ -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 diff --git a/replay/sv_basejob.cpp b/replay/sv_basejob.cpp new file mode 100644 index 0000000..aeb0d8e --- /dev/null +++ b/replay/sv_basejob.cpp @@ -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 ); + } +} + +//----------------------------------------------------------------------------------------
\ No newline at end of file diff --git a/replay/sv_basejob.h b/replay/sv_basejob.h new file mode 100644 index 0000000..35c1bfb --- /dev/null +++ b/replay/sv_basejob.h @@ -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 diff --git a/replay/sv_commands.cpp b/replay/sv_commands.cpp new file mode 100644 index 0000000..d2fd72e --- /dev/null +++ b/replay/sv_commands.cpp @@ -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] ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_cvars.cpp b/replay/sv_cvars.cpp new file mode 100644 index 0000000..c04fb94 --- /dev/null +++ b/replay/sv_cvars.cpp @@ -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." ); + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_filepublish.cpp b/replay/sv_filepublish.cpp new file mode 100644 index 0000000..f456a62 --- /dev/null +++ b/replay/sv_filepublish.cpp @@ -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 ¶ms ) + { + 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 ¶ms ) +{ + Assert( !params.m_pHeaderData || ( params.m_pHeaderData && params.m_pCallbackHandler ) ); + + IFilePublisher *pResult; + + pResult = new CLocalFileserverPublisher(); + + pResult->Publish( params ); + + return pResult; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_filepublish.h b/replay/sv_filepublish.h new file mode 100644 index 0000000..2dd76d2 --- /dev/null +++ b/replay/sv_filepublish.h @@ -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 ¶ms ) = 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 ¶ms ); + +//---------------------------------------------------------------------------------------- + +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 diff --git a/replay/sv_fileservercleanup.cpp b/replay/sv_fileservercleanup.cpp new file mode 100644 index 0000000..c3ac061 --- /dev/null +++ b/replay/sv_fileservercleanup.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_fileservercleanup.h b/replay/sv_fileservercleanup.h new file mode 100644 index 0000000..65939e0 --- /dev/null +++ b/replay/sv_fileservercleanup.h @@ -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 diff --git a/replay/sv_publishtest.cpp b/replay/sv_publishtest.cpp new file mode 100644 index 0000000..c981803 --- /dev/null +++ b/replay/sv_publishtest.cpp @@ -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(); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_publishtest.h b/replay/sv_publishtest.h new file mode 100644 index 0000000..2b7ca7d --- /dev/null +++ b/replay/sv_publishtest.h @@ -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 diff --git a/replay/sv_recordingsession.cpp b/replay/sv_recordingsession.cpp new file mode 100644 index 0000000..3b010d1 --- /dev/null +++ b/replay/sv_recordingsession.cpp @@ -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; +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_recordingsession.h b/replay/sv_recordingsession.h new file mode 100644 index 0000000..025e9fd --- /dev/null +++ b/replay/sv_recordingsession.h @@ -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 diff --git a/replay/sv_recordingsessionblock.cpp b/replay/sv_recordingsessionblock.cpp new file mode 100644 index 0000000..7a9ca63 --- /dev/null +++ b/replay/sv_recordingsessionblock.cpp @@ -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 ) ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_recordingsessionblock.h b/replay/sv_recordingsessionblock.h new file mode 100644 index 0000000..4d473eb --- /dev/null +++ b/replay/sv_recordingsessionblock.h @@ -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 diff --git a/replay/sv_recordingsessionblockmanager.cpp b/replay/sv_recordingsessionblockmanager.cpp new file mode 100644 index 0000000..469827d --- /dev/null +++ b/replay/sv_recordingsessionblockmanager.cpp @@ -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" ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_recordingsessionblockmanager.h b/replay/sv_recordingsessionblockmanager.h new file mode 100644 index 0000000..f7e7e35 --- /dev/null +++ b/replay/sv_recordingsessionblockmanager.h @@ -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
\ No newline at end of file diff --git a/replay/sv_recordingsessionmanager.cpp b/replay/sv_recordingsessionmanager.cpp new file mode 100644 index 0000000..58efcb1 --- /dev/null +++ b/replay/sv_recordingsessionmanager.cpp @@ -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 ); + } +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_recordingsessionmanager.h b/replay/sv_recordingsessionmanager.h new file mode 100644 index 0000000..15f41cb --- /dev/null +++ b/replay/sv_recordingsessionmanager.h @@ -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 diff --git a/replay/sv_replaycontext.cpp b/replay/sv_replaycontext.cpp new file mode 100644 index 0000000..8c627fe --- /dev/null +++ b/replay/sv_replaycontext.cpp @@ -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 ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_replaycontext.h b/replay/sv_replaycontext.h new file mode 100644 index 0000000..672494b --- /dev/null +++ b/replay/sv_replaycontext.h @@ -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 diff --git a/replay/sv_sessionblockpublisher.cpp b/replay/sv_sessionblockpublisher.cpp new file mode 100644 index 0000000..d00c309 --- /dev/null +++ b/replay/sv_sessionblockpublisher.cpp @@ -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 + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_sessionblockpublisher.h b/replay/sv_sessionblockpublisher.h new file mode 100644 index 0000000..c9cfcf8 --- /dev/null +++ b/replay/sv_sessionblockpublisher.h @@ -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 diff --git a/replay/sv_sessioninfopublisher.cpp b/replay/sv_sessioninfopublisher.cpp new file mode 100644 index 0000000..dda091c --- /dev/null +++ b/replay/sv_sessioninfopublisher.cpp @@ -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(); + } +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_sessioninfopublisher.h b/replay/sv_sessioninfopublisher.h new file mode 100644 index 0000000..fafb62e --- /dev/null +++ b/replay/sv_sessioninfopublisher.h @@ -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 diff --git a/replay/sv_sessionpublishmanager.cpp b/replay/sv_sessionpublishmanager.cpp new file mode 100644 index 0000000..96921d5 --- /dev/null +++ b/replay/sv_sessionpublishmanager.cpp @@ -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 + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_sessionpublishmanager.h b/replay/sv_sessionpublishmanager.h new file mode 100644 index 0000000..c928915 --- /dev/null +++ b/replay/sv_sessionpublishmanager.h @@ -0,0 +1,76 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#ifndef SV_SESSIONPUBLISHMANAGER_H +#define SV_SESSIONPUBLISHMANAGER_H +#ifdef _WIN32 +#pragma once +#endif + +//---------------------------------------------------------------------------------------- + +#include "replay/replayhandle.h" + +//---------------------------------------------------------------------------------------- + +class CServerRecordingSession; +class CSessionBlockPublisher; +class CSessionInfoPublisher; + +//---------------------------------------------------------------------------------------- + +// +// CSessionPublishManager takes care of all the publishing for a particular session. +// For asynchronous publishing of block and session info data, publishing can +// sometimes overlap between rounds, as is sometimes the case for FTP. +// +// A CSessionPublishManager instance are created by passing in the in-progress recording +// session into the constructor. +// +// CSessionRecorder maintains a list of CSessionPublishManager instances and cleans +// them up once all publishing for their corresponding session is completed. +// + +class CSessionPublishManager +{ +public: + CSessionPublishManager( CServerRecordingSession *pSession ); + ~CSessionPublishManager(); + + void Think(); // Called explicitly + + // Finish any publish jobs synchronously + void PublishAllSynchronous(); + + // 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; + + // Unlock the associated session - this should only be called if IsDone() returns true. + void UnlockSession(); + + // Abort publishing + void AbortPublish(); + +#ifdef _DEBUG + void Validate(); +#endif + +private: + CServerRecordingSession *m_pSession; + CSessionBlockPublisher *m_pBlockPublisher; + CSessionInfoPublisher *m_pSessionInfoPublisher; +}; + +//---------------------------------------------------------------------------------------- + +#endif // SV_SESSIONPUBLISHMANAGER_H diff --git a/replay/sv_sessionrecorder.cpp b/replay/sv_sessionrecorder.cpp new file mode 100644 index 0000000..b52665e --- /dev/null +++ b/replay/sv_sessionrecorder.cpp @@ -0,0 +1,223 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#include "sv_sessionrecorder.h" +#include "replay/replayutils.h" +#include "replay/shared_defs.h" +#include "baserecordingsessionblock.h" +#include "replaysystem.h" +#include "baserecordingsessionblockmanager.h" +#include "sv_recordingsessionmanager.h" +#include "sv_replaycontext.h" +#include "sv_sessionpublishmanager.h" +#include "sv_recordingsession.h" +#include "sv_recordingsessionblock.h" +#include "fmtstr.h" +#include "vprof.h" +#include "iserver.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#undef CreateEvent + +//---------------------------------------------------------------------------------------- + +#define SERVER_REPLAY_INDEX_FILENAME ".replayindex" +#define SERVER_REPLAY_ERROR_LOST "The server crashed before the replay could be finalized. Replay lost." + +//---------------------------------------------------------------------------------------- + +CSessionRecorder::CSessionRecorder() +: m_bRecordingAborted( false ), + m_nCurrentRecordingStartTick( -1 ) +{ +} + +CSessionRecorder::~CSessionRecorder() +{ +} + +bool CSessionRecorder::Init() +{ + g_pFullFileSystem->CreateDirHierarchy( Replay_va( "%s%s", SV_GetBasePath(), SUBDIR_SESSIONS ) ); + return true; +} + +void CSessionRecorder::AbortCurrentSessionRecording() +{ + StopRecording( true ); + + CSessionPublishManager *pCurrentPublishManager = GetCurrentPublishManager(); + if ( !pCurrentPublishManager ) + { + AssertMsg( 0, "Could not get current publish manager." ); + return; + } + + pCurrentPublishManager->AbortPublish(); + + m_bRecordingAborted = true; +} + +void CSessionRecorder::SetCurrentRecordingStartTick( int nStartTick ) +{ + m_nCurrentRecordingStartTick = nStartTick; +} + +void CSessionRecorder::PublishAllSynchronous() +{ + FOR_EACH_LL( m_lstPublishManagers, i ) + { + m_lstPublishManagers[ i ]->PublishAllSynchronous(); + } +} + +void CSessionRecorder::StartRecording() +{ + m_bRecordingAborted = false; + + IServer *pServer = ReplayServerAsIServer(); + if ( !pServer || !pServer->IsActive() ) + { + ConMsg( "ERROR: Replay not active.\n" ); + return; + } + + // We only care about local fileserver path in the case that we aren't offloading files to an external sfileserver + const char *pWritePath = g_pServerReplayContext->GetLocalFileServerPath(); + if ( ( !pWritePath || !pWritePath[0] ) ) + { + ConMsg( "\n*\n* ERROR: Failed to begin record: make sure \"replay_local_fileserver_path\" refers to a valid path!\n** replay_local_fileserver_path is currently set to: \"%s\"\n*\n\n", pWritePath ); + return; + } + + IReplayServer *pReplayServer = ReplayServer(); + if ( pReplayServer->IsRecording() ) + { + ConMsg( "ERROR: Replay already recording.\n" ); + return; + } + + // Tell the replay server to begin recording + pReplayServer->StartRecording(); + + // Notify session manager + CBaseRecordingSession *pSession = SV_GetRecordingSessionManager()->OnSessionStart( m_nCurrentRecordingStartTick, NULL ); + + // Create a new publish manager and add it. The dump interval and any additional setup is done there. + CreateAndAddNewPublishManager( static_cast< CServerRecordingSession * >( pSession ) ); +} + +void CSessionRecorder::CreateAndAddNewPublishManager( CServerRecordingSession *pSession ) +{ + CSessionPublishManager *pNewPublishManager = new CSessionPublishManager( pSession ); + + // Let the publish manager know that it is the 'current' publish manager. + pNewPublishManager->OnStartRecording(); + + // Add to the head of the list, since the desired convention is for the list to be + // sorted from newest to oldest. + m_lstPublishManagers.AddToHead( pNewPublishManager ); +} + +float CSessionRecorder::GetNextThinkTime() const +{ + return 0.0f; +} + +void CSessionRecorder::Think() +{ + CBaseThinker::Think(); + + VPROF_BUDGET( "CSessionRecorder::Think", VPROF_BUDGETGROUP_REPLAY ); + + // This gets called even if replay is disabled. This is intentional. + PublishThink(); +} + +CSessionPublishManager *CSessionRecorder::GetCurrentPublishManager() const +{ + if ( !m_lstPublishManagers.Count() ) + return NULL; + + return m_lstPublishManagers[ m_lstPublishManagers.Head() ]; +} + +void CSessionRecorder::PublishThink() +{ + UpdateSessionLocks(); +} + +void CSessionRecorder::UpdateSessionLocks() +{ + for ( int i = m_lstPublishManagers.Head(); i != m_lstPublishManagers.InvalidIndex(); ) + { + CSessionPublishManager *pCurManager = m_lstPublishManagers[ i ]; + + // Cache off 'next' in case we delete the current object + const int itNext = m_lstPublishManagers.Next( i ); + + if ( pCurManager->IsDone() ) + { +#ifdef _DEBUG + pCurManager->Validate(); +#endif + + // We can unlock the associated session now. + pCurManager->UnlockSession(); + + // Remove and delete it. + m_lstPublishManagers.Remove( i ); + delete pCurManager; + + IF_REPLAY_DBG( Warning( "\n---\n*\n* All publishing done for session. %i still publishing.\n*\n---\n", m_lstPublishManagers.Count() ) ); + } + else + { + pCurManager->Think(); + } + + i = itNext; + } +} + +void CSessionRecorder::StopRecording( bool bAborting ) +{ +#if !defined( DEDICATED ) + if ( g_pEngineClient->IsPlayingReplayDemo() ) + return; +#endif + if ( !ReplayServer() ) + return; + + DBG( "StopRecording()\n" ); + + CServerRecordingSession *pSession = SV_GetRecordingSessionInProgress(); + if ( pSession ) + { + // Mark the session as not recording + pSession->OnStopRecording(); + + // Get the current publish manager and notify it that recording has stopped. + CSessionPublishManager *pManager = GetCurrentPublishManager(); + if ( pManager ) + { + pManager->OnStopRecord( bAborting ); + } + + // Notify session manager - the session will be flagged for unload or deletion, but + // will not actually be free'd until it is "unlocked" by the publish manager. + SV_GetRecordingSessionManager()->OnSessionEnd(); + } + + // Stop recording + ReplayServer()->StopRecording(); + + // Clear replay_recording + extern ConVar replay_recording; + replay_recording.SetValue( 0 ); +} + +//---------------------------------------------------------------------------------------- diff --git a/replay/sv_sessionrecorder.h b/replay/sv_sessionrecorder.h new file mode 100644 index 0000000..1987df5 --- /dev/null +++ b/replay/sv_sessionrecorder.h @@ -0,0 +1,78 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#ifndef SV_SESSIONRECORDER_H +#define SV_SESSIONRECORDER_H +#ifdef _WIN32 +#pragma once +#endif + +//---------------------------------------------------------------------------------------- + +#include "basethinker.h" +#include "utllinkedlist.h" +#include "replay/ireplaysessionrecorder.h" +#include "replay/replayhandle.h" +#include "sv_filepublish.h" + +//---------------------------------------------------------------------------------------- + +class IGameEvent; +class CServerRecordingSessionBlock; +class CServerRecordingSessionManager; +class CBaseRecordingSessionBlockManager; +class CSessionPublishManager; +class IDemoBuffer; +class CServerRecordingSession; + +//---------------------------------------------------------------------------------------- + +class CSessionRecorder : public CBaseThinker, + public IReplaySessionRecorder +{ +public: + CSessionRecorder(); + ~CSessionRecorder(); + + bool Init(); + void AbortCurrentSessionRecording(); + + int GetCurrentRecordingStartTick() const { return m_nCurrentRecordingStartTick; } + + void UpdateSessionLocks(); // Looks at publish managers and unlocks sessions as needed + + // + // IReplaySessionRecorder + // + virtual void StartRecording(); // Will waiting for recording to stop, then will begin recording + virtual void StopRecording( bool bAborting ); + virtual void SetCurrentRecordingStartTick( int nStartTick ); + + // Finish any publish jobs synchronously + void PublishAllSynchronous(); + + bool RecordingAborted() const { return m_bRecordingAborted; } + +private: + // + // CBaseThinker + // + float GetNextThinkTime() const; + void Think(); + + void PublishThink(); + + CSessionPublishManager *GetCurrentPublishManager() const; // Get the publish manager for the currently recording session + void CreateAndAddNewPublishManager( CServerRecordingSession *pSession ); + + int m_nCurrentRecordingStartTick; + bool m_bRecordingAborted; + + typedef CUtlLinkedList< CSessionPublishManager *, int > PublishManagerContainer_t; + PublishManagerContainer_t m_lstPublishManagers; +}; + +//---------------------------------------------------------------------------------------- + +#endif // SV_SESSIONRECORDER_H diff --git a/replay/thinkmanager.cpp b/replay/thinkmanager.cpp new file mode 100644 index 0000000..696420c --- /dev/null +++ b/replay/thinkmanager.cpp @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#include "thinkmanager.h" +#include "ithinker.h" +#include "replay/ienginereplay.h" +#include "replay_dbg.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//---------------------------------------------------------------------------------------- + +extern IEngineReplay *g_pEngine; + +//---------------------------------------------------------------------------------------- + +void CThinkManager::AddThinker( IThinker *pThinker ) +{ + Assert( m_lstManagers.Find( pThinker ) == m_lstManagers.InvalidIndex() ); + m_lstManagers.AddToTail( pThinker ); +} + +void CThinkManager::RemoveThinker( IThinker *pThinker ) +{ + int it = m_lstManagers.Find( pThinker ); Assert( it != m_lstManagers.InvalidIndex() ); + m_lstManagers.Remove( it ); +} + +void CThinkManager::Think() +{ + FOR_EACH_LL( m_lstManagers, i ) + { + IThinker *pCurThinker = m_lstManagers[ i ]; + if ( !pCurThinker->ShouldThink() ) + continue; + + pCurThinker->Think(); + pCurThinker->PostThink(); + } +} + +//---------------------------------------------------------------------------------------- + +static CThinkManager s_ThinkManager; +IThinkManager *g_pThinkManager = &s_ThinkManager; + +//---------------------------------------------------------------------------------------- diff --git a/replay/thinkmanager.h b/replay/thinkmanager.h new file mode 100644 index 0000000..76385cc --- /dev/null +++ b/replay/thinkmanager.h @@ -0,0 +1,44 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#ifndef FLUSHMANAGER_H +#define FLUSHMANAGER_H +#ifdef _WIN32 +#pragma once +#endif + +//---------------------------------------------------------------------------------------- + +#include "utllinkedlist.h" +#include "ithinkmanager.h" + +//---------------------------------------------------------------------------------------- + +class IThinker; + +//---------------------------------------------------------------------------------------- + +// +// Generic think manager - only worries about CGenericPersistentManagers for +// now though - not a CGenericPersistentManager's Think() can result in subtle +// bugs where files don't get saved properly. +// +class CThinkManager : public IThinkManager +{ +public: + virtual void AddThinker( IThinker *pThinker ); + virtual void RemoveThinker( IThinker *pThinker ); + + void Think(); + + CUtlLinkedList< IThinker *, int > m_lstManagers; +}; + +//---------------------------------------------------------------------------------------- + +extern IThinkManager *g_pThinkManager; + +//---------------------------------------------------------------------------------------- + +#endif // FLUSHMANAGER_H |