//========= 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 // 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