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