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