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.
522 lines
14 KiB
522 lines
14 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
//=======================================================================================// |
|
|
|
#include "replaysystem.h" |
|
#include "tier2/tier2.h" |
|
#include "iserver.h" |
|
#include "iclient.h" |
|
#include "icliententitylist.h" |
|
#include "igameevents.h" |
|
#include "replay/ireplaymovierenderer.h" |
|
#include "replay/ireplayscreenshotsystem.h" |
|
#include "replay/replayutils.h" |
|
#include "replay/replaylib.h" |
|
#include "sv_sessionrecorder.h" |
|
#include "sv_recordingsession.h" |
|
#include "cl_screenshotmanager.h" |
|
#include "netmessages.h" |
|
#include "thinkmanager.h" |
|
#include "managertest.h" |
|
#include "vprof.h" |
|
#include "sv_fileservercleanup.h" |
|
|
|
#if !defined( _X360 ) |
|
#include "winlite.h" |
|
#include "xbox/xboxstubs.h" |
|
#endif |
|
|
|
// TODO: Deal with linux build includes |
|
#ifdef IS_WINDOWS_PC |
|
#include <winsock.h> |
|
#endif |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
#undef CreateEvent // Can't call IGameEventManager2::CreateEvent() without this |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
#if !defined( DEDICATED ) |
|
IEngineClientReplay *g_pEngineClient = NULL; |
|
CClientReplayContext *g_pClientReplayContextInternal = NULL; |
|
IVDebugOverlay *g_pDebugOverlay = NULL; |
|
IDownloadSystem *g_pDownloadSystem = NULL; |
|
#endif |
|
|
|
vgui::ILocalize *g_pVGuiLocalize = NULL; |
|
CServerReplayContext *g_pServerReplayContext = NULL; |
|
IClientReplay *g_pClient = NULL; |
|
IServerReplay *g_pServer = NULL; |
|
IEngineReplay *g_pEngine = NULL; |
|
IGameEventManager2 *g_pGameEventManager = NULL; |
|
IEngineTrace *g_pEngineTraceClient = NULL; |
|
IReplayDemoPlayer *g_pReplayDemoPlayer = NULL; |
|
IClientEntityList *entitylist = NULL; // icliententitylist.h forces the use of this name by externing in the header |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
#define REPLAY_INIT( exp_ ) \ |
|
if ( !( exp_ ) ) \ |
|
{ \ |
|
Warning( "CReplaySystem::Connect() failed on: \"%s\"!\n", #exp_ ); \ |
|
return false; \ |
|
} |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
class CReplaySystem : public CTier2AppSystem< IReplaySystem > |
|
{ |
|
typedef CTier2AppSystem< IReplaySystem > BaseClass; |
|
|
|
public: |
|
virtual bool Connect( CreateInterfaceFn fnFactory ) |
|
{ |
|
REPLAY_INIT( fnFactory ); |
|
REPLAY_INIT( BaseClass::Connect( fnFactory ) ); |
|
|
|
ConVar_Register( FCVAR_CLIENTDLL ); |
|
|
|
REPLAY_INIT( g_pFullFileSystem ); |
|
|
|
g_pEngine = (IEngineReplay *)fnFactory( ENGINE_REPLAY_INTERFACE_VERSION, NULL ); |
|
REPLAY_INIT( g_pEngine ); |
|
|
|
REPLAY_INIT( g_pEngine->IsSupportedModAndPlatform() ); |
|
|
|
#if !defined( DEDICATED ) |
|
g_pEngineClient = (IEngineClientReplay *)fnFactory( ENGINE_REPLAY_CLIENT_INTERFACE_VERSION, NULL ); |
|
REPLAY_INIT( g_pEngineClient ); |
|
|
|
g_pEngineTraceClient = (IEngineTrace *)fnFactory( INTERFACEVERSION_ENGINETRACE_CLIENT, NULL ); |
|
REPLAY_INIT( g_pEngineTraceClient ); |
|
|
|
g_pReplayDemoPlayer = (IReplayDemoPlayer *)fnFactory( INTERFACEVERSION_REPLAYDEMOPLAYER, NULL ); |
|
REPLAY_INIT( g_pReplayDemoPlayer ); |
|
|
|
g_pVGuiLocalize = (vgui::ILocalize *)fnFactory( VGUI_LOCALIZE_INTERFACE_VERSION, NULL ); |
|
REPLAY_INIT( g_pVGuiLocalize ); |
|
|
|
g_pDebugOverlay = ( IVDebugOverlay * )fnFactory( VDEBUG_OVERLAY_INTERFACE_VERSION, NULL ); |
|
REPLAY_INIT( g_pDebugOverlay ); |
|
#endif |
|
|
|
g_pGameEventManager = (IGameEventManager2 *)fnFactory( INTERFACEVERSION_GAMEEVENTSMANAGER2, NULL ); |
|
REPLAY_INIT( g_pGameEventManager ); |
|
|
|
#if !defined( DEDICATED ) |
|
g_pDownloadSystem = (IDownloadSystem *)fnFactory( INTERFACEVERSION_DOWNLOADSYSTEM, NULL ); |
|
REPLAY_INIT( g_pDownloadSystem ); |
|
|
|
// Create client context now if not running a dedicated server |
|
if ( !g_pEngine->IsDedicated() ) |
|
{ |
|
g_pClientReplayContextInternal = new CClientReplayContext(); |
|
} |
|
|
|
// ...and create server replay context if we are |
|
else |
|
#endif |
|
{ |
|
g_pServerReplayContext = new CServerReplayContext(); |
|
} |
|
|
|
#if defined( DEDICATED ) |
|
REPLAY_INIT( ReplayLib_Init( g_pEngine->GetGameDir(), NULL ) ) // Init without the client replay context |
|
#else |
|
REPLAY_INIT( ReplayLib_Init( g_pEngine->GetGameDir(), g_pClientReplayContextInternal ) ); |
|
#endif |
|
|
|
Test(); |
|
|
|
return true; |
|
} |
|
|
|
virtual void Disconnect() |
|
{ |
|
BaseClass::Disconnect(); |
|
} |
|
|
|
virtual InitReturnVal_t Init() |
|
{ |
|
InitReturnVal_t nRetVal = BaseClass::Init(); |
|
if ( nRetVal != INIT_OK ) |
|
return nRetVal; |
|
|
|
return INIT_OK; |
|
} |
|
|
|
virtual void Shutdown() |
|
{ |
|
BaseClass::Shutdown(); |
|
|
|
#if !defined( DEDICATED ) |
|
delete g_pClientReplayContextInternal; |
|
g_pClientReplayContextInternal = NULL; |
|
#endif |
|
|
|
delete g_pServerReplayContext; |
|
g_pServerReplayContext = NULL; |
|
} |
|
|
|
virtual void Think() |
|
{ |
|
VPROF_BUDGET( "CReplaySystem::Think", VPROF_BUDGETGROUP_REPLAY ); |
|
|
|
g_pThinkManager->Think(); |
|
} |
|
|
|
virtual bool IsReplayEnabled() |
|
{ |
|
extern ConVar replay_enable; |
|
return replay_enable.GetInt() != 0; |
|
} |
|
|
|
virtual bool IsRecording() |
|
{ |
|
// NOTE: demoplayer->IsPlayingBack() needs to be checked here, as "replay_enable" and |
|
// "replay_recording" will inevitably get stored with signon data in any playing demo. |
|
// If the !demoplayer->IsPlayingBack() line is omitted below, Replay_IsRecording() |
|
// becomes useless during demo playback and will always return true. |
|
extern ConVar replay_recording; |
|
#if !defined( DEDICATED ) |
|
return IsReplayEnabled() && |
|
replay_recording.GetInt() && |
|
!g_pEngineClient->IsDemoPlayingBack(); |
|
#else |
|
return IsReplayEnabled() && |
|
replay_recording.GetInt(); |
|
#endif |
|
} |
|
|
|
//---------------------------------------------------------------------------------------- |
|
// Client-specific implementation: |
|
//---------------------------------------------------------------------------------------- |
|
|
|
virtual bool CL_Init( CreateInterfaceFn fnClientFactory ) |
|
{ |
|
#if !defined( DEDICATED ) |
|
g_pClient = (IClientReplay *)fnClientFactory( CLIENT_REPLAY_INTERFACE_VERSION, NULL ); |
|
if ( !g_pClient ) |
|
return false; |
|
|
|
entitylist = (IClientEntityList *)fnClientFactory( VCLIENTENTITYLIST_INTERFACE_VERSION, NULL ); |
|
if ( !entitylist ) |
|
return false; |
|
|
|
if ( !g_pClientReplayContextInternal->Init( fnClientFactory ) ) |
|
return false; |
|
|
|
return true; |
|
#else |
|
return false; |
|
#endif |
|
} |
|
|
|
virtual void CL_Shutdown() |
|
{ |
|
#if !defined( DEDICATED ) |
|
if ( g_pClientReplayContextInternal && g_pClientReplayContextInternal->IsInitialized() ) |
|
{ |
|
g_pClientReplayContextInternal->Shutdown(); |
|
} |
|
#endif |
|
} |
|
|
|
virtual void CL_Render() |
|
{ |
|
#if !defined( DEDICATED ) |
|
// If the replay system wants to take a screenshot, do it now |
|
if ( g_pClientReplayContextInternal->m_pScreenshotManager->ShouldCaptureScreenshot() ) |
|
{ |
|
g_pClientReplayContextInternal->m_pScreenshotManager->DoCaptureScreenshot(); |
|
return; |
|
} |
|
|
|
// Currently rendering? NOTE: GetMovieRenderer() only returns a valid ptr during rendering |
|
IReplayMovieRenderer *pReplayMovieRenderer = g_pClientReplayContextInternal->GetMovieRenderer(); |
|
if ( !pReplayMovieRenderer ) |
|
return; |
|
|
|
pReplayMovieRenderer->RenderVideo(); |
|
#endif |
|
} |
|
|
|
virtual IClientReplayContext *CL_GetContext() |
|
{ |
|
#if !defined( DEDICATED ) |
|
return g_pClientReplayContextInternal; |
|
#else |
|
return NULL; |
|
#endif |
|
} |
|
|
|
//---------------------------------------------------------------------------------------- |
|
// Server-specific implementation: |
|
//---------------------------------------------------------------------------------------- |
|
|
|
virtual bool SV_Init( CreateInterfaceFn fnServerFactory ) |
|
{ |
|
if ( !g_pEngine->IsDedicated() || !g_pServerReplayContext ) |
|
return false; |
|
|
|
g_pServer = (IServerReplay *)fnServerFactory( SERVER_REPLAY_INTERFACE_VERSION, NULL ); |
|
if ( !g_pServer ) |
|
return false; |
|
|
|
Assert( !ReplayServer() ); |
|
|
|
return g_pServerReplayContext->Init( fnServerFactory ); |
|
} |
|
|
|
virtual void SV_Shutdown() |
|
{ |
|
if ( g_pServerReplayContext && g_pServerReplayContext->IsInitialized() ) |
|
{ |
|
g_pServerReplayContext->Shutdown(); |
|
} |
|
} |
|
|
|
virtual IServerReplayContext *SV_GetContext() |
|
{ |
|
return g_pServerReplayContext; |
|
} |
|
|
|
virtual bool SV_ShouldBeginRecording( bool bIsInWaitingForPlayers ) |
|
{ |
|
extern ConVar replay_enable; |
|
|
|
return !bIsInWaitingForPlayers && |
|
#if !defined( DEDICATED ) |
|
!g_pEngineClient->IsPlayingReplayDemo() && |
|
#endif |
|
replay_enable.GetBool(); |
|
} |
|
|
|
virtual void SV_NotifyReplayRequested() |
|
{ |
|
if ( !g_pEngine->IsSupportedModAndPlatform() ) |
|
return; |
|
|
|
CServerRecordingSession *pSession = SV_GetRecordingSessionInProgress(); |
|
if ( !pSession ) |
|
return; |
|
|
|
// A replay was requested - notify the session so we don't throw it away at the end of the round |
|
pSession->NotifyReplayRequested(); |
|
} |
|
|
|
virtual void SV_SendReplayEvent( const char *pEventName, int nClientSlot ) |
|
{ |
|
// Attempt to create the event |
|
IGameEvent *pEvent = g_pGameEventManager->CreateEvent( pEventName, true ); |
|
if ( !pEvent ) |
|
return; |
|
|
|
SV_SendReplayEvent( pEvent, nClientSlot ); |
|
} |
|
|
|
virtual void SV_SendReplayEvent( IGameEvent *pEvent, int nClientSlot/*=-1*/ ) |
|
{ |
|
IServer *pGameServer = g_pEngine->GetGameServer(); |
|
|
|
if ( !pEvent ) |
|
return; |
|
|
|
// Write event info to SVC_GameEvent msg |
|
char buffer_data[MAX_EVENT_BYTES]; |
|
SVC_GameEvent msg; |
|
msg.SetReliable( false ); |
|
msg.m_DataOut.StartWriting( buffer_data, sizeof( buffer_data ) ); |
|
if ( !g_pGameEventManager->SerializeEvent( pEvent, &msg.m_DataOut ) ) |
|
{ |
|
DevMsg( "Replay_SendReplayEvent(): failed to serialize event '%s'.\n", pEvent->GetName() ); |
|
goto free_event; |
|
} |
|
|
|
// Send to all clients? |
|
if ( nClientSlot == -1 ) |
|
{ |
|
for ( int i = 0; i < pGameServer->GetClientCount(); ++i ) |
|
{ |
|
IClient *pClient = pGameServer->GetClient( i ); |
|
if ( pClient ) |
|
{ |
|
// Send the message |
|
pClient->SendNetMsg( msg ); |
|
} |
|
} |
|
} |
|
else // Send to just the one client? |
|
{ |
|
IClient *pClient = pGameServer->GetClient( nClientSlot ); |
|
if ( pClient ) |
|
{ |
|
// Send the message |
|
pClient->SendNetMsg( msg ); |
|
} |
|
} |
|
|
|
free_event: |
|
g_pGameEventManager->FreeEvent( pEvent ); |
|
} |
|
|
|
virtual void SV_EndRecordingSession( bool bForceSynchronousPublish/*=false*/ ) |
|
{ |
|
if ( !g_pEngine->IsSupportedModAndPlatform() ) |
|
return; |
|
|
|
if ( !ReplayServer() ) |
|
return; |
|
|
|
SV_GetSessionRecorder()->StopRecording( false ); |
|
|
|
if ( bForceSynchronousPublish ) |
|
{ |
|
// Publish all files |
|
SV_GetSessionRecorder()->PublishAllSynchronous(); |
|
|
|
// This should unlock all sessions |
|
SV_GetSessionRecorder()->UpdateSessionLocks(); |
|
|
|
// Let the session manager do any cleanup - this will remove files associated with a ditched |
|
// session. For example, if the server was shut down in the middle of a round where no one |
|
// saved a replay, the files will be published synchronously above and then cleaned up |
|
// synchronously here. |
|
SV_GetRecordingSessionManager()->DoSessionCleanup(); |
|
|
|
// Since the recording session manager will kick off a cleanup job, we need to wait for it |
|
// here since we're shutting down. |
|
SV_GetFileserverCleaner()->BlockForCompletion(); |
|
} |
|
} |
|
|
|
void Test() |
|
{ |
|
#if !defined( DEDICATED ) && _DEBUG |
|
// This gets called after interfaces are hooked up, and before any of the |
|
// internal replay systems get init'd. |
|
CTestManager::Test(); |
|
#endif |
|
} |
|
}; |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
static CReplaySystem s_Replay; |
|
IReplaySystem *g_pReplay = &s_Replay; |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CReplaySystem, IReplaySystem, REPLAY_INTERFACE_VERSION, |
|
s_Replay ); |
|
|
|
//---------------------------------------------------------------------------------------- |
|
|
|
void Replay_MsgBox( const char *pText ) |
|
{ |
|
g_pClient->DisplayReplayMessage( pText, false, true, NULL ); |
|
} |
|
|
|
void Replay_MsgBox( const wchar_t *pText ) |
|
{ |
|
g_pClient->DisplayReplayMessage( pText, false, true, NULL ); |
|
} |
|
|
|
const char *Replay_GetDownloadURLPath() |
|
{ |
|
static char s_szDownloadURLPath[MAX_OSPATH]; |
|
extern ConVar replay_fileserver_path; // NOTE: replicated |
|
|
|
V_strcpy_safe( s_szDownloadURLPath, replay_fileserver_path.GetString() ); |
|
V_StripTrailingSlash( s_szDownloadURLPath ); |
|
V_FixSlashes( s_szDownloadURLPath, '/' ); |
|
|
|
// Get rid of starting slash |
|
if ( s_szDownloadURLPath[0] == '/' ) |
|
return &s_szDownloadURLPath[1]; |
|
|
|
return s_szDownloadURLPath; |
|
} |
|
|
|
const char *Replay_GetDownloadURL() |
|
{ |
|
#if 0 |
|
// Get the local host name |
|
char szHostname[MAX_OSPATH]; |
|
if ( gethostname( szHostname, sizeof( szHostname ) ) == -1 ) |
|
{ |
|
Error( "Failed to send to Replay to client - couldn't get local IP.\n" ); |
|
return ""; |
|
} |
|
#endif |
|
|
|
// Construct the URL based on replicated cvars |
|
static char s_szFileURL[ MAX_OSPATH ]; |
|
extern ConVar replay_fileserver_protocol; |
|
extern ConVar replay_fileserver_host; |
|
extern ConVar replay_fileserver_port; |
|
V_snprintf( |
|
s_szFileURL, sizeof( s_szFileURL ), |
|
"%s://%s:%i/%s/", |
|
replay_fileserver_protocol.GetString(), |
|
replay_fileserver_host.GetString(), |
|
replay_fileserver_port.GetInt(), |
|
Replay_GetDownloadURLPath() |
|
); |
|
|
|
// Cleanup |
|
V_FixDoubleSlashes( s_szFileURL + V_strlen("http://") ); |
|
|
|
return s_szFileURL; |
|
} |
|
|
|
//---------------------------------------------------------------------------------------- |
|
// Purpose: (client/server) Crack a URL into a base and a path |
|
// NOTE: the URL *must contain a port* ! |
|
// |
|
// Example: http://some.base.url:8080/a/path/here.txt cracks into: |
|
// pBaseURL = "http://some.base.url:8080" |
|
// pURLPath = "/a/path/here.txt" |
|
//---------------------------------------------------------------------------------------- |
|
void Replay_CrackURL( const char *pURL, char *pBaseURLOut, char *pURLPathOut ) |
|
{ |
|
const char *pColon; |
|
const char *pURLPath; |
|
|
|
// Must at least have "http://" |
|
if ( V_strlen(pURL) < 6 ) |
|
goto fail; |
|
|
|
// Skip protocol ':' (eg http://) |
|
pColon = V_strstr( pURL, ":" ); |
|
if ( !pColon ) |
|
goto fail; |
|
|
|
// Find next colon |
|
pColon = V_strstr( pColon + 1, ":" ); |
|
if ( !pColon ) |
|
goto fail; |
|
|
|
// Copies "http[s]://<address>:<port> |
|
pURLPath = V_strstr( pColon, "/" ); |
|
V_strncpy( pBaseURLOut, pURL, pURLPath - pURL + 1 ); |
|
V_strcpy( pURLPathOut, pURLPath ); |
|
|
|
return; |
|
|
|
fail: |
|
AssertMsg( 0, "Replay_CrackURL() was passed an invalid URL and has failed. This should never happen." ); |
|
} |
|
|
|
#ifndef DEDICATED |
|
void Replay_HudMsg( const char *pText, const char *pSound, bool bUrgent ) |
|
{ |
|
g_pClient->DisplayReplayMessage( pText, bUrgent, false, pSound ); |
|
} |
|
#endif |
|
|
|
//----------------------------------------------------------------------------------------
|
|
|