680 lines
21 KiB
C++
680 lines
21 KiB
C++
//======= Copyright (c) 1996-2009, Valve Corporation, All rights reserved. ======
|
|
//
|
|
// TODO:
|
|
// - USE A MEMPOOL OF SOME SORT
|
|
// - GET STALE FRAME REMOVAL WORKING
|
|
// - MAKE THREAD SAFE - NEED TO BE ABLE TO WRITE A .DEM FILE ON A SEP THREAD AND
|
|
// NOT BE THROWING AWAY ELEMENTS THAT ARE BEING READ. NEED A FLAG IN DATACHUNK
|
|
// FOR WHETHER AN INSTANCE IS "IN USE," IE BEING READ TO WRITE TO A DEM FILE.
|
|
//
|
|
//===============================================================================
|
|
|
|
#include "demobuffer.h"
|
|
#include "edict.h"
|
|
#include "host.h"
|
|
#include "tier1/mempool.h"
|
|
#include "vstdlib/jobthread.h"
|
|
|
|
#include "replayserver.h" // TODO: Remove
|
|
#include "sv_client.h"
|
|
#ifndef DEDICATED
|
|
#include "cdll_int.h"
|
|
#include "client.h"
|
|
#endif
|
|
|
|
#include "tier0/memdbgon.h" // NOTE: Must go last!
|
|
|
|
#ifndef DEDICATED
|
|
#if !defined( _DEBUG )
|
|
// These are the buffers defining how demo data is flushed to disk:
|
|
// We allocate 5 MB buffer (worth about 5 min of gameplay)
|
|
// once over 4 MB is used up we will commit the first 1 MB to disk
|
|
// This throttles disk IO, and ensures that the last 3 MB are always
|
|
// contained in memory and not committed to disk to prevent file peeking
|
|
// for cheating purposes during gameplay
|
|
#define DISK_DEMO_BUFFER_TOTAL_SIZE (5*1024*1024)
|
|
#define DISK_DEMO_BUFFER_FLUSH_CONSIDER_SIZE (4*1024*1024)
|
|
#define DISK_DEMO_BUFFER_FLUSH_TODISK_SIZE (1*1024*1024)
|
|
#else
|
|
// Smaller sizes in debug engine.dll build to hammer on the subsystems involved
|
|
#define DISK_DEMO_BUFFER_TOTAL_SIZE (5*20*1024)
|
|
#define DISK_DEMO_BUFFER_FLUSH_CONSIDER_SIZE (4*20*1024)
|
|
#define DISK_DEMO_BUFFER_FLUSH_TODISK_SIZE (1*20*1024)
|
|
#endif
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Specialty class with overrides for stream buffer
|
|
//-----------------------------------------------------------------------------
|
|
class CDiskDemoBuffer : public IDemoBuffer
|
|
{
|
|
public:
|
|
CDiskDemoBuffer()
|
|
: m_pBuffer( NULL )
|
|
{
|
|
m_nDecodedOffset = -1;
|
|
}
|
|
|
|
~CDiskDemoBuffer()
|
|
{
|
|
m_pBuffer->Close();
|
|
delete m_pBuffer;
|
|
}
|
|
|
|
virtual bool Init( DemoBufferInitParams_t const& params )
|
|
{
|
|
// Convert to proper type
|
|
StreamDemoBufferInitParams_t const* pParams = dynamic_cast< StreamDemoBufferInitParams_t const* >( ¶ms ); Assert( pParams );
|
|
|
|
// Allocate buffer
|
|
m_pBuffer = new CUtlStreamBuffer();
|
|
if ( !m_pBuffer )
|
|
return false;
|
|
|
|
#ifndef DEDICATED
|
|
// Force a very large memory buffer on the clients, this prevents peeking into the demo stream
|
|
m_pBuffer->EnsureCapacity( DISK_DEMO_BUFFER_TOTAL_SIZE );
|
|
#endif
|
|
|
|
// Demo files are always little endian
|
|
m_pBuffer->SetBigEndian( false );
|
|
m_bufDecoded.SetBigEndian( false );
|
|
|
|
// Open the file
|
|
// m_pBuffer->Open( pParams->pFilename, pParams->pszPath, pParams->nFlags, pParams->nOpenFileFlags ); // For main integration...
|
|
m_pBuffer->Open( pParams->pFilename, pParams->pszPath, pParams->nFlags );
|
|
m_nDecodedOffset = -1;
|
|
|
|
m_pPlaybackParams = NULL;
|
|
#ifndef DEDICATED
|
|
extern IDemoPlayer *demoplayer;
|
|
extern IBaseClientDLL *g_ClientDLL;
|
|
if ( demoplayer && g_ClientDLL )
|
|
{
|
|
m_pPlaybackParams = demoplayer->GetDemoPlaybackParameters();
|
|
}
|
|
#endif
|
|
|
|
return IsInitialized();
|
|
}
|
|
|
|
virtual void NotifySignonComplete() {}
|
|
|
|
virtual void WriteHeader( void const *pData, int nSize )
|
|
{
|
|
// Byteswap
|
|
demoheader_t littleEndianHeader = *((demoheader_t*)pData);
|
|
ByteSwap_demoheader_t( littleEndianHeader );
|
|
|
|
// Goto file start
|
|
SeekPut( true, 0 );
|
|
|
|
// Write
|
|
Put( pData, nSize );
|
|
}
|
|
|
|
virtual void NotifyBeginFrame() {}
|
|
virtual void NotifyEndFrame() {}
|
|
|
|
virtual void PutChar( char c ) { m_pBuffer->PutChar( c ); }
|
|
virtual void PutUnsignedChar( unsigned char uc ) { m_pBuffer->PutUnsignedChar( uc ); }
|
|
virtual void PutInt( int i ) { m_pBuffer->PutInt( i ); }
|
|
|
|
virtual void WriteTick( int nTick ) { m_pBuffer->PutInt( nTick ); }
|
|
|
|
virtual char GetChar() OVERRIDE
|
|
{
|
|
COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, sizeof( char ) );
|
|
return readRequest.GetReadBuffer()->GetChar();
|
|
}
|
|
virtual unsigned char GetUnsignedChar() OVERRIDE
|
|
{
|
|
COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, sizeof( unsigned char ) );
|
|
return readRequest.GetReadBuffer()->GetUnsignedChar();
|
|
}
|
|
virtual int GetInt() OVERRIDE
|
|
{
|
|
COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, sizeof( int ) );
|
|
return readRequest.GetReadBuffer()->GetInt();
|
|
}
|
|
|
|
virtual void Get( void* pMem, int size ) OVERRIDE
|
|
{
|
|
COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, size );
|
|
readRequest.GetReadBuffer()->Get( pMem, size );
|
|
}
|
|
virtual void Put( const void* pMem, int size )
|
|
{
|
|
m_pBuffer->Put( pMem, size );
|
|
|
|
#ifndef DEDICATED
|
|
if ( ( size > 0 ) && ( m_pBuffer->TellPut() > 0 ) &&
|
|
( ( ( ( char* ) m_pBuffer->PeekPut() ) - ( ( char * ) m_pBuffer->Base() ) ) > (
|
|
( GetBaseLocalClient().IsActive() && GetBaseLocalClient().ishltv ) ? 2048 : DISK_DEMO_BUFFER_FLUSH_CONSIDER_SIZE
|
|
) ) )
|
|
{ // Periodically try to flush the demo buffer to disk
|
|
m_pBuffer->TryFlushToFile( DISK_DEMO_BUFFER_FLUSH_TODISK_SIZE );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
virtual bool IsValid() const { return m_pBuffer && m_pBuffer->IsValid(); }
|
|
virtual bool IsInitialized() const { return IsValid() && m_pBuffer->IsOpen(); }
|
|
|
|
inline CUtlBuffer::SeekType_t GetSeekType( bool bAbsolute ) { return bAbsolute ? CUtlBuffer::SEEK_HEAD : CUtlBuffer::SEEK_CURRENT; }
|
|
|
|
// Change where I'm writing (put)/reading (get)
|
|
virtual void SeekPut( bool bAbsolute, int offset ) { m_pBuffer->SeekPut( GetSeekType( bAbsolute ), offset ); }
|
|
virtual void SeekGet( bool bAbsolute, int offset ) { m_pBuffer->SeekGet( GetSeekType( bAbsolute ), offset ); }
|
|
|
|
// Where am I writing (put)/reading (get)?
|
|
virtual int TellPut( ) const { return m_pBuffer->TellPut(); }
|
|
virtual int TellGet( ) const { return m_pBuffer->TellGet(); }
|
|
|
|
virtual int TellMaxPut( ) const { return m_pBuffer->TellMaxPut(); }
|
|
|
|
virtual void UpdateStartTick( int& nStartTick ) const {}
|
|
virtual void DumpToFile( char const* pFilename, const demoheader_t &header ) const {}
|
|
|
|
private:
|
|
CUtlStreamBuffer *m_pBuffer;
|
|
CUtlBuffer m_bufDecoded;
|
|
int m_nDecodedOffset;
|
|
CDemoPlaybackParameters_t const *m_pPlaybackParams;
|
|
|
|
class COnTheFlyDemoBufferReadInfo
|
|
{
|
|
public:
|
|
COnTheFlyDemoBufferReadInfo( CDemoPlaybackParameters_t const *pPlaybackParams, CUtlBuffer *pRawData, CUtlBuffer *pDecodeCache, int *pDecodedOffset, int numBytesRequired )
|
|
{
|
|
m_nReadFromBufferOriginalSeekPos = 0;
|
|
#ifndef DEDICATED
|
|
if ( pPlaybackParams )
|
|
{
|
|
// Read from the nearest 16-byte aligned location
|
|
int nOriginalGet = pRawData->TellGet();
|
|
if ( ( (*pDecodedOffset) < 0 ) || // nothing decoded
|
|
( nOriginalGet < (*pDecodedOffset) ) || // reading earlier
|
|
( nOriginalGet + numBytesRequired > (*pDecodedOffset) + pDecodeCache->TellPut() ) ) // could read beyond decoded buffer
|
|
{
|
|
int nNearestAlignedLocation = nOriginalGet &~0xF;
|
|
int blockRead = ( nOriginalGet + numBytesRequired - nNearestAlignedLocation + 0xF )&~0xF;
|
|
blockRead = MAX( 1024, blockRead ); // decrypt chunks of 1K bytes at a time
|
|
|
|
*pDecodedOffset = nNearestAlignedLocation;
|
|
pDecodeCache->EnsureCapacity( blockRead );
|
|
int numBytesSeekBack = nOriginalGet - nNearestAlignedLocation;
|
|
if ( numBytesSeekBack )
|
|
pRawData->SeekGet( pRawData->SEEK_CURRENT, - numBytesSeekBack ); // seek back
|
|
|
|
int numBytes = MIN( blockRead, pRawData->TellMaxPut() - nNearestAlignedLocation );
|
|
pRawData->Get( pDecodeCache->Base(), numBytes );
|
|
m_nReadFromBufferOriginalSeekPos += -numBytes+numBytesSeekBack;
|
|
pDecodeCache->SeekPut( pDecodeCache->SEEK_HEAD, numBytes );
|
|
|
|
// Decode the chunk
|
|
extern IBaseClientDLL *g_ClientDLL;
|
|
g_ClientDLL->PrepareSignedEvidenceData( pDecodeCache->Base(), numBytes, pPlaybackParams );
|
|
}
|
|
|
|
int nSeekInDecodedBuffer = nOriginalGet - *pDecodedOffset;
|
|
pDecodeCache->SeekGet( pDecodeCache->SEEK_HEAD, nSeekInDecodedBuffer );
|
|
|
|
//
|
|
// Set the read state
|
|
//
|
|
m_pReadFromBuffer = pDecodeCache;
|
|
m_pSeekSyncBuffer = pRawData;
|
|
m_nReadFromBufferOriginalSeekPos += -nSeekInDecodedBuffer;
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
//
|
|
// Read raw state
|
|
//
|
|
m_pReadFromBuffer = pRawData;
|
|
m_pSeekSyncBuffer = NULL;
|
|
}
|
|
~COnTheFlyDemoBufferReadInfo()
|
|
{
|
|
if ( m_pSeekSyncBuffer && m_pReadFromBuffer )
|
|
m_pSeekSyncBuffer->SeekGet( m_pSeekSyncBuffer->SEEK_CURRENT, m_pReadFromBuffer->TellGet() + m_nReadFromBufferOriginalSeekPos );
|
|
}
|
|
|
|
CUtlBuffer * GetReadBuffer() const { return m_pReadFromBuffer; }
|
|
private:
|
|
CUtlBuffer *m_pReadFromBuffer;
|
|
CUtlBuffer *m_pSeekSyncBuffer;
|
|
int m_nReadFromBufferOriginalSeekPos;
|
|
};
|
|
};
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Specialty class with overrides for stream buffer
|
|
//-----------------------------------------------------------------------------
|
|
#if defined( REPLAY_ENABLED )
|
|
class CMemoryDemoBuffer : public IDemoBuffer
|
|
{
|
|
private:
|
|
static int const CACHE_SIZE = 1024 * 512;
|
|
|
|
uint8* m_pDataCache; // Data cache for temporary writing
|
|
uint8* m_pWrite; // Current write position (based on m_pDataCache)
|
|
int m_nBufferSize; // Total buffer size
|
|
int m_nMaxPut; // What's the most I've ever written?
|
|
int m_nCurrentTickOffset; // Offset (relative to m_pDataCache) of tick - needed so we can rewrite ticks before dumping to disk
|
|
|
|
struct DataChunk_t
|
|
{
|
|
int nCurrentTickOffset;
|
|
int nTickcount;
|
|
int nDeltaTickcount;
|
|
int nSize;
|
|
uint8 pData[1];
|
|
};
|
|
|
|
inline DataChunk_t* AllocDataChunk( int nSize, int nCurrentTickOffset )
|
|
{
|
|
Assert( nCurrentTickOffset >= 0 );
|
|
|
|
int nActualSize = sizeof(DataChunk_t) + nSize - 1; Assert( nActualSize < CACHE_SIZE );
|
|
|
|
DataChunk_t* pNewFrame = (DataChunk_t*)new uint8[ nActualSize ];
|
|
pNewFrame->nSize = nSize;
|
|
pNewFrame->nCurrentTickOffset = nCurrentTickOffset;
|
|
pNewFrame->nTickcount = -1;
|
|
|
|
// TODO: pass in the delta tick - get rid of #include "replay" etc.
|
|
pNewFrame->nDeltaTickcount = ( replay && replay->m_MasterClient ) ? replay->m_MasterClient->m_nDeltaTick : -1;
|
|
|
|
return pNewFrame;
|
|
}
|
|
|
|
bool m_bSignonComplete;
|
|
|
|
DataChunk_t* m_pSignonData;
|
|
|
|
typedef unsigned short Iterator_t;
|
|
CUtlLinkedList< DataChunk_t*, Iterator_t > m_lstFrames; // Represents a list of demo frames
|
|
|
|
inline int GetTickCount()
|
|
{
|
|
extern CGlobalVars g_ServerGlobalVariables;
|
|
return g_ServerGlobalVariables.tickcount;
|
|
}
|
|
|
|
void RemoveStaleFrames()
|
|
{
|
|
// Don't remove any frames in the midst of a write operation
|
|
if ( m_nWriteCount > 0 )
|
|
return;
|
|
|
|
extern ConVar replay_movielength;
|
|
|
|
#ifdef _DEBUG
|
|
int nNumFramesRemoved = 0;
|
|
#endif
|
|
|
|
// Here we remove any frames that are beyond the length of the movie.
|
|
Iterator_t i = m_lstFrames.Head();
|
|
while ( i != m_lstFrames.InvalidIndex() )
|
|
{
|
|
if ( m_lstFrames[ i ]->nTickcount >= GetTickCount() - TIME_TO_TICKS( replay_movielength.GetInt() ) )
|
|
break;
|
|
|
|
m_lstFrames.Remove( i );
|
|
i = m_lstFrames.Head();
|
|
#ifdef _DEBUG
|
|
++nNumFramesRemoved;
|
|
#endif
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
if ( nNumFramesRemoved > 0 )
|
|
{
|
|
DevMsg( "Replay: Removed %d frames(s) from recording buffer.\n", nNumFramesRemoved );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public:
|
|
CMemoryDemoBuffer()
|
|
:
|
|
m_nBufferSize( 0 ),
|
|
m_nMaxPut( 0 ),
|
|
m_nCurrentTickOffset( -1 ),
|
|
m_pWrite( NULL ),
|
|
m_pSignonData( NULL ),
|
|
m_bSignonComplete( false )
|
|
{
|
|
}
|
|
|
|
~CMemoryDemoBuffer()
|
|
{
|
|
delete [] m_pDataCache;
|
|
delete m_pSignonData;
|
|
|
|
// Free all list entries
|
|
m_lstFrames.PurgeAndDeleteElements();
|
|
}
|
|
|
|
virtual bool Init( DemoBufferInitParams_t const& params )
|
|
{
|
|
m_pDataCache = new uint8[ CACHE_SIZE ];
|
|
m_pWrite = m_pDataCache;
|
|
return true;
|
|
}
|
|
|
|
virtual bool IsInitialized() const { return m_pDataCache != NULL; }
|
|
|
|
virtual bool IsValid() const { return IsInitialized(); }
|
|
|
|
virtual void WriteHeader( const void *pData, int nSize )
|
|
{
|
|
// There is no need to write the header until we dump the file to disk.
|
|
|
|
/*
|
|
// NOTE: Byteswap happens in dump
|
|
|
|
// The header gets written twice, once at demo start, and once at demo stop.
|
|
// If this is the first time, just write to the beginning of the cache
|
|
if ( !m_bSignonComplete )
|
|
{
|
|
Assert( m_pWrite == m_pDataCache ); // Make sure this is the first thing we're writing
|
|
Put( pData, nSize );
|
|
}
|
|
else // Otherwise, write to the beginning of the header
|
|
{
|
|
AssertValidWritePtr( m_pHeaderData, nSize );
|
|
V_memcpy( m_pHeaderData, pData, nSize );
|
|
}
|
|
*/
|
|
}
|
|
|
|
virtual void NotifySignonComplete()
|
|
{
|
|
Assert( !m_pSignonData );
|
|
|
|
// Compute size and allocate
|
|
int nSize = m_pWrite - m_pDataCache; Assert( nSize >= 0 );
|
|
m_pSignonData = AllocDataChunk( nSize, -1 );
|
|
|
|
// NOTE: No need to set m_pSignonData->nTickcount.
|
|
|
|
// We're done with signon data, copy it over from the cache
|
|
V_memcpy( m_pSignonData->pData, m_pDataCache, nSize );
|
|
|
|
m_pWrite = NULL;
|
|
m_bSignonComplete = true;
|
|
}
|
|
|
|
virtual void NotifyBeginFrame()
|
|
{
|
|
if ( !m_bSignonComplete )
|
|
return;
|
|
|
|
Assert( m_pWrite == 0 );
|
|
m_pWrite = m_pDataCache;
|
|
}
|
|
|
|
virtual void NotifyEndFrame()
|
|
{
|
|
if ( !m_bSignonComplete )
|
|
return;
|
|
|
|
RemoveStaleFrames();
|
|
|
|
// Allocate a new data chunk
|
|
int nSize = m_pWrite - m_pDataCache; Assert( nSize >= 0 );
|
|
DataChunk_t* pNewFrame = AllocDataChunk( nSize, m_nCurrentTickOffset );
|
|
|
|
// Set the time
|
|
pNewFrame->nTickcount = GetTickCount();
|
|
|
|
// Copy data from cache to new frame
|
|
V_memcpy( pNewFrame->pData, m_pDataCache, nSize );
|
|
|
|
// Add new frame to list
|
|
m_lstFrames.AddToTail( pNewFrame );
|
|
|
|
#ifdef _DEBUG
|
|
m_pWrite = NULL;
|
|
#endif
|
|
}
|
|
|
|
// Change where I'm writing (put)/reading (get)
|
|
virtual void SeekGet( bool bAbsolute, int offset )
|
|
{
|
|
// Don't call this.
|
|
Assert( 0 );
|
|
}
|
|
|
|
virtual void SeekPut( bool bAbsolute, int nOffset )
|
|
{
|
|
// The only time this should get called is if we are about to write the header.
|
|
Assert( bAbsolute && nOffset == 0 );
|
|
}
|
|
|
|
// Where am I writing (put)/reading (get)?
|
|
virtual int TellPut( ) const { return m_pWrite - m_pDataCache; }
|
|
virtual int TellGet( ) const { Assert( 0 ); return 0; }
|
|
|
|
// What's the most I've ever written?
|
|
virtual int TellMaxPut( ) const { return m_nMaxPut; }
|
|
|
|
// Get functions should never get called.
|
|
virtual char GetChar() { Assert( 0 ); return 0; }
|
|
virtual unsigned char GetUnsignedChar() { Assert( 0 ); return 0; }
|
|
virtual int GetInt() { Assert( 0 ); return 0; }
|
|
virtual void Get( void* pMem, int size ) { Assert( 0 ); }
|
|
|
|
virtual void PutChar( char c ) { Put( &c, sizeof( c ) ); }
|
|
virtual void PutUnsignedChar( unsigned char uc ) { Put( &uc, sizeof( uc ) ); }
|
|
virtual void PutInt( int i ) { Put( &i, sizeof( i ) ); }
|
|
virtual void Put( const void* pMem, int nSize )
|
|
{
|
|
Assert( m_pWrite - m_pDataCache + nSize < CACHE_SIZE );
|
|
V_memcpy( m_pWrite, pMem, nSize );
|
|
m_pWrite += nSize;
|
|
m_nBufferSize += nSize;
|
|
m_nMaxPut = MAX( m_nMaxPut, nSize );
|
|
}
|
|
|
|
virtual void WriteTick( int nTick )
|
|
{
|
|
// Cache the relative position of the tick in memory for the given frame
|
|
m_nCurrentTickOffset = m_pWrite - m_pDataCache;
|
|
|
|
// Write the tick
|
|
PutInt( nTick );
|
|
}
|
|
|
|
//
|
|
// For thread safety - this counter keeps us from removing stale frames while writing a .dem file.
|
|
// The idea here is that if the counter is anything but zero we should not remove stale frames.
|
|
// We increment before creating a new job, and decrement from within that job, at the end. We
|
|
// pass an iterator as an argument into the job's constructor so we know where to stop iterating
|
|
// across the frame list.
|
|
//
|
|
mutable CInterlockedIntT<int> m_nWriteCount;
|
|
|
|
//
|
|
// Threaded .dem file write
|
|
//
|
|
class CDemWriteJob : public CJob
|
|
{
|
|
public:
|
|
CDemWriteJob( const CMemoryDemoBuffer *pDemobuffer, const char *pFilename, Iterator_t itTail, const demoheader_t &header )
|
|
: m_pDemobuffer( pDemobuffer ), m_pFilename( pFilename ), m_itTail( itTail ), m_Header( header )
|
|
{
|
|
Assert( pFilename && pFilename[0] );
|
|
}
|
|
|
|
virtual JobStatus_t DoExecute()
|
|
{
|
|
// TODO: Does it make sense to return JOB_OK here even on failure?
|
|
JobStatus_t nResult = JOB_OK;
|
|
|
|
if ( m_pFilename && m_pFilename[0] != '\0' )
|
|
{
|
|
// Open the file
|
|
CUtlStreamBuffer buf( m_pFilename, NULL );
|
|
if ( !buf.IsOpen() )
|
|
{
|
|
Warning( "demobuffer: Failed to open file for writing, %s\n", m_pFilename );
|
|
}
|
|
else
|
|
{
|
|
// NOTE: We include the sync tick frame as part of our signon data, which makes the header signon length vary by 6 bytes
|
|
// (2 chars and 1 int) from our own signon data size.
|
|
const int nTickSyncFrameSize = 6;
|
|
Assert( m_pDemobuffer->m_pSignonData->nSize == m_Header.signonlength + nTickSyncFrameSize );
|
|
|
|
// Compute adjusted time/ticks/frames, since we may have removed stale frames
|
|
const CUtlLinkedList< CMemoryDemoBuffer::DataChunk_t*, CMemoryDemoBuffer::Iterator_t > &lstFrames = m_pDemobuffer->m_lstFrames;
|
|
Iterator_t itHead = lstFrames.Head();
|
|
Iterator_t itTail = lstFrames.Tail();
|
|
|
|
Assert( itHead != lstFrames.InvalidIndex() );
|
|
Assert( itTail != lstFrames.InvalidIndex() );
|
|
|
|
const DataChunk_t *pHead = lstFrames.Element( itHead );
|
|
const DataChunk_t *pTail = lstFrames.Element( itTail );
|
|
|
|
demoheader_t littleEndianHeader = m_Header;
|
|
littleEndianHeader.playback_time = TICKS_TO_TIME( pTail->nTickcount - pHead->nTickcount );
|
|
littleEndianHeader.playback_ticks = pTail->nTickcount - pHead->nTickcount;
|
|
littleEndianHeader.playback_frames = lstFrames.Count();
|
|
|
|
// Byteswap
|
|
ByteSwap_demoheader_t( littleEndianHeader );
|
|
|
|
// Write header
|
|
buf.Put( &littleEndianHeader, sizeof( littleEndianHeader ) );
|
|
|
|
// Write signon data
|
|
AssertValidReadPtr( m_pDemobuffer->m_pSignonData );
|
|
buf.Put( m_pDemobuffer->m_pSignonData->pData, m_pDemobuffer->m_pSignonData->nSize );
|
|
|
|
#if 1
|
|
Iterator_t itStart = m_pDemobuffer->m_lstFrames.Head();
|
|
#else
|
|
// TEST: Skip the first one
|
|
Iterator_t itStart = m_pDemobuffer->m_lstFrames.Next( m_lstFrames.Head() );
|
|
#endif
|
|
|
|
// Get first recording tick (NOTE: not start global tick). Recording ticks start at 0 but
|
|
// when we remove stale frames the first recording tick becomes greater than zero. We use
|
|
// nStartTick here to shift all frame recording ticks down nStartTick ticks.
|
|
int nStartTick;
|
|
if ( itHead != m_pDemobuffer->m_lstFrames.InvalidIndex() )
|
|
{
|
|
DataChunk_t *pFrame = m_pDemobuffer->m_lstFrames[ itHead ];
|
|
int *pTickData = reinterpret_cast< int * >( pFrame->pData + pFrame->nCurrentTickOffset );
|
|
V_memcpy( &nStartTick, pTickData, sizeof( int ) );
|
|
}
|
|
|
|
// Write frames
|
|
for ( Iterator_t i = itStart; i != m_pDemobuffer->m_lstFrames.InvalidIndex(); i = m_pDemobuffer->m_lstFrames.Next( i ) )
|
|
{
|
|
DataChunk_t *pFrame = m_pDemobuffer->m_lstFrames[ i ]; Assert( pFrame->nSize >= 0 );
|
|
|
|
// Overwrite the tick for the given frame if one exists
|
|
if ( nStartTick > 0 && pFrame->nCurrentTickOffset >= 0 )
|
|
{
|
|
int nNewTick;
|
|
int *pTickData = reinterpret_cast< int * >( pFrame->pData + pFrame->nCurrentTickOffset );
|
|
|
|
// Copy tick from buffer to nNewTick
|
|
V_memcpy( &nNewTick, pTickData, sizeof( int ) );
|
|
|
|
// Subtract out start tick
|
|
nNewTick -= nStartTick;
|
|
|
|
// Copy back to buffer
|
|
V_memcpy( pTickData, &nNewTick, sizeof( int ) );
|
|
}
|
|
|
|
buf.Put( pFrame->pData, pFrame->nSize );
|
|
}
|
|
|
|
// Write dem_stop cmd
|
|
buf.PutUnsignedChar( dem_stop );
|
|
buf.PutInt( m_pDemobuffer->m_lstFrames.Element( m_pDemobuffer->m_lstFrames.Tail() )->nTickcount );
|
|
buf.PutChar( 0 );
|
|
|
|
buf.Close();
|
|
}
|
|
}
|
|
|
|
// Decrement the write counter
|
|
m_pDemobuffer->m_nWriteCount--;
|
|
|
|
return nResult;
|
|
}
|
|
|
|
private:
|
|
const CMemoryDemoBuffer *m_pDemobuffer;
|
|
const char *m_pFilename;
|
|
Iterator_t m_itTail;
|
|
const demoheader_t &m_Header;
|
|
};
|
|
|
|
virtual void UpdateStartTick( int& nStartTick ) const
|
|
{
|
|
Iterator_t itHead = m_lstFrames.Head();
|
|
if ( itHead == m_lstFrames.InvalidIndex() )
|
|
return;
|
|
|
|
nStartTick = m_lstFrames[ itHead ]->nTickcount;
|
|
}
|
|
|
|
virtual void DumpToFile( const char *pFilename, const demoheader_t &header ) const
|
|
{
|
|
Assert( !IsX360() ); // TODO: Not supporting 360 yet. Need alternate thread pool setup to do so.
|
|
|
|
// HACK:
|
|
int n = m_nWriteCount;
|
|
|
|
// Critical section
|
|
m_nWriteCount++;
|
|
|
|
// Start a new thread
|
|
CDemWriteJob* pJob = new CDemWriteJob( this, pFilename, m_lstFrames.Tail(), header );
|
|
g_pThreadPool->AddJob( pJob );
|
|
|
|
while ( m_nWriteCount > n )
|
|
DevMsg( "Waiting for file dump to complete\n" );
|
|
}
|
|
};
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Factory function
|
|
//-----------------------------------------------------------------------------
|
|
IDemoBuffer *CreateDemoBuffer( bool bMemoryBuffer, const DemoBufferInitParams_t& params )
|
|
{
|
|
IDemoBuffer *pRet;
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( bMemoryBuffer )
|
|
{
|
|
pRet = static_cast< IDemoBuffer* >( new CMemoryDemoBuffer() );
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
pRet = static_cast< IDemoBuffer* >( new CDiskDemoBuffer() );
|
|
}
|
|
|
|
if ( !pRet->Init( params ) )
|
|
{
|
|
delete pRet;
|
|
return NULL;
|
|
}
|
|
|
|
return pRet;
|
|
}
|