Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

665 lines
18 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. //=======================================================================================//
  4. #include "cl_replaymanager.h"
  5. #include "replay/ienginereplay.h"
  6. #include "replay/iclientreplay.h"
  7. #include "replay/ireplaymoviemanager.h"
  8. #include "replay/ireplayfactory.h"
  9. #include "replay/replayutils.h"
  10. #include "replay/ireplaymovierenderer.h"
  11. #include "replay/shared_defs.h"
  12. #include "baserecordingsession.h"
  13. #include "cl_screenshotmanager.h"
  14. #include "cl_recordingsession.h"
  15. #include "cl_recordingsessionblock.h"
  16. #include "replaysystem.h"
  17. #include "cl_replaymoviemanager.h"
  18. #include "replay_dbg.h"
  19. #include "inetchannel.h"
  20. #include "cl_replaycontext.h"
  21. #include <time.h>
  22. #include "vprof.h"
  23. // memdbgon must be the last include file in a .cpp file!!!
  24. #include "tier0/memdbgon.h"
  25. //----------------------------------------------------------------------------------------
  26. extern IEngineClientReplay *g_pEngineClient;
  27. extern ConVar replay_postdeathrecordtime;
  28. //----------------------------------------------------------------------------------------
  29. #define REPLAY_INDEX_VERSION 0
  30. //----------------------------------------------------------------------------------------
  31. CReplayManager::CReplayManager()
  32. : m_pPendingReplay( NULL ),
  33. m_pReplayLastLife( NULL ),
  34. m_pReplayThisLife( NULL ),
  35. m_flPlayerSpawnCreateReplayFailTime( 0.0f )
  36. {
  37. }
  38. CReplayManager::~CReplayManager()
  39. {
  40. }
  41. bool CReplayManager::Init( CreateInterfaceFn fnCreateFactory )
  42. {
  43. // Get out if the user is running an unsupported mod or platform
  44. if ( !g_pEngine->IsSupportedModAndPlatform() )
  45. return false;
  46. // Clear anything already loaded (since we reuse the same instance)
  47. Clear();
  48. // Register replay factory
  49. m_pReplayFactory = GetReplayFactory( fnCreateFactory ); Assert( m_pReplayFactory );
  50. // Load all replays from disk
  51. if ( !BaseClass::Init() )
  52. {
  53. Warning( "Failed to load replay history!\n" );
  54. }
  55. // Session manager init'd by this point - go through and link up replays to sessions
  56. CL_GetRecordingSessionManager()->OnReplaysLoaded();
  57. return true;
  58. }
  59. void CReplayManager::Shutdown()
  60. {
  61. // Get out if the user is running an unsupported mod or platform
  62. if ( !g_pEngine->IsSupportedModAndPlatform() )
  63. return;
  64. // Make sure we aren't waiting to write
  65. BaseClass::Shutdown(); // Saves
  66. }
  67. IReplayFactory *CReplayManager::GetReplayFactory( CreateInterfaceFn fnCreateFactory )
  68. {
  69. return (IReplayFactory *)fnCreateFactory( INTERFACE_VERSION_REPLAY_FACTORY, NULL );
  70. }
  71. void CReplayManager::OnSessionStart()
  72. {
  73. // The pending replay doesn't exist yet at this point as far as I've seen, since the "replay_sessioninfo"
  74. // event comes down a frame or more after the "replay_recording" replicated cvar is set to 1, which is
  75. // what triggers AttemptToSetupNewReplay().
  76. if ( !m_pPendingReplay )
  77. {
  78. AttemptToSetupNewReplay();
  79. }
  80. if ( m_pPendingReplay )
  81. {
  82. // Link up the pending replay to the recording session in progress
  83. if ( m_pPendingReplay->m_hSession == REPLAY_HANDLE_INVALID )
  84. {
  85. ReplayHandle_t hSessionInProgress = CL_GetRecordingSessionManager()->GetRecordingSessionInProgress()->GetHandle();
  86. m_pPendingReplay->m_hSession = hSessionInProgress;
  87. }
  88. // Make sure the spawn tick has the proper server start tick subtracted out
  89. if ( m_pPendingReplay->m_nSpawnTick < 0 )
  90. {
  91. const int nServerStartTick = CL_GetRecordingSessionManager()->m_ServerRecordingState.m_nStartTick; Assert( nServerStartTick > 0 );
  92. m_pPendingReplay->m_nSpawnTick = MAX( 0, -m_pPendingReplay->m_nSpawnTick - nServerStartTick );
  93. }
  94. }
  95. }
  96. void CReplayManager::OnSessionEnd()
  97. {
  98. // Complete the pending replay, if there is one
  99. CompletePendingReplay();
  100. }
  101. const char *CReplayManager::GetRelativeIndexPath() const
  102. {
  103. return Replay_va( "%s%c", SUBDIR_REPLAYS, CORRECT_PATH_SEPARATOR );
  104. }
  105. CReplay *CReplayManager::Create()
  106. {
  107. return m_pReplayFactory->Create();
  108. }
  109. IReplayContext *CReplayManager::GetReplayContext() const
  110. {
  111. return g_pClientReplayContextInternal;
  112. }
  113. bool CReplayManager::ShouldLoadObj( const CReplay *pReplay ) const
  114. {
  115. return pReplay && pReplay->m_bComplete;
  116. }
  117. void CReplayManager::OnObjLoaded( CReplay *pReplay )
  118. {
  119. if ( !pReplay )
  120. return;
  121. pReplay->m_bSavedDuringThisSession = false;
  122. }
  123. int CReplayManager::GetVersion() const
  124. {
  125. return REPLAY_INDEX_VERSION;
  126. }
  127. void CReplayManager::ClearPendingReplay()
  128. {
  129. m_pPendingReplay = NULL;
  130. }
  131. void CReplayManager::SanityCheckReplay( CReplay *pReplay )
  132. {
  133. if ( !pReplay )
  134. return;
  135. // DEBUG: Make sure this replay does not already exist in the list
  136. FOR_EACH_VEC( Replays(), i )
  137. {
  138. if ( Replays()[ i ]->GetHandle() == pReplay->GetHandle() )
  139. {
  140. IF_REPLAY_DBG( Warning( "Replay %i already found in history!\n", pReplay->GetHandle() ) );
  141. }
  142. }
  143. if ( pReplay->m_nDeathTick < pReplay->m_nSpawnTick )
  144. {
  145. IF_REPLAY_DBG( Warning( "Spawn tick (%i) is greater than death tick (%i)!\n", pReplay->m_nSpawnTick, pReplay->m_nDeathTick ) );
  146. }
  147. }
  148. void CReplayManager::SaveDanglingReplay()
  149. {
  150. if ( !m_pReplayThisLife )
  151. return;
  152. if ( m_pReplayThisLife->m_bRequestedByUser )
  153. {
  154. CompletePendingReplay();
  155. FlagReplayForFlush( m_pReplayThisLife, false );
  156. }
  157. }
  158. void CReplayManager::FreeLifeIfNotSaved( CReplay *&pReplay )
  159. {
  160. if ( pReplay )
  161. {
  162. if ( !pReplay->m_bSaved && !IsDirty( pReplay ) )
  163. {
  164. CleanupReplay( pReplay );
  165. }
  166. else
  167. {
  168. // If it's been saved, don't free the memory, just clear the pointer
  169. pReplay = NULL;
  170. }
  171. }
  172. }
  173. void CReplayManager::CleanupReplay( CReplay *&pReplay )
  174. {
  175. if ( !pReplay )
  176. return;
  177. // Get rid of a replay that was never committed:
  178. // Remove screenshots taken
  179. CL_GetScreenshotManager()->DeleteScreenshotsForReplay( pReplay );
  180. // Free
  181. delete pReplay;
  182. pReplay = NULL;
  183. }
  184. void CReplayManager::OnReplayRecordingCvarChanged()
  185. {
  186. DBG( "OnReplayRecordingCvarChanged()\n" );
  187. // If set to 0, get out - we don't care
  188. extern ConVar replay_recording;
  189. if ( !replay_recording.GetBool() )
  190. {
  191. DBG( " replay_recording is false...aborting\n" );
  192. return;
  193. }
  194. // If OnPlayerSpawn() hasn't failed to create the scratch replay, get out
  195. if ( m_flPlayerSpawnCreateReplayFailTime == 0.0f )
  196. {
  197. DBG( " m_flPlayerSpawnCreateReplayFailTime == 0.0f...aborting.\n" );
  198. return;
  199. }
  200. DBG( " Calling AttemptToSetupNewReplay()\n" );
  201. // Try to create & setup again
  202. AttemptToSetupNewReplay();
  203. // Reset
  204. m_flPlayerSpawnCreateReplayFailTime = 0.0f;
  205. }
  206. void CReplayManager::OnClientSideDisconnect()
  207. {
  208. SaveDanglingReplay();
  209. ClearPendingReplay();
  210. FreeLifeIfNotSaved( m_pReplayLastLife );
  211. FreeLifeIfNotSaved( m_pReplayThisLife );
  212. m_flPlayerSpawnCreateReplayFailTime = 0.0f;
  213. }
  214. void CReplayManager::CommitPendingReplayAndBeginDownload()
  215. {
  216. // Update the last session block we should download
  217. CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( m_pReplayThisLife->m_hSession ) );
  218. const int iPostDeathBlockIndex = pSession->UpdateLastBlockToDownload();
  219. // Update the # of blocks required to reconstruct the replay
  220. m_pReplayThisLife->m_iMaxSessionBlockRequired = iPostDeathBlockIndex;
  221. Commit( m_pReplayThisLife );
  222. }
  223. void CReplayManager::CompletePendingReplay()
  224. {
  225. // Get out if no pending replay
  226. if ( !m_pPendingReplay )
  227. return;
  228. // Get session associated w/ the replay
  229. CBaseRecordingSession *pSession = CL_GetRecordingSessionManager()->FindSession( m_pPendingReplay->m_hSession );
  230. // Sometimes the session isn't valid here, like when we're first joining a server
  231. if ( !pSession )
  232. return;
  233. Assert( pSession->m_nServerStartRecordTick >= 0 );
  234. // Cache death tick
  235. m_pPendingReplay->m_nDeathTick = g_pEngineClient->GetLastServerTickTime() - pSession->m_nServerStartRecordTick;
  236. SanityCheckReplay( m_pPendingReplay );
  237. // Calc replay length
  238. m_pPendingReplay->m_flLength = g_pEngine->TicksToTime(
  239. m_pPendingReplay->m_nDeathTick -
  240. m_pPendingReplay->m_nSpawnTick +
  241. g_pEngine->TimeToTicks( replay_postdeathrecordtime.GetFloat() )
  242. );
  243. // Cache player slot so we can start playback of replays from recorder player's perspective
  244. m_pPendingReplay->m_nPlayerSlot = g_pEngineClient->GetPlayerSlot() + 1;
  245. // Setup status
  246. m_pPendingReplay->m_nStatus = CReplay::REPLAYSTATUS_DOWNLOADPHASE;
  247. // The replay is now "complete," ie has all the data needed
  248. m_pPendingReplay->m_bComplete = true;
  249. // Let derived classes do whatever it wants
  250. m_pPendingReplay->OnComplete();
  251. // If the replay was requested by the user already, update the # of blocks we should download & commit the replay
  252. if ( m_pPendingReplay->m_bRequestedByUser )
  253. {
  254. #ifdef DBGFLAG_ASSERT
  255. Assert( m_pReplayThisLife->m_bComplete );
  256. #endif
  257. CommitPendingReplayAndBeginDownload();
  258. }
  259. // Before we copy the pointer to "this life," end recording so the replay can do any cleanup (eg listening for game events)
  260. m_pPendingReplay->OnEndRecording();
  261. // Cache off scratch replay to "this life"
  262. m_pReplayThisLife = m_pPendingReplay;
  263. ClearPendingReplay();
  264. }
  265. bool CReplayManager::Commit( CReplay *pNewReplay )
  266. {
  267. if ( !g_pClientReplayContextInternal->IsInitialized() || !pNewReplay )
  268. return false;
  269. SanityCheckReplay( pNewReplay );
  270. // NOTE: Marks index as dirty, as well as pNewReplay
  271. Add( pNewReplay );
  272. // Save now
  273. Save();
  274. return true;
  275. }
  276. //
  277. // IReplayManager implementation
  278. //
  279. CReplay *CReplayManager::GetReplay( ReplayHandle_t hReplay )
  280. {
  281. if ( m_pReplayThisLife && m_pReplayThisLife->GetHandle() == hReplay )
  282. return m_pReplayThisLife;
  283. return Find( hReplay );
  284. }
  285. void CReplayManager::DeleteReplay( ReplayHandle_t hReplay, bool bNotifyUI )
  286. {
  287. CReplay *pReplay = GetReplay( hReplay ); Assert( pReplay );
  288. // The session manager will delete the .dem, the session .dmx and remove the session
  289. // item itself if this is the last replay associated with it.
  290. CL_GetRecordingSessionManager()->OnReplayDeleted( pReplay );
  291. // Notify the replay browser if necessary
  292. if ( bNotifyUI )
  293. {
  294. extern IClientReplay *g_pClient;
  295. g_pClient->OnDeleteReplay( hReplay );
  296. }
  297. // Remove it
  298. Remove( pReplay );
  299. // If the replay deleted was just saved and we haven't respawned yet,
  300. // we need to clear out some stuff so GetReplay() doesn't crash.
  301. if ( m_pReplayThisLife == pReplay )
  302. {
  303. m_pReplayThisLife = NULL;
  304. m_pPendingReplay = NULL;
  305. }
  306. if ( m_pReplayLastLife == pReplay )
  307. {
  308. m_pReplayLastLife = NULL;
  309. }
  310. }
  311. void CReplayManager::FlagReplayForFlush( CReplay *pReplay, bool bForceImmediate )
  312. {
  313. FlagForFlush( pReplay, bForceImmediate );
  314. }
  315. int CReplayManager::GetUnrenderedReplayCount()
  316. {
  317. int nCount = 0;
  318. FOR_EACH_VEC( m_vecObjs, i )
  319. {
  320. CReplay *pCurReplay = m_vecObjs[ i ];
  321. if ( !pCurReplay->m_bRendered &&
  322. pCurReplay->m_nStatus == CReplay::REPLAYSTATUS_READYTOCONVERT )
  323. {
  324. ++nCount;
  325. }
  326. }
  327. return nCount;
  328. }
  329. void CReplayManager::InitReplay( CReplay *pReplay )
  330. {
  331. // Setup record time right now
  332. pReplay->m_RecordTime.InitDateAndTimeToNow();
  333. // Store start time
  334. pReplay->m_flStartTime = g_pEngine->GetHostTime();
  335. // Get map name (w/o the path)
  336. V_FileBase( g_pEngineClient->GetLevelName(), m_pPendingReplay->m_szMapName, sizeof( m_pPendingReplay->m_szMapName ) );
  337. // Give the replay a default name
  338. pReplay->AutoNameTitleIfEmpty();
  339. }
  340. CReplay *CReplayManager::CreatePendingReplay()
  341. {
  342. Assert( m_pPendingReplay == NULL );
  343. m_pPendingReplay = CreateAndGenerateHandle();
  344. // If we've already begun recording, link to the session now, otherwise link once
  345. // we start recording.
  346. CBaseRecordingSession *pSessionInProgress = CL_GetRecordingSessionInProgress();
  347. if ( pSessionInProgress )
  348. {
  349. m_pPendingReplay->m_hSession = pSessionInProgress->GetHandle();
  350. }
  351. InitReplay( m_pPendingReplay );
  352. // Setup replay handle for screenshots
  353. CL_GetScreenshotManager()->SetScreenshotReplay( m_pPendingReplay->GetHandle() );
  354. return m_pPendingReplay;
  355. }
  356. void CReplayManager::AttemptToSetupNewReplay()
  357. {
  358. DBG( "AttemptToSetupNewReplay()\n" );
  359. if ( !g_pReplay->IsRecording() || g_pEngineClient->IsPlayingReplayDemo() )
  360. {
  361. DBG( " Aborting...not recording, or playing back replay.\n" );
  362. m_flPlayerSpawnCreateReplayFailTime = g_pEngine->GetHostTime();
  363. return;
  364. }
  365. // Create the replay if necessary - we only do setup if we're creating
  366. // a new replay, because on a full update this function may be called
  367. // even though we're not actually spawning.
  368. if ( !m_pPendingReplay )
  369. {
  370. DBG( " Creating new replay...\n" );
  371. // If there is a "last life" replay that was not saved already, delete it
  372. FreeLifeIfNotSaved( m_pReplayLastLife );
  373. // Cache last life
  374. m_pReplayLastLife = m_pReplayThisLife;
  375. // Create the scratch replay (sets m_pPendingReplay and returns it)
  376. CReplay *pPendingReplay = CreatePendingReplay();
  377. SanityCheckReplay( pPendingReplay );
  378. // "This life" is the scratch replay
  379. m_pReplayThisLife = pPendingReplay;
  380. // Setup spawn tick
  381. const int nServerStartTick = CL_GetRecordingSessionManager()->m_ServerRecordingState.m_nStartTick;
  382. pPendingReplay->m_nSpawnTick = g_pEngineClient->GetLastServerTickTime() - nServerStartTick;
  383. if ( nServerStartTick == 0 )
  384. {
  385. // Didn't receive the replay_sessioninfo event yet - make spawn tick negative so when the
  386. // event IS received, we can detect that the server start tick still needs to be subtracted.
  387. pPendingReplay->m_nSpawnTick *= -1;
  388. }
  389. // Setup post-death record time
  390. extern ConVar replay_postdeathrecordtime;
  391. pPendingReplay->m_nPostDeathRecordTime = replay_postdeathrecordtime.GetFloat();
  392. // Let the replay know we're recording
  393. pPendingReplay->OnBeginRecording();
  394. }
  395. else
  396. {
  397. DBG( " NOT creating new replay.\n" );
  398. }
  399. // Served its purpose
  400. m_flPlayerSpawnCreateReplayFailTime = 0.0f;
  401. }
  402. void CReplayManager::Think()
  403. {
  404. VPROF_BUDGET( "CReplayManager::Think", VPROF_BUDGETGROUP_REPLAY );
  405. BaseClass::Think();
  406. DebugThink();
  407. // Only update pending replay, since it's recording
  408. // NOTE: we use Sys_FloatTime() here, since the client sets the next update time with engine->Time(),
  409. // which also uses Sys_FloatTime().
  410. if ( m_pPendingReplay && m_pPendingReplay->m_flNextUpdateTime <= Sys_FloatTime() )
  411. {
  412. m_pPendingReplay->Update(); // Allow the replay's Update() function to set the next update time
  413. }
  414. }
  415. void CReplayManager::DebugThink()
  416. {
  417. // Debugging
  418. if ( replay_debug.GetBool() )
  419. {
  420. const char *pReplayNames[] = { "Scratch", "This life", "Last life" };
  421. CReplay *pReplays[] = { m_pPendingReplay, m_pReplayThisLife, m_pReplayLastLife };
  422. for ( int i = 0; i < 3; ++i )
  423. {
  424. CReplay *pCurReplay = pReplays[ i ];
  425. if ( !pCurReplay )
  426. {
  427. g_pEngineClient->Con_NPrintf( i, "%s: NULL", pReplayNames[ i ] );
  428. continue;
  429. }
  430. g_pEngineClient->Con_NPrintf( i, "%s: handle=%i [%i, %i] C? %s R? %s MaxBlock: %i", pReplayNames[ i ],
  431. pCurReplay->GetHandle(), pCurReplay->m_nSpawnTick,
  432. pCurReplay->m_nDeathTick, pCurReplay->m_bComplete ? "YES" : "NO",
  433. pCurReplay->m_bRequestedByUser ? "YES" : "NO",
  434. pCurReplay->m_iMaxSessionBlockRequired
  435. );
  436. // Screenshot handle
  437. int nCurLine = 5;
  438. g_pEngineClient->Con_NPrintf( nCurLine, "Screenshot replay: handle=%i", CL_GetScreenshotManager()->GetScreenshotReplay() );
  439. nCurLine += 2;
  440. // Saved replay handles
  441. g_pEngineClient->Con_NPrintf( nCurLine++, "REPLAYS:" );
  442. FOR_EACH_REPLAY( j )
  443. {
  444. CReplay *pReplay = GET_REPLAY_AT( j );
  445. g_pEngineClient->Con_NPrintf( nCurLine++, "%i: handle=%i ticks=[%i %i]", i, pReplay->GetHandle(),
  446. pReplay->m_nSpawnTick, pReplay->m_nDeathTick );
  447. }
  448. // Current tick:
  449. g_pEngineClient->Con_NPrintf( ++nCurLine, "MAIN tick: %f", g_pEngineClient->GetLastServerTickTime() );
  450. g_pEngineClient->Con_NPrintf( ++nCurLine, " server tick: %f", g_pEngineClient->GetLastServerTickTime() );
  451. nCurLine += 2;
  452. }
  453. }
  454. }
  455. float CReplayManager::GetNextThinkTime() const
  456. {
  457. return g_pEngine->GetHostTime() + 0.1f;
  458. }
  459. CReplay *CReplayManager::GetPlayingReplay()
  460. {
  461. return g_pReplayDemoPlayer->GetCurrentReplay();
  462. }
  463. CReplay *CReplayManager::GetReplayForCurrentLife()
  464. {
  465. return m_pReplayThisLife;
  466. }
  467. void CReplayManager::GetReplays( CUtlLinkedList< CReplay *, int > &lstReplays )
  468. {
  469. lstReplays.RemoveAll();
  470. FOR_EACH_REPLAY( i )
  471. {
  472. lstReplays.AddToTail( GET_REPLAY_AT( i ) );
  473. }
  474. }
  475. void CReplayManager::GetReplaysAsQueryableItems( CUtlLinkedList< IQueryableReplayItem *, int > &lstReplays )
  476. {
  477. lstReplays.RemoveAll();
  478. FOR_EACH_REPLAY( i )
  479. {
  480. lstReplays.AddToHead( dynamic_cast< IQueryableReplayItem * >( GET_REPLAY_AT( i ) ) );
  481. }
  482. if ( m_pPendingReplay &&
  483. !m_pPendingReplay->m_bComplete &&
  484. m_pPendingReplay->m_bRequestedByUser )
  485. {
  486. Assert( lstReplays.Find( m_pPendingReplay ) == lstReplays.InvalidIndex() );
  487. lstReplays.AddToHead( m_pPendingReplay );
  488. }
  489. }
  490. int CReplayManager::GetNumReplaysDependentOnSession( ReplayHandle_t hSession )
  491. {
  492. int nResult = 0;
  493. FOR_EACH_REPLAY( i )
  494. {
  495. CReplay *pCurReplay = GET_REPLAY_AT( i );
  496. if ( pCurReplay->m_hSession == hSession )
  497. {
  498. ++nResult;
  499. }
  500. }
  501. return nResult;
  502. }
  503. const char *CReplayManager::GetReplaysDir() const
  504. {
  505. return GetIndexPath();
  506. }
  507. float CReplayManager::GetDownloadProgress( const CReplay *pReplay )
  508. {
  509. // Give each downloadable session block equal weight since we won't know the size of blocks that
  510. // have not been created/written yet on the server.
  511. // Go through all blocks in the replay and figure out how many bytes have been downloaded
  512. float flSum = 0.0f;
  513. CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pReplay->m_hSession ) ); Assert( pSession );
  514. if ( !pSession )
  515. return 0.0f;
  516. const CBaseRecordingSession::BlockContainer_t &vecBlocks = pSession->GetBlocks();
  517. FOR_EACH_VEC( vecBlocks, i )
  518. {
  519. CClientRecordingSessionBlock *pCurBlock = CL_CastBlock( vecBlocks[ i ] );
  520. if ( !pReplay->IsSignificantBlock( pCurBlock->m_iReconstruction ) )
  521. continue;
  522. // Calculate progress for this block
  523. Assert( pCurBlock->m_uFileSize > 0 );
  524. const float flSubProgress = pCurBlock->m_uFileSize == 0 ? 0.0f : clamp( (float)pCurBlock->m_uBytesDownloaded / pCurBlock->m_uFileSize, 0.0f, 1.0f );
  525. flSum += flSubProgress;
  526. }
  527. // Account for blocks that haven't been created yet
  528. // NOTE: This will cause a bug in download progress if the round ends and cuts the number of
  529. // expected blocks down - but that situation is probably less likely to occur than the situation
  530. // where a client is expecting more blocks that *will* be created. To avoid pops in the latter
  531. // situation, we account for those blocks here.
  532. const int nTotalSubBlocks = pReplay->m_iMaxSessionBlockRequired + 1;
  533. // Calculate mean
  534. Assert( nTotalSubBlocks > 0 );
  535. return nTotalSubBlocks == 0 ? 0.0f : ( flSum / (float)nTotalSubBlocks );
  536. }
  537. //----------------------------------------------------------------------------------------