summaryrefslogtreecommitdiff
path: root/replay/cl_performancecontroller.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_performancecontroller.cpp
downloadarchived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.tar.xz
archived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.zip
Diffstat (limited to 'replay/cl_performancecontroller.cpp')
-rw-r--r--replay/cl_performancecontroller.cpp1026
1 files changed, 1026 insertions, 0 deletions
diff --git a/replay/cl_performancecontroller.cpp b/replay/cl_performancecontroller.cpp
new file mode 100644
index 0000000..f662585
--- /dev/null
+++ b/replay/cl_performancecontroller.cpp
@@ -0,0 +1,1026 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+//=======================================================================================//
+
+#include "cl_performancecontroller.h"
+#include "cl_replaycontext.h"
+#include "globalvars_base.h"
+#include "cl_replaycontext.h"
+#include "replay/replay.h"
+#include "replay/ireplaycamera.h"
+#include "replay/replayutils.h"
+#include "replay/ireplayperformanceplaybackhandler.h"
+#include "replay/ireplayperformanceeditor.h"
+#include "filesystem.h"
+#include "KeyValues.h"
+#include "replaysystem.h"
+#include "cl_replaymanager.h"
+#include "vprof.h"
+#include "cl_performance_common.h"
+#include "engine/ivdebugoverlay.h"
+#include "utlbuffer.h"
+
+#undef Yield
+#include "vstdlib/jobthread.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+//----------------------------------------------------------------------------------------
+
+#ifdef _DEBUG
+ConVar replay_simulate_long_save( "replay_simulate_long_save", "0", FCVAR_DONTRECORD, "Simulate a long save. Seconds." );
+#endif
+
+//----------------------------------------------------------------------------------------
+
+class CSaveJob : public CJob
+{
+public:
+ CSaveJob( KeyValues *pInData, const char *pFullFilename )
+ : m_pInData( pInData )
+ {
+ SetFlags( GetFlags() | JF_IO );
+
+ V_strncpy( m_szFullFilename, pFullFilename, sizeof( m_szFullFilename ) );
+ }
+
+ virtual JobStatus_t DoExecute()
+ {
+ if ( !m_pInData )
+ return JOB_FAILED;
+
+ if ( !V_strlen( m_szFullFilename ) )
+ return JOB_FAILED;
+
+#ifdef _DEBUG
+ const int nDelay = replay_simulate_long_save.GetInt();
+ if ( nDelay )
+ {
+ ThreadSleep( nDelay * 1000 );
+ }
+#endif
+
+ return m_pInData->SaveToFile( g_pFullFileSystem, m_szFullFilename, "MOD" ) ? JOB_OK : JOB_FAILED;
+ }
+
+private:
+ KeyValues *m_pInData;
+ char m_szFullFilename[MAX_OSPATH];
+};
+
+//----------------------------------------------------------------------------------------
+
+CPerformanceController::CPerformanceController()
+: m_pRoot( NULL ),
+ m_pCurEvent( NULL ),
+ m_pDbgRoot( NULL ),
+ m_pPlaybackHandler( NULL ),
+ m_pSetViewEvent( NULL ),
+ m_pSaveJob( NULL ),
+ m_bViewOverrideMode( false ),
+ m_bDirty( false ),
+ m_bLastSaveStatus( false ),
+ m_bRewinding( false ),
+ m_pSavedPerformance( NULL ),
+ m_pScratchPerformance( NULL ),
+ m_hReplay( REPLAY_HANDLE_INVALID ),
+ m_nState( STATE_DORMANT ),
+ m_pEditor( NULL ),
+ m_flLastCamSetViewTime( 0.0f ),
+ m_flTimeScale( 1.0f )
+{
+}
+
+CPerformanceController::~CPerformanceController()
+{
+ Cleanup();
+}
+
+void CPerformanceController::Cleanup()
+{
+ AssertMsg(
+ !m_pScratchPerformance || m_pScratchPerformance != m_pSavedPerformance,
+ "Sanity check failed. We should never be assigning saved to scratch or vice versa."
+ );
+
+ m_pSavedPerformance = NULL;
+
+ if ( m_pScratchPerformance )
+ {
+ delete m_pScratchPerformance;
+ m_pScratchPerformance = NULL;
+ }
+
+ CleanupStream();
+ CleanupDbgStream();
+
+ ClearDirtyFlag();
+
+ m_pCurEvent = NULL;
+ m_pEditor = NULL;
+ m_pPlaybackHandler = NULL;
+ m_hReplay = REPLAY_HANDLE_INVALID;
+ m_nState = STATE_DORMANT;
+ m_flLastCamSetViewTime = 0.0f;
+ m_flTimeScale = 1.0f;
+
+ // Remove all queued events
+ FOR_EACH_LL( m_EventQueue, i )
+ {
+ m_EventQueue[ i ]->deleteThis();
+ }
+ m_EventQueue.RemoveAll();
+}
+
+void CPerformanceController::CleanupStream()
+{
+ if ( m_pRoot )
+ {
+ m_pRoot->deleteThis();
+ m_pRoot = NULL;
+ }
+}
+
+void CPerformanceController::CleanupDbgStream()
+{
+ if ( m_pDbgRoot )
+ {
+ m_pDbgRoot->deleteThis();
+ m_pDbgRoot = NULL;
+ }
+}
+
+float CPerformanceController::GetTime() const
+{
+ Assert( m_pCurEvent );
+ return atof( m_pCurEvent->GetName() );
+}
+
+void CPerformanceController::SetEditor( IReplayPerformanceEditor *pEditor )
+{
+ AssertMsg( pEditor, "This is bad. You must supply a valid editor pointer." );
+
+ // Cache editor
+ m_pEditor = pEditor;
+}
+
+void CPerformanceController::StartRecording( CReplay *pReplay, bool bSnip )
+{
+ Assert( !IsRecording() );
+
+ AssertMsg(
+ m_nState == STATE_PLAYING || !m_pRoot,
+ "Unless we're playing, root should be NULL here"
+ );
+
+ if ( m_nState == STATE_DORMANT )
+ {
+ Assert( !m_pSavedPerformance );
+
+ // Create the performance KeyValues
+ m_pRoot = new KeyValues( "performance" );
+ }
+ else if ( m_nState == STATE_PLAYING )
+ {
+ // Nuke everything after the current event, or does nothing if we've past the end of the playback stream
+ if ( bSnip )
+ {
+ Snip();
+ }
+
+ // When we go from playback to recording, we need to reset override view
+ g_pClient->GetReplayCamera()->ClearOverrideView();
+ }
+
+ // Update the state
+ m_nState = STATE_RECORDING;
+
+ // Mark as dirty
+ m_bDirty = true;
+}
+
+void CPerformanceController::Stop()
+{
+ Assert( !m_bRewinding );
+
+ ClearRewinding();
+ Cleanup();
+}
+
+bool CPerformanceController::DumpStreamToFileAsync( const char *pFullFilename )
+{
+ // m_pRoot can be NULL if the user only set an in and/or out point, and wants to save.
+ if ( !m_pRoot )
+ return true;
+
+ // Save the file
+ m_pSaveJob = new CSaveJob( m_pRoot, pFullFilename );
+ if ( !m_pSaveJob )
+ return false;
+
+ IThreadPool *pThreadPool = CL_GetThreadPool();
+ if ( !pThreadPool )
+ return false;
+
+ pThreadPool->AddJob( m_pSaveJob );
+
+ return true;
+}
+
+bool CPerformanceController::FlushReplay()
+{
+ // Get the replay
+ CReplay *pReplay = GetReplay( m_hReplay );
+ if ( !pReplay )
+ return false;
+
+ // Add the performance to the replay and save
+ Assert( !m_pSavedPerformance || pReplay->HasPerformance( m_pSavedPerformance ) );
+ CL_GetReplayManager()->FlagReplayForFlush( pReplay, true );
+
+ return true;
+}
+
+bool CPerformanceController::SaveAsync()
+{
+ if ( !m_pRoot )
+ return false;
+
+ if ( !m_pScratchPerformance )
+ {
+ AssertMsg( 0, "Scratch performance should always be valid at this point." );
+ return false;
+ }
+
+ // NOTE: m_pSavedPerformance should always be valid here, as 'save' is disabled until it
+ // has an actual performance to save to.
+
+ // Copy the relevant data from scratch -> saved - we want to preserve the filename
+ // the saved performance, and have no reason to copy over duplicate data (eg the replay
+ // handle).
+ m_pSavedPerformance->CopyTicks( m_pScratchPerformance );
+
+ // Copy title
+ V_wcsncpy( m_pSavedPerformance->m_wszTitle, m_pScratchPerformance->m_wszTitle, sizeof( m_pSavedPerformance->m_wszTitle ) );
+
+ // Use the saved performance's filename
+ DumpStreamToFileAsync( m_pSavedPerformance->GetFullPerformanceFilename() );
+
+ // Save the replay file
+ FlushReplay();
+
+ // Clear dirty flag
+ ClearDirtyFlag();
+
+ return true;
+}
+
+bool CPerformanceController::SaveAsAsync( const wchar_t *pTitle )
+{
+ //
+ // NOTE: This function assumes the following:
+ //
+ // * We've already dealt with checking the given title versus existing performances
+ // in the replay and that the user has selected to overwrite.
+ //
+
+ CReplay *pReplay = m_pEditor->GetReplay();
+ if ( !pReplay )
+ {
+ AssertMsg( 0, "Replay must exist!" );
+ return false;
+ }
+
+ // Find existing performance in replay, if it exists.
+ CReplayPerformance *pExistingPerformance = pReplay->GetPerformanceWithTitle( pTitle );
+ if ( !pExistingPerformance )
+ {
+ // Create and add a new performance to the replay with a unique filename - do not generate a title since we will
+ // use the incoming title.
+ CReplayPerformance *pCopy = pReplay->AddNewPerformance( false, true );
+
+ // Copy the ticks, which is all we care about
+ pCopy->CopyTicks( m_pScratchPerformance );
+
+ // Set the title
+ pCopy->SetTitle( pTitle );
+
+ // Dump to the new file and save the replay
+ if ( !DumpStreamToFileAsync( pCopy->GetFullPerformanceFilename() ) ||
+ !FlushReplay() )
+ {
+ return false;
+ }
+
+ // If we didn't spawn a thread, we want this to be true here, since the replay flushed
+ // and DumpStreamToFileAsync() succeeded.
+ m_bLastSaveStatus = true;
+
+ // Saved performance is now replaced with the newly created performance
+ m_pSavedPerformance = pCopy;
+
+ // Clear dirty flag
+ ClearDirtyFlag();
+
+ return true;
+ }
+
+ // Overwriting an existing performance?
+ else
+ {
+ // Performance with the given name already exists - overwrite it (again, this function
+ // assumes that any UI around asking the user if they're sure they want to replace has
+ // already been navigated, and the user has selected to overwrite).
+ m_pSavedPerformance = pExistingPerformance;
+ }
+
+ // Copy the title to the scratch
+ V_wcsncpy( m_pScratchPerformance->m_wszTitle, pTitle, MAX_TAKE_TITLE_LENGTH * sizeof( wchar_t ) );
+
+ // Attempt to save
+ if ( !SaveAsync() )
+ return false;
+
+ // Clear dirty flag
+ ClearDirtyFlag();
+
+ return true;
+}
+
+bool CPerformanceController::IsSaving() const
+{
+ return m_pSaveJob != NULL;
+}
+
+void CPerformanceController::SaveThink()
+{
+ if ( !m_pSaveJob )
+ return;
+
+ if ( m_pSaveJob->IsFinished() )
+ {
+ // Cache save status
+ m_bLastSaveStatus = m_pSaveJob->GetStatus() == JOB_OK;
+
+ m_pSaveJob->Release();
+ m_pSaveJob = NULL;
+ }
+}
+
+bool CPerformanceController::GetLastSaveStatus() const
+{
+ return m_bLastSaveStatus;
+}
+
+void CPerformanceController::ClearDirtyFlag()
+{
+ m_bDirty = false;
+}
+
+bool CPerformanceController::IsRecording() const
+{
+ return m_nState == STATE_RECORDING;
+}
+
+bool CPerformanceController::IsPlaying() const
+{
+ return m_nState == STATE_PLAYING;
+}
+
+bool CPerformanceController::IsPlaybackDataLeft()
+{
+ return m_pCurEvent && m_pCurEvent->GetNextTrueSubKey();
+}
+
+bool CPerformanceController::IsDirty() const
+{
+ return m_bDirty;
+}
+
+void CPerformanceController::NotifyDirty()
+{
+ AssertMsg( GetPerformance() != NULL, "Can't mark empty performance as dirty." );
+ m_bDirty = true;
+}
+
+void CPerformanceController::OnSignonStateFull()
+{
+ if ( !g_pEngineClient->IsDemoPlayingBack() )
+ return;
+
+ // User hit rewind button (which reloads the map)?
+ if ( m_bRewinding )
+ {
+ // Setup controller for playback from existing data.
+ SetupPlaybackExistingStream();
+
+ // Clear rewinding
+ ClearRewinding();
+
+ // Let the editor know the rewind has completed.
+ m_pEditor->OnRewindComplete();
+ }
+ else
+ {
+ AssertMsg( !m_pScratchPerformance, "Scratch replay should not be valid yet." );
+
+ // If we've gotten this far and the replay is invalid, we're likely playing back a
+ // regular demo and didn't early out somewhere up the chain.
+ CReplay *pReplay = g_pReplayDemoPlayer->GetCurrentReplay();
+ if ( !pReplay )
+ return;
+
+ // Cache replay
+ m_hReplay = pReplay->GetHandle();
+
+ // Play a performance from the beginning.
+ CReplayPerformance *pPerformance = g_pReplayDemoPlayer->GetCurrentPerformance();
+ if ( pPerformance )
+ {
+ SetupPlaybackFromPerformance( pPerformance );
+
+ // Make a copy of the performance we're playing back so the user can make changes
+ // w/o fucking up the original.
+ m_pScratchPerformance = pPerformance->MakeCopy();
+ }
+ else
+ {
+ CreateNewScratchPerformance( pReplay );
+ }
+ }
+}
+
+float CPerformanceController::GetPlaybackTimeScale() const
+{
+ return m_flTimeScale;
+}
+
+void CPerformanceController::CreateNewScratchPerformance( CReplay *pReplay )
+{
+ // Create a new performance, but don't add it to the replay yet
+ m_pScratchPerformance = CL_GetPerformanceManager()->CreatePerformance( pReplay );
+
+ // Give it a default name
+ m_pScratchPerformance->AutoNameIfHasNoTitle( pReplay->m_szMapName );
+
+ // Generate a filename for the new performance
+ m_pScratchPerformance->SetFilename( CL_GetPerformanceManager()->GeneratePerformanceFilename( pReplay ) );
+}
+
+//----------------------------------------------------------------------------------------
+
+void CPerformanceController::NotifyPauseState( bool bPaused )
+{
+ if ( m_bPaused == bPaused )
+ return;
+
+ m_bPaused = bPaused;
+
+ // Unpause?
+ if ( !bPaused )
+ {
+ // Add queued events
+ for( int i = m_EventQueue.Tail(); i != m_EventQueue.InvalidIndex(); i = m_EventQueue.Previous( i ) )
+ {
+ KeyValues *pCurEvent = m_EventQueue[ i ];
+ AddEvent( pCurEvent );
+ }
+
+ m_EventQueue.RemoveAll();
+ }
+}
+
+CReplayPerformance *CPerformanceController::GetPerformance()
+{
+ return m_pScratchPerformance;
+}
+
+CReplayPerformance *CPerformanceController::GetSavedPerformance()
+{
+ return m_pSavedPerformance;
+}
+
+bool CPerformanceController::HasSavedPerformance()
+{
+ return m_pSavedPerformance != NULL;
+}
+
+void CPerformanceController::Snip()
+{
+ if ( !m_pCurEvent )
+ return;
+
+ const float flTime = GetTime();
+
+ // Go through all events and delete anything on or after flSnipTime
+ for ( KeyValues *pCurEvent = m_pRoot->GetFirstTrueSubKey(); pCurEvent != NULL; )
+ {
+ // Get next first, in case we delete
+ KeyValues *pNext = pCurEvent->GetNextTrueSubKey();
+
+ const float flCurEventTime = atof( pCurEvent->GetName() );
+ if ( flCurEventTime >= flTime )
+ {
+ // Delete the key
+ m_pRoot->RemoveSubKey( pCurEvent );
+ pCurEvent->deleteThis();
+ }
+
+ pCurEvent = pNext;
+ }
+}
+
+bool CPerformanceController::IsCameraChangeEvent( int nType ) const
+{
+ return nType >= EVENTTYPE_CAMERA_CHANGE_BEGIN && nType <= EVENTTYPE_CAMERA_CHANGE_END;
+}
+
+void CPerformanceController::NotifyRewinding()
+{
+ m_bRewinding = true;
+ m_flLastCamSetViewTime = 0.0f;
+}
+
+void CPerformanceController::ClearRewinding()
+{
+ m_bRewinding = false;
+}
+
+//----------------------------------------------------------------------------------------
+
+#define CREATE_EVENT( time_, type_ ) \
+ new KeyValues( Replay_va( "%f", time_ ), "type", type_ )
+
+#define RECORD_EVENT_( event_, time_, type_ ) \
+ event_ = CREATE_EVENT( time_, type_ ); \
+ AddEvent( event_ )
+
+#define RECORD_EVENT( event_, time_, type_ ) \
+ KeyValues *event_ = RECORD_EVENT_( event_, time_, type_ )
+
+#define QUEUE_OR_RECORD_EVENT( event_, time_, type_ ) \
+ if ( !m_pRoot ) \
+ return; \
+ \
+ KeyValues *event_; \
+ if ( m_bPaused ) \
+ { \
+ KeyValues *pQueuedEvent = CREATE_EVENT( time_, type_ ); \
+ event_ = pQueuedEvent; \
+ m_EventQueue.AddToHead( pQueuedEvent ); \
+ RemoveDuplicateEventsFromQueue(); \
+ } \
+ else \
+ { \
+ RECORD_EVENT_( event_, time_, type_ ); \
+ }
+
+void CPerformanceController::RemoveDuplicateEventsFromQueue()
+{
+ // Add queued events - only add the most recent camera change event, and the most recent
+ // player change event.
+ bool bFoundCameraChange = false;
+ bool bFoundPlayerChange = false;
+ bool bFoundSetView = false;
+ bool bFoundTimeScale = false;
+
+ for( int i = m_EventQueue.Head(); i != m_EventQueue.InvalidIndex(); )
+ {
+ KeyValues *pCurEvent = m_EventQueue[ i ];
+ const int nType = pCurEvent->GetInt( "type" );
+
+ bool bDitchEvent = false;
+ bool bSetupCut = false;
+
+ // Determine whether we should record the event or not
+ if ( nType == EVENTTYPE_CHANGEPLAYER )
+ {
+ bDitchEvent = bFoundPlayerChange;
+ bFoundPlayerChange = true;
+ }
+ else if ( IsCameraChangeEvent( nType ) )
+ {
+ bDitchEvent = bFoundCameraChange;
+ bFoundCameraChange = true;
+ }
+ else if ( nType == EVENTTYPE_CAMERA_SETVIEW )
+ {
+ bDitchEvent = bFoundSetView;
+ bFoundSetView = true;
+ bSetupCut = true; // If we end up keeping this event, it should be a cut.
+ }
+ else if ( nType == EVENTTYPE_TIMESCALE )
+ {
+ bDitchEvent = bFoundTimeScale;
+ bFoundTimeScale = true;
+ }
+
+ // Setup as cut
+ if ( bSetupCut )
+ {
+ pCurEvent->SetInt( "cut", 1 );
+ }
+
+ int itNext = m_EventQueue.Next( i );
+
+ if ( bDitchEvent )
+ {
+#if _DEBUG
+ CUtlBuffer buf;
+ pCurEvent->RecursiveSaveToFile( buf, 1 );
+ IF_REPLAY_DBG( Warning( "Ditching event of type %s\n...", ( const char * )buf.Base() ) );
+#endif
+
+ // Free the event
+ pCurEvent->deleteThis();
+ m_EventQueue.Remove( i );
+ }
+
+ i = itNext;
+ }
+}
+
+void CPerformanceController::AddEvent( KeyValues *pEvent )
+{
+ IF_REPLAY_DBG2(
+ CUtlBuffer buf;
+ pEvent->RecursiveSaveToFile( buf, 1 );
+ Warning( "Recording event:\n%s\n", ( const char * )buf.Base() );
+ );
+ m_pRoot->AddSubKey( pEvent );
+}
+
+void CPerformanceController::AddEvent_Camera_Change_FirstPerson( float flTime, int nEntityIndex )
+{
+ QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CAMERA_CHANGE_FIRSTPERSON );
+ pEvent->SetInt( "ent", nEntityIndex );
+}
+
+void CPerformanceController::AddEvent_Camera_Change_ThirdPerson( float flTime, int nEntityIndex )
+{
+ QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CAMERA_CHANGE_THIRDPERSON );
+ pEvent->SetInt( "ent", nEntityIndex );
+}
+
+void CPerformanceController::AddEvent_Camera_Change_Free( float flTime )
+{
+ QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CAMERA_CHANGE_FREE );
+}
+
+void CPerformanceController::AddEvent_Camera_ChangePlayer( float flTime, int nEntIndex )
+{
+ QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_CHANGEPLAYER );
+ pEvent->SetInt( "ent", nEntIndex );
+}
+
+void CPerformanceController::AddEvent_Camera_SetView( const SetViewParams_t &params )
+{
+ QUEUE_OR_RECORD_EVENT( pEvent, params.m_flTime, EVENTTYPE_CAMERA_SETVIEW );
+ pEvent->SetString( "pos", Replay_va( "%f %f %f", params.m_pOrigin->x, params.m_pOrigin->y, params.m_pOrigin->z ) );
+ pEvent->SetString( "ang", Replay_va( "%f %f %f", params.m_pAngles->x, params.m_pAngles->y, params.m_pAngles->z ) );
+ pEvent->SetFloat( "fov", params.m_flFov );
+ pEvent->SetFloat( "a", params.m_flAccel );
+ pEvent->SetFloat( "s", params.m_flSpeed );
+ pEvent->SetFloat( "rf", params.m_flRotationFilter );
+}
+
+void CPerformanceController::AddEvent_TimeScale( float flTime, float flScale )
+{
+ QUEUE_OR_RECORD_EVENT( pEvent, flTime, EVENTTYPE_TIMESCALE );
+ pEvent->SetFloat( "scale", flScale );
+
+ m_flTimeScale = flScale;
+}
+
+//----------------------------------------------------------------------------------------
+
+bool CPerformanceController::SetupPlaybackHandler()
+{
+ IReplayPerformancePlaybackHandler *pHandler = g_pClient->GetPerformancePlaybackHandler();
+ if ( !pHandler )
+ return false;
+
+ // Cache
+ m_pPlaybackHandler = pHandler;
+
+ return true;
+}
+
+void CPerformanceController::FinishBeginPerformancePlayback()
+{
+ // Root should be setup by now
+ Assert( m_pRoot );
+
+ // Make sure the camera isn't setup for camera override
+ // TODO: Definitely need this here?
+ g_pClient->GetReplayCamera()->ClearOverrideView();
+
+ // Set to initial event
+ m_pCurEvent = m_pRoot->GetFirstTrueSubKey();
+
+ IF_REPLAY_DBG(
+ m_pDbgRoot = m_pRoot->MakeCopy();
+ );
+
+ m_nState = STATE_PLAYING;
+ m_bViewOverrideMode = false;
+}
+
+void CPerformanceController::SetupPlaybackExistingStream()
+{
+ // m_pRoot can be NULL here if the user is watching the original replay and has rewound
+ // without changing anything.
+ if ( !m_pRoot )
+ return;
+
+ if ( !SetupPlaybackHandler() )
+ return;
+
+ FinishBeginPerformancePlayback();
+}
+
+void CPerformanceController::SetupPlaybackFromPerformance( CReplayPerformance *pPerformance )
+{
+ AssertMsg( !m_pSavedPerformance, "This probably hit because either SaveNow() or Discard() were not called. One of those should always be called on disconnect after watching or editing a replay." );
+ AssertMsg( !m_pScratchPerformance, "Scratch performance should be NULL here." );
+
+ if ( !pPerformance )
+ return;
+
+ if ( !pPerformance->m_pReplay )
+ {
+ AssertMsg( 0, "Performance passed in with an invalid replay pointer! This bad!" );
+ return;
+ }
+
+ if ( !SetupPlaybackHandler() )
+ return;
+
+ // Cache off the performance and replay for playback
+ m_pSavedPerformance = pPerformance;
+ m_pScratchPerformance = NULL;
+ m_hReplay = pPerformance->m_pReplay->GetHandle();
+
+ // Read the file
+ Assert( !m_pRoot );
+ const char *pFilename = pPerformance->GetFullPerformanceFilename();
+ m_pRoot = new KeyValues( pFilename );
+ if ( !m_pRoot->LoadFromFile( g_pFullFileSystem, pFilename ) )
+ {
+ Warning( "Failed to load replay file, \"%s\"!\n", pFilename );
+ return;
+ }
+
+ FinishBeginPerformancePlayback();
+}
+
+void CPerformanceController::ReadSetViewEvent( KeyValues *pEventSubKey, Vector &origin, QAngle &angles, float &fov,
+ float *pAccel, float *pSpeed, float *pRotFilter )
+{
+ const char *pViewStr[2];
+
+ pViewStr[0] = pEventSubKey->GetString( "pos" );
+ pViewStr[1] = pEventSubKey->GetString( "ang" );
+
+ sscanf( pViewStr[0], "%f %f %f", &origin.x, &origin.y, &origin.z );
+ sscanf( pViewStr[1], "%f %f %f", &angles.x, &angles.y, &angles.z );
+ fov = pEventSubKey->GetFloat( "fov", 90 );
+
+ if ( pAccel && pSpeed && pRotFilter )
+ {
+ *pAccel = pEventSubKey->GetFloat( "a" );
+ *pSpeed = pEventSubKey->GetFloat( "s" );
+ *pRotFilter = pEventSubKey->GetFloat( "rf" );
+ }
+}
+
+void CPerformanceController::PlaybackThink()
+{
+ static Vector aOrigin[3];
+ static QAngle aAngles[3];
+ static float aFov[3];
+ float flAccel = 0.0f, flSpeed = 0.0f, flRotFilter = 0.0f;
+
+ KeyValues *pSearch = NULL;
+ float t;
+
+ if ( !IsPlaying() )
+ return;
+
+ if ( !m_pCurEvent )
+ return;
+
+ if ( !m_pPlaybackHandler )
+ return;
+
+ CReplay *pReplay = GetReplay( m_hReplay );
+ if ( !pReplay )
+ return;
+
+ const CGlobalVarsBase *g_pClientGlobalVariables = g_pEngineClient->GetClientGlobalVars();
+
+ const int nReplaySpawnTick = pReplay->m_nSpawnTick;
+ Assert( nReplaySpawnTick >= 0 );
+ const float flCurTime = g_pClientGlobalVariables->curtime - g_pEngine->TicksToTime( nReplaySpawnTick );
+
+ float flEventTime = 0;
+ bool bShouldCut = false;
+
+ while ( 1 )
+ {
+ // Get event time
+ flEventTime = GetTime();
+
+ // Get out if this event shouldn't fire yet
+ if ( flEventTime > flCurTime )
+ break;
+
+ IF_REPLAY_DBG2(
+ CUtlBuffer buf;
+ m_pCurEvent->RecursiveSaveToFile( buf, 1 );
+ Warning( "%s\n", ( const char * )buf.Base() );
+ );
+
+ switch ( m_pCurEvent->GetInt( "type", EVENTTYPE_INVALID ) )
+ {
+ case EVENTTYPE_CAMERA_CHANGE_FIRSTPERSON:
+ m_bViewOverrideMode = false;
+ m_pPlaybackHandler->OnEvent_Camera_Change_FirstPerson( flEventTime, m_pCurEvent->GetInt( "ent" ) );
+ break;
+
+ case EVENTTYPE_CAMERA_CHANGE_THIRDPERSON:
+ m_bViewOverrideMode = true;
+ m_pPlaybackHandler->OnEvent_Camera_Change_ThirdPerson( flEventTime, m_pCurEvent->GetInt( "ent" ) );
+ break;
+
+ case EVENTTYPE_CAMERA_CHANGE_FREE:
+ m_bViewOverrideMode = true;
+ m_pPlaybackHandler->OnEvent_Camera_Change_Free( flEventTime );
+ break;
+
+ case EVENTTYPE_CHANGEPLAYER:
+ m_pPlaybackHandler->OnEvent_Camera_ChangePlayer( flEventTime, m_pCurEvent->GetInt( "ent" ) );
+ break;
+
+ case EVENTTYPE_CAMERA_SETVIEW:
+ AssertMsg( m_bViewOverrideMode, "Camera mode needs to be set before a setview can take effect." );
+
+ if ( m_bViewOverrideMode )
+ {
+ // Get sample for current time
+ ReadSetViewEvent( m_pCurEvent, aOrigin[0], aAngles[0], aFov[0], &flAccel, &flSpeed, &flRotFilter );
+// g_pEngineClient->Con_NPrintf( 0, "sample 0 time: %f", flEventTime );
+ m_flLastCamSetViewTime = flEventTime;
+ m_pSetViewEvent = m_pCurEvent; // Stomp any previous set view - we want the last one
+ bShouldCut = bShouldCut || m_pCurEvent->GetBool( "cut" ); // We cut if any set-view event cuts, otherwise we will interpolate
+ }
+ break;
+
+ case EVENTTYPE_TIMESCALE:
+ m_flTimeScale = m_pCurEvent->GetFloat( "scale" );
+ m_pPlaybackHandler->OnEvent_TimeScale( flEventTime, m_flTimeScale );
+ break;
+
+ default:
+ AssertMsg( 0, "Unknown event in performance playback!\n" );
+ Warning( "Unknown event in performance playback!\n" );
+ }
+
+ // Get next event (or NULL if there isn't one)
+ m_pCurEvent = m_pCurEvent->GetNextTrueSubKey();
+
+ // Get out if no more events
+ if ( !m_pCurEvent )
+ break;
+ }
+
+ // If in override mode, interpolate and setup camera
+ if ( m_bViewOverrideMode && m_pSetViewEvent )
+ {
+ if ( bShouldCut )
+ {
+ DBG2( "CUT\n" );
+ aOrigin[2] = aOrigin[0];
+ aAngles[2] = aAngles[0];
+ aFov[2] = aFov[0];
+ }
+ else
+ {
+ // Default second sample to first, in case we don't find a sample to interpolate with
+ aOrigin[1] = aOrigin[0];
+ aAngles[1] = aAngles[0];
+ aFov[1] = aFov[0];
+
+ // Parameter for interpolation
+ t = 0.0f;
+
+ // Seek forward to half a second from current event time and see if there
+ // are any other set view events.
+ pSearch = m_pSetViewEvent->GetNextTrueSubKey();
+ while ( pSearch )
+ {
+ // Another sample not available
+ float flSearchTime = atof( pSearch->GetName() );
+ if ( flSearchTime > m_flLastCamSetViewTime + 0.5f )
+ break;
+
+ if ( pSearch->GetInt( "type", EVENTTYPE_INVALID ) == EVENTTYPE_CAMERA_SETVIEW )
+ {
+ // Found next sample within half a second - calc interpolation parameter & get data
+ float flDiff = flSearchTime - m_flLastCamSetViewTime;
+ Assert( flDiff > 0.0f );
+ if ( flDiff > 0.0f )
+ {
+ t = clamp ( ( flCurTime - m_flLastCamSetViewTime ) / flDiff, 0.0f, 1.0f );
+
+ // If the next set-view is a cut, we don't want to interpolate
+ if ( pSearch->GetBool( "cut" ) )
+ {
+ const int iSrc = clamp( (int)( .5f + t ), 0, 1 ); // Round t to 0 or 1, so we set the camera to the current frame if t < 0.5f, and we set the camera to the 'cut'/next frame if t >= 0.5.
+ aOrigin[2] = aOrigin[ iSrc ];
+ aAngles[2] = aAngles[ iSrc ];
+ aFov[2] = aFov[ iSrc ];
+ }
+ else
+ {
+ ReadSetViewEvent( pSearch, aOrigin[1], aAngles[1], aFov[1], &flAccel, &flSpeed, &flRotFilter );
+ }
+ }
+ break;
+ }
+
+ pSearch = pSearch->GetNextTrueSubKey();
+ }
+
+ // Interpolate
+ aOrigin[2] = Lerp( t, aOrigin[0], aOrigin[1] );
+ aAngles[2] = Lerp( t, aAngles[0], aAngles[1] ); // NOTE: Calls QuaternionSlerp() internally
+ aFov[2] = Lerp( t, aFov[0], aFov[1] );
+ }
+
+ // Setup current view
+ SetViewParams_t params( flEventTime, &aOrigin[2], &aAngles[2], aFov[2], flAccel, flSpeed, flRotFilter );
+ m_pPlaybackHandler->OnEvent_Camera_SetView( params );
+ }
+
+ IF_REPLAY_DBG( DebugRender() );
+}
+
+void CPerformanceController::DebugRender()
+{
+ KeyValues *pIt = m_pDbgRoot->GetFirstTrueSubKey();
+
+ Vector prevpos, pos;
+ QAngle angles;
+ float fov;
+ bool bPrev = false;
+
+ g_pDebugOverlay->ClearDeadOverlays();
+
+ while ( pIt )
+ {
+ if ( pIt->GetInt( "type", EVENTTYPE_INVALID ) == EVENTTYPE_CAMERA_SETVIEW )
+ {
+ ReadSetViewEvent( pIt, pos, angles, fov, NULL, NULL, NULL );
+
+ // Skip first view event since no previous
+ if ( !bPrev )
+ {
+ bPrev = true;
+ }
+ else
+ {
+ const bool bCut = pIt->GetBool( "cut" );
+ const int r = bCut ? 0 : 255;
+ const int g = bCut ? 255 : 0;
+ const int b = 0;
+ Vector tickpos = pos + Vector(10,0,0);
+ g_pDebugOverlay->AddLineOverlay( prevpos, pos, r, g, b, true, 0.0f );
+ g_pDebugOverlay->AddLineOverlay( pos, tickpos, 0, 255, 255, true, 0.0f );
+ }
+
+ prevpos = pos;
+ }
+
+ pIt = pIt->GetNextTrueSubKey();
+ }
+}
+
+//----------------------------------------------------------------------------------------
+
+void CPerformanceController::Think()
+{
+ VPROF_BUDGET( "CReplayPerformancePlayer::Think", VPROF_BUDGETGROUP_REPLAY );
+
+ CBaseThinker::Think();
+
+ PlaybackThink();
+}
+
+float CPerformanceController::GetNextThinkTime() const
+{
+ return 0.0f;
+}
+
+//---------------------------------------------------------------------------------------- \ No newline at end of file