diff options
Diffstat (limited to 'replay/cl_recordingsession.cpp')
| -rw-r--r-- | replay/cl_recordingsession.cpp | 451 |
1 files changed, 451 insertions, 0 deletions
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(); +} + +//---------------------------------------------------------------------------------------- |