mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-19 06:28:19 +00:00
9ea665cd69
When the demo recording is not stopped properly (e.g. client crashes during recording), the timeline markers and the demo length are not updated. The length is always set to zero when starting the recording, but uninitialized memory is written for the timeline markers, which causes these demos to likely show the maximum number of markers with some markers possibly being outside the ticks that the demo contains. This is fixed by initially setting all timeline marker data to zero when starting the recording.
1237 lines
32 KiB
C++
1237 lines
32 KiB
C++
/* (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>
|
|
#include <base/system.h>
|
|
|
|
#include <engine/console.h>
|
|
#include <engine/storage.h>
|
|
|
|
#include <engine/shared/config.h>
|
|
|
|
#if defined(CONF_VIDEORECORDER)
|
|
#include <engine/shared/video.h>
|
|
#endif
|
|
|
|
#include "compression.h"
|
|
#include "demo.h"
|
|
#include "memheap.h"
|
|
#include "network.h"
|
|
#include "snapshot.h"
|
|
|
|
const double g_aSpeeds[g_DemoSpeeds] = {0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 20.0, 24.0, 28.0, 32.0, 40.0, 48.0, 56.0, 64.0};
|
|
const CUuid SHA256_EXTENSION =
|
|
{{0x6b, 0xe6, 0xda, 0x4a, 0xce, 0xbd, 0x38, 0x0c,
|
|
0x9b, 0x5b, 0x12, 0x89, 0xc8, 0x42, 0xd7, 0x80}};
|
|
|
|
static const unsigned char gs_aHeaderMarker[7] = {'T', 'W', 'D', 'E', 'M', 'O', 0};
|
|
static const unsigned char gs_CurVersion = 6;
|
|
static const unsigned char gs_OldVersion = 3;
|
|
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;
|
|
|
|
static const ColorRGBA gs_DemoPrintColor{0.75f, 0.7f, 0.7f, 1.0f};
|
|
|
|
CDemoRecorder::CDemoRecorder(class CSnapshotDelta *pSnapshotDelta, bool NoMapData)
|
|
{
|
|
m_File = 0;
|
|
m_aCurrentFilename[0] = '\0';
|
|
m_pfnFilter = 0;
|
|
m_pUser = 0;
|
|
m_LastTickMarker = -1;
|
|
m_pSnapshotDelta = pSnapshotDelta;
|
|
m_NoMapData = NoMapData;
|
|
}
|
|
|
|
// Record
|
|
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 MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser)
|
|
{
|
|
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, gs_DemoPrintColor);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if(m_File)
|
|
{
|
|
io_close(DemoFile);
|
|
return -1;
|
|
}
|
|
|
|
bool CloseMapFile = false;
|
|
|
|
if(MapFile)
|
|
io_seek(MapFile, 0, IOSEEK_START);
|
|
|
|
char aSha256[SHA256_MAXSTRSIZE];
|
|
if(pSha256)
|
|
sha256_str(*pSha256, aSha256, sizeof(aSha256));
|
|
|
|
if(!pMapData && !MapFile)
|
|
{
|
|
// open mapfile
|
|
char aMapFilename[128];
|
|
// try the downloaded maps
|
|
if(pSha256)
|
|
{
|
|
str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%s.map", pMap, aSha256);
|
|
}
|
|
else
|
|
{
|
|
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, gs_DemoPrintColor);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
CloseMapFile = true;
|
|
}
|
|
|
|
if(m_NoMapData)
|
|
MapSize = 0;
|
|
else if(MapFile)
|
|
MapSize = io_length(MapFile);
|
|
|
|
// write header
|
|
CDemoHeader Header;
|
|
mem_zero(&Header, sizeof(Header));
|
|
mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker));
|
|
Header.m_Version = gs_CurVersion;
|
|
str_copy(Header.m_aNetversion, pNetVersion);
|
|
str_copy(Header.m_aMapName, pMap);
|
|
uint_to_bytes_be(Header.m_aMapSize, MapSize);
|
|
uint_to_bytes_be(Header.m_aMapCrc, Crc);
|
|
str_copy(Header.m_aType, pType);
|
|
// Header.m_Length - add this on stop
|
|
str_timestamp(Header.m_aTimestamp, sizeof(Header.m_aTimestamp));
|
|
io_write(DemoFile, &Header, sizeof(Header));
|
|
|
|
CTimelineMarkers TimelineMarkers;
|
|
mem_zero(&TimelineMarkers, sizeof(TimelineMarkers));
|
|
io_write(DemoFile, &TimelineMarkers, sizeof(TimelineMarkers)); // fill this on stop
|
|
|
|
//Write Sha256
|
|
io_write(DemoFile, SHA256_EXTENSION.m_aData, sizeof(SHA256_EXTENSION.m_aData));
|
|
io_write(DemoFile, pSha256, sizeof(SHA256_DIGEST));
|
|
|
|
if(m_NoMapData)
|
|
{
|
|
}
|
|
else if(pMapData)
|
|
{
|
|
io_write(DemoFile, pMapData, MapSize);
|
|
}
|
|
else
|
|
{
|
|
// write map data
|
|
while(true)
|
|
{
|
|
unsigned char aChunk[1024 * 64];
|
|
mem_zero(aChunk, sizeof(aChunk));
|
|
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);
|
|
}
|
|
|
|
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, gs_DemoPrintColor);
|
|
}
|
|
m_File = DemoFile;
|
|
str_copy(m_aCurrentFilename, pFilename);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Tickmarker
|
|
7 = Always set
|
|
6 = Keyframe flag
|
|
0-5 = Delta tick
|
|
|
|
Normal
|
|
7 = Not set
|
|
5-6 = Type
|
|
0-4 = Size
|
|
*/
|
|
|
|
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,
|
|
CHUNKMASK_TYPE = 0x60,
|
|
CHUNKMASK_SIZE = 0x1f,
|
|
|
|
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)
|
|
{
|
|
unsigned char aChunk[5];
|
|
aChunk[0] = CHUNKTYPEFLAG_TICKMARKER;
|
|
uint_to_bytes_be(aChunk + 1, Tick);
|
|
|
|
if(Keyframe)
|
|
aChunk[0] |= CHUNKTICKFLAG_KEYFRAME;
|
|
|
|
io_write(m_File, aChunk, sizeof(aChunk));
|
|
}
|
|
else
|
|
{
|
|
unsigned char aChunk[1];
|
|
aChunk[0] = CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_TICK_COMPRESSED | (Tick - m_LastTickMarker);
|
|
io_write(m_File, aChunk, sizeof(aChunk));
|
|
}
|
|
|
|
m_LastTickMarker = Tick;
|
|
if(m_FirstTick < 0)
|
|
m_FirstTick = Tick;
|
|
}
|
|
|
|
void CDemoRecorder::Write(int Type, const void *pData, int Size)
|
|
{
|
|
if(!m_File)
|
|
return;
|
|
|
|
if(Size > 64 * 1024)
|
|
return;
|
|
|
|
/* pad the data with 0 so we get an alignment of 4,
|
|
else the compression won't work and miss some bytes */
|
|
char aBuffer[64 * 1024];
|
|
char aBuffer2[64 * 1024];
|
|
mem_copy(aBuffer2, pData, Size);
|
|
while(Size & 3)
|
|
aBuffer2[Size++] = 0;
|
|
Size = CVariableInt::Compress(aBuffer2, Size, aBuffer, sizeof(aBuffer)); // buffer2 -> buffer
|
|
if(Size < 0)
|
|
return;
|
|
|
|
Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2)); // buffer -> buffer2
|
|
if(Size < 0)
|
|
return;
|
|
|
|
unsigned char aChunk[3];
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// write snapshot
|
|
Write(CHUNKTYPE_SNAPSHOT, pData, Size);
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
Write(CHUNKTYPE_MESSAGE, pData, Size);
|
|
}
|
|
|
|
int CDemoRecorder::Stop()
|
|
{
|
|
if(!m_File)
|
|
return -1;
|
|
|
|
// add the demo length to the header
|
|
io_seek(m_File, gs_LengthOffset, IOSEEK_START);
|
|
unsigned char aLength[4];
|
|
int_to_bytes_be(aLength, Length());
|
|
io_write(m_File, aLength, sizeof(aLength));
|
|
|
|
// add the timeline markers to the header
|
|
io_seek(m_File, gs_NumMarkersOffset, IOSEEK_START);
|
|
unsigned char aNumMarkers[4];
|
|
int_to_bytes_be(aNumMarkers, m_NumTimelineMarkers);
|
|
io_write(m_File, aNumMarkers, sizeof(aNumMarkers));
|
|
for(int i = 0; i < m_NumTimelineMarkers; i++)
|
|
{
|
|
unsigned char aMarker[4];
|
|
int_to_bytes_be(aMarker, m_aTimelineMarkers[i]);
|
|
io_write(m_File, aMarker, sizeof(aMarker));
|
|
}
|
|
|
|
io_close(m_File);
|
|
m_File = 0;
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Stopped recording", gs_DemoPrintColor);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CDemoRecorder::AddDemoMarker()
|
|
{
|
|
if(m_LastTickMarker < 0)
|
|
return;
|
|
AddDemoMarker(m_LastTickMarker);
|
|
}
|
|
|
|
void CDemoRecorder::AddDemoMarker(int Tick)
|
|
{
|
|
dbg_assert(Tick >= 0, "invalid marker tick");
|
|
if(m_NumTimelineMarkers >= MAX_TIMELINE_MARKERS)
|
|
return;
|
|
|
|
// not more than 1 marker in a second
|
|
if(m_NumTimelineMarkers > 0)
|
|
{
|
|
int Diff = Tick - m_aTimelineMarkers[m_NumTimelineMarkers - 1];
|
|
if(Diff < (float)SERVER_TICK_SPEED)
|
|
return;
|
|
}
|
|
|
|
m_aTimelineMarkers[m_NumTimelineMarkers++] = Tick;
|
|
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Added timeline marker", gs_DemoPrintColor);
|
|
}
|
|
|
|
CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc)
|
|
{
|
|
Construct(pSnapshotDelta);
|
|
|
|
m_UpdateIntraTimesFunc = UpdateIntraTimesFunc;
|
|
}
|
|
|
|
CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta)
|
|
{
|
|
Construct(pSnapshotDelta);
|
|
}
|
|
|
|
void CDemoPlayer::Construct(class CSnapshotDelta *pSnapshotDelta)
|
|
{
|
|
m_File = 0;
|
|
m_pKeyFrames = 0;
|
|
m_SpeedIndex = 4;
|
|
|
|
m_pSnapshotDelta = pSnapshotDelta;
|
|
m_LastSnapshotDataSize = -1;
|
|
}
|
|
|
|
void CDemoPlayer::SetListener(IListener *pListener)
|
|
{
|
|
m_pListener = pListener;
|
|
}
|
|
|
|
int CDemoPlayer::ReadChunkHeader(int *pType, int *pSize, int *pTick)
|
|
{
|
|
*pSize = 0;
|
|
*pType = 0;
|
|
|
|
if(m_File == NULL)
|
|
return -1;
|
|
|
|
unsigned char Chunk = 0;
|
|
if(io_read(m_File, &Chunk, sizeof(Chunk)) != sizeof(Chunk))
|
|
return -1;
|
|
|
|
if(Chunk & CHUNKTYPEFLAG_TICKMARKER)
|
|
{
|
|
// decode tick marker
|
|
int Tickdelta_legacy = Chunk & (CHUNKMASK_TICK_LEGACY); // compatibility
|
|
*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
|
|
{
|
|
unsigned char aTickdata[4];
|
|
if(io_read(m_File, aTickdata, sizeof(aTickdata)) != sizeof(aTickdata))
|
|
return -1;
|
|
*pTick = bytes_be_to_int(aTickdata);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// decode normal chunk
|
|
*pType = (Chunk & CHUNKMASK_TYPE) >> 5;
|
|
*pSize = Chunk & CHUNKMASK_SIZE;
|
|
|
|
if(*pSize == 30)
|
|
{
|
|
unsigned char aSizedata[1];
|
|
if(io_read(m_File, aSizedata, sizeof(aSizedata)) != sizeof(aSizedata))
|
|
return -1;
|
|
*pSize = aSizedata[0];
|
|
}
|
|
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];
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CDemoPlayer::ScanFile()
|
|
{
|
|
CHeap Heap;
|
|
CKeyFrameSearch *pFirstKey = 0;
|
|
CKeyFrameSearch *pCurrentKey = 0;
|
|
int ChunkTick = 0;
|
|
|
|
long StartPos = io_tell(m_File);
|
|
m_Info.m_SeekablePoints = 0;
|
|
|
|
while(true)
|
|
{
|
|
long CurrentPos = io_tell(m_File);
|
|
|
|
int ChunkSize, ChunkType;
|
|
if(ReadChunkHeader(&ChunkType, &ChunkSize, &ChunkTick))
|
|
break;
|
|
|
|
// read the chunk
|
|
if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
|
|
{
|
|
if(ChunkType & CHUNKTICKFLAG_KEYFRAME)
|
|
{
|
|
CKeyFrameSearch *pKey;
|
|
|
|
// 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++;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// copy all the frames to an array instead for fast access
|
|
int i;
|
|
m_pKeyFrames = (CKeyFrame *)calloc(maximum(m_Info.m_SeekablePoints, 1), sizeof(CKeyFrame));
|
|
for(pCurrentKey = pFirstKey, i = 0; pCurrentKey; pCurrentKey = pCurrentKey->m_pNext, i++)
|
|
m_pKeyFrames[i] = pCurrentKey->m_Frame;
|
|
|
|
// destroy the temporary heap and seek back to the start
|
|
io_seek(m_File, StartPos, IOSEEK_START);
|
|
}
|
|
|
|
void CDemoPlayer::DoTick()
|
|
{
|
|
// update ticks
|
|
m_Info.m_PreviousTick = m_Info.m_Info.m_CurrentTick;
|
|
m_Info.m_Info.m_CurrentTick = m_Info.m_NextTick;
|
|
int ChunkTick = m_Info.m_Info.m_CurrentTick;
|
|
|
|
int64_t Freq = time_freq();
|
|
int64_t CurtickStart = (m_Info.m_Info.m_CurrentTick) * Freq / SERVER_TICK_SPEED;
|
|
int64_t PrevtickStart = (m_Info.m_PreviousTick) * Freq / SERVER_TICK_SPEED;
|
|
m_Info.m_IntraTick = (m_Info.m_CurrentTime - PrevtickStart) / (float)(CurtickStart - PrevtickStart);
|
|
m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PrevtickStart) / (float)(Freq / SERVER_TICK_SPEED);
|
|
m_Info.m_TickTime = (m_Info.m_CurrentTime - PrevtickStart) / (float)Freq;
|
|
if(m_UpdateIntraTimesFunc)
|
|
m_UpdateIntraTimesFunc();
|
|
|
|
bool GotSnapshot = false;
|
|
while(true)
|
|
{
|
|
int ChunkType, ChunkSize;
|
|
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");
|
|
#if defined(CONF_VIDEORECORDER)
|
|
if(IVideo::Current())
|
|
Stop();
|
|
#endif
|
|
if(m_Info.m_PreviousTick == -1)
|
|
{
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", "empty demo");
|
|
Stop();
|
|
}
|
|
else
|
|
Pause();
|
|
break;
|
|
}
|
|
|
|
// read the chunk
|
|
int DataSize = 0;
|
|
static char s_aData[CSnapshot::MAX_SIZE];
|
|
if(ChunkSize)
|
|
{
|
|
static char s_aCompresseddata[CSnapshot::MAX_SIZE];
|
|
if(io_read(m_File, s_aCompresseddata, ChunkSize) != (unsigned)ChunkSize)
|
|
{
|
|
// stop on error or eof
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "error reading chunk");
|
|
Stop();
|
|
break;
|
|
}
|
|
|
|
static char s_aDecompressed[CSnapshot::MAX_SIZE];
|
|
DataSize = CNetBase::Decompress(s_aCompresseddata, ChunkSize, s_aDecompressed, sizeof(s_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");
|
|
Stop();
|
|
break;
|
|
}
|
|
|
|
DataSize = CVariableInt::Decompress(s_aDecompressed, DataSize, s_aData, sizeof(s_aData));
|
|
|
|
if(DataSize < 0)
|
|
{
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "error during intpack decompression");
|
|
Stop();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(ChunkType == CHUNKTYPE_DELTA)
|
|
{
|
|
// process delta snapshot
|
|
static char s_aNewsnap[CSnapshot::MAX_SIZE];
|
|
CSnapshot *pNewsnap = (CSnapshot *)s_aNewsnap;
|
|
DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot *)m_aLastSnapshotData, pNewsnap, s_aData, DataSize);
|
|
|
|
if(DataSize < 0)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
else if(!pNewsnap->IsValid(DataSize))
|
|
{
|
|
if(m_pConsole)
|
|
{
|
|
char aBuf[256];
|
|
str_format(aBuf, sizeof(aBuf), "snapshot delta invalid. DataSize=%d", DataSize);
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(m_pListener)
|
|
m_pListener->OnDemoPlayerSnapshot(s_aNewsnap, DataSize);
|
|
|
|
m_LastSnapshotDataSize = DataSize;
|
|
mem_copy(m_aLastSnapshotData, s_aNewsnap, DataSize);
|
|
GotSnapshot = true;
|
|
}
|
|
}
|
|
else if(ChunkType == CHUNKTYPE_SNAPSHOT)
|
|
{
|
|
// process full snapshot
|
|
CSnapshot *pSnap = (CSnapshot *)s_aData;
|
|
if(!pSnap->IsValid(DataSize))
|
|
{
|
|
if(m_pConsole)
|
|
{
|
|
char aBuf[256];
|
|
str_format(aBuf, sizeof(aBuf), "snapshot invalid. DataSize=%d", DataSize);
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GotSnapshot = true;
|
|
|
|
m_LastSnapshotDataSize = DataSize;
|
|
mem_copy(m_aLastSnapshotData, s_aData, DataSize);
|
|
if(m_pListener)
|
|
m_pListener->OnDemoPlayerSnapshot(s_aData, DataSize);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if there were no snapshots in this tick, replay the last one
|
|
if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
|
|
{
|
|
GotSnapshot = true;
|
|
m_pListener->OnDemoPlayerSnapshot(m_aLastSnapshotData, m_LastSnapshotDataSize);
|
|
}
|
|
|
|
// 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(s_aData, DataSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CDemoPlayer::Pause()
|
|
{
|
|
m_Info.m_Info.m_Paused = true;
|
|
#if defined(CONF_VIDEORECORDER)
|
|
if(IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
|
|
IVideo::Current()->Pause(true);
|
|
#endif
|
|
}
|
|
|
|
void CDemoPlayer::Unpause()
|
|
{
|
|
m_Info.m_Info.m_Paused = false;
|
|
#if defined(CONF_VIDEORECORDER)
|
|
if(IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
|
|
IVideo::Current()->Pause(false);
|
|
#endif
|
|
}
|
|
|
|
int CDemoPlayer::Load(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, int StorageType)
|
|
{
|
|
m_pConsole = pConsole;
|
|
m_File = pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType);
|
|
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);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// store the filename
|
|
str_copy(m_aFilename, pFilename);
|
|
|
|
// 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;
|
|
|
|
m_LastSnapshotDataSize = -1;
|
|
|
|
// read the header
|
|
io_read(m_File, &m_Info.m_Header, sizeof(m_Info.m_Header));
|
|
if(mem_comp(m_Info.m_Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0)
|
|
{
|
|
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);
|
|
}
|
|
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));
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// get demo type
|
|
if(!str_comp(m_Info.m_Header.m_aType, "client"))
|
|
m_DemoType = DEMOTYPE_CLIENT;
|
|
else if(!str_comp(m_Info.m_Header.m_aType, "server"))
|
|
m_DemoType = DEMOTYPE_SERVER;
|
|
else
|
|
m_DemoType = DEMOTYPE_INVALID;
|
|
|
|
// read map
|
|
unsigned MapSize = bytes_be_to_uint(m_Info.m_Header.m_aMapSize);
|
|
|
|
// check if we already have the map
|
|
// TODO: improve map checking (maps folder, check crc)
|
|
unsigned Crc = bytes_be_to_uint(m_Info.m_Header.m_aMapCrc);
|
|
|
|
// save byte offset of map for later use
|
|
m_MapOffset = io_tell(m_File);
|
|
io_skip(m_File, MapSize);
|
|
|
|
// store map information
|
|
m_MapInfo.m_Crc = Crc;
|
|
m_MapInfo.m_Sha256 = Sha256;
|
|
m_MapInfo.m_Size = MapSize;
|
|
str_copy(m_MapInfo.m_aName, m_Info.m_Header.m_aMapName);
|
|
|
|
if(m_Info.m_Header.m_Version > gs_OldVersion)
|
|
{
|
|
// get timeline markers
|
|
int Num = bytes_be_to_int(m_Info.m_TimelineMarkers.m_aNumTimelineMarkers);
|
|
m_Info.m_Info.m_NumTimelineMarkers = clamp<int>(Num, 0, MAX_TIMELINE_MARKERS);
|
|
for(int i = 0; i < m_Info.m_Info.m_NumTimelineMarkers; i++)
|
|
{
|
|
m_Info.m_Info.m_aTimelineMarkers[i] = bytes_be_to_int(m_Info.m_TimelineMarkers.m_aTimelineMarkers[i]);
|
|
}
|
|
}
|
|
|
|
// scan the file for interesting points
|
|
ScanFile();
|
|
|
|
// reset slice markers
|
|
g_Config.m_ClDemoSliceBegin = -1;
|
|
g_Config.m_ClDemoSliceEnd = -1;
|
|
|
|
// ready for playback
|
|
return 0;
|
|
}
|
|
|
|
unsigned char *CDemoPlayer::GetMapData(class IStorage *pStorage)
|
|
{
|
|
if(!m_MapInfo.m_Size)
|
|
return 0;
|
|
|
|
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);
|
|
return pMapData;
|
|
}
|
|
|
|
bool CDemoPlayer::ExtractMap(class IStorage *pStorage)
|
|
{
|
|
unsigned char *pMapData = GetMapData(pStorage);
|
|
if(!pMapData)
|
|
return false;
|
|
|
|
// handle sha256
|
|
SHA256_DIGEST Sha256 = SHA256_ZEROED;
|
|
if(m_Info.m_Header.m_Version >= gs_Sha256Version)
|
|
Sha256 = m_MapInfo.m_Sha256;
|
|
else
|
|
{
|
|
Sha256 = sha256(pMapData, m_MapInfo.m_Size);
|
|
m_MapInfo.m_Sha256 = Sha256;
|
|
}
|
|
|
|
// construct name
|
|
char aSha[SHA256_MAXSTRSIZE], aMapFilename[128];
|
|
sha256_str(Sha256, aSha, sizeof(aSha));
|
|
str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%s.map", m_Info.m_Header.m_aMapName, 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;
|
|
}
|
|
|
|
int64_t 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_t Now = time_get();
|
|
if(s_Recording)
|
|
{
|
|
s_Recording = false;
|
|
m_Info.m_LastUpdate = Now;
|
|
}
|
|
return Now;
|
|
}
|
|
#else
|
|
return time_get();
|
|
#endif
|
|
}
|
|
|
|
int CDemoPlayer::Play()
|
|
{
|
|
// fill in previous and next tick
|
|
while(m_Info.m_PreviousTick == -1 && IsPlaying())
|
|
DoTick();
|
|
|
|
// set start info
|
|
m_Info.m_CurrentTime = m_Info.m_PreviousTick * time_freq() / SERVER_TICK_SPEED;
|
|
m_Info.m_LastUpdate = Time();
|
|
return 0;
|
|
}
|
|
|
|
int CDemoPlayer::SeekPercent(float Percent)
|
|
{
|
|
int WantedTick = m_Info.m_Info.m_FirstTick + round_truncate((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 + round_truncate(Seconds * (float)SERVER_TICK_SPEED);
|
|
return SetPos(WantedTick);
|
|
}
|
|
|
|
int CDemoPlayer::SeekTick(ETickOffset TickOffset)
|
|
{
|
|
int WantedTick;
|
|
switch(TickOffset)
|
|
{
|
|
case TICK_CURRENT:
|
|
WantedTick = m_Info.m_Info.m_CurrentTick;
|
|
break;
|
|
case TICK_PREVIOUS:
|
|
WantedTick = m_Info.m_PreviousTick;
|
|
break;
|
|
case TICK_NEXT:
|
|
WantedTick = m_Info.m_NextTick;
|
|
break;
|
|
default:
|
|
dbg_assert(false, "Invalid TickOffset");
|
|
WantedTick = -1;
|
|
break;
|
|
}
|
|
|
|
// +1 because SetPos will seek until the given tick is the next tick that
|
|
// will be played back, whereas we want the wanted tick to be played now.
|
|
return SetPos(WantedTick + 1);
|
|
}
|
|
|
|
int CDemoPlayer::SetPos(int WantedTick)
|
|
{
|
|
if(!m_File)
|
|
return -1;
|
|
|
|
WantedTick = clamp(WantedTick, m_Info.m_Info.m_FirstTick, m_Info.m_Info.m_LastTick);
|
|
const int KeyFrameWantedTick = WantedTick - 5; // -5 because we have to have a current tick and previous tick when we do the playback
|
|
const float Percent = (KeyFrameWantedTick - m_Info.m_Info.m_FirstTick) / (float)(m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick);
|
|
|
|
// get correct key frame
|
|
int KeyFrame = clamp((int)(m_Info.m_SeekablePoints * Percent), 0, m_Info.m_SeekablePoints - 1);
|
|
while(KeyFrame < m_Info.m_SeekablePoints - 1 && m_pKeyFrames[KeyFrame].m_Tick < KeyFrameWantedTick)
|
|
KeyFrame++;
|
|
while(KeyFrame > 0 && m_pKeyFrames[KeyFrame].m_Tick > KeyFrameWantedTick)
|
|
KeyFrame--;
|
|
|
|
// seek to the correct key frame
|
|
io_seek(m_File, m_pKeyFrames[KeyFrame].m_Filepos, IOSEEK_START);
|
|
|
|
m_Info.m_NextTick = -1;
|
|
m_Info.m_Info.m_CurrentTick = -1;
|
|
m_Info.m_PreviousTick = -1;
|
|
|
|
// playback everything until we hit our tick
|
|
while(m_Info.m_NextTick < WantedTick)
|
|
DoTick();
|
|
|
|
Play();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CDemoPlayer::SetSpeed(float Speed)
|
|
{
|
|
m_Info.m_Info.m_Speed = clamp(Speed, 0.f, 256.f);
|
|
}
|
|
|
|
void CDemoPlayer::SetSpeedIndex(int Offset)
|
|
{
|
|
m_SpeedIndex = clamp(m_SpeedIndex + Offset, 0, (int)(std::size(g_aSpeeds) - 1));
|
|
SetSpeed(g_aSpeeds[m_SpeedIndex]);
|
|
}
|
|
|
|
int CDemoPlayer::Update(bool RealTime)
|
|
{
|
|
int64_t Now = Time();
|
|
int64_t Deltatime = Now - m_Info.m_LastUpdate;
|
|
m_Info.m_LastUpdate = Now;
|
|
|
|
if(!IsPlaying())
|
|
return 0;
|
|
|
|
const int64_t Freq = time_freq();
|
|
if(!m_Info.m_Info.m_Paused)
|
|
{
|
|
m_Info.m_CurrentTime += (int64_t)(Deltatime * (double)m_Info.m_Info.m_Speed);
|
|
|
|
while(!m_Info.m_Info.m_Paused)
|
|
{
|
|
int64_t CurtickStart = (m_Info.m_Info.m_CurrentTick) * Freq / SERVER_TICK_SPEED;
|
|
|
|
// break if we are ready
|
|
if(RealTime && CurtickStart > m_Info.m_CurrentTime)
|
|
break;
|
|
|
|
// do one more tick
|
|
DoTick();
|
|
}
|
|
}
|
|
|
|
// update intratick
|
|
{
|
|
int64_t CurtickStart = (m_Info.m_Info.m_CurrentTick) * Freq / SERVER_TICK_SPEED;
|
|
int64_t PrevtickStart = (m_Info.m_PreviousTick) * Freq / SERVER_TICK_SPEED;
|
|
m_Info.m_IntraTick = (m_Info.m_CurrentTime - PrevtickStart) / (float)(CurtickStart - PrevtickStart);
|
|
m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PrevtickStart) / (float)(Freq / SERVER_TICK_SPEED);
|
|
m_Info.m_TickTime = (m_Info.m_CurrentTime - PrevtickStart) / (float)Freq;
|
|
if(m_UpdateIntraTimesFunc)
|
|
m_UpdateIntraTimesFunc();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int CDemoPlayer::Stop()
|
|
{
|
|
#if defined(CONF_VIDEORECORDER)
|
|
if(IVideo::Current())
|
|
IVideo::Current()->Stop();
|
|
#endif
|
|
|
|
if(!m_File)
|
|
return -1;
|
|
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", "Stopped playback");
|
|
io_close(m_File);
|
|
m_File = 0;
|
|
free(m_pKeyFrames);
|
|
m_pKeyFrames = 0;
|
|
str_copy(m_aFilename, "");
|
|
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;
|
|
}
|
|
|
|
int Length = pEnd > pExtractedName ? minimum(BufferSize, (int)(pEnd - pExtractedName + 1)) : BufferSize;
|
|
str_copy(pBuffer, pExtractedName, Length);
|
|
}
|
|
|
|
bool CDemoPlayer::GetDemoInfo(class IStorage *pStorage, const char *pFilename, int StorageType, CDemoHeader *pDemoHeader, CTimelineMarkers *pTimelineMarkers, CMapInfo *pMapInfo) const
|
|
{
|
|
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));
|
|
|
|
str_copy(pMapInfo->m_aName, pDemoHeader->m_aMapName);
|
|
pMapInfo->m_Crc = bytes_be_to_int(pDemoHeader->m_aMapCrc);
|
|
|
|
SHA256_DIGEST Sha256 = SHA256_ZEROED;
|
|
if(pDemoHeader->m_Version >= gs_Sha256Version)
|
|
{
|
|
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;
|
|
|
|
pMapInfo->m_Size = bytes_be_to_int(pDemoHeader->m_aMapSize);
|
|
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
class CDemoPlayer DemoPlayer(m_pSnapshotDelta);
|
|
class CDemoRecorder DemoRecorder(m_pSnapshotDelta);
|
|
|
|
m_pDemoPlayer = &DemoPlayer;
|
|
m_pDemoRecorder = &DemoRecorder;
|
|
|
|
m_pDemoPlayer->SetListener(this);
|
|
|
|
m_SliceFrom = StartTick;
|
|
m_SliceTo = EndTick;
|
|
m_Stop = false;
|
|
|
|
if(m_pDemoPlayer->Load(m_pStorage, m_pConsole, pDemo, IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
|
|
return;
|
|
|
|
const CMapInfo *pMapInfo = m_pDemoPlayer->GetMapInfo();
|
|
const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
|
|
|
|
SHA256_DIGEST Sha256 = pMapInfo->m_Sha256;
|
|
if(pInfo->m_Header.m_Version < gs_Sha256Version)
|
|
{
|
|
if(m_pDemoPlayer->ExtractMap(m_pStorage))
|
|
Sha256 = pMapInfo->m_Sha256;
|
|
}
|
|
|
|
unsigned char *pMapData = m_pDemoPlayer->GetMapData(m_pStorage);
|
|
const int Result = m_pDemoRecorder->Start(m_pStorage, m_pConsole, pDst, m_pNetVersion, pMapInfo->m_aName, &Sha256, pMapInfo->m_Crc, "client", pMapInfo->m_Size, pMapData, NULL, pfnFilter, pUser) == -1;
|
|
free(pMapData);
|
|
if(Result != 0)
|
|
return;
|
|
|
|
m_pDemoPlayer->Play();
|
|
|
|
while(m_pDemoPlayer->IsPlaying() && !m_Stop)
|
|
{
|
|
m_pDemoPlayer->Update(false);
|
|
|
|
if(pInfo->m_Info.m_Paused)
|
|
break;
|
|
}
|
|
|
|
// Copy timeline markers to sliced demo
|
|
for(int i = 0; i < pInfo->m_Info.m_NumTimelineMarkers; i++)
|
|
{
|
|
if((m_SliceFrom == -1 || pInfo->m_Info.m_aTimelineMarkers[i] >= m_SliceFrom) && (m_SliceTo == -1 || pInfo->m_Info.m_aTimelineMarkers[i] <= m_SliceTo))
|
|
{
|
|
m_pDemoRecorder->AddDemoMarker(pInfo->m_Info.m_aTimelineMarkers[i]);
|
|
}
|
|
}
|
|
|
|
m_pDemoPlayer->Stop();
|
|
m_pDemoRecorder->Stop();
|
|
} // NOLINT(clang-analyzer-unix.Malloc)
|
|
|
|
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);
|
|
}
|