1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include <base/log.h>
4#include <base/math.h>
5#include <base/system.h>
6
7#include <engine/console.h>
8#include <engine/shared/config.h>
9#include <engine/storage.h>
10
11#if defined(CONF_VIDEORECORDER)
12#include <engine/shared/video.h>
13#endif
14
15#include "compression.h"
16#include "demo.h"
17#include "network.h"
18#include "snapshot.h"
19
20const CUuid SHA256_EXTENSION =
21 {.m_aData: {0x6b, 0xe6, 0xda, 0x4a, 0xce, 0xbd, 0x38, 0x0c,
22 0x9b, 0x5b, 0x12, 0x89, 0xc8, 0x42, 0xd7, 0x80}};
23
24static const unsigned char gs_CurVersion = 6;
25static const unsigned char gs_OldVersion = 3;
26static const unsigned char gs_Sha256Version = 6;
27static const unsigned char gs_VersionTickCompression = 5; // demo files with this version or higher will use `CHUNKTICKFLAG_TICK_COMPRESSED`
28
29// TODO: rewrite all logs in this file using log_log_color, and remove gs_DemoPrintColor and m_pConsole
30static constexpr ColorRGBA gs_DemoPrintColor{0.75f, 0.7f, 0.7f, 1.0f};
31static constexpr LOG_COLOR DEMO_PRINT_COLOR = {.r: 191, .g: 178, .b: 178};
32
33bool CDemoHeader::Valid() const
34{
35 // Check marker and ensure that strings are zero-terminated and valid UTF-8.
36 return mem_comp(a: m_aMarker, b: gs_aHeaderMarker, size: sizeof(gs_aHeaderMarker)) == 0 &&
37 mem_has_null(block: m_aNetversion, size: sizeof(m_aNetversion)) && str_utf8_check(str: m_aNetversion) &&
38 mem_has_null(block: m_aMapName, size: sizeof(m_aMapName)) && str_utf8_check(str: m_aMapName) &&
39 mem_has_null(block: m_aType, size: sizeof(m_aType)) && str_utf8_check(str: m_aType) &&
40 mem_has_null(block: m_aTimestamp, size: sizeof(m_aTimestamp)) && str_utf8_check(str: m_aTimestamp);
41}
42
43CDemoRecorder::CDemoRecorder(class CSnapshotDelta *pSnapshotDelta, bool NoMapData)
44{
45 m_File = nullptr;
46 m_aCurrentFilename[0] = '\0';
47 m_pfnFilter = nullptr;
48 m_pUser = nullptr;
49 m_LastTickMarker = -1;
50 m_pSnapshotDelta = pSnapshotDelta;
51 m_NoMapData = NoMapData;
52}
53
54CDemoRecorder::~CDemoRecorder()
55{
56 dbg_assert(m_File == 0, "Demo recorder was not stopped");
57}
58
59// Record
60int CDemoRecorder::Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetVersion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned Crc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser)
61{
62 dbg_assert(m_File == 0, "Demo recorder already recording");
63
64 m_pConsole = pConsole;
65 m_pStorage = pStorage;
66
67 if(!str_valid_filename(str: fs_filename(path: pFilename)))
68 {
69 log_error_color(DEMO_PRINT_COLOR, "demo_recorder", "The name '%s' cannot be used for demos because not all platforms support it", pFilename);
70 return -1;
71 }
72
73 IOHANDLE DemoFile = pStorage->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
74 if(!DemoFile)
75 {
76 if(m_pConsole)
77 {
78 char aBuf[64 + IO_MAX_PATH_LENGTH];
79 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Unable to open '%s' for recording", pFilename);
80 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
81 }
82 return -1;
83 }
84
85 bool CloseMapFile = false;
86
87 if(MapFile)
88 io_seek(io: MapFile, offset: 0, origin: IOSEEK_START);
89
90 char aSha256[SHA256_MAXSTRSIZE];
91 sha256_str(digest: Sha256, str: aSha256, max_len: sizeof(aSha256));
92
93 if(!pMapData && !MapFile)
94 {
95 // open mapfile
96 char aMapFilename[IO_MAX_PATH_LENGTH];
97 // try the downloaded maps
98 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "downloadedmaps/%s_%s.map", pMap, aSha256);
99 MapFile = pStorage->OpenFile(pFilename: aMapFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL);
100 if(!MapFile)
101 {
102 // try the normal maps folder
103 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "maps/%s.map", pMap);
104 MapFile = pStorage->OpenFile(pFilename: aMapFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL);
105 }
106 if(!MapFile)
107 {
108 // search for the map within subfolders
109 char aBuf[IO_MAX_PATH_LENGTH];
110 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "%s.map", pMap);
111 if(pStorage->FindFile(pFilename: aMapFilename, pPath: "maps", Type: IStorage::TYPE_ALL, pBuffer: aBuf, BufferSize: sizeof(aBuf)))
112 MapFile = pStorage->OpenFile(pFilename: aBuf, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL);
113 }
114 if(!MapFile)
115 {
116 if(m_pConsole)
117 {
118 char aBuf[32 + IO_MAX_PATH_LENGTH];
119 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Unable to open mapfile '%s'", pMap);
120 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
121 }
122 return -1;
123 }
124
125 CloseMapFile = true;
126 }
127
128 if(m_NoMapData)
129 {
130 MapSize = 0;
131 }
132 else if(MapFile)
133 {
134 const int64_t MapFileSize = io_length(io: MapFile);
135 if(MapFileSize > (int64_t)std::numeric_limits<unsigned>::max())
136 {
137 if(CloseMapFile)
138 {
139 io_close(io: MapFile);
140 }
141 MapSize = 0;
142 if(m_pConsole)
143 {
144 char aBuf[32 + IO_MAX_PATH_LENGTH];
145 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Mapfile '%s' too large for demo, recording without it", pMap);
146 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
147 }
148 }
149 else
150 {
151 MapSize = MapFileSize;
152 }
153 }
154
155 // write header
156 CDemoHeader Header;
157 mem_zero(block: &Header, size: sizeof(Header));
158 mem_copy(dest: Header.m_aMarker, source: gs_aHeaderMarker, size: sizeof(Header.m_aMarker));
159 Header.m_Version = gs_CurVersion;
160 str_copy(dst&: Header.m_aNetversion, src: pNetVersion);
161 str_copy(dst&: Header.m_aMapName, src: pMap);
162 uint_to_bytes_be(bytes: Header.m_aMapSize, value: MapSize);
163 uint_to_bytes_be(bytes: Header.m_aMapCrc, value: Crc);
164 str_copy(dst&: Header.m_aType, src: pType);
165 // Header.m_Length - add this on stop
166 str_timestamp(buffer: Header.m_aTimestamp, buffer_size: sizeof(Header.m_aTimestamp));
167 io_write(io: DemoFile, buffer: &Header, size: sizeof(Header));
168
169 CTimelineMarkers TimelineMarkers;
170 mem_zero(block: &TimelineMarkers, size: sizeof(TimelineMarkers));
171 io_write(io: DemoFile, buffer: &TimelineMarkers, size: sizeof(TimelineMarkers)); // fill this on stop
172
173 // Write Sha256
174 io_write(io: DemoFile, buffer: SHA256_EXTENSION.m_aData, size: sizeof(SHA256_EXTENSION.m_aData));
175 io_write(io: DemoFile, buffer: &Sha256, size: sizeof(SHA256_DIGEST));
176
177 if(MapSize == 0)
178 {
179 }
180 else if(pMapData)
181 {
182 io_write(io: DemoFile, buffer: pMapData, size: MapSize);
183 }
184 else
185 {
186 // write map data
187 while(true)
188 {
189 unsigned char aChunk[1024 * 64];
190 int Bytes = io_read(io: MapFile, buffer: &aChunk, size: sizeof(aChunk));
191 if(Bytes <= 0)
192 break;
193 io_write(io: DemoFile, buffer: &aChunk, size: Bytes);
194 }
195 if(CloseMapFile)
196 io_close(io: MapFile);
197 else
198 io_seek(io: MapFile, offset: 0, origin: IOSEEK_START);
199 }
200
201 m_LastKeyFrame = -1;
202 m_LastTickMarker = -1;
203 m_FirstTick = -1;
204 m_NumTimelineMarkers = 0;
205
206 if(m_pConsole)
207 {
208 char aBuf[32 + IO_MAX_PATH_LENGTH];
209 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Recording to '%s'", pFilename);
210 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
211 }
212
213 m_pfnFilter = pfnFilter;
214 m_pUser = pUser;
215
216 m_File = DemoFile;
217 str_copy(dst&: m_aCurrentFilename, src: pFilename);
218
219 return 0;
220}
221
222/*
223 Tickmarker
224 7 = Always set
225 6 = Keyframe flag
226 0-5 = Delta tick
227
228 Normal
229 7 = Not set
230 5-6 = Type
231 0-4 = Size
232*/
233
234enum
235{
236 CHUNKTYPEFLAG_TICKMARKER = 0x80,
237 CHUNKTICKFLAG_KEYFRAME = 0x40, // only when tickmarker is set
238 CHUNKTICKFLAG_TICK_COMPRESSED = 0x20, // when we store the tick value in the first chunk
239
240 CHUNKMASK_TICK = 0x1f,
241 CHUNKMASK_TICK_LEGACY = 0x3f,
242 CHUNKMASK_TYPE = 0x60,
243 CHUNKMASK_SIZE = 0x1f,
244
245 CHUNKTYPE_SNAPSHOT = 1,
246 CHUNKTYPE_MESSAGE = 2,
247 CHUNKTYPE_DELTA = 3,
248};
249
250void CDemoRecorder::WriteTickMarker(int Tick, bool Keyframe)
251{
252 if(m_LastTickMarker == -1 || Tick - m_LastTickMarker > CHUNKMASK_TICK || Keyframe)
253 {
254 unsigned char aChunk[sizeof(int32_t) + 1];
255 aChunk[0] = CHUNKTYPEFLAG_TICKMARKER;
256 uint_to_bytes_be(bytes: aChunk + 1, value: Tick);
257
258 if(Keyframe)
259 aChunk[0] |= CHUNKTICKFLAG_KEYFRAME;
260
261 io_write(io: m_File, buffer: aChunk, size: sizeof(aChunk));
262 }
263 else
264 {
265 unsigned char aChunk[1];
266 aChunk[0] = CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_TICK_COMPRESSED | (Tick - m_LastTickMarker);
267 io_write(io: m_File, buffer: aChunk, size: sizeof(aChunk));
268 }
269
270 m_LastTickMarker = Tick;
271 if(m_FirstTick < 0)
272 m_FirstTick = Tick;
273}
274
275void CDemoRecorder::Write(int Type, const void *pData, int Size)
276{
277 if(!m_File)
278 return;
279
280 if(Size > 64 * 1024)
281 return;
282
283 /* pad the data with 0 so we get an alignment of 4,
284 else the compression won't work and miss some bytes */
285 char aBuffer[64 * 1024];
286 char aBuffer2[64 * 1024];
287 mem_copy(dest: aBuffer2, source: pData, size: Size);
288 while(Size & 3)
289 aBuffer2[Size++] = 0;
290 Size = CVariableInt::Compress(pSrc: aBuffer2, SrcSize: Size, pDst: aBuffer, DstSize: sizeof(aBuffer)); // buffer2 -> buffer
291 if(Size < 0)
292 return;
293
294 Size = CNetBase::Compress(pData: aBuffer, DataSize: Size, pOutput: aBuffer2, OutputSize: sizeof(aBuffer2)); // buffer -> buffer2
295 if(Size < 0)
296 return;
297
298 unsigned char aChunk[3];
299 aChunk[0] = ((Type & 0x3) << 5);
300 if(Size < 30)
301 {
302 aChunk[0] |= Size;
303 io_write(io: m_File, buffer: aChunk, size: 1);
304 }
305 else
306 {
307 if(Size < 256)
308 {
309 aChunk[0] |= 30;
310 aChunk[1] = Size & 0xff;
311 io_write(io: m_File, buffer: aChunk, size: 2);
312 }
313 else
314 {
315 aChunk[0] |= 31;
316 aChunk[1] = Size & 0xff;
317 aChunk[2] = Size >> 8;
318 io_write(io: m_File, buffer: aChunk, size: 3);
319 }
320 }
321
322 io_write(io: m_File, buffer: aBuffer2, size: Size);
323}
324
325void CDemoRecorder::RecordSnapshot(int Tick, const void *pData, int Size)
326{
327 if(m_LastKeyFrame == -1 || (Tick - m_LastKeyFrame) > SERVER_TICK_SPEED * 5)
328 {
329 // write full tickmarker
330 WriteTickMarker(Tick, Keyframe: true);
331
332 // write snapshot
333 Write(Type: CHUNKTYPE_SNAPSHOT, pData, Size);
334
335 m_LastKeyFrame = Tick;
336 mem_copy(dest: m_aLastSnapshotData, source: pData, size: Size);
337 }
338 else
339 {
340 // write tickmarker
341 WriteTickMarker(Tick, Keyframe: false);
342
343 // create delta
344 char aDeltaData[CSnapshot::MAX_SIZE + sizeof(int)];
345 m_pSnapshotDelta->SetStaticsize(ItemType: protocol7::NETEVENTTYPE_SOUNDWORLD, Size: true);
346 m_pSnapshotDelta->SetStaticsize(ItemType: protocol7::NETEVENTTYPE_DAMAGE, Size: true);
347 const int DeltaSize = m_pSnapshotDelta->CreateDelta(pFrom: (CSnapshot *)m_aLastSnapshotData, pTo: (CSnapshot *)pData, pDstData: &aDeltaData);
348 if(DeltaSize)
349 {
350 // record delta
351 Write(Type: CHUNKTYPE_DELTA, pData: aDeltaData, Size: DeltaSize);
352 mem_copy(dest: m_aLastSnapshotData, source: pData, size: Size);
353 }
354 }
355}
356
357void CDemoRecorder::RecordMessage(const void *pData, int Size)
358{
359 if(m_pfnFilter)
360 {
361 if(m_pfnFilter(pData, Size, m_pUser))
362 {
363 return;
364 }
365 }
366 Write(Type: CHUNKTYPE_MESSAGE, pData, Size);
367}
368
369int CDemoRecorder::Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename)
370{
371 if(!m_File)
372 return -1;
373
374 if(Mode == IDemoRecorder::EStopMode::KEEP_FILE)
375 {
376 // add the demo length to the header
377 io_seek(io: m_File, offsetof(CDemoHeader, m_aLength), origin: IOSEEK_START);
378 unsigned char aLength[sizeof(int32_t)];
379 uint_to_bytes_be(bytes: aLength, value: Length());
380 io_write(io: m_File, buffer: aLength, size: sizeof(aLength));
381
382 // add the timeline markers to the header
383 io_seek(io: m_File, offset: sizeof(CDemoHeader) + offsetof(CTimelineMarkers, m_aNumTimelineMarkers), origin: IOSEEK_START);
384 unsigned char aNumMarkers[sizeof(int32_t)];
385 uint_to_bytes_be(bytes: aNumMarkers, value: m_NumTimelineMarkers);
386 io_write(io: m_File, buffer: aNumMarkers, size: sizeof(aNumMarkers));
387 for(int i = 0; i < m_NumTimelineMarkers; i++)
388 {
389 unsigned char aMarker[sizeof(int32_t)];
390 uint_to_bytes_be(bytes: aMarker, value: m_aTimelineMarkers[i]);
391 io_write(io: m_File, buffer: aMarker, size: sizeof(aMarker));
392 }
393 }
394
395 io_close(io: m_File);
396 m_File = nullptr;
397
398 if(Mode == IDemoRecorder::EStopMode::REMOVE_FILE)
399 {
400 if(!m_pStorage->RemoveFile(pFilename: m_aCurrentFilename, Type: IStorage::TYPE_SAVE))
401 {
402 if(m_pConsole)
403 {
404 char aBuf[64 + IO_MAX_PATH_LENGTH];
405 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Could not remove demo file '%s'.", m_aCurrentFilename);
406 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
407 }
408 return -1;
409 }
410 }
411 else if(pTargetFilename[0] != '\0')
412 {
413 if(!m_pStorage->RenameFile(pOldFilename: m_aCurrentFilename, pNewFilename: pTargetFilename, Type: IStorage::TYPE_SAVE))
414 {
415 if(m_pConsole)
416 {
417 char aBuf[64 + 2 * IO_MAX_PATH_LENGTH];
418 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Could not move demo file '%s' to '%s'.", m_aCurrentFilename, pTargetFilename);
419 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
420 }
421 return -1;
422 }
423 }
424
425 if(m_pConsole)
426 {
427 char aBuf[64 + IO_MAX_PATH_LENGTH];
428 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Stopped recording to '%s'", m_aCurrentFilename);
429 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
430 }
431
432 return 0;
433}
434
435void CDemoRecorder::AddDemoMarker()
436{
437 if(m_LastTickMarker < 0)
438 return;
439 AddDemoMarker(Tick: m_LastTickMarker);
440}
441
442void CDemoRecorder::AddDemoMarker(int Tick)
443{
444 dbg_assert(Tick >= 0, "invalid marker tick");
445 if(m_NumTimelineMarkers >= MAX_TIMELINE_MARKERS)
446 {
447 if(m_pConsole)
448 {
449 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Too many timeline markers", PrintColor: gs_DemoPrintColor);
450 }
451 return;
452 }
453
454 // not more than 1 marker in a second
455 if(m_NumTimelineMarkers > 0)
456 {
457 const int Diff = Tick - m_aTimelineMarkers[m_NumTimelineMarkers - 1];
458 if(Diff < (float)SERVER_TICK_SPEED)
459 {
460 if(m_pConsole)
461 {
462 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Previous timeline marker too close", PrintColor: gs_DemoPrintColor);
463 }
464 return;
465 }
466 }
467
468 m_aTimelineMarkers[m_NumTimelineMarkers++] = Tick;
469
470 if(m_pConsole)
471 {
472 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Added timeline marker", PrintColor: gs_DemoPrintColor);
473 }
474}
475
476CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc)
477{
478 Construct(pSnapshotDelta, UseVideo);
479
480 m_UpdateIntraTimesFunc = UpdateIntraTimesFunc;
481}
482
483CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo)
484{
485 Construct(pSnapshotDelta, UseVideo);
486}
487
488CDemoPlayer::~CDemoPlayer()
489{
490 dbg_assert(m_File == 0, "Demo player not stopped");
491}
492
493void CDemoPlayer::Construct(class CSnapshotDelta *pSnapshotDelta, bool UseVideo)
494{
495 m_File = nullptr;
496 m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;
497
498 m_pSnapshotDelta = pSnapshotDelta;
499 m_LastSnapshotDataSize = -1;
500 m_pListener = nullptr;
501 m_UseVideo = UseVideo;
502
503 m_aFilename[0] = '\0';
504 m_aErrorMessage[0] = '\0';
505}
506
507void CDemoPlayer::SetListener(IListener *pListener)
508{
509 m_pListener = pListener;
510}
511
512CDemoPlayer::EReadChunkHeaderResult CDemoPlayer::ReadChunkHeader(int *pType, int *pSize, int *pTick)
513{
514 *pSize = 0;
515 *pType = 0;
516
517 unsigned char Chunk = 0;
518 if(io_read(io: m_File, buffer: &Chunk, size: sizeof(Chunk)) != sizeof(Chunk))
519 return CHUNKHEADER_EOF;
520
521 if(Chunk & CHUNKTYPEFLAG_TICKMARKER)
522 {
523 // decode tick marker
524 int TickdeltaLegacy = Chunk & CHUNKMASK_TICK_LEGACY; // compatibility
525 *pType = Chunk & (CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_KEYFRAME);
526
527 int NewTick;
528 if(m_Info.m_Header.m_Version < gs_VersionTickCompression && TickdeltaLegacy != 0)
529 {
530 if(*pTick < 0) // initial tick not initialized before a tick delta
531 return CHUNKHEADER_ERROR;
532 NewTick = *pTick + TickdeltaLegacy;
533 }
534 else if(Chunk & CHUNKTICKFLAG_TICK_COMPRESSED)
535 {
536 if(*pTick < 0) // initial tick not initialized before a tick delta
537 return CHUNKHEADER_ERROR;
538 int Tickdelta = Chunk & CHUNKMASK_TICK;
539 NewTick = *pTick + Tickdelta;
540 }
541 else
542 {
543 unsigned char aTickdata[sizeof(int32_t)];
544 if(io_read(io: m_File, buffer: aTickdata, size: sizeof(aTickdata)) != sizeof(aTickdata))
545 return CHUNKHEADER_ERROR;
546 NewTick = bytes_be_to_uint(bytes: aTickdata);
547 }
548 if(NewTick < MIN_TICK || NewTick >= MAX_TICK) // invalid tick
549 return CHUNKHEADER_ERROR;
550 *pTick = NewTick;
551 }
552 else
553 {
554 // decode normal chunk
555 *pType = (Chunk & CHUNKMASK_TYPE) >> 5;
556 *pSize = Chunk & CHUNKMASK_SIZE;
557
558 if(*pSize == 30)
559 {
560 unsigned char aSizedata[1];
561 if(io_read(io: m_File, buffer: aSizedata, size: sizeof(aSizedata)) != sizeof(aSizedata))
562 return CHUNKHEADER_ERROR;
563 *pSize = aSizedata[0];
564 }
565 else if(*pSize == 31)
566 {
567 unsigned char aSizedata[2];
568 if(io_read(io: m_File, buffer: aSizedata, size: sizeof(aSizedata)) != sizeof(aSizedata))
569 return CHUNKHEADER_ERROR;
570 *pSize = (aSizedata[1] << 8) | aSizedata[0];
571 }
572 }
573
574 return CHUNKHEADER_SUCCESS;
575}
576
577CDemoPlayer::EScanFileResult CDemoPlayer::ScanFile()
578{
579 const int64_t StartPos = io_tell(io: m_File);
580 if(StartPos < 0)
581 {
582 return EScanFileResult::ERROR_UNRECOVERABLE;
583 }
584
585 const auto &ResetToStartPosition = [&](EScanFileResult Result) -> EScanFileResult {
586 if(io_seek(io: m_File, offset: StartPos, origin: IOSEEK_START) != 0)
587 {
588 m_vKeyFrames.clear();
589 return EScanFileResult::ERROR_UNRECOVERABLE;
590 }
591 return Result;
592 };
593
594 int ChunkTick = -1;
595 if(!m_vKeyFrames.empty())
596 {
597 if(io_seek(io: m_File, offset: m_vKeyFrames.back().m_Filepos, origin: IOSEEK_START) != 0)
598 {
599 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
600 }
601 int ChunkType, ChunkSize;
602 const EReadChunkHeaderResult Result = ReadChunkHeader(pType: &ChunkType, pSize: &ChunkSize, pTick: &ChunkTick);
603 if(Result != CHUNKHEADER_SUCCESS ||
604 (ChunkSize > 0 && io_skip(io: m_File, size: ChunkSize) != 0))
605 {
606 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
607 }
608 }
609
610 while(true)
611 {
612 const int64_t CurrentPos = io_tell(io: m_File);
613 if(CurrentPos < 0)
614 {
615 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
616 }
617
618 int ChunkType, ChunkSize;
619 const EReadChunkHeaderResult Result = ReadChunkHeader(pType: &ChunkType, pSize: &ChunkSize, pTick: &ChunkTick);
620 if(Result == CHUNKHEADER_EOF)
621 {
622 break;
623 }
624 else if(Result == CHUNKHEADER_ERROR)
625 {
626 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
627 }
628
629 if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
630 {
631 if(ChunkType & CHUNKTICKFLAG_KEYFRAME)
632 {
633 m_vKeyFrames.emplace_back(args: CurrentPos, args&: ChunkTick);
634 }
635 if(m_Info.m_Info.m_FirstTick == -1)
636 {
637 m_Info.m_Info.m_FirstTick = ChunkTick;
638 }
639 m_Info.m_Info.m_LastTick = ChunkTick;
640 }
641 else if(ChunkSize)
642 {
643 if(io_skip(io: m_File, size: ChunkSize) != 0)
644 {
645 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
646 }
647 }
648 }
649
650 // Cannot start playback without at least one keyframe
651 return ResetToStartPosition(m_vKeyFrames.empty() ? EScanFileResult::ERROR_UNRECOVERABLE : EScanFileResult::SUCCESS);
652}
653
654void CDemoPlayer::DoTick()
655{
656 // update ticks
657 m_Info.m_PreviousTick = m_Info.m_Info.m_CurrentTick;
658 m_Info.m_Info.m_CurrentTick = m_Info.m_NextTick;
659 int ChunkTick = m_Info.m_Info.m_CurrentTick;
660
661 UpdateTimes();
662
663 bool GotSnapshot = false;
664 while(true)
665 {
666 int ChunkType, ChunkSize;
667 const EReadChunkHeaderResult Result = ReadChunkHeader(pType: &ChunkType, pSize: &ChunkSize, pTick: &ChunkTick);
668 if(Result == CHUNKHEADER_EOF)
669 {
670 if(m_Info.m_PreviousTick == -1)
671 {
672 Stop(pErrorMessage: "Empty demo");
673 }
674 else
675 {
676 Pause();
677 // Stop rendering when reaching end of file
678#if defined(CONF_VIDEORECORDER)
679 if(m_UseVideo && IVideo::Current())
680 Stop();
681#endif
682 }
683 break;
684 }
685 else if(Result == CHUNKHEADER_ERROR)
686 {
687 Stop(pErrorMessage: "Error reading chunk header");
688 break;
689 }
690
691 // read the chunk
692 int DataSize = 0;
693 if(ChunkSize)
694 {
695 if(io_read(io: m_File, buffer: m_aCompressedSnapshotData, size: ChunkSize) != (unsigned)ChunkSize)
696 {
697 Stop(pErrorMessage: "Error reading chunk data");
698 break;
699 }
700
701 DataSize = CNetBase::Decompress(pData: m_aCompressedSnapshotData, DataSize: ChunkSize, pOutput: m_aDecompressedSnapshotData, OutputSize: sizeof(m_aDecompressedSnapshotData));
702 if(DataSize < 0)
703 {
704 Stop(pErrorMessage: "Error during network decompression");
705 break;
706 }
707
708 DataSize = CVariableInt::Decompress(pSrc: m_aDecompressedSnapshotData, SrcSize: DataSize, pDst: m_aChunkData, DstSize: sizeof(m_aChunkData));
709 if(DataSize < 0)
710 {
711 Stop(pErrorMessage: "Error during intpack decompression");
712 break;
713 }
714 }
715
716 if(ChunkType == CHUNKTYPE_DELTA)
717 {
718 // process delta snapshot
719 CSnapshot *pNewsnap = (CSnapshot *)m_aSnapshot;
720 DataSize = m_pSnapshotDelta->UnpackDelta(pFrom: (CSnapshot *)m_aLastSnapshotData, pTo: pNewsnap, pSrcData: m_aChunkData, DataSize, Sixup: IsSixup());
721
722 if(DataSize < 0)
723 {
724 if(m_pConsole)
725 {
726 char aBuf[64];
727 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Error unpacking snapshot delta. DataSize=%d", DataSize);
728 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
729 }
730 }
731 else if(!pNewsnap->IsValid(ActualSize: DataSize))
732 {
733 if(m_pConsole)
734 {
735 char aBuf[64];
736 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Snapshot delta invalid. DataSize=%d", DataSize);
737 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
738 }
739 }
740 else
741 {
742 if(m_pListener)
743 m_pListener->OnDemoPlayerSnapshot(pData: m_aSnapshot, Size: DataSize);
744
745 m_LastSnapshotDataSize = DataSize;
746 mem_copy(dest: m_aLastSnapshotData, source: m_aSnapshot, size: DataSize);
747 GotSnapshot = true;
748 }
749 }
750 else if(ChunkType == CHUNKTYPE_SNAPSHOT)
751 {
752 // process full snapshot
753 CSnapshot *pSnap = (CSnapshot *)m_aChunkData;
754 if(!pSnap->IsValid(ActualSize: DataSize))
755 {
756 if(m_pConsole)
757 {
758 char aBuf[64];
759 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Snapshot invalid. DataSize=%d", DataSize);
760 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
761 }
762 }
763 else
764 {
765 GotSnapshot = true;
766
767 m_LastSnapshotDataSize = DataSize;
768 mem_copy(dest: m_aLastSnapshotData, source: m_aChunkData, size: DataSize);
769 if(m_pListener)
770 m_pListener->OnDemoPlayerSnapshot(pData: m_aChunkData, Size: DataSize);
771 }
772 }
773 else
774 {
775 // if there were no snapshots in this tick, replay the last one
776 if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
777 {
778 GotSnapshot = true;
779 m_pListener->OnDemoPlayerSnapshot(pData: m_aLastSnapshotData, Size: m_LastSnapshotDataSize);
780 }
781
782 // check the remaining types
783 if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
784 {
785 m_Info.m_NextTick = ChunkTick;
786 break;
787 }
788 else if(ChunkType == CHUNKTYPE_MESSAGE)
789 {
790 if(m_pListener)
791 m_pListener->OnDemoPlayerMessage(pData: m_aChunkData, Size: DataSize);
792 }
793 }
794 }
795}
796
797void CDemoPlayer::Pause()
798{
799 m_Info.m_Info.m_Paused = true;
800#if defined(CONF_VIDEORECORDER)
801 if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
802 IVideo::Current()->Pause(Pause: true);
803#endif
804}
805
806void CDemoPlayer::Unpause()
807{
808 m_Info.m_Info.m_Paused = false;
809#if defined(CONF_VIDEORECORDER)
810 if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
811 IVideo::Current()->Pause(Pause: false);
812#endif
813}
814
815int CDemoPlayer::Load(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, int StorageType)
816{
817 dbg_assert(m_File == 0, "Demo player already playing");
818
819 m_pConsole = pConsole;
820 str_copy(dst&: m_aFilename, src: pFilename);
821 str_copy(dst&: m_aErrorMessage, src: "");
822
823 if(m_pConsole)
824 {
825 char aBuf[32 + IO_MAX_PATH_LENGTH];
826 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Loading demo '%s'", pFilename);
827 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
828 }
829
830 // clear the playback info
831 mem_zero(block: &m_Info, size: sizeof(m_Info));
832 m_Info.m_Info.m_FirstTick = -1;
833 m_Info.m_Info.m_LastTick = -1;
834 m_Info.m_NextTick = -1;
835 m_Info.m_Info.m_CurrentTick = -1;
836 m_Info.m_PreviousTick = -1;
837 m_Info.m_Info.m_Speed = 1;
838 m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;
839 m_LastSnapshotDataSize = -1;
840
841 if(!GetDemoInfo(pStorage, pConsole: m_pConsole, pFilename, StorageType, pDemoHeader: &m_Info.m_Header, pTimelineMarkers: &m_Info.m_TimelineMarkers, pMapInfo: &m_MapInfo, pFile: &m_File, pErrorMessage: m_aErrorMessage, ErrorMessageSize: sizeof(m_aErrorMessage)))
842 {
843 str_copy(dst&: m_aFilename, src: "");
844 return -1;
845 }
846 m_Sixup = str_startswith(str: m_Info.m_Header.m_aNetversion, prefix: "0.7");
847
848 // save byte offset of map for later use
849 m_MapOffset = io_tell(io: m_File);
850 if(m_MapOffset < 0 || io_skip(io: m_File, size: m_MapInfo.m_Size) != 0)
851 {
852 Stop(pErrorMessage: "Error skipping map data");
853 return -1;
854 }
855
856 if(m_Info.m_Header.m_Version > gs_OldVersion)
857 {
858 // get timeline markers
859 int Num = bytes_be_to_uint(bytes: m_Info.m_TimelineMarkers.m_aNumTimelineMarkers);
860 m_Info.m_Info.m_NumTimelineMarkers = std::clamp<int>(val: Num, lo: 0, hi: MAX_TIMELINE_MARKERS);
861 for(int i = 0; i < m_Info.m_Info.m_NumTimelineMarkers; i++)
862 {
863 m_Info.m_Info.m_aTimelineMarkers[i] = bytes_be_to_uint(bytes: m_Info.m_TimelineMarkers.m_aTimelineMarkers[i]);
864 }
865 }
866
867 // Scan the file for interesting points
868 if(ScanFile() == EScanFileResult::ERROR_UNRECOVERABLE)
869 {
870 Stop(pErrorMessage: "Error scanning demo file");
871 return -1;
872 }
873 m_Info.m_LiveStateUpdating = true;
874
875 // reset slice markers
876 g_Config.m_ClDemoSliceBegin = -1;
877 g_Config.m_ClDemoSliceEnd = -1;
878
879 // ready for playback
880 return 0;
881}
882
883unsigned char *CDemoPlayer::GetMapData(class IStorage *pStorage)
884{
885 if(!m_MapInfo.m_Size)
886 return nullptr;
887
888 const int64_t CurSeek = io_tell(io: m_File);
889 if(CurSeek < 0 || io_seek(io: m_File, offset: m_MapOffset, origin: IOSEEK_START) != 0)
890 return nullptr;
891 unsigned char *pMapData = (unsigned char *)malloc(size: m_MapInfo.m_Size);
892 if(io_read(io: m_File, buffer: pMapData, size: m_MapInfo.m_Size) != m_MapInfo.m_Size ||
893 io_seek(io: m_File, offset: CurSeek, origin: IOSEEK_START) != 0)
894 {
895 free(ptr: pMapData);
896 return nullptr;
897 }
898 return pMapData;
899}
900
901bool CDemoPlayer::ExtractMap(class IStorage *pStorage)
902{
903 unsigned char *pMapData = GetMapData(pStorage);
904 if(!pMapData)
905 return false;
906
907 // handle sha256
908 SHA256_DIGEST Sha256 = SHA256_ZEROED;
909 if(m_Info.m_Header.m_Version >= gs_Sha256Version)
910 Sha256 = m_MapInfo.m_Sha256;
911 else
912 {
913 Sha256 = sha256(message: pMapData, message_len: m_MapInfo.m_Size);
914 m_MapInfo.m_Sha256 = Sha256;
915 }
916
917 // construct name
918 char aSha[SHA256_MAXSTRSIZE], aMapFilename[IO_MAX_PATH_LENGTH];
919 sha256_str(digest: Sha256, str: aSha, max_len: sizeof(aSha));
920 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "downloadedmaps/%s_%s.map", m_Info.m_Header.m_aMapName, aSha);
921
922 // save map
923 IOHANDLE MapFile = pStorage->OpenFile(pFilename: aMapFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
924 if(!MapFile)
925 {
926 free(ptr: pMapData);
927 return false;
928 }
929
930 io_write(io: MapFile, buffer: pMapData, size: m_MapInfo.m_Size);
931 io_close(io: MapFile);
932
933 // free data
934 free(ptr: pMapData);
935 return true;
936}
937
938int64_t CDemoPlayer::Time()
939{
940#if defined(CONF_VIDEORECORDER)
941 if(m_UseVideo && IVideo::Current())
942 {
943 if(!m_WasRecording)
944 {
945 m_WasRecording = true;
946 m_Info.m_LastUpdate = IVideo::Time();
947 }
948 return IVideo::Time();
949 }
950 else
951 {
952 const int64_t Now = time_get();
953 if(m_WasRecording)
954 {
955 m_WasRecording = false;
956 m_Info.m_LastUpdate = Now;
957 }
958 return Now;
959 }
960#else
961 return time_get();
962#endif
963}
964
965void CDemoPlayer::Play()
966{
967 // Fill in previous and next tick
968 while(m_Info.m_PreviousTick == -1)
969 {
970 DoTick();
971 if(!IsPlaying())
972 {
973 // Empty demo or error playing tick
974 return;
975 }
976 }
977
978 // Initialize playback time. Using `set_new_tick` is essential so that `Time`
979 // returns the updated time, otherwise the delta between `m_LastUpdate` and
980 // the value that `Time` returns when called in the `Update` function can be
981 // very large depending on the time required to load the demo, which causes
982 // demo playback to start later. This ensures it always starts at 00:00.
983 set_new_tick();
984 m_Info.m_CurrentTime = m_Info.m_PreviousTick * time_freq() / SERVER_TICK_SPEED;
985 m_Info.m_LastUpdate = Time();
986 if(m_Info.m_LiveStateUpdating && m_Info.m_LastScan <= 0)
987 {
988 m_Info.m_LastScan = m_Info.m_LastUpdate;
989 }
990}
991
992bool CDemoPlayer::SeekPercent(float Percent)
993{
994 int WantedTick = m_Info.m_Info.m_FirstTick + round_truncate(f: (m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick) * Percent);
995 return SetPos(WantedTick);
996}
997
998bool CDemoPlayer::SeekTime(float Seconds)
999{
1000 int WantedTick = m_Info.m_Info.m_CurrentTick + round_truncate(f: Seconds * (float)SERVER_TICK_SPEED);
1001 return SetPos(WantedTick);
1002}
1003
1004bool CDemoPlayer::SeekTick(ETickOffset TickOffset)
1005{
1006 int WantedTick;
1007 switch(TickOffset)
1008 {
1009 case TICK_CURRENT:
1010 // TODO: WantedTick == m_Info.m_NextTick is used to update spectator info when paused so seeking must be done
1011 WantedTick = m_Info.m_Info.m_CurrentTick;
1012 break;
1013 case TICK_PREVIOUS:
1014 WantedTick = m_Info.m_PreviousTick;
1015 break;
1016 case TICK_NEXT:
1017 WantedTick = m_Info.m_NextTick;
1018 break;
1019 default:
1020 dbg_assert_failed("Invalid TickOffset");
1021 }
1022
1023 // +1 because SetPos will seek until the given tick is the next tick that
1024 // will be played back, whereas we want the wanted tick to be played now.
1025 return SetPos(WantedTick + 1);
1026}
1027
1028bool CDemoPlayer::SetPos(int WantedTick)
1029{
1030 if(!m_File)
1031 return false;
1032
1033 // TODO: WantedTick == m_Info.m_NextTick is used to update spectator info when paused so seeking must be done
1034
1035 int LastSeekableTick = m_Info.m_Info.m_LastTick;
1036 if(m_Info.m_Info.m_LiveDemo)
1037 {
1038 // Make sure we don't seek all the way until the end in a live demo because the chunk data may not be fully written.
1039 LastSeekableTick -= 2 * SERVER_TICK_SPEED;
1040 }
1041 if(LastSeekableTick < m_Info.m_Info.m_FirstTick)
1042 {
1043 WantedTick = m_Info.m_Info.m_FirstTick;
1044 }
1045 else
1046 {
1047 WantedTick = std::clamp(val: WantedTick, lo: m_Info.m_Info.m_FirstTick, hi: LastSeekableTick);
1048 }
1049
1050 // Just the next tick
1051 if(WantedTick == m_Info.m_NextTick + 1)
1052 {
1053 // This does handle looping correctly
1054 DoTick();
1055 Play();
1056 return true;
1057 }
1058
1059 const int KeyFrameWantedTick = WantedTick - 5; // -5 because we have to have a current tick and previous tick when we do the playback
1060 const float Percent = (KeyFrameWantedTick - m_Info.m_Info.m_FirstTick) / (float)(m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick);
1061
1062 // get correct key frame
1063 size_t KeyFrame = std::clamp<size_t>(val: m_vKeyFrames.size() * Percent, lo: 0, hi: m_vKeyFrames.size() - 1);
1064 while(KeyFrame < m_vKeyFrames.size() - 1 && m_vKeyFrames[KeyFrame].m_Tick < KeyFrameWantedTick)
1065 KeyFrame++;
1066 while(KeyFrame > 0 && m_vKeyFrames[KeyFrame].m_Tick > KeyFrameWantedTick)
1067 KeyFrame--;
1068
1069 if(WantedTick <= m_Info.m_Info.m_CurrentTick || // if we are seeking backwards (must be <= for high bandwidth demos) OR
1070 m_Info.m_Info.m_CurrentTick < m_vKeyFrames[KeyFrame].m_Tick || // we are before the wanted KeyFrame OR
1071 (KeyFrame != m_vKeyFrames.size() - 1 && m_Info.m_Info.m_CurrentTick >= m_vKeyFrames[KeyFrame + 1].m_Tick)) // we are after the wanted KeyFrame
1072 {
1073 if(io_seek(io: m_File, offset: m_vKeyFrames[KeyFrame].m_Filepos, origin: IOSEEK_START) != 0)
1074 {
1075 Stop(pErrorMessage: "Error seeking keyframe position");
1076 return false;
1077 }
1078 m_Info.m_NextTick = -1;
1079 m_Info.m_Info.m_CurrentTick = -1;
1080 m_Info.m_PreviousTick = -1;
1081 }
1082
1083 // playback everything until we hit our tick
1084 while(m_Info.m_NextTick < WantedTick)
1085 {
1086 DoTick();
1087 if(!IsPlaying())
1088 {
1089 return false;
1090 }
1091 }
1092
1093 Play();
1094
1095 return true;
1096}
1097
1098void CDemoPlayer::SetSpeed(float Speed)
1099{
1100 m_Info.m_Info.m_Speed = std::clamp(val: Speed, lo: 0.f, hi: 256.f);
1101}
1102
1103void CDemoPlayer::SetSpeedIndex(int SpeedIndex)
1104{
1105 dbg_assert(SpeedIndex >= 0 && SpeedIndex < (int)std::size(DEMO_SPEEDS), "invalid SpeedIndex");
1106 m_SpeedIndex = SpeedIndex;
1107 SetSpeed(DEMO_SPEEDS[m_SpeedIndex]);
1108}
1109
1110void CDemoPlayer::AdjustSpeedIndex(int Offset)
1111{
1112 SetSpeedIndex(std::clamp(val: m_SpeedIndex + Offset, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1)));
1113}
1114
1115void CDemoPlayer::Update(bool RealTime)
1116{
1117 const int64_t Now = Time();
1118 const int64_t Freq = time_freq();
1119 const int64_t DeltaTime = Now - m_Info.m_LastUpdate;
1120 m_Info.m_LastUpdate = Now;
1121
1122 if(m_Info.m_LiveStateUpdating)
1123 {
1124 // Determine if demo is live and still being written to, by scanning
1125 // file again and checking if more ticks are available than before.
1126 if(Now - m_Info.m_LastScan > Freq)
1127 {
1128 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1129 const EScanFileResult ScanResult = ScanFile();
1130 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1131 {
1132 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning demo file to determine live state");
1133 return;
1134 }
1135 else if(ScanResult == EScanFileResult::SUCCESS)
1136 {
1137 // Live state is known when ScanFile succeeded.
1138 m_Info.m_LiveStateUpdating = false;
1139 }
1140 else
1141 {
1142 m_Info.m_LiveStateFailedCount++;
1143 if(m_Info.m_LiveStateFailedCount >= 15)
1144 {
1145 // ScanFile keeps failing, which should be unlikely, so this is probably
1146 // not a live demo but a regular demo that is truncated at the end.
1147 m_Info.m_LiveStateUpdating = false;
1148 }
1149 }
1150 // Check if we got more ticks also when ScanFile failed, because
1151 // it could still have found more ticks.
1152 if(m_Info.m_Info.m_LastTick > PreviousLastTick)
1153 {
1154 m_Info.m_Info.m_LiveDemo = true;
1155 m_Info.m_LiveStateUpdating = false;
1156 m_Info.m_LiveStateUnchangedCount = 0;
1157 }
1158 m_Info.m_LastScan = Now;
1159 // Try again later if ScanFile failed and no more ticks were found.
1160 }
1161 }
1162 else if(m_Info.m_Info.m_LiveDemo)
1163 {
1164 // Scan live demo at tick frequency to smoothly update total time.
1165 if(Now - m_Info.m_LastScan > Freq / SERVER_TICK_SPEED)
1166 {
1167 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1168 const EScanFileResult ScanResult = ScanFile();
1169 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1170 {
1171 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning live demo file");
1172 return;
1173 }
1174 else if(ScanResult == EScanFileResult::SUCCESS &&
1175 m_Info.m_Info.m_LastTick == PreviousLastTick)
1176 {
1177 m_Info.m_LiveStateUnchangedCount++;
1178 if(m_Info.m_LiveStateUnchangedCount >= 2 * SERVER_TICK_SPEED)
1179 {
1180 // Assume demo stopped being live if we scanned the demo
1181 // successfully for 2 seconds without reading new ticks.
1182 m_Info.m_Info.m_LiveDemo = false;
1183 }
1184 }
1185 else
1186 {
1187 m_Info.m_LiveStateUnchangedCount = 0;
1188 }
1189 m_Info.m_LastScan = Now;
1190 }
1191 }
1192
1193 if(!IsPlaying())
1194 {
1195 return;
1196 }
1197
1198 if(!m_Info.m_Info.m_Paused)
1199 {
1200 if(m_Info.m_Info.m_LiveDemo &&
1201 m_Info.m_Info.m_Speed > 1.0f &&
1202 m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick <= (DeltaTime * (double)m_Info.m_Info.m_Speed / Freq + 2) * SERVER_TICK_SPEED)
1203 {
1204 // Reset to default speed if we are fast-forwarding to the end of a live demo,
1205 // to prevent playback error due to final demo chunk data still being written.
1206 SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
1207 }
1208
1209 m_Info.m_CurrentTime += (int64_t)(DeltaTime * (double)m_Info.m_Info.m_Speed);
1210
1211 // Do more ticks until we reach the current time.
1212 while(!m_Info.m_Info.m_Paused)
1213 {
1214 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1215 if(RealTime && CurrentTickStart > m_Info.m_CurrentTime)
1216 {
1217 break;
1218 }
1219 DoTick();
1220 if(!IsPlaying())
1221 {
1222 return;
1223 }
1224 }
1225 }
1226
1227 UpdateTimes();
1228}
1229
1230void CDemoPlayer::UpdateTimes()
1231{
1232 const int64_t Freq = time_freq();
1233 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1234 const int64_t PreviousTickStart = m_Info.m_PreviousTick * Freq / SERVER_TICK_SPEED;
1235 m_Info.m_IntraTick = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(CurrentTickStart - PreviousTickStart);
1236 m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(Freq / SERVER_TICK_SPEED);
1237 m_Info.m_TickTime = (m_Info.m_CurrentTime - PreviousTickStart) / (float)Freq;
1238 m_Info.m_Info.m_LivePlayback = m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick < 3 * SERVER_TICK_SPEED;
1239
1240 if(m_UpdateIntraTimesFunc)
1241 {
1242 m_UpdateIntraTimesFunc();
1243 }
1244}
1245
1246void CDemoPlayer::Stop(const char *pErrorMessage)
1247{
1248#if defined(CONF_VIDEORECORDER)
1249 if(m_UseVideo && IVideo::Current())
1250 IVideo::Current()->Stop();
1251 m_WasRecording = false;
1252#endif
1253
1254 if(!m_File)
1255 return;
1256
1257 if(m_pConsole)
1258 {
1259 char aBuf[256];
1260 if(pErrorMessage[0] == '\0')
1261 str_copy(dst&: aBuf, src: "Stopped playback");
1262 else
1263 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Stopped playback due to error: %s", pErrorMessage);
1264 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
1265 }
1266
1267 io_close(io: m_File);
1268 m_File = nullptr;
1269 m_vKeyFrames.clear();
1270 str_copy(dst&: m_aFilename, src: "");
1271 str_copy(dst&: m_aErrorMessage, src: pErrorMessage);
1272}
1273
1274void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const
1275{
1276 IStorage::StripPathAndExtension(pFilename: m_aFilename, pBuffer, BufferSize);
1277}
1278
1279bool CDemoPlayer::GetDemoInfo(IStorage *pStorage, IConsole *pConsole, const char *pFilename, int StorageType, CDemoHeader *pDemoHeader, CTimelineMarkers *pTimelineMarkers, CMapInfo *pMapInfo, IOHANDLE *pFile, char *pErrorMessage, size_t ErrorMessageSize) const
1280{
1281 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1282 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1283 mem_zero(block: pMapInfo, size: sizeof(CMapInfo));
1284
1285 IOHANDLE File = pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType);
1286 if(!File)
1287 {
1288 if(pErrorMessage != nullptr)
1289 str_copy(dst: pErrorMessage, src: "Could not open demo file", dst_size: ErrorMessageSize);
1290 return false;
1291 }
1292
1293 if(io_read(io: File, buffer: pDemoHeader, size: sizeof(CDemoHeader)) != sizeof(CDemoHeader) || !pDemoHeader->Valid())
1294 {
1295 if(pErrorMessage != nullptr)
1296 str_copy(dst: pErrorMessage, src: "Error reading demo header", dst_size: ErrorMessageSize);
1297 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1298 io_close(io: File);
1299 return false;
1300 }
1301
1302 if(pDemoHeader->m_Version < gs_OldVersion)
1303 {
1304 if(pErrorMessage != nullptr)
1305 str_format(buffer: pErrorMessage, buffer_size: ErrorMessageSize, format: "Demo version '%d' is not supported", pDemoHeader->m_Version);
1306 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1307 io_close(io: File);
1308 return false;
1309 }
1310 else if(pDemoHeader->m_Version > gs_OldVersion)
1311 {
1312 if(io_read(io: File, buffer: pTimelineMarkers, size: sizeof(CTimelineMarkers)) != sizeof(CTimelineMarkers))
1313 {
1314 if(pErrorMessage != nullptr)
1315 str_copy(dst: pErrorMessage, src: "Error reading timeline markers", dst_size: ErrorMessageSize);
1316 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1317 io_close(io: File);
1318 return false;
1319 }
1320 }
1321
1322 SHA256_DIGEST Sha256 = SHA256_ZEROED;
1323 if(pDemoHeader->m_Version >= gs_Sha256Version)
1324 {
1325 CUuid ExtensionUuid = {};
1326 const unsigned ExtensionUuidSize = io_read(io: File, buffer: &ExtensionUuid.m_aData, size: sizeof(ExtensionUuid.m_aData));
1327 if(ExtensionUuidSize == sizeof(ExtensionUuid.m_aData) && ExtensionUuid == SHA256_EXTENSION)
1328 {
1329 if(io_read(io: File, buffer: &Sha256, size: sizeof(SHA256_DIGEST)) != sizeof(SHA256_DIGEST))
1330 {
1331 if(pErrorMessage != nullptr)
1332 str_copy(dst: pErrorMessage, src: "Error reading SHA256", dst_size: ErrorMessageSize);
1333 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1334 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1335 io_close(io: File);
1336 return false;
1337 }
1338 }
1339 else
1340 {
1341 // This hopes whatever happened during the version increment didn't add something here
1342 if(pConsole)
1343 {
1344 pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: "Demo version incremented, but not by DDNet");
1345 }
1346 if(io_seek(io: File, offset: -(int64_t)ExtensionUuidSize, origin: IOSEEK_CUR) != 0)
1347 {
1348 if(pErrorMessage != nullptr)
1349 str_copy(dst: pErrorMessage, src: "Error rewinding SHA256 extension UUID", dst_size: ErrorMessageSize);
1350 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1351 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1352 io_close(io: File);
1353 return false;
1354 }
1355 }
1356 }
1357
1358 str_copy(dst&: pMapInfo->m_aName, src: pDemoHeader->m_aMapName);
1359 pMapInfo->m_Sha256 = Sha256;
1360 pMapInfo->m_Crc = bytes_be_to_uint(bytes: pDemoHeader->m_aMapCrc);
1361 pMapInfo->m_Size = bytes_be_to_uint(bytes: pDemoHeader->m_aMapSize);
1362
1363 if(pFile == nullptr)
1364 io_close(io: File);
1365 else
1366 *pFile = File;
1367
1368 return true;
1369}
1370
1371class CDemoRecordingListener : public CDemoPlayer::IListener
1372{
1373public:
1374 CDemoRecorder *m_pDemoRecorder;
1375 CDemoPlayer *m_pDemoPlayer;
1376 bool m_Stop;
1377 int m_StartTick;
1378 int m_EndTick;
1379
1380 void OnDemoPlayerSnapshot(void *pData, int Size) override
1381 {
1382 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1383
1384 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1385 m_Stop = true;
1386 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1387 m_pDemoRecorder->RecordSnapshot(Tick: pInfo->m_Info.m_CurrentTick, pData, Size);
1388 }
1389
1390 void OnDemoPlayerMessage(void *pData, int Size) override
1391 {
1392 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1393
1394 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1395 m_Stop = true;
1396 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1397 m_pDemoRecorder->RecordMessage(pData, Size);
1398 }
1399};
1400
1401void CDemoEditor::Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage)
1402{
1403 m_pSnapshotDelta = pSnapshotDelta;
1404 m_pConsole = pConsole;
1405 m_pStorage = pStorage;
1406}
1407
1408bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser)
1409{
1410 CDemoPlayer DemoPlayer(m_pSnapshotDelta, false);
1411 if(DemoPlayer.Load(pStorage: m_pStorage, pConsole: m_pConsole, pFilename: pDemo, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
1412 return false;
1413
1414 const CMapInfo *pMapInfo = DemoPlayer.GetMapInfo();
1415 const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();
1416
1417 SHA256_DIGEST Sha256 = pMapInfo->m_Sha256;
1418 if(pInfo->m_Header.m_Version < gs_Sha256Version)
1419 {
1420 if(DemoPlayer.ExtractMap(pStorage: m_pStorage))
1421 Sha256 = pMapInfo->m_Sha256;
1422 }
1423
1424 CDemoRecorder DemoRecorder(m_pSnapshotDelta);
1425 unsigned char *pMapData = DemoPlayer.GetMapData(pStorage: m_pStorage);
1426 const int Result = DemoRecorder.Start(pStorage: m_pStorage, pConsole: m_pConsole, pFilename: pDst, pNetVersion: pInfo->m_Header.m_aNetversion, pMap: pMapInfo->m_aName, Sha256, Crc: pMapInfo->m_Crc, pType: pInfo->m_Header.m_aType, MapSize: pMapInfo->m_Size, pMapData, MapFile: nullptr, pfnFilter, pUser) == -1;
1427 free(ptr: pMapData);
1428 if(Result != 0)
1429 {
1430 DemoPlayer.Stop();
1431 return false;
1432 }
1433
1434 CDemoRecordingListener Listener;
1435 Listener.m_pDemoRecorder = &DemoRecorder;
1436 Listener.m_pDemoPlayer = &DemoPlayer;
1437 Listener.m_Stop = false;
1438 Listener.m_StartTick = StartTick;
1439 Listener.m_EndTick = EndTick;
1440 DemoPlayer.SetListener(&Listener);
1441
1442 DemoPlayer.Play();
1443
1444 while(DemoPlayer.IsPlaying() && !Listener.m_Stop)
1445 {
1446 DemoPlayer.Update(RealTime: false);
1447
1448 if(pInfo->m_Info.m_Paused)
1449 break;
1450 }
1451
1452 // Copy timeline markers to sliced demo
1453 for(int i = 0; i < pInfo->m_Info.m_NumTimelineMarkers; i++)
1454 {
1455 if((StartTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] >= StartTick) && (EndTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] <= EndTick))
1456 {
1457 DemoRecorder.AddDemoMarker(Tick: pInfo->m_Info.m_aTimelineMarkers[i]);
1458 }
1459 }
1460
1461 DemoPlayer.Stop();
1462 DemoRecorder.Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
1463 return true;
1464}
1465