mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-13 11:38:19 +00:00
ca8fcc823c
SHA256 was chosen because it is reasonably standard, the file names don't explode in length (this rules out SHA512) and it is supported by basically all versions of OpenSSL (this rules out SHA512/256 and SHA3). The protocol is changed in a backward compatible way: The supporting server sends the SHA256 corresponding to the map in the `MAP_DETAILS` message prior to sending the `MAP_CHANGE` message. The client saves the SHA256 obtained from the `MAP_DETAILS` message until the next `MAP_CHANGE` message. For servers not supporting this protocol, the client falls back to simply opening maps like in the previous scheme. Remove the `map_version` tool, it is not being used and would have been a little bit effort to update. Use the OpenSSL implementation of SHA256 if it is supported, otherwise fall back to a public domain one. Fix #1127.
1007 lines
26 KiB
C++
1007 lines
26 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>
|
|
|
|
#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;
|
|
|
|
IOHANDLE DemoFile = pStorage->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
|
|
if(!DemoFile)
|
|
{
|
|
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;
|
|
}
|
|
|
|
m_pConsole = pConsole;
|
|
|
|
bool CloseMapFile = false;
|
|
|
|
if(MapFile)
|
|
io_seek(MapFile, 0, IOSEEK_START);
|
|
|
|
if(!pMapData && !MapFile)
|
|
{
|
|
// open mapfile
|
|
char aMapFilename[128];
|
|
// 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)
|
|
{
|
|
// 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)
|
|
{
|
|
// 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)
|
|
{
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
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;
|
|
|
|
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_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
|
|
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "end of file");
|
|
if(m_Info.m_PreviousTick == -1)
|
|
{
|
|
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
|
|
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
|
|
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)
|
|
{
|
|
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
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
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 inforation
|
|
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 = min(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 interessting 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();
|
|
}
|
|
|
|
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_get();
|
|
return 0;
|
|
}
|
|
|
|
int CDemoPlayer::SetPos(float Percent)
|
|
{
|
|
int Keyframe;
|
|
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 = m_Info.m_Info.m_FirstTick + (int)((m_Info.m_Info.m_LastTick-m_Info.m_Info.m_FirstTick)*Percent) - 5;
|
|
|
|
Keyframe = (int)(m_Info.m_SeekablePoints*Percent);
|
|
|
|
if(Keyframe < 0 || Keyframe >= m_Info.m_SeekablePoints)
|
|
return -1;
|
|
|
|
// get correct key frame
|
|
if(m_pKeyFrames[Keyframe].m_Tick < WantedTick)
|
|
while(Keyframe < m_Info.m_SeekablePoints-1 && m_pKeyFrames[Keyframe].m_Tick < WantedTick)
|
|
Keyframe++;
|
|
|
|
while(Keyframe && m_pKeyFrames[Keyframe].m_Tick > WantedTick)
|
|
Keyframe--;
|
|
|
|
// seek to the correct keyframe
|
|
io_seek(m_File, m_pKeyFrames[Keyframe].m_Filepos, IOSEEK_START);
|
|
|
|
//m_Info.start_tick = -1;
|
|
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 = Speed;
|
|
}
|
|
|
|
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_get();
|
|
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)
|
|
{
|
|
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(!m_File)
|
|
return -1;
|
|
|
|
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 ? min(BufferSize, (int)(pEnd-pExtractedName+1)) : BufferSize;
|
|
str_copy(pBuffer, pExtractedName, Length);
|
|
}
|
|
|
|
bool CDemoPlayer::GetDemoInfo(class IStorage *pStorage, const char *pFilename, int StorageType, CDemoHeader *pDemoHeader) const
|
|
{
|
|
if(!pDemoHeader)
|
|
return false;
|
|
|
|
mem_zero(pDemoHeader, sizeof(CDemoHeader));
|
|
|
|
IOHANDLE File = pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType);
|
|
if(!File)
|
|
return false;
|
|
|
|
io_read(File, pDemoHeader, sizeof(CDemoHeader));
|
|
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_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);
|
|
}
|