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 std::optional<SHA256_DIGEST> Sha256;
909 if(m_Info.m_Header.m_Version >= gs_Sha256Version)
910 {
911 Sha256 = m_MapInfo.m_Sha256;
912 dbg_assert(Sha256.has_value(), "SHA256 missing for version %d demo", m_Info.m_Header.m_Version);
913 }
914 else
915 {
916 Sha256 = sha256(message: pMapData, message_len: m_MapInfo.m_Size);
917 m_MapInfo.m_Sha256 = Sha256;
918 }
919
920 // construct name
921 char aSha[SHA256_MAXSTRSIZE], aMapFilename[IO_MAX_PATH_LENGTH];
922 sha256_str(digest: Sha256.value(), str: aSha, max_len: sizeof(aSha));
923 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "downloadedmaps/%s_%s.map", m_Info.m_Header.m_aMapName, aSha);
924
925 // save map
926 IOHANDLE MapFile = pStorage->OpenFile(pFilename: aMapFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
927 if(!MapFile)
928 {
929 free(ptr: pMapData);
930 return false;
931 }
932
933 io_write(io: MapFile, buffer: pMapData, size: m_MapInfo.m_Size);
934 io_close(io: MapFile);
935
936 // free data
937 free(ptr: pMapData);
938 return true;
939}
940
941int64_t CDemoPlayer::Time()
942{
943#if defined(CONF_VIDEORECORDER)
944 if(m_UseVideo && IVideo::Current())
945 {
946 if(!m_WasRecording)
947 {
948 m_WasRecording = true;
949 m_Info.m_LastUpdate = IVideo::Time();
950 }
951 return IVideo::Time();
952 }
953 else
954 {
955 const int64_t Now = time_get();
956 if(m_WasRecording)
957 {
958 m_WasRecording = false;
959 m_Info.m_LastUpdate = Now;
960 }
961 return Now;
962 }
963#else
964 return time_get();
965#endif
966}
967
968void CDemoPlayer::Play()
969{
970 // Fill in previous and next tick
971 while(m_Info.m_PreviousTick == -1)
972 {
973 DoTick();
974 if(!IsPlaying())
975 {
976 // Empty demo or error playing tick
977 return;
978 }
979 }
980
981 // Initialize playback time. Using `set_new_tick` is essential so that `Time`
982 // returns the updated time, otherwise the delta between `m_LastUpdate` and
983 // the value that `Time` returns when called in the `Update` function can be
984 // very large depending on the time required to load the demo, which causes
985 // demo playback to start later. This ensures it always starts at 00:00.
986 set_new_tick();
987 m_Info.m_CurrentTime = m_Info.m_PreviousTick * time_freq() / SERVER_TICK_SPEED;
988 m_Info.m_LastUpdate = Time();
989 if(m_Info.m_LiveStateUpdating && m_Info.m_LastScan <= 0)
990 {
991 m_Info.m_LastScan = m_Info.m_LastUpdate;
992 }
993}
994
995bool CDemoPlayer::SeekPercent(float Percent)
996{
997 int WantedTick = m_Info.m_Info.m_FirstTick + round_truncate(f: (m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick) * Percent);
998 return SetPos(WantedTick);
999}
1000
1001bool CDemoPlayer::SeekTime(float Seconds)
1002{
1003 int WantedTick = m_Info.m_Info.m_CurrentTick + round_truncate(f: Seconds * (float)SERVER_TICK_SPEED);
1004 return SetPos(WantedTick);
1005}
1006
1007bool CDemoPlayer::SeekTick(ETickOffset TickOffset)
1008{
1009 int WantedTick;
1010 switch(TickOffset)
1011 {
1012 case TICK_CURRENT:
1013 // TODO: WantedTick == m_Info.m_NextTick is used to update spectator info when paused so seeking must be done
1014 WantedTick = m_Info.m_Info.m_CurrentTick;
1015 break;
1016 case TICK_PREVIOUS:
1017 WantedTick = m_Info.m_PreviousTick;
1018 break;
1019 case TICK_NEXT:
1020 WantedTick = m_Info.m_NextTick;
1021 break;
1022 default:
1023 dbg_assert_failed("Invalid TickOffset");
1024 }
1025
1026 // +1 because SetPos will seek until the given tick is the next tick that
1027 // will be played back, whereas we want the wanted tick to be played now.
1028 return SetPos(WantedTick + 1);
1029}
1030
1031bool CDemoPlayer::SetPos(int WantedTick)
1032{
1033 if(!m_File)
1034 return false;
1035
1036 // TODO: WantedTick == m_Info.m_NextTick is used to update spectator info when paused so seeking must be done
1037
1038 int LastSeekableTick = m_Info.m_Info.m_LastTick;
1039 if(m_Info.m_Info.m_LiveDemo)
1040 {
1041 // 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.
1042 LastSeekableTick -= 2 * SERVER_TICK_SPEED;
1043 }
1044 if(LastSeekableTick < m_Info.m_Info.m_FirstTick)
1045 {
1046 WantedTick = m_Info.m_Info.m_FirstTick;
1047 }
1048 else
1049 {
1050 WantedTick = std::clamp(val: WantedTick, lo: m_Info.m_Info.m_FirstTick, hi: LastSeekableTick);
1051 }
1052
1053 // Just the next tick
1054 if(WantedTick == m_Info.m_NextTick + 1)
1055 {
1056 // This does handle looping correctly
1057 DoTick();
1058 Play();
1059 return true;
1060 }
1061
1062 const int KeyFrameWantedTick = WantedTick - 5; // -5 because we have to have a current tick and previous tick when we do the playback
1063 const float Percent = (KeyFrameWantedTick - m_Info.m_Info.m_FirstTick) / (float)(m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick);
1064
1065 // get correct key frame
1066 size_t KeyFrame = std::clamp<size_t>(val: m_vKeyFrames.size() * Percent, lo: 0, hi: m_vKeyFrames.size() - 1);
1067 while(KeyFrame < m_vKeyFrames.size() - 1 && m_vKeyFrames[KeyFrame].m_Tick < KeyFrameWantedTick)
1068 KeyFrame++;
1069 while(KeyFrame > 0 && m_vKeyFrames[KeyFrame].m_Tick > KeyFrameWantedTick)
1070 KeyFrame--;
1071
1072 if(WantedTick <= m_Info.m_Info.m_CurrentTick || // if we are seeking backwards (must be <= for high bandwidth demos) OR
1073 m_Info.m_Info.m_CurrentTick < m_vKeyFrames[KeyFrame].m_Tick || // we are before the wanted KeyFrame OR
1074 (KeyFrame != m_vKeyFrames.size() - 1 && m_Info.m_Info.m_CurrentTick >= m_vKeyFrames[KeyFrame + 1].m_Tick)) // we are after the wanted KeyFrame
1075 {
1076 if(io_seek(io: m_File, offset: m_vKeyFrames[KeyFrame].m_Filepos, origin: IOSEEK_START) != 0)
1077 {
1078 Stop(pErrorMessage: "Error seeking keyframe position");
1079 return false;
1080 }
1081 m_Info.m_NextTick = -1;
1082 m_Info.m_Info.m_CurrentTick = -1;
1083 m_Info.m_PreviousTick = -1;
1084 }
1085
1086 // playback everything until we hit our tick
1087 while(m_Info.m_NextTick < WantedTick)
1088 {
1089 DoTick();
1090 if(!IsPlaying())
1091 {
1092 return false;
1093 }
1094 }
1095
1096 Play();
1097
1098 return true;
1099}
1100
1101void CDemoPlayer::SetSpeed(float Speed)
1102{
1103 m_Info.m_Info.m_Speed = std::clamp(val: Speed, lo: 0.f, hi: 256.f);
1104}
1105
1106void CDemoPlayer::SetSpeedIndex(int SpeedIndex)
1107{
1108 dbg_assert(SpeedIndex >= 0 && SpeedIndex < (int)std::size(DEMO_SPEEDS), "invalid SpeedIndex");
1109 m_SpeedIndex = SpeedIndex;
1110 SetSpeed(DEMO_SPEEDS[m_SpeedIndex]);
1111}
1112
1113void CDemoPlayer::AdjustSpeedIndex(int Offset)
1114{
1115 SetSpeedIndex(std::clamp(val: m_SpeedIndex + Offset, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1)));
1116}
1117
1118void CDemoPlayer::Update(bool RealTime)
1119{
1120 const int64_t Now = Time();
1121 const int64_t Freq = time_freq();
1122 const int64_t DeltaTime = Now - m_Info.m_LastUpdate;
1123 m_Info.m_LastUpdate = Now;
1124
1125 if(m_Info.m_LiveStateUpdating)
1126 {
1127 // Determine if demo is live and still being written to, by scanning
1128 // file again and checking if more ticks are available than before.
1129 if(Now - m_Info.m_LastScan > Freq)
1130 {
1131 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1132 const EScanFileResult ScanResult = ScanFile();
1133 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1134 {
1135 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning demo file to determine live state");
1136 return;
1137 }
1138 else if(ScanResult == EScanFileResult::SUCCESS)
1139 {
1140 // Live state is known when ScanFile succeeded.
1141 m_Info.m_LiveStateUpdating = false;
1142 }
1143 else
1144 {
1145 m_Info.m_LiveStateFailedCount++;
1146 if(m_Info.m_LiveStateFailedCount >= 15)
1147 {
1148 // ScanFile keeps failing, which should be unlikely, so this is probably
1149 // not a live demo but a regular demo that is truncated at the end.
1150 m_Info.m_LiveStateUpdating = false;
1151 }
1152 }
1153 // Check if we got more ticks also when ScanFile failed, because
1154 // it could still have found more ticks.
1155 if(m_Info.m_Info.m_LastTick > PreviousLastTick)
1156 {
1157 m_Info.m_Info.m_LiveDemo = true;
1158 m_Info.m_LiveStateUpdating = false;
1159 m_Info.m_LiveStateUnchangedCount = 0;
1160 }
1161 m_Info.m_LastScan = Now;
1162 // Try again later if ScanFile failed and no more ticks were found.
1163 }
1164 }
1165 else if(m_Info.m_Info.m_LiveDemo)
1166 {
1167 // Scan live demo at tick frequency to smoothly update total time.
1168 if(Now - m_Info.m_LastScan > Freq / SERVER_TICK_SPEED)
1169 {
1170 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1171 const EScanFileResult ScanResult = ScanFile();
1172 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1173 {
1174 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning live demo file");
1175 return;
1176 }
1177 else if(ScanResult == EScanFileResult::SUCCESS &&
1178 m_Info.m_Info.m_LastTick == PreviousLastTick)
1179 {
1180 m_Info.m_LiveStateUnchangedCount++;
1181 if(m_Info.m_LiveStateUnchangedCount >= 2 * SERVER_TICK_SPEED)
1182 {
1183 // Assume demo stopped being live if we scanned the demo
1184 // successfully for 2 seconds without reading new ticks.
1185 m_Info.m_Info.m_LiveDemo = false;
1186 }
1187 }
1188 else
1189 {
1190 m_Info.m_LiveStateUnchangedCount = 0;
1191 }
1192 m_Info.m_LastScan = Now;
1193 }
1194 }
1195
1196 if(!IsPlaying())
1197 {
1198 return;
1199 }
1200
1201 if(!m_Info.m_Info.m_Paused)
1202 {
1203 if(m_Info.m_Info.m_LiveDemo &&
1204 m_Info.m_Info.m_Speed > 1.0f &&
1205 m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick <= (DeltaTime * (double)m_Info.m_Info.m_Speed / Freq + 2) * (float)SERVER_TICK_SPEED)
1206 {
1207 // Reset to default speed if we are fast-forwarding to the end of a live demo,
1208 // to prevent playback error due to final demo chunk data still being written.
1209 SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
1210 }
1211
1212 m_Info.m_CurrentTime += (int64_t)(DeltaTime * (double)m_Info.m_Info.m_Speed);
1213
1214 // Do more ticks until we reach the current time.
1215 while(!m_Info.m_Info.m_Paused)
1216 {
1217 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1218 if(RealTime && CurrentTickStart > m_Info.m_CurrentTime)
1219 {
1220 break;
1221 }
1222 DoTick();
1223 if(!IsPlaying())
1224 {
1225 return;
1226 }
1227 }
1228 }
1229
1230 UpdateTimes();
1231}
1232
1233void CDemoPlayer::UpdateTimes()
1234{
1235 const int64_t Freq = time_freq();
1236 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1237 const int64_t PreviousTickStart = m_Info.m_PreviousTick * Freq / SERVER_TICK_SPEED;
1238 m_Info.m_IntraTick = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(CurrentTickStart - PreviousTickStart);
1239 m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(Freq / SERVER_TICK_SPEED);
1240 m_Info.m_TickTime = (m_Info.m_CurrentTime - PreviousTickStart) / (float)Freq;
1241 m_Info.m_Info.m_LivePlayback = m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick < 3 * SERVER_TICK_SPEED;
1242
1243 if(m_UpdateIntraTimesFunc)
1244 {
1245 m_UpdateIntraTimesFunc();
1246 }
1247}
1248
1249void CDemoPlayer::Stop(const char *pErrorMessage)
1250{
1251#if defined(CONF_VIDEORECORDER)
1252 if(m_UseVideo && IVideo::Current())
1253 IVideo::Current()->Stop();
1254 m_WasRecording = false;
1255#endif
1256
1257 if(!m_File)
1258 return;
1259
1260 if(m_pConsole)
1261 {
1262 char aBuf[256];
1263 if(pErrorMessage[0] == '\0')
1264 str_copy(dst&: aBuf, src: "Stopped playback");
1265 else
1266 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Stopped playback due to error: %s", pErrorMessage);
1267 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
1268 }
1269
1270 io_close(io: m_File);
1271 m_File = nullptr;
1272 m_vKeyFrames.clear();
1273 str_copy(dst&: m_aFilename, src: "");
1274 str_copy(dst&: m_aErrorMessage, src: pErrorMessage);
1275}
1276
1277void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const
1278{
1279 IStorage::StripPathAndExtension(pFilename: m_aFilename, pBuffer, BufferSize);
1280}
1281
1282bool 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
1283{
1284 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1285 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1286 pMapInfo->m_aName[0] = '\0';
1287 pMapInfo->m_Sha256 = std::nullopt;
1288 pMapInfo->m_Crc = 0;
1289 pMapInfo->m_Size = 0;
1290
1291 IOHANDLE File = pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType);
1292 if(!File)
1293 {
1294 if(pErrorMessage != nullptr)
1295 str_copy(dst: pErrorMessage, src: "Could not open demo file", dst_size: ErrorMessageSize);
1296 return false;
1297 }
1298
1299 if(io_read(io: File, buffer: pDemoHeader, size: sizeof(CDemoHeader)) != sizeof(CDemoHeader) || !pDemoHeader->Valid())
1300 {
1301 if(pErrorMessage != nullptr)
1302 str_copy(dst: pErrorMessage, src: "Error reading demo header", dst_size: ErrorMessageSize);
1303 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1304 io_close(io: File);
1305 return false;
1306 }
1307
1308 if(pDemoHeader->m_Version < gs_OldVersion)
1309 {
1310 if(pErrorMessage != nullptr)
1311 str_format(buffer: pErrorMessage, buffer_size: ErrorMessageSize, format: "Demo version '%d' is not supported", pDemoHeader->m_Version);
1312 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1313 io_close(io: File);
1314 return false;
1315 }
1316 else if(pDemoHeader->m_Version > gs_OldVersion)
1317 {
1318 if(io_read(io: File, buffer: pTimelineMarkers, size: sizeof(CTimelineMarkers)) != sizeof(CTimelineMarkers))
1319 {
1320 if(pErrorMessage != nullptr)
1321 str_copy(dst: pErrorMessage, src: "Error reading timeline markers", dst_size: ErrorMessageSize);
1322 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1323 io_close(io: File);
1324 return false;
1325 }
1326 }
1327
1328 std::optional<SHA256_DIGEST> Sha256;
1329 if(pDemoHeader->m_Version >= gs_Sha256Version)
1330 {
1331 CUuid ExtensionUuid = {};
1332 const unsigned ExtensionUuidSize = io_read(io: File, buffer: &ExtensionUuid.m_aData, size: sizeof(ExtensionUuid.m_aData));
1333 if(ExtensionUuidSize == sizeof(ExtensionUuid.m_aData) && ExtensionUuid == SHA256_EXTENSION)
1334 {
1335 SHA256_DIGEST ReadSha256;
1336 if(io_read(io: File, buffer: &ReadSha256, size: sizeof(SHA256_DIGEST)) != sizeof(SHA256_DIGEST))
1337 {
1338 if(pErrorMessage != nullptr)
1339 str_copy(dst: pErrorMessage, src: "Error reading SHA256", dst_size: ErrorMessageSize);
1340 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1341 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1342 io_close(io: File);
1343 return false;
1344 }
1345 Sha256 = ReadSha256;
1346 }
1347 else
1348 {
1349 // This hopes whatever happened during the version increment didn't add something here
1350 if(pConsole)
1351 {
1352 pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: "Demo version incremented, but not by DDNet");
1353 }
1354 if(io_seek(io: File, offset: -(int64_t)ExtensionUuidSize, origin: IOSEEK_CUR) != 0)
1355 {
1356 if(pErrorMessage != nullptr)
1357 str_copy(dst: pErrorMessage, src: "Error rewinding SHA256 extension UUID", dst_size: ErrorMessageSize);
1358 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1359 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1360 io_close(io: File);
1361 return false;
1362 }
1363 }
1364 }
1365
1366 str_copy(dst&: pMapInfo->m_aName, src: pDemoHeader->m_aMapName);
1367 pMapInfo->m_Sha256 = Sha256;
1368 pMapInfo->m_Crc = bytes_be_to_uint(bytes: pDemoHeader->m_aMapCrc);
1369 pMapInfo->m_Size = bytes_be_to_uint(bytes: pDemoHeader->m_aMapSize);
1370
1371 if(pFile == nullptr)
1372 io_close(io: File);
1373 else
1374 *pFile = File;
1375
1376 return true;
1377}
1378
1379class CDemoRecordingListener : public CDemoPlayer::IListener
1380{
1381public:
1382 CDemoRecorder *m_pDemoRecorder;
1383 CDemoPlayer *m_pDemoPlayer;
1384 bool m_Stop;
1385 int m_StartTick;
1386 int m_EndTick;
1387
1388 void OnDemoPlayerSnapshot(void *pData, int Size) override
1389 {
1390 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1391
1392 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1393 m_Stop = true;
1394 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1395 m_pDemoRecorder->RecordSnapshot(Tick: pInfo->m_Info.m_CurrentTick, pData, Size);
1396 }
1397
1398 void OnDemoPlayerMessage(void *pData, int Size) override
1399 {
1400 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1401
1402 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1403 m_Stop = true;
1404 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1405 m_pDemoRecorder->RecordMessage(pData, Size);
1406 }
1407};
1408
1409void CDemoEditor::Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage)
1410{
1411 m_pSnapshotDelta = pSnapshotDelta;
1412 m_pConsole = pConsole;
1413 m_pStorage = pStorage;
1414}
1415
1416bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser)
1417{
1418 CDemoPlayer DemoPlayer(m_pSnapshotDelta, false);
1419 if(DemoPlayer.Load(pStorage: m_pStorage, pConsole: m_pConsole, pFilename: pDemo, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
1420 return false;
1421
1422 const CMapInfo *pMapInfo = DemoPlayer.GetMapInfo();
1423 const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();
1424
1425 std::optional<SHA256_DIGEST> Sha256 = pMapInfo->m_Sha256;
1426 if(pInfo->m_Header.m_Version < gs_Sha256Version)
1427 {
1428 if(DemoPlayer.ExtractMap(pStorage: m_pStorage))
1429 {
1430 Sha256 = pMapInfo->m_Sha256;
1431 }
1432 }
1433 if(!Sha256.has_value())
1434 {
1435 log_error_color(DEMO_PRINT_COLOR, "demo/slice", "Failed to start demo slicing because map SHA256 could not be determined.");
1436 return false;
1437 }
1438
1439 CDemoRecorder DemoRecorder(m_pSnapshotDelta);
1440 unsigned char *pMapData = DemoPlayer.GetMapData(pStorage: m_pStorage);
1441 const int Result = DemoRecorder.Start(pStorage: m_pStorage, pConsole: m_pConsole, pFilename: pDst, pNetVersion: pInfo->m_Header.m_aNetversion, pMap: pMapInfo->m_aName, Sha256: Sha256.value(), Crc: pMapInfo->m_Crc, pType: pInfo->m_Header.m_aType, MapSize: pMapInfo->m_Size, pMapData, MapFile: nullptr, pfnFilter, pUser) == -1;
1442 free(ptr: pMapData);
1443 if(Result != 0)
1444 {
1445 DemoPlayer.Stop();
1446 return false;
1447 }
1448
1449 CDemoRecordingListener Listener;
1450 Listener.m_pDemoRecorder = &DemoRecorder;
1451 Listener.m_pDemoPlayer = &DemoPlayer;
1452 Listener.m_Stop = false;
1453 Listener.m_StartTick = StartTick;
1454 Listener.m_EndTick = EndTick;
1455 DemoPlayer.SetListener(&Listener);
1456
1457 DemoPlayer.Play();
1458
1459 while(DemoPlayer.IsPlaying() && !Listener.m_Stop)
1460 {
1461 DemoPlayer.Update(RealTime: false);
1462
1463 if(pInfo->m_Info.m_Paused)
1464 break;
1465 }
1466
1467 // Copy timeline markers to sliced demo
1468 for(int i = 0; i < pInfo->m_Info.m_NumTimelineMarkers; i++)
1469 {
1470 if((StartTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] >= StartTick) && (EndTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] <= EndTick))
1471 {
1472 DemoRecorder.AddDemoMarker(Tick: pInfo->m_Info.m_aTimelineMarkers[i]);
1473 }
1474 }
1475
1476 DemoPlayer.Stop();
1477 DemoRecorder.Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
1478 return true;
1479}
1480