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.
532 lines
16 KiB
532 lines
16 KiB
//========= Copyright (c) 1996-2009, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
//=======================================================================================//
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
#include "replayhistorymanager.h"
|
|
#include "client.h"
|
|
#include "net_chan.h"
|
|
#include "dmxloader/dmxelement.h"
|
|
#include <time.h>
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
#define REPLAY_HISTORY_FILE_CLIENT "client_replay_history.dmx"
|
|
#define REPLAY_HISTORY_FILE_SERVER "server_replay_history.dmx"
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
void CClientReplayHistoryEntryData::BeginDownload()
|
|
{
|
|
// Request the .dem file from the server
|
|
GetBaseLocalClient().m_NetChannel->RequestFile( m_szFilename, true );
|
|
|
|
m_bTransferring = true;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
BEGIN_DMXELEMENT_UNPACK( CBaseReplayHistoryEntryData )
|
|
DMXELEMENT_UNPACK_FIELD_STRING( "filename", "NONE", m_szFilename )
|
|
DMXELEMENT_UNPACK_FIELD_STRING( "map" , "NONE", m_szMapName )
|
|
|
|
DMXELEMENT_UNPACK_FIELD( "lifespan" , "0", int , m_nLifeSpan )
|
|
DMXELEMENT_UNPACK_FIELD( "demo_length", "0", DmeTime_t, m_DemoLength )
|
|
DMXELEMENT_UNPACK_FIELD( "transferred", "0", int , m_nBytesTransferred )
|
|
DMXELEMENT_UNPACK_FIELD( "size" , "0", int , m_nSize )
|
|
DMXELEMENT_UNPACK_FIELD( "transferid" , "0", int , m_nTransferId )
|
|
DMXELEMENT_UNPACK_FIELD( "complete" , "0", bool , m_bTransferComplete )
|
|
DMXELEMENT_UNPACK_FIELD( "downloading", "0", bool , m_bTransferring )
|
|
END_DMXELEMENT_UNPACK( CBaseReplayHistoryEntryData, s_ClientEntryDataUnpack )
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
template< class T >
|
|
class CBaseReplayHistoryManager : public IReplayHistoryManager
|
|
{
|
|
public:
|
|
CBaseReplayHistoryManager()
|
|
: m_bInit( false )
|
|
{
|
|
}
|
|
|
|
virtual void Init()
|
|
{
|
|
// Load all entries from disk
|
|
if ( !LoadEntriesFromDisk() )
|
|
{
|
|
Warning( "Replay history file %s not found.\n", GetCacheFilename() );
|
|
}
|
|
|
|
m_bInit = true;
|
|
}
|
|
|
|
virtual bool IsInitialized() const { return m_bInit; }
|
|
|
|
virtual void Shutdown()
|
|
{
|
|
m_bInit = false;
|
|
m_lstEntries.PurgeAndDeleteElements();
|
|
}
|
|
|
|
virtual int GetNumEntries() const
|
|
{
|
|
return m_lstEntries.Count();
|
|
}
|
|
|
|
virtual const CBaseReplayHistoryEntryData *GetEntryAtIndex( int iIndex ) const
|
|
{
|
|
Assert( iIndex >= 0 && iIndex < GetNumEntries() );
|
|
return static_cast< CBaseReplayHistoryEntryData *>( m_lstEntries[ iIndex ] );
|
|
}
|
|
|
|
virtual CBaseReplayHistoryEntryData *FindEntry( const char *pFilename )
|
|
{
|
|
FOR_EACH_LL( m_lstEntries, i )
|
|
{
|
|
if ( !V_stricmp( pFilename, m_lstEntries[ i ]->m_szFilename ) )
|
|
{
|
|
return static_cast< T *>( m_lstEntries[ i ] );
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
virtual void FlushEntriesToDisk()
|
|
{
|
|
Assert( m_bInit );
|
|
|
|
DECLARE_DMX_CONTEXT();
|
|
|
|
CDmxElement* pEntries = CreateDmxElement( "Entries" );
|
|
CDmxElementModifyScope modify( pEntries );
|
|
|
|
int const nNumDemos = m_lstEntries.Count();
|
|
pEntries->SetValue( "num_demos", nNumDemos );
|
|
|
|
CDmxAttribute* pDemoEntriesAttr = pEntries->AddAttribute( "demos" );
|
|
CUtlVector< CDmxElement* >& entries = pDemoEntriesAttr->GetArrayForEdit< CDmxElement* >();
|
|
|
|
modify.Release();
|
|
|
|
FOR_EACH_LL( m_lstEntries, i )
|
|
{
|
|
T *pEntryData = m_lstEntries[ i ];
|
|
|
|
CDmxElement* pEntryElement = CreateDmxElement( "demo" );
|
|
entries.AddToTail( pEntryElement );
|
|
|
|
CDmxElementModifyScope modifyClass( pEntryElement );
|
|
pEntryElement->AddAttributesFromStructure( pEntryData, s_ClientEntryDataUnpack );
|
|
|
|
pEntryElement->SetValue( "record_time", pEntryData->m_nRecordTime );
|
|
|
|
RecordAdditionalEntryData( pEntryData, pEntryElement );
|
|
}
|
|
|
|
{
|
|
MEM_ALLOC_CREDIT();
|
|
const char *pFilename = GetCacheFilename();
|
|
if ( !SerializeDMX( pFilename, "GAME", false, pEntries ) )
|
|
{
|
|
Warning( "Replay: Failed to write ragdoll cache, %s.\n", pFilename );
|
|
return;
|
|
}
|
|
}
|
|
|
|
CleanupDMX( pEntries );
|
|
}
|
|
|
|
bool LoadEntriesFromDisk()
|
|
{
|
|
Assert( !m_bInit );
|
|
|
|
const char* pFilename = GetCacheFilename();
|
|
|
|
DECLARE_DMX_CONTEXT();
|
|
|
|
// Attempt to read from disk
|
|
CDmxElement* pDemos = NULL;
|
|
if ( !UnserializeDMX( pFilename, "GAME", false, &pDemos ) )
|
|
return false;
|
|
|
|
CUtlVector< CDmxElement* > const& demos = pDemos->GetArray< CDmxElement* >( "demos" );
|
|
for ( int i = 0; i < demos.Count(); ++i )
|
|
{
|
|
CDmxElement* pCurDemoInput = demos[ i ];
|
|
|
|
// Create a new ragdoll entry and add to list
|
|
T *pNewEntry = new T();
|
|
m_lstEntries.AddToTail( pNewEntry );
|
|
|
|
// Read
|
|
pCurDemoInput->UnpackIntoStructure( pNewEntry, s_ClientEntryDataUnpack );
|
|
|
|
// This should always be false
|
|
pNewEntry->m_bTransferring = false;
|
|
|
|
// Load record time
|
|
pNewEntry->m_nRecordTime = pCurDemoInput->GetValue( "record_time", 0 );
|
|
|
|
LoadAdditionalEntryData( pNewEntry, pCurDemoInput );
|
|
}
|
|
|
|
// Cleanup
|
|
CleanupDMX( pDemos );
|
|
|
|
PostLoadEntries();
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual void StopDownloads()
|
|
{
|
|
FOR_EACH_LL( m_lstEntries, i )
|
|
{
|
|
m_lstEntries[ i ]->m_bTransferring = false;
|
|
}
|
|
}
|
|
|
|
virtual const char *GetCacheFilename() const = 0;
|
|
|
|
protected:
|
|
//
|
|
// Called from FlushEntriesToDisk() for each entry - opportunity to record additional data
|
|
//
|
|
virtual void RecordAdditionalEntryData( const CBaseReplayHistoryEntryData *pEntry, CDmxElement *pElement ) {}
|
|
|
|
//
|
|
// Called from LoadEntriesFromDisk() for each entry - opportunity to load additional data
|
|
//
|
|
virtual void LoadAdditionalEntryData( CBaseReplayHistoryEntryData *pEntry, CDmxElement *pElement ) {}
|
|
|
|
//
|
|
// Called at the end of LoadEntriesFromDisk()
|
|
//
|
|
virtual void PostLoadEntries() {}
|
|
|
|
virtual void Update() {}
|
|
|
|
CUtlLinkedList< T* > m_lstEntries;
|
|
|
|
private:
|
|
bool m_bInit;
|
|
};
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
CON_COMMAND_F( replay_add_test_client_history_entry, "Add a test entry to the replay client history manager", 0 )
|
|
{
|
|
// Record in client history
|
|
extern ConVar replay_demolifespan;
|
|
CClientReplayHistoryEntryData *pNewEntry = new CClientReplayHistoryEntryData();
|
|
if ( !pNewEntry )
|
|
return;
|
|
tm now;
|
|
Plat_GetLocalTime( &now );
|
|
time_t now_time_t = mktime( &now );
|
|
pNewEntry->m_nRecordTime = static_cast< int >( now_time_t );
|
|
pNewEntry->m_nLifeSpan = replay_demolifespan.GetInt() * 24 * 3600;
|
|
pNewEntry->m_DemoLength.SetSeconds( 0 );
|
|
V_strcpy( pNewEntry->m_szFilename, "test_filename.dem" );
|
|
V_strcpy( pNewEntry->m_szMapName, "mapname" );
|
|
V_strcpy( pNewEntry->m_szServerAddress, "192.168.0.1" );
|
|
pNewEntry->m_nBytesTransferred = 0;
|
|
pNewEntry->m_bTransferComplete = false;
|
|
pNewEntry->m_nSize = atoi( args[3] );
|
|
pNewEntry->m_bTransferring = false;
|
|
pNewEntry->m_nTransferId = -1;
|
|
if ( !g_pClientReplayHistoryManager->RecordEntry( pNewEntry ) )
|
|
{
|
|
Warning( "Replay: Failed to record entry.\n" );
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
class CClientReplayHistoryManager : public CBaseReplayHistoryManager< CClientReplayHistoryEntryData >
|
|
{
|
|
public:
|
|
virtual const char *GetCacheFilename() const { return REPLAY_HISTORY_FILE_CLIENT; }
|
|
|
|
virtual void Update()
|
|
{
|
|
if ( !GetBaseLocalClient().m_NetChannel )
|
|
return;
|
|
|
|
FOR_EACH_LL( m_lstEntries, i )
|
|
{
|
|
CClientReplayHistoryEntryData *pEntry = m_lstEntries[ i ];
|
|
if ( !pEntry->m_bTransferComplete )
|
|
{
|
|
GetBaseLocalClient().m_NetChannel->GetStreamProgress( FLOW_INCOMING, &pEntry->m_nBytesTransferred, &pEntry->m_nSize );
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual bool RecordEntry( CBaseReplayHistoryEntryData *pNewEntry )
|
|
{
|
|
if ( !IsInitialized() || !pNewEntry )
|
|
return false;
|
|
|
|
m_lstEntries.AddToTail( static_cast< CClientReplayHistoryEntryData * >( pNewEntry ) );
|
|
|
|
// Write all entries to disk now, just to be safe
|
|
FlushEntriesToDisk();
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual void RecordAdditionalEntryData( const CBaseReplayHistoryEntryData *pEntry, CDmxElement *pElement )
|
|
{
|
|
const CClientReplayHistoryEntryData *pClientEntry = static_cast< const CClientReplayHistoryEntryData *>( pEntry );
|
|
pElement->SetValue( "server", pClientEntry->m_szServerAddress );
|
|
}
|
|
|
|
virtual void LoadAdditionalEntryData( CBaseReplayHistoryEntryData *pEntry, CDmxElement *pElement )
|
|
{
|
|
CClientReplayHistoryEntryData *pClientEntry = static_cast< CClientReplayHistoryEntryData *>( pEntry );
|
|
V_strcpy( pClientEntry->m_szServerAddress, pElement->GetValueString( "server" ) );
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
class CServerReplayHistoryManager : public CBaseReplayHistoryManager< CServerReplayHistoryEntryData >
|
|
{
|
|
public:
|
|
CServerReplayHistoryManager()
|
|
: m_flNextScheduledCleanup( 0.0f )
|
|
{
|
|
}
|
|
|
|
virtual const char *GetCacheFilename() const { return REPLAY_HISTORY_FILE_SERVER; }
|
|
|
|
// To be used with UpdateDemoFileEntries()
|
|
enum EUpdateDemoFileEntryFlags
|
|
{
|
|
UPDATE_DELETESTALEFROMDISK = 0x1, // Delete stale demos from disk
|
|
UPDATE_PRINTSTATS = 0x2, // Print statistics on all files
|
|
UPDATE_SYNC = 0x4, // If the file does not exist on disk anymore, remove it from the history file
|
|
UPDATE_REMOVEEXPIREDENTRIES = 0x8, // Remove any expired entries and flush to disk
|
|
};
|
|
|
|
bool UpdateDemoFileEntries( int nFlags )
|
|
{
|
|
bool bRemovedAny = false;
|
|
bool bFlushToDisk = false;
|
|
|
|
time_t now = time( NULL );
|
|
|
|
if ( nFlags & UPDATE_PRINTSTATS )
|
|
{
|
|
Msg( "\nReplay history stats\n" );
|
|
Msg( "----------------------------------------------------\n" );
|
|
}
|
|
|
|
int i = m_lstEntries.Head();
|
|
while ( i != m_lstEntries.InvalidIndex() )
|
|
{
|
|
CServerReplayHistoryEntryData *pEntry = static_cast< CServerReplayHistoryEntryData *>( m_lstEntries[ i ] );
|
|
|
|
time_t recordtime = static_cast< time_t >( pEntry->m_nRecordTime + pEntry->m_nLifeSpan );
|
|
double delta = difftime( recordtime, now );
|
|
|
|
// If the file is no longer on disk and it should be
|
|
if ( ( nFlags & UPDATE_SYNC ) &&
|
|
( pEntry->m_nFileStatus == CServerReplayHistoryEntryData::FILESTATUS_EXISTS ) &&
|
|
!g_pFullFileSystem->FileExists( pEntry->m_szFilename ) )
|
|
{
|
|
pEntry->m_nFileStatus = CServerReplayHistoryEntryData::FILESTATUS_NOTONDISK;
|
|
bFlushToDisk = true;
|
|
}
|
|
|
|
// Stale demo file?
|
|
bool bStale = false;
|
|
if ( pEntry->m_nFileStatus == CServerReplayHistoryEntryData::FILESTATUS_EXISTS && delta <= 0 )
|
|
{
|
|
bRemovedAny = true;
|
|
bStale = true;
|
|
|
|
// Delete the file from disk
|
|
if ( g_pFullFileSystem->FileExists( pEntry->m_szFilename ) )
|
|
{
|
|
if ( nFlags & UPDATE_DELETESTALEFROMDISK )
|
|
{
|
|
Assert( 0 ); // Just making sure this gets hit...
|
|
|
|
Msg( "Replay: Removing stale demo \"%s\" from disk.\n", pEntry->m_szFilename );
|
|
|
|
// Remove the file from disk
|
|
g_pFullFileSystem->RemoveFile( pEntry->m_szFilename );
|
|
|
|
// Mark as deleted
|
|
pEntry->m_nFileStatus = CServerReplayHistoryEntryData::FILESTATUS_EXPIRED;
|
|
|
|
bFlushToDisk = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print stats if necessary
|
|
if ( nFlags & UPDATE_PRINTSTATS )
|
|
{
|
|
static const int nSecsPerDay = 86400;
|
|
int nDays = (int)delta / nSecsPerDay;
|
|
int nHours = (int)delta % nSecsPerDay / 3600;
|
|
int nMins = (int)delta % 60;
|
|
Msg( "Demo \"%s\" ", pEntry->m_szFilename );
|
|
if ( pEntry->m_nFileStatus == CServerReplayHistoryEntryData::FILESTATUS_EXPIRED )
|
|
{
|
|
Msg( "expired and was removed from disk.\n" );
|
|
}
|
|
else if ( pEntry->m_nFileStatus == CServerReplayHistoryEntryData::FILESTATUS_EXISTS )
|
|
{
|
|
Msg( "expires in %i days, %i hours, %i mins.\n", nDays, nHours, nMins );
|
|
}
|
|
else
|
|
{
|
|
Msg( "not found on disk.\n" );
|
|
}
|
|
}
|
|
|
|
int itCurrent = i;
|
|
|
|
// Update iterator before we do any syncing
|
|
i = m_lstEntries.Next( i );
|
|
|
|
// Sync what's in memory with what's actually on disk
|
|
if ( ( nFlags & UPDATE_REMOVEEXPIREDENTRIES ) &&
|
|
pEntry->m_nFileStatus == CServerReplayHistoryEntryData::FILESTATUS_EXPIRED )
|
|
{
|
|
// Remove the element
|
|
m_lstEntries.Remove( itCurrent );
|
|
|
|
AssertValidReadWritePtr( pEntry ); // TODO: Make sure this test fails!
|
|
|
|
bFlushToDisk = true;
|
|
}
|
|
}
|
|
|
|
// Flush?
|
|
if ( bFlushToDisk )
|
|
{
|
|
FlushEntriesToDisk();
|
|
}
|
|
|
|
if ( nFlags & UPDATE_PRINTSTATS )
|
|
{
|
|
Msg( "\n" );
|
|
}
|
|
|
|
return bRemovedAny;
|
|
}
|
|
|
|
virtual void Update()
|
|
{
|
|
if ( host_time < m_flNextScheduledCleanup )
|
|
return;
|
|
|
|
extern ConVar replay_cleanup_time;
|
|
m_flNextScheduledCleanup += replay_cleanup_time.GetInt() * 3600;
|
|
|
|
UpdateDemoFileEntries( UPDATE_SYNC | UPDATE_DELETESTALEFROMDISK );
|
|
}
|
|
|
|
virtual bool RecordEntry( CBaseReplayHistoryEntryData *pNewEntry )
|
|
{
|
|
if ( !IsInitialized() || !pNewEntry )
|
|
return false;
|
|
|
|
m_lstEntries.AddToTail( static_cast< CServerReplayHistoryEntryData * >( pNewEntry ) );
|
|
|
|
// Write all entries to disk now, just to be safe
|
|
FlushEntriesToDisk();
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual void RecordAdditionalEntryData( const CBaseReplayHistoryEntryData *pEntry, CDmxElement *pElement )
|
|
{
|
|
const CServerReplayHistoryEntryData *pServerEntry = static_cast< const CServerReplayHistoryEntryData *>( pEntry );
|
|
pElement->SetValue( "client_steam_id", pServerEntry->m_uClientSteamId );
|
|
pElement->SetValue( "file_status", (int)pServerEntry->m_nFileStatus );
|
|
}
|
|
|
|
virtual void LoadAdditionalEntryData( CBaseReplayHistoryEntryData *pEntry, CDmxElement *pElement )
|
|
{
|
|
CServerReplayHistoryEntryData *pServerEntry = static_cast< CServerReplayHistoryEntryData *>( pEntry );
|
|
pServerEntry->m_uClientSteamId = pElement->GetValue( "client_steam_id", (uint64)0 );
|
|
pServerEntry->m_nFileStatus = (CServerReplayHistoryEntryData::EFileStatus)pElement->GetValue< int >( "file_status", (int)CServerReplayHistoryEntryData::FILESTATUS_EXISTS );
|
|
}
|
|
|
|
private:
|
|
float m_flNextScheduledCleanup;
|
|
};
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
inline CServerReplayHistoryManager *GetServerReplayHistoryManager()
|
|
{
|
|
return static_cast< CServerReplayHistoryManager * >( g_pServerReplayHistoryManager );
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
CON_COMMAND_F( replay_delete_stale_demos, "Deletes stale replay demo files", FCVAR_GAMEDLL | FCVAR_DONTRECORD )
|
|
{
|
|
if ( GetServerReplayHistoryManager() &&
|
|
!GetServerReplayHistoryManager()->UpdateDemoFileEntries( CServerReplayHistoryManager::UPDATE_DELETESTALEFROMDISK ) )
|
|
{
|
|
Msg( "No demos were deleted.\n" );
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
CON_COMMAND_F( replay_print_history_stats, "Deletes stale replay demo files", FCVAR_GAMEDLL | FCVAR_DONTRECORD )
|
|
{
|
|
if ( GetServerReplayHistoryManager() )
|
|
{
|
|
GetServerReplayHistoryManager()->UpdateDemoFileEntries( CServerReplayHistoryManager::UPDATE_PRINTSTATS );
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
CON_COMMAND_F( replay_remove_expired_entries, "Removes all expired entries from replay history", FCVAR_GAMEDLL | FCVAR_DONTRECORD )
|
|
{
|
|
if ( GetServerReplayHistoryManager() )
|
|
{
|
|
GetServerReplayHistoryManager()->UpdateDemoFileEntries( CServerReplayHistoryManager::UPDATE_REMOVEEXPIREDENTRIES );
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
IReplayHistoryManager *CreateServerReplayHistoryManager()
|
|
{
|
|
return new CServerReplayHistoryManager();
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
static CClientReplayHistoryManager s_ClientReplayHistoryManager;
|
|
IReplayHistoryManager *g_pClientReplayHistoryManager = &s_ClientReplayHistoryManager;
|
|
IReplayHistoryManager *g_pServerReplayHistoryManager = NULL;
|
|
|
|
// Expose interface to the client (needed by demo browser) - no need to do this for the server.
|
|
EXPOSE_SINGLE_INTERFACE_GLOBALVAR(
|
|
CClientReplayHistoryManager,
|
|
IReplayHistoryManager,
|
|
REPLAYHISTORYMANAGER_INTERFACE_VERSION,
|
|
s_ClientReplayHistoryManager
|
|
);
|
|
|
|
//----------------------------------------------------------------------------------------
|
|
|
|
#endif
|