mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-13 03:28:19 +00:00
7ae4b39574
- videorecorder only works for recording demos now - demoplayer was modified to allow controll over the time that passed in order to get perfect fps, IVideo controlls how much time passed, basically every rendercall sets the time to record the next frame
1077 lines
27 KiB
C++
1077 lines
27 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 <game/generated/protocol.h>
|
|
|
|
#include "compression.h"
|
|
#include "demo.h"
|
|
#include "memheap.h"
|
|
#include "network.h"
|
|
#include "snapshot.h"
|
|
|
|
static const unsigned char gs_aHeaderMarker[7] = {'T', 'W', 'D', 'E', 'M', 'O', 0};
|
|
static const unsigned char gs_ActVersion = 5;
|
|
static const unsigned char gs_OldVersion = 3;
|
|
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;
|
|
|
|
|
|
CDemoRecorder::CDemoRecorder(class CSnapshotDelta *pSnapshotDelta, bool NoMapData)
|
|
{
|
|
m_File = 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 Sha256, unsigned Crc, const char *pType, unsigned int MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser)
|
|
{
|
|
m_pfnFilter = pfnFilter;
|
|
m_pUser = pUser;
|
|
|
|
m_MapSize = MapSize;
|
|
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;
|
|
}
|
|
|
|
CDemoHeader Header;
|
|
CTimelineMarkers TimelineMarkers;
|
|
if(m_File) {
|
|
io_close(DemoFile);
|
|
return -1;
|
|
}
|
|
|
|
bool CloseMapFile = false;
|
|
|
|
if(MapFile)
|
|
io_seek(MapFile, 0, IOSEEK_START);
|
|
|
|
if(!pMapData && !MapFile)
|
|
{
|
|
// open mapfile
|
|
char aMapFilename[128];
|
|
// try the downloaded maps
|
|
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;
|
|
}
|
|
|
|
// write header
|
|
mem_zero(&Header, sizeof(Header));
|
|
mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker));
|
|
Header.m_Version = gs_ActVersion;
|
|
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;
|
|
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
|
|
|
|
if(m_NoMapData)
|
|
{
|
|
}
|
|
else if(pMapData)
|
|
{
|
|
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);
|
|
}
|
|
|
|
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;
|
|
str_copy(m_aCurrentFilename, pFilename, sizeof(m_aCurrentFilename));
|
|
|
|
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;
|
|
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;
|
|
|
|
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)
|
|
{
|
|
char aBuffer[64*1024];
|
|
char aBuffer2[64*1024];
|
|
unsigned char aChunk[3];
|
|
|
|
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 */
|
|
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;
|
|
|
|
|
|
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);
|
|
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));
|
|
}
|
|
|
|
io_close(m_File);
|
|
m_File = 0;
|
|
if(m_pConsole)
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Stopped recording");
|
|
|
|
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");
|
|
}
|
|
|
|
|
|
|
|
CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta)
|
|
{
|
|
m_File = 0;
|
|
m_pKeyFrames = 0;
|
|
m_SpeedIndex = 4;
|
|
|
|
m_TickTime = 0;
|
|
m_Time = 0;
|
|
|
|
m_pSnapshotDelta = pSnapshotDelta;
|
|
m_LastSnapshotDataSize = -1;
|
|
}
|
|
|
|
void CDemoPlayer::SetListener(IListener *pListener)
|
|
{
|
|
m_pListener = pListener;
|
|
}
|
|
|
|
|
|
int CDemoPlayer::ReadChunkHeader(int *pType, int *pSize, int *pTick)
|
|
{
|
|
unsigned char Chunk = 0;
|
|
|
|
*pSize = 0;
|
|
*pType = 0;
|
|
|
|
if(m_File == NULL)
|
|
return -1;
|
|
|
|
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 = (aTickdata[0]<<24) | (aTickdata[1]<<16) | (aTickdata[2]<<8) | aTickdata[3];
|
|
}
|
|
|
|
}
|
|
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()
|
|
{
|
|
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);
|
|
|
|
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
|
|
m_pKeyFrames = (CKeyFrame *)calloc(m_Info.m_SeekablePoints, 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()
|
|
{
|
|
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;
|
|
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");
|
|
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
|
|
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");
|
|
Stop();
|
|
break;
|
|
}
|
|
|
|
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");
|
|
Stop();
|
|
break;
|
|
}
|
|
|
|
DataSize = CVariableInt::Decompress(aDecompressed, DataSize, aData, sizeof(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 aNewsnap[CSnapshot::MAX_SIZE];
|
|
|
|
GotSnapshot = 1;
|
|
|
|
DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot*)m_aLastSnapshotData, (CSnapshot*)aNewsnap, aData, DataSize);
|
|
|
|
if(DataSize >= 0)
|
|
{
|
|
if(m_pListener)
|
|
m_pListener->OnDemoPlayerSnapshot(aNewsnap, DataSize);
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
else if(ChunkType == CHUNKTYPE_SNAPSHOT)
|
|
{
|
|
// process full snapshot
|
|
GotSnapshot = 1;
|
|
|
|
m_LastSnapshotDataSize = DataSize;
|
|
mem_copy(m_aLastSnapshotData, aData, DataSize);
|
|
if(m_pListener)
|
|
m_pListener->OnDemoPlayerSnapshot(aData, DataSize);
|
|
}
|
|
else
|
|
{
|
|
// if there were no snapshots in this tick, replay the last one
|
|
if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
|
|
{
|
|
GotSnapshot = 1;
|
|
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(aData, DataSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CDemoPlayer::Pause()
|
|
{
|
|
m_Info.m_Info.m_Paused = 1;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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, sizeof(m_aFilename));
|
|
|
|
// 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));
|
|
|
|
// 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 = (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]);
|
|
char aMapFilename[128];
|
|
str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%08x.map", m_Info.m_Header.m_aMapName, Crc);
|
|
IOHANDLE MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_READ, IStorage::TYPE_ALL);
|
|
|
|
if(MapFile)
|
|
{
|
|
io_skip(m_File, MapSize);
|
|
io_close(MapFile);
|
|
}
|
|
else if(MapSize > 0)
|
|
{
|
|
// get map data
|
|
unsigned char *pMapData = (unsigned char *)malloc(MapSize);
|
|
io_read(m_File, pMapData, MapSize);
|
|
|
|
// save map
|
|
MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
|
|
io_write(MapFile, pMapData, MapSize);
|
|
io_close(MapFile);
|
|
|
|
// free data
|
|
free(pMapData);
|
|
}
|
|
|
|
// store map information
|
|
m_MapInfo.m_Crc = Crc;
|
|
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);
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
int CDemoPlayer::NextFrame()
|
|
{
|
|
DoTick();
|
|
return IsPlaying();
|
|
}
|
|
|
|
const int64 CDemoPlayer::time()
|
|
{
|
|
#if defined(CONF_VIDEORECORDER)
|
|
return IVideo::Current() ? IVideo::time() : time_get();
|
|
#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.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();
|
|
return 0;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if(!m_File)
|
|
return -1;
|
|
|
|
// -5 because we have to have a current tick and previous tick when we do the playback
|
|
WantedTick = clamp(WantedTick, m_Info.m_Info.m_FirstTick, m_Info.m_Info.m_LastTick) - 5;
|
|
|
|
// get correct key frame
|
|
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--;
|
|
}
|
|
|
|
// 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_PreviousTick < WantedTick && IsPlaying())
|
|
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)(sizeof(g_aSpeeds)/sizeof(g_aSpeeds[0])-1));
|
|
SetSpeed(g_aSpeeds[m_SpeedIndex]);
|
|
}
|
|
|
|
int CDemoPlayer::Update(bool RealTime)
|
|
{
|
|
int64 Now = time();
|
|
int64 Deltatime = Now-m_Info.m_LastUpdate;
|
|
m_Info.m_LastUpdate = Now;
|
|
|
|
if(!IsPlaying())
|
|
return 0;
|
|
|
|
if(m_Info.m_Info.m_Paused)
|
|
{
|
|
|
|
}
|
|
else
|
|
{
|
|
int64 Freq = time_freq();
|
|
m_Info.m_CurrentTime += (int64)(Deltatime*(double)m_Info.m_Info.m_Speed);
|
|
|
|
while(1)
|
|
{
|
|
int64 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();
|
|
|
|
if(m_Info.m_Info.m_Paused)
|
|
return 0;
|
|
}
|
|
|
|
// update intratick
|
|
{
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
m_Time += m_TickTime;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int CDemoPlayer::Stop()
|
|
{
|
|
#if defined(CONF_VIDEORECORDER)
|
|
if (IVideo::Current())
|
|
{
|
|
IVideo::Current()->stop();
|
|
delete IVideo::Current();
|
|
}
|
|
#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, "", sizeof(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) const
|
|
{
|
|
if(!pDemoHeader || !pTimelineMarkers)
|
|
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));
|
|
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) == -1)
|
|
return;
|
|
|
|
const CDemoPlayer::CMapInfo *pMapInfo = m_pDemoPlayer->GetMapInfo();
|
|
SHA256_DIGEST Fake;
|
|
for(unsigned i = 0; i < sizeof(Fake.data); i++)
|
|
{
|
|
Fake.data[i] = 0xff;
|
|
}
|
|
if (m_pDemoRecorder->Start(m_pStorage, m_pConsole, pDst, m_pNetVersion, pMapInfo->m_aName, Fake, pMapInfo->m_Crc, "client", pMapInfo->m_Size, NULL, NULL, pfnFilter, pUser) == -1)
|
|
return;
|
|
|
|
|
|
m_pDemoPlayer->Play();
|
|
const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
|
|
|
|
while (m_pDemoPlayer->IsPlaying() && !m_Stop) {
|
|
m_pDemoPlayer->Update(false);
|
|
|
|
if (pInfo->m_Info.m_Paused)
|
|
break;
|
|
}
|
|
|
|
m_pDemoPlayer->Stop();
|
|
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);
|
|
}
|