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.
1026 lines
26 KiB
1026 lines
26 KiB
//========= 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; |
|
} |
|
|
|
//----------------------------------------------------------------------------------------
|