ddnet/src/engine/shared/demo.cpp

1195 lines
30 KiB
C++
Raw Normal View History

2010-11-20 10:37:14 +00:00
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <base/math.h>
2010-05-29 07:25:38 +00:00
#include <base/system.h>
#include <engine/console.h>
2010-05-29 07:25:38 +00:00
#include <engine/storage.h>
#include <engine/shared/config.h>
#if defined(CONF_VIDEORECORDER)
#include <engine/shared/video.h>
#endif
#include <game/generated/protocol.h>
#include "compression.h"
2010-09-12 10:16:51 +00:00
#include "demo.h"
2010-05-29 07:25:38 +00:00
#include "memheap.h"
#include "network.h"
#include "snapshot.h"
2010-05-29 07:25:38 +00:00
2010-09-12 11:03:54 +00:00
static const unsigned char gs_aHeaderMarker[7] = {'T', 'W', 'D', 'E', 'M', 'O', 0};
2019-10-14 00:27:08 +00:00
static const unsigned char gs_ActVersion = 6;
static const unsigned char gs_OldVersion = 3;
2019-12-17 14:44:54 +00:00
static const unsigned char gs_Sha256Version = 6;
static const unsigned char gs_VersionTickCompression = 5; // demo files with this version or higher will use `CHUNKTICKFLAG_TICK_COMPRESSED`
static const int gs_LengthOffset = 152;
static const int gs_NumMarkersOffset = 176;
2010-05-29 07:25:38 +00:00
CDemoRecorder::CDemoRecorder(class CSnapshotDelta *pSnapshotDelta, bool NoMapData)
2010-05-29 07:25:38 +00:00
{
m_File = 0;
m_aCurrentFilename[0] = '\0';
m_pfnFilter = 0;
m_pUser = 0;
2010-05-29 07:25:38 +00:00
m_LastTickMarker = -1;
m_pSnapshotDelta = pSnapshotDelta;
m_NoMapData = NoMapData;
2010-05-29 07:25:38 +00:00
}
// Record
2020-01-03 09:12:37 +00:00
int CDemoRecorder::Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetVersion, const char *pMap, SHA256_DIGEST *pSha256, unsigned Crc, const char *pType, unsigned int MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser)
2010-05-29 07:25:38 +00:00
{
m_pfnFilter = pfnFilter;
m_pUser = pUser;
m_pMapData = pMapData;
m_pConsole = pConsole;
IOHANDLE DemoFile = pStorage->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!DemoFile)
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Unable to open '%s' for recording", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf);
}
return -1;
}
2010-05-29 07:25:38 +00:00
CDemoHeader Header;
CTimelineMarkers TimelineMarkers;
if(m_File) {
io_close(DemoFile);
2010-05-29 07:25:38 +00:00
return -1;
}
2010-05-29 07:25:38 +00:00
bool CloseMapFile = false;
2010-09-12 14:56:13 +00:00
if(MapFile)
io_seek(MapFile, 0, IOSEEK_START);
2020-01-03 09:12:37 +00:00
char aSha256[SHA256_MAXSTRSIZE];
if(pSha256)
sha256_str(*pSha256, aSha256, sizeof(aSha256));
if(!pMapData && !MapFile)
2010-05-29 07:25:38 +00:00
{
// open mapfile
char aMapFilename[128];
// try the downloaded maps
2020-01-03 09:12:37 +00:00
if(pSha256)
{
str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%08x_%s.map", pMap, Crc, aSha256);
MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_READ, IStorage::TYPE_ALL);
}
if(!MapFile)
{
// try the downloaded maps without sha
str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%08x.map", pMap, Crc);
MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_READ, IStorage::TYPE_ALL);
}
if(!MapFile)
{
// try the normal maps folder
str_format(aMapFilename, sizeof(aMapFilename), "maps/%s.map", pMap);
MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_READ, IStorage::TYPE_ALL);
}
if(!MapFile)
{
// search for the map within subfolders
char aBuf[512];
str_format(aMapFilename, sizeof(aMapFilename), "%s.map", pMap);
if(pStorage->FindFile(aMapFilename, "maps", IStorage::TYPE_ALL, aBuf, sizeof(aBuf)))
MapFile = pStorage->OpenFile(aBuf, IOFLAG_READ, IStorage::TYPE_ALL);
}
if(!MapFile)
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Unable to open mapfile '%s'", pMap);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf);
}
return -1;
}
CloseMapFile = true;
2010-05-29 07:25:38 +00:00
}
2020-02-19 12:37:31 +00:00
if(MapFile)
MapSize = io_length(MapFile);
2010-05-29 07:25:38 +00:00
// write header
mem_zero(&Header, sizeof(Header));
mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker));
2010-09-12 14:56:13 +00:00
Header.m_Version = gs_ActVersion;
2010-05-29 07:25:38 +00:00
str_copy(Header.m_aNetversion, pNetVersion, sizeof(Header.m_aNetversion));
str_copy(Header.m_aMapName, pMap, sizeof(Header.m_aMapName));
Header.m_aMapSize[0] = (MapSize>>24)&0xff;
Header.m_aMapSize[1] = (MapSize>>16)&0xff;
Header.m_aMapSize[2] = (MapSize>>8)&0xff;
Header.m_aMapSize[3] = (MapSize)&0xff;
Header.m_aMapCrc[0] = (Crc>>24)&0xff;
Header.m_aMapCrc[1] = (Crc>>16)&0xff;
Header.m_aMapCrc[2] = (Crc>>8)&0xff;
Header.m_aMapCrc[3] = (Crc)&0xff;
2010-05-29 07:25:38 +00:00
str_copy(Header.m_aType, pType, sizeof(Header.m_aType));
// Header.m_Length - add this on stop
str_timestamp(Header.m_aTimestamp, sizeof(Header.m_aTimestamp));
io_write(DemoFile, &Header, sizeof(Header));
io_write(DemoFile, &TimelineMarkers, sizeof(TimelineMarkers)); // fill this on stop
2019-12-17 14:44:54 +00:00
//Write Sha256
io_write(DemoFile, SHA256_EXTENSION.m_aData, sizeof(SHA256_EXTENSION.m_aData));
2020-01-03 09:12:37 +00:00
io_write(DemoFile, pSha256, sizeof(SHA256_DIGEST));
if(m_NoMapData)
{
}
else if(pMapData)
2010-09-12 14:56:13 +00:00
{
io_write(DemoFile, pMapData, MapSize);
}
else
{
// write map data
while(1)
{
unsigned char aChunk[1024*64];
int Bytes = io_read(MapFile, &aChunk, sizeof(aChunk));
if(Bytes <= 0)
break;
io_write(DemoFile, &aChunk, Bytes);
}
if(CloseMapFile)
io_close(MapFile);
else
io_seek(MapFile, 0, IOSEEK_START);
}
2010-05-29 07:25:38 +00:00
m_LastKeyFrame = -1;
m_LastTickMarker = -1;
m_FirstTick = -1;
m_NumTimelineMarkers = 0;
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Recording to '%s'", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf);
}
m_File = DemoFile;
2019-05-20 21:55:40 +00:00
str_copy(m_aCurrentFilename, pFilename, sizeof(m_aCurrentFilename));
2010-05-29 07:25:38 +00:00
return 0;
}
/*
Tickmarker
7 = Always set
6 = Keyframe flag
0-5 = Delta tick
2010-05-29 07:25:38 +00:00
Normal
7 = Not set
5-6 = Type
0-4 = Size
2010-05-29 07:25:38 +00:00
*/
enum
{
CHUNKTYPEFLAG_TICKMARKER = 0x80,
CHUNKTICKFLAG_KEYFRAME = 0x40, // only when tickmarker is set
CHUNKTICKFLAG_TICK_COMPRESSED = 0x20, // when we store the tick value in the first chunk
CHUNKMASK_TICK = 0x1f,
CHUNKMASK_TICK_LEGACY = 0x3f,
2010-05-29 07:25:38 +00:00
CHUNKMASK_TYPE = 0x60,
CHUNKMASK_SIZE = 0x1f,
2010-05-29 07:25:38 +00:00
CHUNKTYPE_SNAPSHOT = 1,
CHUNKTYPE_MESSAGE = 2,
CHUNKTYPE_DELTA = 3,
CHUNKFLAG_BIGSIZE = 0x10
};
void CDemoRecorder::WriteTickMarker(int Tick, int Keyframe)
{
if(m_LastTickMarker == -1 || Tick-m_LastTickMarker > CHUNKMASK_TICK || Keyframe)
2010-05-29 07:25:38 +00:00
{
unsigned char aChunk[5];
aChunk[0] = CHUNKTYPEFLAG_TICKMARKER;
aChunk[1] = (Tick>>24)&0xff;
aChunk[2] = (Tick>>16)&0xff;
aChunk[3] = (Tick>>8)&0xff;
aChunk[4] = (Tick)&0xff;
if(Keyframe)
aChunk[0] |= CHUNKTICKFLAG_KEYFRAME;
2010-05-29 07:25:38 +00:00
io_write(m_File, aChunk, sizeof(aChunk));
}
else
{
unsigned char aChunk[1];
aChunk[0] = CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_TICK_COMPRESSED | (Tick-m_LastTickMarker);
2010-05-29 07:25:38 +00:00
io_write(m_File, aChunk, sizeof(aChunk));
}
2010-05-29 07:25:38 +00:00
m_LastTickMarker = Tick;
if(m_FirstTick < 0)
m_FirstTick = Tick;
2010-05-29 07:25:38 +00:00
}
void CDemoRecorder::Write(int Type, const void *pData, int Size)
{
char aBuffer[64*1024];
char aBuffer2[64*1024];
unsigned char aChunk[3];
2010-05-29 07:25:38 +00:00
if(!m_File)
return;
2013-08-04 02:24:33 +00:00
if(Size > 64*1024)
return;
2010-05-29 07:25:38 +00:00
/* pad the data with 0 so we get an alignment of 4,
else the compression won't work and miss some bytes */
mem_copy(aBuffer2, pData, Size);
while(Size&3)
aBuffer2[Size++] = 0;
2017-09-16 17:30:08 +00:00
Size = CVariableInt::Compress(aBuffer2, Size, aBuffer, sizeof(aBuffer)); // buffer2 -> buffer
if(Size < 0)
return;
2010-05-29 07:25:38 +00:00
Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2)); // buffer -> buffer2
2017-09-16 17:30:08 +00:00
if(Size < 0)
return;
2010-05-29 07:25:38 +00:00
aChunk[0] = ((Type&0x3)<<5);
if(Size < 30)
{
aChunk[0] |= Size;
io_write(m_File, aChunk, 1);
}
else
{
if(Size < 256)
{
aChunk[0] |= 30;
aChunk[1] = Size&0xff;
io_write(m_File, aChunk, 2);
}
else
{
aChunk[0] |= 31;
aChunk[1] = Size&0xff;
aChunk[2] = Size>>8;
io_write(m_File, aChunk, 3);
}
}
2010-05-29 07:25:38 +00:00
io_write(m_File, aBuffer2, Size);
}
void CDemoRecorder::RecordSnapshot(int Tick, const void *pData, int Size)
{
if(m_LastKeyFrame == -1 || (Tick-m_LastKeyFrame) > SERVER_TICK_SPEED*5)
{
// write full tickmarker
WriteTickMarker(Tick, 1);
2010-05-29 07:25:38 +00:00
// write snapshot
Write(CHUNKTYPE_SNAPSHOT, pData, Size);
2010-05-29 07:25:38 +00:00
m_LastKeyFrame = Tick;
mem_copy(m_aLastSnapshotData, pData, Size);
}
else
{
// create delta, prepend tick
char aDeltaData[CSnapshot::MAX_SIZE+sizeof(int)];
int DeltaSize;
// write tickmarker
WriteTickMarker(Tick, 0);
2010-05-29 07:25:38 +00:00
DeltaSize = m_pSnapshotDelta->CreateDelta((CSnapshot*)m_aLastSnapshotData, (CSnapshot*)pData, &aDeltaData);
if(DeltaSize)
{
// record delta
Write(CHUNKTYPE_DELTA, aDeltaData, DeltaSize);
mem_copy(m_aLastSnapshotData, pData, Size);
}
}
}
void CDemoRecorder::RecordMessage(const void *pData, int Size)
{
if(m_pfnFilter)
{
if(m_pfnFilter(pData, Size, m_pUser))
{
return;
}
}
2010-05-29 07:25:38 +00:00
Write(CHUNKTYPE_MESSAGE, pData, Size);
}
int CDemoRecorder::Stop()
2010-05-29 07:25:38 +00:00
{
if(!m_File)
return -1;
// add the demo length to the header
io_seek(m_File, gs_LengthOffset, IOSEEK_START);
int DemoLength = Length();
char aLength[4];
aLength[0] = (DemoLength>>24)&0xff;
aLength[1] = (DemoLength>>16)&0xff;
aLength[2] = (DemoLength>>8)&0xff;
aLength[3] = (DemoLength)&0xff;
io_write(m_File, aLength, sizeof(aLength));
// add the timeline markers to the header
io_seek(m_File, gs_NumMarkersOffset, IOSEEK_START);
char aNumMarkers[4];
aNumMarkers[0] = (m_NumTimelineMarkers>>24)&0xff;
aNumMarkers[1] = (m_NumTimelineMarkers>>16)&0xff;
aNumMarkers[2] = (m_NumTimelineMarkers>>8)&0xff;
aNumMarkers[3] = (m_NumTimelineMarkers)&0xff;
io_write(m_File, aNumMarkers, sizeof(aNumMarkers));
for(int i = 0; i < m_NumTimelineMarkers; i++)
{
int Marker = m_aTimelineMarkers[i];
char aMarker[4];
aMarker[0] = (Marker>>24)&0xff;
aMarker[1] = (Marker>>16)&0xff;
aMarker[2] = (Marker>>8)&0xff;
aMarker[3] = (Marker)&0xff;
io_write(m_File, aMarker, sizeof(aMarker));
}
2010-05-29 07:25:38 +00:00
io_close(m_File);
m_File = 0;
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Stopped recording");
2010-05-29 07:25:38 +00:00
return 0;
}
void CDemoRecorder::AddDemoMarker()
{
if(m_LastTickMarker < 0 || m_NumTimelineMarkers >= MAX_TIMELINE_MARKERS)
return;
// not more than 1 marker in a second
if(m_NumTimelineMarkers > 0)
{
int Diff = m_LastTickMarker - m_aTimelineMarkers[m_NumTimelineMarkers-1];
if(Diff < SERVER_TICK_SPEED*1.0f)
return;
}
m_aTimelineMarkers[m_NumTimelineMarkers++] = m_LastTickMarker;
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Added timeline marker");
}
2010-05-29 07:25:38 +00:00
CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta)
{
m_File = 0;
m_pKeyFrames = 0;
m_SpeedIndex = 4;
2010-05-29 07:25:38 +00:00
m_TickTime = 0;
m_Time = 0;
2010-05-29 07:25:38 +00:00
m_pSnapshotDelta = pSnapshotDelta;
m_LastSnapshotDataSize = -1;
}
void CDemoPlayer::SetListener(IListener *pListener)
2010-05-29 07:25:38 +00:00
{
m_pListener = pListener;
2010-05-29 07:25:38 +00:00
}
int CDemoPlayer::ReadChunkHeader(int *pType, int *pSize, int *pTick)
{
unsigned char Chunk = 0;
2010-05-29 07:25:38 +00:00
*pSize = 0;
*pType = 0;
if(m_File == NULL)
return -1;
2010-05-29 07:25:38 +00:00
if(io_read(m_File, &Chunk, sizeof(Chunk)) != sizeof(Chunk))
return -1;
2010-05-29 07:25:38 +00:00
if(Chunk&CHUNKTYPEFLAG_TICKMARKER)
{
// decode tick marker
int Tickdelta_legacy = Chunk&(CHUNKMASK_TICK_LEGACY); // compatibility
2010-05-29 07:25:38 +00:00
*pType = Chunk&(CHUNKTYPEFLAG_TICKMARKER|CHUNKTICKFLAG_KEYFRAME);
if(m_Info.m_Header.m_Version < gs_VersionTickCompression && Tickdelta_legacy != 0)
{
*pTick += Tickdelta_legacy;
}
else if(Chunk&(CHUNKTICKFLAG_TICK_COMPRESSED))
{
int Tickdelta = Chunk&(CHUNKMASK_TICK);
*pTick += Tickdelta;
}
else
2010-05-29 07:25:38 +00:00
{
unsigned char aTickdata[4];
if(io_read(m_File, aTickdata, sizeof(aTickdata)) != sizeof(aTickdata))
return -1;
*pTick = (aTickdata[0]<<24) | (aTickdata[1]<<16) | (aTickdata[2]<<8) | aTickdata[3];
}
2010-05-29 07:25:38 +00:00
}
else
{
// decode normal chunk
*pType = (Chunk&CHUNKMASK_TYPE)>>5;
*pSize = Chunk&CHUNKMASK_SIZE;
2010-05-29 07:25:38 +00:00
if(*pSize == 30)
{
unsigned char aSizedata[1];
if(io_read(m_File, aSizedata, sizeof(aSizedata)) != sizeof(aSizedata))
return -1;
*pSize = aSizedata[0];
2010-05-29 07:25:38 +00:00
}
else if(*pSize == 31)
{
unsigned char aSizedata[2];
if(io_read(m_File, aSizedata, sizeof(aSizedata)) != sizeof(aSizedata))
return -1;
*pSize = (aSizedata[1]<<8) | aSizedata[0];
}
}
2010-05-29 07:25:38 +00:00
return 0;
}
void CDemoPlayer::ScanFile()
{
long StartPos;
CHeap Heap;
CKeyFrameSearch *pFirstKey = 0;
CKeyFrameSearch *pCurrentKey = 0;
//DEMOREC_CHUNK chunk;
int ChunkSize, ChunkType, ChunkTick = 0;
int i;
StartPos = io_tell(m_File);
m_Info.m_SeekablePoints = 0;
while(1)
{
long CurrentPos = io_tell(m_File);
2010-05-29 07:25:38 +00:00
if(ReadChunkHeader(&ChunkType, &ChunkSize, &ChunkTick))
break;
2010-05-29 07:25:38 +00:00
// read the chunk
if(ChunkType&CHUNKTYPEFLAG_TICKMARKER)
{
if(ChunkType&CHUNKTICKFLAG_KEYFRAME)
{
CKeyFrameSearch *pKey;
2010-05-29 07:25:38 +00:00
// save the position
pKey = (CKeyFrameSearch *)Heap.Allocate(sizeof(CKeyFrameSearch));
pKey->m_Frame.m_Filepos = CurrentPos;
pKey->m_Frame.m_Tick = ChunkTick;
pKey->m_pNext = 0;
if(pCurrentKey)
pCurrentKey->m_pNext = pKey;
if(!pFirstKey)
pFirstKey = pKey;
pCurrentKey = pKey;
m_Info.m_SeekablePoints++;
}
2010-05-29 07:25:38 +00:00
if(m_Info.m_Info.m_FirstTick == -1)
m_Info.m_Info.m_FirstTick = ChunkTick;
m_Info.m_Info.m_LastTick = ChunkTick;
}
else if(ChunkSize)
io_skip(m_File, ChunkSize);
2010-05-29 07:25:38 +00:00
}
// copy all the frames to an array instead for fast access
m_pKeyFrames = (CKeyFrame *)calloc(m_Info.m_SeekablePoints, sizeof(CKeyFrame));
2010-05-29 07:25:38 +00:00
for(pCurrentKey = pFirstKey, i = 0; pCurrentKey; pCurrentKey = pCurrentKey->m_pNext, i++)
m_pKeyFrames[i] = pCurrentKey->m_Frame;
2010-05-29 07:25:38 +00:00
// destroy the temporary heap and seek back to the start
io_seek(m_File, StartPos, IOSEEK_START);
}
void CDemoPlayer::DoTick()
{
static char aCompresseddata[CSnapshot::MAX_SIZE];
static char aDecompressed[CSnapshot::MAX_SIZE];
static char aData[CSnapshot::MAX_SIZE];
int ChunkType, ChunkTick, ChunkSize;
int DataSize = 0;
2010-05-29 07:25:38 +00:00
int GotSnapshot = 0;
// update ticks
m_Info.m_PreviousTick = m_Info.m_Info.m_CurrentTick;
m_Info.m_Info.m_CurrentTick = m_Info.m_NextTick;
ChunkTick = m_Info.m_Info.m_CurrentTick;
while(1)
{
if(ReadChunkHeader(&ChunkType, &ChunkSize, &ChunkTick))
{
// stop on error or eof
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "end of file");
2019-09-26 15:28:29 +00:00
#if defined(CONF_VIDEORECORDER)
if (IVideo::Current())
Stop();
2019-09-26 15:28:29 +00:00
#endif
if(m_Info.m_PreviousTick == -1)
{
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", "empty demo");
Stop();
}
else
Pause();
2010-05-29 07:25:38 +00:00
break;
}
2010-05-29 07:25:38 +00:00
// read the chunk
if(ChunkSize)
{
if(io_read(m_File, aCompresseddata, ChunkSize) != (unsigned)ChunkSize)
{
// stop on error or eof
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "error reading chunk");
2010-05-29 07:25:38 +00:00
Stop();
break;
}
2010-05-29 07:25:38 +00:00
DataSize = CNetBase::Decompress(aCompresseddata, ChunkSize, aDecompressed, sizeof(aDecompressed));
if(DataSize < 0)
{
// stop on error or eof
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "error during network decompression");
2010-05-29 07:25:38 +00:00
Stop();
break;
}
2017-09-16 17:30:08 +00:00
DataSize = CVariableInt::Decompress(aDecompressed, DataSize, aData, sizeof(aData));
2010-05-29 07:25:38 +00:00
if(DataSize < 0)
{
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "error during intpack decompression");
2010-05-29 07:25:38 +00:00
Stop();
break;
}
}
2010-05-29 07:25:38 +00:00
if(ChunkType == CHUNKTYPE_DELTA)
{
// process delta snapshot
static char aNewsnap[CSnapshot::MAX_SIZE];
2010-05-29 07:25:38 +00:00
GotSnapshot = 1;
2010-05-29 07:25:38 +00:00
DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot*)m_aLastSnapshotData, (CSnapshot*)aNewsnap, aData, DataSize);
2010-05-29 07:25:38 +00:00
if(DataSize >= 0)
{
if(m_pListener)
m_pListener->OnDemoPlayerSnapshot(aNewsnap, DataSize);
2010-05-29 07:25:38 +00:00
m_LastSnapshotDataSize = DataSize;
mem_copy(m_aLastSnapshotData, aNewsnap, DataSize);
}
else
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "error during unpacking of delta, err=%d", DataSize);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
}
}
2010-05-29 07:25:38 +00:00
}
else if(ChunkType == CHUNKTYPE_SNAPSHOT)
{
// process full snapshot
GotSnapshot = 1;
2010-05-29 07:25:38 +00:00
m_LastSnapshotDataSize = DataSize;
mem_copy(m_aLastSnapshotData, aData, DataSize);
if(m_pListener)
m_pListener->OnDemoPlayerSnapshot(aData, DataSize);
2010-05-29 07:25:38 +00:00
}
else
{
// if there were no snapshots in this tick, replay the last one
if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
2010-05-29 07:25:38 +00:00
{
GotSnapshot = 1;
m_pListener->OnDemoPlayerSnapshot(m_aLastSnapshotData, m_LastSnapshotDataSize);
2010-05-29 07:25:38 +00:00
}
2010-05-29 07:25:38 +00:00
// check the remaining types
if(ChunkType&CHUNKTYPEFLAG_TICKMARKER)
{
m_Info.m_NextTick = ChunkTick;
break;
}
else if(ChunkType == CHUNKTYPE_MESSAGE)
{
if(m_pListener)
m_pListener->OnDemoPlayerMessage(aData, DataSize);
2010-05-29 07:25:38 +00:00
}
}
}
}
void CDemoPlayer::Pause()
{
m_Info.m_Info.m_Paused = 1;
#if defined(CONF_VIDEORECORDER)
if(IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
IVideo::Current()->pause(true);
#endif
2010-05-29 07:25:38 +00:00
}
void CDemoPlayer::Unpause()
{
if(m_Info.m_Info.m_Paused)
{
/*m_Info.start_tick = m_Info.current_tick;
m_Info.start_time = time_get();*/
m_Info.m_Info.m_Paused = 0;
}
#if defined(CONF_VIDEORECORDER)
if(IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
IVideo::Current()->pause(false);
#endif
2010-05-29 07:25:38 +00:00
}
2010-10-06 21:07:35 +00:00
int CDemoPlayer::Load(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, int StorageType)
2010-05-29 07:25:38 +00:00
{
m_pConsole = pConsole;
2010-10-06 21:07:35 +00:00
m_File = pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType);
2010-05-29 07:25:38 +00:00
if(!m_File)
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "could not open '%s'", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", aBuf);
}
2010-05-29 07:25:38 +00:00
return -1;
}
// store the filename
str_copy(m_aFilename, pFilename, sizeof(m_aFilename));
2010-05-29 07:25:38 +00:00
// clear the playback info
mem_zero(&m_Info, sizeof(m_Info));
m_Info.m_Info.m_FirstTick = -1;
m_Info.m_Info.m_LastTick = -1;
m_Info.m_NextTick = -1;
m_Info.m_Info.m_CurrentTick = -1;
m_Info.m_PreviousTick = -1;
m_Info.m_Info.m_Speed = 1;
m_SpeedIndex = 4;
2010-05-29 07:25:38 +00:00
m_LastSnapshotDataSize = -1;
// read the header
io_read(m_File, &m_Info.m_Header, sizeof(m_Info.m_Header));
2010-09-12 11:03:54 +00:00
if(mem_comp(m_Info.m_Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0)
2010-05-29 07:25:38 +00:00
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "'%s' is not a demo file", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", aBuf);
}
2010-05-29 07:25:38 +00:00
io_close(m_File);
m_File = 0;
return -1;
}
if(m_Info.m_Header.m_Version < gs_OldVersion)
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "demo version %d is not supported", m_Info.m_Header.m_Version);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", aBuf);
}
io_close(m_File);
m_File = 0;
return -1;
}
else if(m_Info.m_Header.m_Version > gs_OldVersion)
io_read(m_File, &m_Info.m_TimelineMarkers, sizeof(m_Info.m_TimelineMarkers));
2019-12-17 14:44:54 +00:00
SHA256_DIGEST Sha256 = SHA256_ZEROED;
if(m_Info.m_Header.m_Version >= gs_Sha256Version)
{
CUuid ExtensionUuid = {};
io_read(m_File, &ExtensionUuid.m_aData, sizeof(ExtensionUuid.m_aData));
if(ExtensionUuid == SHA256_EXTENSION)
{
io_read(m_File, &Sha256, sizeof(SHA256_DIGEST)); // need a safe read
}
else
{
// This hopes whatever happened during the version increment didn't add something here
dbg_msg("demo", "demo version incremented, but not by ddnet");
io_seek(m_File, -(int)sizeof(ExtensionUuid.m_aData), IOSEEK_CUR);
}
}
2019-10-14 00:27:08 +00:00
// get demo type
if(!str_comp(m_Info.m_Header.m_aType, "client"))
2011-03-28 20:08:52 +00:00
m_DemoType = DEMOTYPE_CLIENT;
else if(!str_comp(m_Info.m_Header.m_aType, "server"))
m_DemoType = DEMOTYPE_SERVER;
2011-03-28 20:08:52 +00:00
else
m_DemoType = DEMOTYPE_INVALID;
// read map
unsigned MapSize = (m_Info.m_Header.m_aMapSize[0]<<24) | (m_Info.m_Header.m_aMapSize[1]<<16) | (m_Info.m_Header.m_aMapSize[2]<<8) | (m_Info.m_Header.m_aMapSize[3]);
// check if we already have the map
// TODO: improve map checking (maps folder, check crc)
unsigned Crc = (m_Info.m_Header.m_aMapCrc[0]<<24) | (m_Info.m_Header.m_aMapCrc[1]<<16) | (m_Info.m_Header.m_aMapCrc[2]<<8) | (m_Info.m_Header.m_aMapCrc[3]);
// save byte offset of map for later use
m_MapOffset = io_tell(m_File);
io_skip(m_File, MapSize);
2018-07-10 09:29:02 +00:00
// store map information
2014-08-12 14:21:06 +00:00
m_MapInfo.m_Crc = Crc;
2019-12-17 14:44:54 +00:00
m_MapInfo.m_Sha256 = Sha256;
2014-08-12 14:21:06 +00:00
m_MapInfo.m_Size = MapSize;
str_copy(m_MapInfo.m_aName, m_Info.m_Header.m_aMapName, sizeof(m_MapInfo.m_aName));
if(m_Info.m_Header.m_Version > gs_OldVersion)
{
// get timeline markers
int Num = ((m_Info.m_TimelineMarkers.m_aNumTimelineMarkers[0]<<24)&0xFF000000) | ((m_Info.m_TimelineMarkers.m_aNumTimelineMarkers[1]<<16)&0xFF0000) |
((m_Info.m_TimelineMarkers.m_aNumTimelineMarkers[2]<<8)&0xFF00) | (m_Info.m_TimelineMarkers.m_aNumTimelineMarkers[3]&0xFF);
2019-04-26 19:36:49 +00:00
m_Info.m_Info.m_NumTimelineMarkers = minimum(Num, (int)MAX_TIMELINE_MARKERS);
for(int i = 0; i < m_Info.m_Info.m_NumTimelineMarkers; i++)
{
char *pTimelineMarker = m_Info.m_TimelineMarkers.m_aTimelineMarkers[i];
m_Info.m_Info.m_aTimelineMarkers[i] = ((pTimelineMarker[0]<<24)&0xFF000000) | ((pTimelineMarker[1]<<16)&0xFF0000) |
((pTimelineMarker[2]<<8)&0xFF00) | (pTimelineMarker[3]&0xFF);
}
}
2018-07-10 09:29:02 +00:00
// scan the file for interesting points
2010-05-29 07:25:38 +00:00
ScanFile();
// reset slice markers
g_Config.m_ClDemoSliceBegin = -1;
g_Config.m_ClDemoSliceEnd = -1;
2010-05-29 07:25:38 +00:00
// ready for playback
return 0;
}
bool CDemoPlayer::ExtractMap(class IStorage *pStorage)
{
if(!m_MapInfo.m_Size)
return false;
long CurSeek = io_tell(m_File);
// get map data
io_seek(m_File, m_MapOffset, IOSEEK_START);
unsigned char *pMapData = (unsigned char *)malloc(m_MapInfo.m_Size);
io_read(m_File, pMapData, m_MapInfo.m_Size);
io_seek(m_File, CurSeek, IOSEEK_START);
2019-10-14 00:27:08 +00:00
// handle sha256
2019-12-17 14:44:54 +00:00
SHA256_DIGEST Sha256 = SHA256_ZEROED;
if(m_Info.m_Header.m_Version >= gs_Sha256Version)
Sha256 = m_MapInfo.m_Sha256;
2019-10-14 00:27:08 +00:00
else
{
2019-12-17 14:44:54 +00:00
Sha256 = sha256(pMapData, m_MapInfo.m_Size);
m_MapInfo.m_Sha256 = Sha256;
2019-10-14 00:27:08 +00:00
}
// construct name
char aSha[SHA256_MAXSTRSIZE], aMapFilename[128];
2019-12-17 14:44:54 +00:00
sha256_str(Sha256, aSha, sizeof(aSha));
str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%08x_%s.map", m_Info.m_Header.m_aMapName, m_MapInfo.m_Crc, aSha);
// save map
IOHANDLE MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!MapFile)
return false;
io_write(MapFile, pMapData, m_MapInfo.m_Size);
io_close(MapFile);
// free data
free(pMapData);
return true;
}
2010-05-29 07:25:38 +00:00
int CDemoPlayer::NextFrame()
{
DoTick();
return IsPlaying();
}
2017-10-26 20:31:02 +00:00
int64 CDemoPlayer::time()
{
#if defined(CONF_VIDEORECORDER)
static bool s_Recording = false;
if (IVideo::Current())
{
if (!s_Recording)
{
s_Recording = true;
m_Info.m_LastUpdate = IVideo::time();
}
return IVideo::time();
}
else
{
int64 Now = time_get();
if (s_Recording)
{
s_Recording = false;
m_Info.m_LastUpdate = Now;
}
return Now;
}
#else
return time_get();
#endif
}
2010-05-29 07:25:38 +00:00
int CDemoPlayer::Play()
{
// fill in previous and next tick
while(m_Info.m_PreviousTick == -1 && IsPlaying())
DoTick();
2010-05-29 07:25:38 +00:00
// set start info
/*m_Info.start_tick = m_Info.previous_tick;
m_Info.start_time = time_get();*/
m_Info.m_CurrentTime = m_Info.m_PreviousTick*time_freq()/SERVER_TICK_SPEED;
m_Info.m_LastUpdate = time();
2010-05-29 07:25:38 +00:00
return 0;
}
2019-04-15 18:39:39 +00:00
int CDemoPlayer::SeekPercent(float Percent)
{
int WantedTick = m_Info.m_Info.m_FirstTick + ((m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick) * Percent);
return SetPos(WantedTick);
}
int CDemoPlayer::SeekTime(float Seconds)
{
int WantedTick = m_Info.m_Info.m_CurrentTick + (Seconds * SERVER_TICK_SPEED);
return SetPos(WantedTick);
}
int CDemoPlayer::SetPos(int WantedTick)
2010-05-29 07:25:38 +00:00
{
if(!m_File)
return -1;
2010-05-29 07:25:38 +00:00
// -5 because we have to have a current tick and previous tick when we do the playback
2019-04-15 18:39:39 +00:00
WantedTick = clamp(WantedTick, m_Info.m_Info.m_FirstTick, m_Info.m_Info.m_LastTick) - 5;
2010-05-29 07:25:38 +00:00
// get correct key frame
2019-04-15 18:39:39 +00:00
int KeyFrame = 0;
while(KeyFrame < m_Info.m_SeekablePoints - 1 && m_pKeyFrames[KeyFrame].m_Tick < WantedTick)
{
KeyFrame++;
}
while(KeyFrame > 0 && m_pKeyFrames[KeyFrame].m_Tick > WantedTick)
{
KeyFrame--;
}
2019-04-15 18:39:39 +00:00
// seek to the correct key frame
io_seek(m_File, m_pKeyFrames[KeyFrame].m_Filepos, IOSEEK_START);
2010-05-29 07:25:38 +00:00
m_Info.m_NextTick = -1;
m_Info.m_Info.m_CurrentTick = -1;
m_Info.m_PreviousTick = -1;
// playback everything until we hit our tick
2014-08-31 01:13:42 +00:00
while(m_Info.m_PreviousTick < WantedTick && IsPlaying())
2010-05-29 07:25:38 +00:00
DoTick();
2010-05-29 07:25:38 +00:00
Play();
2010-05-29 07:25:38 +00:00
return 0;
}
void CDemoPlayer::SetSpeed(float Speed)
{
2019-04-02 21:16:01 +00:00
m_Info.m_Info.m_Speed = clamp(Speed, 0.f, 256.f);
2010-05-29 07:25:38 +00:00
}
void CDemoPlayer::SetSpeedIndex(int Offset)
{
m_SpeedIndex = clamp(m_SpeedIndex + Offset, 0, (int)(sizeof(g_aSpeeds)/sizeof(g_aSpeeds[0])-1));
SetSpeed(g_aSpeeds[m_SpeedIndex]);
}
2014-08-12 14:21:06 +00:00
int CDemoPlayer::Update(bool RealTime)
2010-05-29 07:25:38 +00:00
{
int64 Now = time();
2010-05-29 07:25:38 +00:00
int64 Deltatime = Now-m_Info.m_LastUpdate;
m_Info.m_LastUpdate = Now;
2010-05-29 07:25:38 +00:00
if(!IsPlaying())
return 0;
2010-05-29 07:25:38 +00:00
if(m_Info.m_Info.m_Paused)
{
2010-05-29 07:25:38 +00:00
}
else
{
int64 Freq = time_freq();
m_Info.m_CurrentTime += (int64)(Deltatime*(double)m_Info.m_Info.m_Speed);
2010-05-29 07:25:38 +00:00
while(1)
{
int64 CurtickStart = (m_Info.m_Info.m_CurrentTick)*Freq/SERVER_TICK_SPEED;
// break if we are ready
2014-08-12 14:21:06 +00:00
if(RealTime && CurtickStart > m_Info.m_CurrentTime)
2010-05-29 07:25:38 +00:00
break;
2010-05-29 07:25:38 +00:00
// do one more tick
DoTick();
2010-05-29 07:25:38 +00:00
if(m_Info.m_Info.m_Paused)
return 0;
}
// update intratick
{
2010-05-29 07:25:38 +00:00
int64 CurtickStart = (m_Info.m_Info.m_CurrentTick)*Freq/SERVER_TICK_SPEED;
int64 PrevtickStart = (m_Info.m_PreviousTick)*Freq/SERVER_TICK_SPEED;
m_Info.m_IntraTick = (m_Info.m_CurrentTime - PrevtickStart) / (float)(CurtickStart-PrevtickStart);
m_Info.m_TickTime = (m_Info.m_CurrentTime - PrevtickStart) / (float)Freq;
}
2010-05-29 07:25:38 +00:00
if(m_Info.m_Info.m_CurrentTick == m_Info.m_PreviousTick ||
m_Info.m_Info.m_CurrentTick == m_Info.m_NextTick)
{
if(m_pConsole)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "tick error prev=%d cur=%d next=%d",
m_Info.m_PreviousTick, m_Info.m_Info.m_CurrentTick, m_Info.m_NextTick);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
}
2010-05-29 07:25:38 +00:00
}
m_Time += m_TickTime;
2010-05-29 07:25:38 +00:00
}
2010-05-29 07:25:38 +00:00
return 0;
}
int CDemoPlayer::Stop()
{
#if defined(CONF_VIDEORECORDER)
if (IVideo::Current())
IVideo::Current()->stop();
#endif
2010-05-29 07:25:38 +00:00
if(!m_File)
return -1;
if(m_pConsole)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", "Stopped playback");
2010-05-29 07:25:38 +00:00
io_close(m_File);
m_File = 0;
free(m_pKeyFrames);
2010-05-29 07:25:38 +00:00
m_pKeyFrames = 0;
str_copy(m_aFilename, "", sizeof(m_aFilename));
2010-05-29 07:25:38 +00:00
return 0;
}
void CDemoPlayer::GetDemoName(char *pBuffer, int BufferSize) const
{
const char *pFileName = m_aFilename;
const char *pExtractedName = pFileName;
const char *pEnd = 0;
for(; *pFileName; ++pFileName)
{
if(*pFileName == '/' || *pFileName == '\\')
pExtractedName = pFileName+1;
else if(*pFileName == '.')
pEnd = pFileName;
}
2011-08-11 08:59:14 +00:00
2019-04-26 19:36:49 +00:00
int Length = pEnd > pExtractedName ? minimum(BufferSize, (int)(pEnd-pExtractedName+1)) : BufferSize;
str_copy(pBuffer, pExtractedName, Length);
}
2010-05-29 07:25:38 +00:00
2019-10-14 00:27:08 +00:00
bool CDemoPlayer::GetDemoInfo(class IStorage *pStorage, const char *pFilename, int StorageType, CDemoHeader *pDemoHeader, CTimelineMarkers *pTimelineMarkers, CMapInfo *pMapInfo) const
{
2019-10-14 00:27:08 +00:00
if(!pDemoHeader || !pTimelineMarkers || !pMapInfo)
return false;
mem_zero(pDemoHeader, sizeof(CDemoHeader));
mem_zero(pTimelineMarkers, sizeof(CTimelineMarkers));
IOHANDLE File = pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType);
if(!File)
return false;
io_read(File, pDemoHeader, sizeof(CDemoHeader));
io_read(File, pTimelineMarkers, sizeof(CTimelineMarkers));
2019-10-14 00:27:08 +00:00
str_copy(pMapInfo->m_aName, pDemoHeader->m_aMapName, sizeof(pMapInfo->m_aName));
pMapInfo->m_Crc = (pDemoHeader->m_aMapCrc[0]<<24) | (pDemoHeader->m_aMapCrc[1]<<16) | (pDemoHeader->m_aMapCrc[2]<<8) | (pDemoHeader->m_aMapCrc[3]);
2019-12-22 16:09:14 +00:00
SHA256_DIGEST Sha256 = SHA256_ZEROED;
2019-12-17 14:44:54 +00:00
if(pDemoHeader->m_Version >= gs_Sha256Version)
2019-12-22 16:09:14 +00:00
{
CUuid ExtensionUuid = {};
io_read(File, &ExtensionUuid.m_aData, sizeof(ExtensionUuid.m_aData));
if(ExtensionUuid == SHA256_EXTENSION)
{
io_read(File, &Sha256, sizeof(SHA256_DIGEST)); // need a safe read
}
else
{
// This hopes whatever happened during the version increment didn't add something here
dbg_msg("demo", "demo version incremented, but not by ddnet");
io_seek(File, -(int)sizeof(ExtensionUuid.m_aData), IOSEEK_CUR);
}
}
pMapInfo->m_Sha256 = Sha256;
2019-10-14 00:27:08 +00:00
pMapInfo->m_Size = (pDemoHeader->m_aMapSize[0]<<24) | (pDemoHeader->m_aMapSize[1]<<16) | (pDemoHeader->m_aMapSize[2]<<8) | (pDemoHeader->m_aMapSize[3]);
io_close(File);
return !(mem_comp(pDemoHeader->m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) || pDemoHeader->m_Version < gs_OldVersion);
}
int CDemoPlayer::GetDemoType() const
{
if(m_File)
return m_DemoType;
return DEMOTYPE_INVALID;
}
2014-08-12 14:21:06 +00:00
void CDemoEditor::Init(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage)
{
m_pNetVersion = pNetVersion;
m_pSnapshotDelta = pSnapshotDelta;
m_pConsole = pConsole;
m_pStorage = pStorage;
}
void CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser)
2014-08-12 14:21:06 +00:00
{
class CDemoPlayer DemoPlayer(m_pSnapshotDelta);
class CDemoRecorder DemoRecorder(m_pSnapshotDelta);
m_pDemoPlayer = &DemoPlayer;
m_pDemoRecorder = &DemoRecorder;
m_pDemoPlayer->SetListener(this);
2014-08-12 14:21:06 +00:00
2015-07-09 00:08:14 +00:00
m_SliceFrom = StartTick;
2014-08-12 14:21:06 +00:00
m_SliceTo = EndTick;
m_Stop = false;
if (m_pDemoPlayer->Load(m_pStorage, m_pConsole, pDemo, IStorage::TYPE_ALL) == -1)
return;
2015-07-09 00:08:14 +00:00
2019-10-14 00:27:08 +00:00
const CMapInfo *pMapInfo = m_pDemoPlayer->GetMapInfo();
2019-12-22 16:09:14 +00:00
const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
SHA256_DIGEST Sha256 = pMapInfo->m_Sha256;
if(pInfo->m_Header.m_Version < gs_Sha256Version)
{
2019-12-22 16:09:14 +00:00
if(m_pDemoPlayer->ExtractMap(m_pStorage))
Sha256 = pMapInfo->m_Sha256;
}
2019-12-22 16:09:14 +00:00
2020-01-03 09:12:37 +00:00
if (m_pDemoRecorder->Start(m_pStorage, m_pConsole, pDst, m_pNetVersion, pMapInfo->m_aName, &Sha256, pMapInfo->m_Crc, "client", pMapInfo->m_Size, NULL, NULL, pfnFilter, pUser) == -1)
2014-08-12 14:21:06 +00:00
return;
m_pDemoPlayer->Play();
while (m_pDemoPlayer->IsPlaying() && !m_Stop) {
m_pDemoPlayer->Update(false);
if (pInfo->m_Info.m_Paused)
break;
}
2019-05-20 21:55:40 +00:00
m_pDemoPlayer->Stop();
2014-08-12 14:21:06 +00:00
m_pDemoRecorder->Stop();
}
void CDemoEditor::OnDemoPlayerSnapshot(void *pData, int Size)
{
const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
if (m_SliceTo != -1 && pInfo->m_Info.m_CurrentTick > m_SliceTo)
m_Stop = true;
else if (m_SliceFrom == -1 || pInfo->m_Info.m_CurrentTick >= m_SliceFrom)
m_pDemoRecorder->RecordSnapshot(pInfo->m_Info.m_CurrentTick, pData, Size);
}
void CDemoEditor::OnDemoPlayerMessage(void *pData, int Size)
{
const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
if (m_SliceTo != -1 && pInfo->m_Info.m_CurrentTick > m_SliceTo)
m_Stop = true;
else if (m_SliceFrom == -1 || pInfo->m_Info.m_CurrentTick >= m_SliceFrom)
m_pDemoRecorder->RecordMessage(pData, Size);
}