/* (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 #include #include #include #include #if defined(CONF_VIDEORECORDER) #include #endif #include #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 defined(CONF_VIDEORECORDER) if (IVideo::Current()) { IVideo::Current()->stop(); delete IVideo::Current(); } #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 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(); } int64 CDemoPlayer::time() { #if defined(CONF_VIDEORECORDER) static bool s_Recording = false; if (IVideo::Current()) { if (!s_Recording) { s_Recording = true; m_Info.m_LastUpdate = IVideo::time(); } return IVideo::time(); } else { int64 Now = time_get(); if (s_Recording) { s_Recording = false; m_Info.m_LastUpdate = Now; } return Now; } #else return time_get(); #endif } 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 #if defined(CONF_VIDEORECORDER) if(IVideo::Current()) IVideo::Current()->SetBreak((double)m_Info.m_Info.m_Speed); #endif 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); }