diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /replay/cl_performancecontroller.cpp | |
| download | archived-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.cpp | 1026 |
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 ¶ms ) +{ + 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 |