summaryrefslogtreecommitdiff
path: root/replay/cl_replaymanager.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /replay/cl_replaymanager.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'replay/cl_replaymanager.cpp')
-rw-r--r--replay/cl_replaymanager.cpp665
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 );
+}
+
+//----------------------------------------------------------------------------------------