//========= 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 ); } } //----------------------------------------------------------------------------------------