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 == nullptr, "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 == nullptr, "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: EIoSeekOrigin::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: EIoSeekOrigin::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 char aDeltaData[CSnapshot::MAX_SIZE];
351 const int DeltaSize = m_pSnapshotDelta->CreateDelta(pFrom: m_LastSnapshotData.AsSnapshot(), pTo: (CSnapshot *)pData, pDstData: &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: EIoSeekOrigin::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: EIoSeekOrigin::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 == nullptr, "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: EIoSeekOrigin::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: EIoSeekOrigin::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 if(m_LastSnapshotDataSize == -1)
733 {
734 Stop(pErrorMessage: "Delta snapshot before any full snapshot");
735 break;
736 }
737
738 // process delta snapshot
739 DataSize = SnapshotDelta()->UnpackDelta(pFrom: m_LastSnapshotData.AsSnapshot(), pTo: &m_Snapshot, pSrcData: m_aChunkData, DataSize);
740
741 if(DataSize < 0)
742 {
743 if(m_pConsole)
744 {
745 char aBuf[64];
746 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Error unpacking snapshot delta. DataSize=%d", DataSize);
747 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
748 }
749 }
750 else if(!m_Snapshot.AsSnapshot()->IsValid(ActualSize: DataSize))
751 {
752 if(m_pConsole)
753 {
754 char aBuf[64];
755 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Snapshot delta invalid. DataSize=%d", DataSize);
756 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
757 }
758 }
759 else
760 {
761 if(m_pListener)
762 m_pListener->OnDemoPlayerSnapshot(pData: m_Snapshot.AsSnapshot(), Size: DataSize);
763
764 m_LastSnapshotDataSize = DataSize;
765 mem_copy(dest: &m_LastSnapshotData, source: &m_Snapshot, size: DataSize);
766 GotSnapshot = true;
767 }
768 }
769 else if(ChunkType == CHUNKTYPE_SNAPSHOT)
770 {
771 // process full snapshot
772 CSnapshot *pSnap = (CSnapshot *)m_aChunkData;
773 if(!pSnap->IsValid(ActualSize: DataSize))
774 {
775 if(m_pConsole)
776 {
777 char aBuf[64];
778 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Snapshot invalid. DataSize=%d", DataSize);
779 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
780 }
781 }
782 else
783 {
784 GotSnapshot = true;
785
786 m_LastSnapshotDataSize = DataSize;
787 mem_copy(dest: &m_LastSnapshotData, source: m_aChunkData, size: DataSize);
788 if(m_pListener)
789 m_pListener->OnDemoPlayerSnapshot(pData: m_aChunkData, Size: DataSize);
790 }
791 }
792 else
793 {
794 // if there were no snapshots in this tick, replay the last one
795 if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
796 {
797 GotSnapshot = true;
798 m_pListener->OnDemoPlayerSnapshot(pData: &m_LastSnapshotData, Size: m_LastSnapshotDataSize);
799 }
800
801 // check the remaining types
802 if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
803 {
804 m_Info.m_NextTick = ChunkTick;
805 break;
806 }
807 else if(ChunkType == CHUNKTYPE_MESSAGE)
808 {
809 if(m_pListener)
810 m_pListener->OnDemoPlayerMessage(pData: m_aChunkData, Size: DataSize);
811 }
812 }
813 }
814}
815
816void CDemoPlayer::Pause()
817{
818 m_Info.m_Info.m_Paused = true;
819#if defined(CONF_VIDEORECORDER)
820 if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
821 IVideo::Current()->Pause(Pause: true);
822#endif
823}
824
825void CDemoPlayer::Unpause()
826{
827 m_Info.m_Info.m_Paused = false;
828#if defined(CONF_VIDEORECORDER)
829 if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
830 IVideo::Current()->Pause(Pause: false);
831#endif
832}
833
834int CDemoPlayer::Load(IStorage *pStorage, IConsole *pConsole, const char *pFilename, int StorageType)
835{
836 dbg_assert(m_File == nullptr, "Demo player already playing");
837
838 m_pConsole = pConsole;
839 str_copy(dst&: m_aFilename, src: pFilename);
840 str_copy(dst&: m_aErrorMessage, src: "");
841
842 if(m_pConsole)
843 {
844 char aBuf[32 + IO_MAX_PATH_LENGTH];
845 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Loading demo '%s'", pFilename);
846 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
847 }
848
849 // clear the playback info
850 mem_zero(block: &m_Info, size: sizeof(m_Info));
851 m_Info.m_Info.m_FirstTick = -1;
852 m_Info.m_Info.m_LastTick = -1;
853 m_Info.m_NextTick = -1;
854 m_Info.m_Info.m_CurrentTick = -1;
855 m_Info.m_PreviousTick = -1;
856 m_Info.m_Info.m_Speed = 1;
857 m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;
858 m_LastSnapshotDataSize = -1;
859
860 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)))
861 {
862 str_copy(dst&: m_aFilename, src: "");
863 return -1;
864 }
865 m_Sixup = str_startswith(str: m_Info.m_Header.m_aNetversion, prefix: "0.7");
866
867 // save byte offset of map for later use
868 m_MapOffset = io_tell(io: m_File);
869 if(m_MapOffset < 0 || io_skip(io: m_File, size: m_MapInfo.m_Size) != 0)
870 {
871 Stop(pErrorMessage: "Error skipping map data");
872 return -1;
873 }
874
875 if(m_Info.m_Header.m_Version > gs_OldVersion)
876 {
877 // get timeline markers
878 int Num = bytes_be_to_uint(bytes: m_Info.m_TimelineMarkers.m_aNumTimelineMarkers);
879 m_Info.m_Info.m_NumTimelineMarkers = std::clamp<int>(val: Num, lo: 0, hi: MAX_TIMELINE_MARKERS);
880 for(int i = 0; i < m_Info.m_Info.m_NumTimelineMarkers; i++)
881 {
882 m_Info.m_Info.m_aTimelineMarkers[i] = bytes_be_to_uint(bytes: m_Info.m_TimelineMarkers.m_aTimelineMarkers[i]);
883 }
884 }
885
886 // Scan the file for interesting points
887 if(ScanFile() == EScanFileResult::ERROR_UNRECOVERABLE)
888 {
889 Stop(pErrorMessage: "Error scanning demo file");
890 return -1;
891 }
892 m_Info.m_LiveStateUpdating = true;
893
894 // reset slice markers
895 g_Config.m_ClDemoSliceBegin = -1;
896 g_Config.m_ClDemoSliceEnd = -1;
897
898 // ready for playback
899 return 0;
900}
901
902unsigned char *CDemoPlayer::GetMapData(IStorage *pStorage)
903{
904 if(!m_MapInfo.m_Size)
905 return nullptr;
906
907 const int64_t CurSeek = io_tell(io: m_File);
908 if(CurSeek < 0 || io_seek(io: m_File, offset: m_MapOffset, origin: EIoSeekOrigin::START) != 0)
909 return nullptr;
910 unsigned char *pMapData = (unsigned char *)malloc(size: m_MapInfo.m_Size);
911 if(io_read(io: m_File, buffer: pMapData, size: m_MapInfo.m_Size) != m_MapInfo.m_Size ||
912 io_seek(io: m_File, offset: CurSeek, origin: EIoSeekOrigin::START) != 0)
913 {
914 free(ptr: pMapData);
915 return nullptr;
916 }
917 return pMapData;
918}
919
920bool CDemoPlayer::ExtractMap(IStorage *pStorage)
921{
922 unsigned char *pMapData = GetMapData(pStorage);
923 if(!pMapData)
924 return false;
925
926 // handle sha256
927 std::optional<SHA256_DIGEST> Sha256;
928 if(m_Info.m_Header.m_Version >= gs_Sha256Version)
929 {
930 Sha256 = m_MapInfo.m_Sha256;
931 dbg_assert(Sha256.has_value(), "SHA256 missing for version %d demo", m_Info.m_Header.m_Version);
932 }
933 else
934 {
935 Sha256 = sha256(message: pMapData, message_len: m_MapInfo.m_Size);
936 m_MapInfo.m_Sha256 = Sha256;
937 }
938
939 // construct name
940 char aSha[SHA256_MAXSTRSIZE], aMapFilename[IO_MAX_PATH_LENGTH];
941 sha256_str(digest: Sha256.value(), str: aSha, max_len: sizeof(aSha));
942 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "downloadedmaps/%s_%s.map", m_Info.m_Header.m_aMapName, aSha);
943
944 // save map
945 IOHANDLE MapFile = pStorage->OpenFile(pFilename: aMapFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
946 if(!MapFile)
947 {
948 free(ptr: pMapData);
949 return false;
950 }
951
952 io_write(io: MapFile, buffer: pMapData, size: m_MapInfo.m_Size);
953 io_close(io: MapFile);
954
955 // free data
956 free(ptr: pMapData);
957 return true;
958}
959
960int64_t CDemoPlayer::Time()
961{
962#if defined(CONF_VIDEORECORDER)
963 if(m_UseVideo && IVideo::Current())
964 {
965 if(!m_WasRecording)
966 {
967 m_WasRecording = true;
968 m_Info.m_LastUpdate = IVideo::Current()->Time();
969 }
970 return IVideo::Current()->Time();
971 }
972 else
973 {
974 const int64_t Now = time_get();
975 if(m_WasRecording)
976 {
977 m_WasRecording = false;
978 m_Info.m_LastUpdate = Now;
979 }
980 return Now;
981 }
982#else
983 return time_get();
984#endif
985}
986
987void CDemoPlayer::Play()
988{
989 // Fill in previous and next tick
990 while(m_Info.m_PreviousTick == -1)
991 {
992 DoTick();
993 if(!IsPlaying())
994 {
995 // Empty demo or error playing tick
996 return;
997 }
998 }
999
1000 // Initialize playback time. Using `set_new_tick` is essential so that `Time`
1001 // returns the updated time, otherwise the delta between `m_LastUpdate` and
1002 // the value that `Time` returns when called in the `Update` function can be
1003 // very large depending on the time required to load the demo, which causes
1004 // demo playback to start later. This ensures it always starts at 00:00.
1005 set_new_tick();
1006 m_Info.m_CurrentTime = m_Info.m_PreviousTick * time_freq() / SERVER_TICK_SPEED;
1007 m_Info.m_LastUpdate = Time();
1008 if(m_Info.m_LiveStateUpdating && m_Info.m_LastScan <= 0)
1009 {
1010 m_Info.m_LastScan = m_Info.m_LastUpdate;
1011 }
1012}
1013
1014bool CDemoPlayer::SeekPercent(float Percent)
1015{
1016 int WantedTick = m_Info.m_Info.m_FirstTick + round_truncate(f: (m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick) * Percent);
1017 return SetPos(WantedTick);
1018}
1019
1020bool CDemoPlayer::SeekTime(float Seconds)
1021{
1022 int WantedTick = m_Info.m_Info.m_CurrentTick + round_truncate(f: Seconds * (float)SERVER_TICK_SPEED);
1023 return SetPos(WantedTick);
1024}
1025
1026bool CDemoPlayer::SeekTick(ETickOffset TickOffset)
1027{
1028 int WantedTick;
1029 switch(TickOffset)
1030 {
1031 case TICK_CURRENT:
1032 // TODO: https://github.com/ddnet/ddnet/issues/11681
1033 WantedTick = m_Info.m_Info.m_CurrentTick;
1034 break;
1035 case TICK_PREVIOUS:
1036 WantedTick = m_Info.m_PreviousTick;
1037 break;
1038 case TICK_NEXT:
1039 WantedTick = m_Info.m_NextTick;
1040 break;
1041 default:
1042 dbg_assert_failed("Invalid TickOffset");
1043 }
1044
1045 // +1 because SetPos will seek until the given tick is the next tick that
1046 // will be played back, whereas we want the wanted tick to be played now.
1047 return SetPos(WantedTick + 1);
1048}
1049
1050bool CDemoPlayer::SetPos(int WantedTick)
1051{
1052 if(!m_File)
1053 return false;
1054
1055 // TODO: Early exit when WantedTick > m_Info.m_Info.m_CurrentTick && WantedTick <= m_Info.m_NextTick with https://github.com/ddnet/ddnet/issues/11681
1056
1057 int LastSeekableTick = m_Info.m_Info.m_LastTick;
1058 if(m_Info.m_Info.m_LiveDemo)
1059 {
1060 // 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.
1061 LastSeekableTick -= 2 * SERVER_TICK_SPEED;
1062 }
1063 if(LastSeekableTick < m_Info.m_Info.m_FirstTick)
1064 {
1065 WantedTick = m_Info.m_Info.m_FirstTick;
1066 }
1067 else
1068 {
1069 WantedTick = std::clamp(val: WantedTick, lo: m_Info.m_Info.m_FirstTick, hi: LastSeekableTick);
1070 }
1071
1072 // Just the next tick
1073 if(WantedTick == m_Info.m_NextTick + 1)
1074 {
1075 DoTick();
1076 Play();
1077 return true;
1078 }
1079
1080 const int KeyFrameWantedTick = WantedTick - 5; // -5 because we have to have a current tick and previous tick when we do the playback
1081 const float Percent = (KeyFrameWantedTick - m_Info.m_Info.m_FirstTick) / (float)(m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick);
1082
1083 // get correct key frame
1084 size_t KeyFrame = std::clamp<size_t>(val: m_vKeyFrames.size() * Percent, lo: 0, hi: m_vKeyFrames.size() - 1);
1085 while(KeyFrame < m_vKeyFrames.size() - 1 && m_vKeyFrames[KeyFrame].m_Tick < KeyFrameWantedTick)
1086 KeyFrame++;
1087 while(KeyFrame > 0 && m_vKeyFrames[KeyFrame].m_Tick > KeyFrameWantedTick)
1088 KeyFrame--;
1089
1090 // TODO Remove `WantedTick <= m_Info.m_NextTick` with https://github.com/ddnet/ddnet/issues/11681
1091 if(WantedTick <= m_Info.m_Info.m_CurrentTick || // if we are seeking backwards (must be <= for high bandwidth demos) OR
1092 WantedTick <= m_Info.m_NextTick || // if seeking to current tick OR
1093 m_Info.m_Info.m_CurrentTick < m_vKeyFrames[KeyFrame].m_Tick || // we are before the wanted KeyFrame OR
1094 (KeyFrame != m_vKeyFrames.size() - 1 && m_Info.m_Info.m_CurrentTick >= m_vKeyFrames[KeyFrame + 1].m_Tick)) // we are after the wanted KeyFrame
1095 {
1096 if(io_seek(io: m_File, offset: m_vKeyFrames[KeyFrame].m_Filepos, origin: EIoSeekOrigin::START) != 0)
1097 {
1098 Stop(pErrorMessage: "Error seeking keyframe position");
1099 return false;
1100 }
1101 m_Info.m_NextTick = -1;
1102 m_Info.m_Info.m_CurrentTick = -1;
1103 m_Info.m_PreviousTick = -1;
1104 }
1105
1106 // playback everything until we hit our tick
1107 while(m_Info.m_NextTick < WantedTick)
1108 {
1109 DoTick();
1110 if(!IsPlaying())
1111 {
1112 return false;
1113 }
1114 }
1115
1116 Play();
1117
1118 return true;
1119}
1120
1121void CDemoPlayer::SetSpeed(float Speed)
1122{
1123 m_Info.m_Info.m_Speed = std::clamp(val: Speed, lo: 0.f, hi: 256.f);
1124}
1125
1126void CDemoPlayer::SetSpeedIndex(int SpeedIndex)
1127{
1128 dbg_assert(SpeedIndex >= 0 && SpeedIndex < (int)std::size(DEMO_SPEEDS), "invalid SpeedIndex");
1129 m_SpeedIndex = SpeedIndex;
1130 SetSpeed(DEMO_SPEEDS[m_SpeedIndex]);
1131}
1132
1133void CDemoPlayer::AdjustSpeedIndex(int Offset)
1134{
1135 SetSpeedIndex(std::clamp(val: m_SpeedIndex + Offset, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1)));
1136}
1137
1138void CDemoPlayer::Update(bool RealTime)
1139{
1140 const int64_t Now = Time();
1141 const int64_t Freq = time_freq();
1142 const int64_t DeltaTime = Now - m_Info.m_LastUpdate;
1143 m_Info.m_LastUpdate = Now;
1144
1145 if(m_Info.m_LiveStateUpdating)
1146 {
1147 // Determine if demo is live and still being written to, by scanning
1148 // file again and checking if more ticks are available than before.
1149 if(Now - m_Info.m_LastScan > Freq)
1150 {
1151 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1152 const EScanFileResult ScanResult = ScanFile();
1153 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1154 {
1155 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning demo file to determine live state");
1156 return;
1157 }
1158 else if(ScanResult == EScanFileResult::SUCCESS)
1159 {
1160 // Live state is known when ScanFile succeeded.
1161 m_Info.m_LiveStateUpdating = false;
1162 }
1163 else
1164 {
1165 m_Info.m_LiveStateFailedCount++;
1166 if(m_Info.m_LiveStateFailedCount >= 15)
1167 {
1168 // ScanFile keeps failing, which should be unlikely, so this is probably
1169 // not a live demo but a regular demo that is truncated at the end.
1170 m_Info.m_LiveStateUpdating = false;
1171 }
1172 }
1173 // Check if we got more ticks also when ScanFile failed, because
1174 // it could still have found more ticks.
1175 if(m_Info.m_Info.m_LastTick > PreviousLastTick)
1176 {
1177 m_Info.m_Info.m_LiveDemo = true;
1178 m_Info.m_LiveStateUpdating = false;
1179 m_Info.m_LiveStateUnchangedCount = 0;
1180 }
1181 m_Info.m_LastScan = Now;
1182 // Try again later if ScanFile failed and no more ticks were found.
1183 }
1184 }
1185 else if(m_Info.m_Info.m_LiveDemo)
1186 {
1187 // Scan live demo at tick frequency to smoothly update total time.
1188 if(Now - m_Info.m_LastScan > Freq / SERVER_TICK_SPEED)
1189 {
1190 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1191 const EScanFileResult ScanResult = ScanFile();
1192 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1193 {
1194 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning live demo file");
1195 return;
1196 }
1197 else if(ScanResult == EScanFileResult::SUCCESS &&
1198 m_Info.m_Info.m_LastTick == PreviousLastTick)
1199 {
1200 m_Info.m_LiveStateUnchangedCount++;
1201 if(m_Info.m_LiveStateUnchangedCount >= 2 * SERVER_TICK_SPEED)
1202 {
1203 // Assume demo stopped being live if we scanned the demo
1204 // successfully for 2 seconds without reading new ticks.
1205 m_Info.m_Info.m_LiveDemo = false;
1206 }
1207 }
1208 else
1209 {
1210 m_Info.m_LiveStateUnchangedCount = 0;
1211 }
1212 m_Info.m_LastScan = Now;
1213 }
1214 }
1215
1216 if(!IsPlaying())
1217 {
1218 return;
1219 }
1220
1221 if(!m_Info.m_Info.m_Paused)
1222 {
1223 if(m_Info.m_Info.m_LiveDemo &&
1224 m_Info.m_Info.m_Speed > 1.0f &&
1225 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)
1226 {
1227 // Reset to default speed if we are fast-forwarding to the end of a live demo,
1228 // to prevent playback error due to final demo chunk data still being written.
1229 SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
1230 }
1231
1232 m_Info.m_CurrentTime += (int64_t)(DeltaTime * (double)m_Info.m_Info.m_Speed);
1233
1234 // Do more ticks until we reach the current time.
1235 while(!m_Info.m_Info.m_Paused)
1236 {
1237 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1238 if(RealTime && CurrentTickStart > m_Info.m_CurrentTime)
1239 {
1240 break;
1241 }
1242 DoTick();
1243 if(!IsPlaying())
1244 {
1245 return;
1246 }
1247 }
1248 }
1249
1250 UpdateTimes();
1251}
1252
1253void CDemoPlayer::UpdateTimes()
1254{
1255 const int64_t Freq = time_freq();
1256 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1257 const int64_t PreviousTickStart = m_Info.m_PreviousTick * Freq / SERVER_TICK_SPEED;
1258 m_Info.m_IntraTick = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(CurrentTickStart - PreviousTickStart);
1259 m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(Freq / SERVER_TICK_SPEED);
1260 m_Info.m_TickTime = (m_Info.m_CurrentTime - PreviousTickStart) / (float)Freq;
1261 m_Info.m_Info.m_LivePlayback = m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick < 3 * SERVER_TICK_SPEED;
1262
1263 if(m_UpdateIntraTimesFunc)
1264 {
1265 m_UpdateIntraTimesFunc();
1266 }
1267}
1268
1269void CDemoPlayer::Stop(const char *pErrorMessage)
1270{
1271#if defined(CONF_VIDEORECORDER)
1272 if(m_UseVideo && IVideo::Current())
1273 IVideo::Current()->Stop();
1274 m_WasRecording = false;
1275#endif
1276
1277 if(!m_File)
1278 return;
1279
1280 if(m_pConsole)
1281 {
1282 char aBuf[256];
1283 if(pErrorMessage[0] == '\0')
1284 str_copy(dst&: aBuf, src: "Stopped playback");
1285 else
1286 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Stopped playback due to error: %s", pErrorMessage);
1287 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
1288 }
1289
1290 io_close(io: m_File);
1291 m_File = nullptr;
1292 m_vKeyFrames.clear();
1293 str_copy(dst&: m_aFilename, src: "");
1294 str_copy(dst&: m_aErrorMessage, src: pErrorMessage);
1295}
1296
1297void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const
1298{
1299 fs_split_file_extension(filename: fs_filename(path: m_aFilename), name: pBuffer, name_size: BufferSize);
1300}
1301
1302bool 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
1303{
1304 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1305 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1306 pMapInfo->m_aName[0] = '\0';
1307 pMapInfo->m_Sha256 = std::nullopt;
1308 pMapInfo->m_Crc = 0;
1309 pMapInfo->m_Size = 0;
1310
1311 IOHANDLE File = pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType);
1312 if(!File)
1313 {
1314 if(pErrorMessage != nullptr)
1315 str_copy(dst: pErrorMessage, src: "Could not open demo file", dst_size: ErrorMessageSize);
1316 return false;
1317 }
1318
1319 if(io_read(io: File, buffer: pDemoHeader, size: sizeof(CDemoHeader)) != sizeof(CDemoHeader) || !pDemoHeader->Valid())
1320 {
1321 if(pErrorMessage != nullptr)
1322 str_copy(dst: pErrorMessage, src: "Error reading demo header", dst_size: ErrorMessageSize);
1323 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1324 io_close(io: File);
1325 return false;
1326 }
1327
1328 if(pDemoHeader->m_Version < gs_OldVersion)
1329 {
1330 if(pErrorMessage != nullptr)
1331 str_format(buffer: pErrorMessage, buffer_size: ErrorMessageSize, format: "Demo version '%d' is not supported", pDemoHeader->m_Version);
1332 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1333 io_close(io: File);
1334 return false;
1335 }
1336 else if(pDemoHeader->m_Version > gs_OldVersion)
1337 {
1338 if(io_read(io: File, buffer: pTimelineMarkers, size: sizeof(CTimelineMarkers)) != sizeof(CTimelineMarkers))
1339 {
1340 if(pErrorMessage != nullptr)
1341 str_copy(dst: pErrorMessage, src: "Error reading timeline markers", dst_size: ErrorMessageSize);
1342 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1343 io_close(io: File);
1344 return false;
1345 }
1346 }
1347
1348 std::optional<SHA256_DIGEST> Sha256;
1349 if(pDemoHeader->m_Version >= gs_Sha256Version)
1350 {
1351 CUuid ExtensionUuid = {};
1352 const unsigned ExtensionUuidSize = io_read(io: File, buffer: &ExtensionUuid.m_aData, size: sizeof(ExtensionUuid.m_aData));
1353 if(ExtensionUuidSize == sizeof(ExtensionUuid.m_aData) && ExtensionUuid == SHA256_EXTENSION)
1354 {
1355 SHA256_DIGEST ReadSha256;
1356 if(io_read(io: File, buffer: &ReadSha256, size: sizeof(SHA256_DIGEST)) != sizeof(SHA256_DIGEST))
1357 {
1358 if(pErrorMessage != nullptr)
1359 str_copy(dst: pErrorMessage, src: "Error reading SHA256", dst_size: ErrorMessageSize);
1360 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1361 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1362 io_close(io: File);
1363 return false;
1364 }
1365 Sha256 = ReadSha256;
1366 }
1367 else
1368 {
1369 // This hopes whatever happened during the version increment didn't add something here
1370 if(pConsole)
1371 {
1372 pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: "Demo version incremented, but not by DDNet");
1373 }
1374 if(io_seek(io: File, offset: -(int64_t)ExtensionUuidSize, origin: EIoSeekOrigin::CURRENT) != 0)
1375 {
1376 if(pErrorMessage != nullptr)
1377 str_copy(dst: pErrorMessage, src: "Error rewinding SHA256 extension UUID", dst_size: ErrorMessageSize);
1378 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1379 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1380 io_close(io: File);
1381 return false;
1382 }
1383 }
1384 }
1385
1386 str_copy(dst&: pMapInfo->m_aName, src: pDemoHeader->m_aMapName);
1387 pMapInfo->m_Sha256 = Sha256;
1388 pMapInfo->m_Crc = bytes_be_to_uint(bytes: pDemoHeader->m_aMapCrc);
1389 pMapInfo->m_Size = bytes_be_to_uint(bytes: pDemoHeader->m_aMapSize);
1390
1391 if(pFile == nullptr)
1392 io_close(io: File);
1393 else
1394 *pFile = File;
1395
1396 return true;
1397}
1398
1399class CDemoRecordingListener : public CDemoPlayer::IListener
1400{
1401public:
1402 CDemoRecorder *m_pDemoRecorder;
1403 CDemoPlayer *m_pDemoPlayer;
1404 bool m_Stop;
1405 int m_StartTick;
1406 int m_EndTick;
1407
1408 void OnDemoPlayerSnapshot(void *pData, int Size) override
1409 {
1410 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1411
1412 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1413 m_Stop = true;
1414 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1415 m_pDemoRecorder->RecordSnapshot(Tick: pInfo->m_Info.m_CurrentTick, pData, Size);
1416 }
1417
1418 void OnDemoPlayerMessage(void *pData, int Size) override
1419 {
1420 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1421
1422 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1423 m_Stop = true;
1424 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1425 m_pDemoRecorder->RecordMessage(pData, Size);
1426 }
1427};
1428
1429void CDemoEditor::Init(CSnapshotDelta *pSnapshotDelta, CSnapshotDelta *pSnapshotDeltaSixup, IConsole *pConsole, IStorage *pStorage)
1430{
1431 m_pSnapshotDelta = pSnapshotDelta;
1432 m_pSnapshotDeltaSixup = pSnapshotDeltaSixup;
1433 m_pConsole = pConsole;
1434 m_pStorage = pStorage;
1435}
1436
1437bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser)
1438{
1439 CDemoPlayer DemoPlayer(m_pSnapshotDelta, m_pSnapshotDeltaSixup, false);
1440 if(DemoPlayer.Load(pStorage: m_pStorage, pConsole: m_pConsole, pFilename: pDemo, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
1441 return false;
1442
1443 const CMapInfo *pMapInfo = DemoPlayer.GetMapInfo();
1444 const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();
1445
1446 std::optional<SHA256_DIGEST> Sha256 = pMapInfo->m_Sha256;
1447 if(pInfo->m_Header.m_Version < gs_Sha256Version)
1448 {
1449 if(DemoPlayer.ExtractMap(pStorage: m_pStorage))
1450 {
1451 Sha256 = pMapInfo->m_Sha256;
1452 }
1453 }
1454 if(!Sha256.has_value())
1455 {
1456 log_error_color(DEMO_PRINT_COLOR, "demo/slice", "Failed to start demo slicing because map SHA256 could not be determined.");
1457 return false;
1458 }
1459
1460 CDemoRecorder DemoRecorder(m_pSnapshotDelta);
1461 unsigned char *pMapData = DemoPlayer.GetMapData(pStorage: m_pStorage);
1462 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;
1463 free(ptr: pMapData);
1464 if(Result != 0)
1465 {
1466 DemoPlayer.Stop();
1467 return false;
1468 }
1469
1470 CDemoRecordingListener Listener;
1471 Listener.m_pDemoRecorder = &DemoRecorder;
1472 Listener.m_pDemoPlayer = &DemoPlayer;
1473 Listener.m_Stop = false;
1474 Listener.m_StartTick = StartTick;
1475 Listener.m_EndTick = EndTick;
1476 DemoPlayer.SetListener(&Listener);
1477
1478 DemoPlayer.Play();
1479
1480 while(DemoPlayer.IsPlaying() && !Listener.m_Stop)
1481 {
1482 DemoPlayer.Update(RealTime: false);
1483
1484 if(pInfo->m_Info.m_Paused)
1485 break;
1486 }
1487
1488 // Copy timeline markers to sliced demo
1489 for(int i = 0; i < pInfo->m_Info.m_NumTimelineMarkers; i++)
1490 {
1491 if((StartTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] >= StartTick) && (EndTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] <= EndTick))
1492 {
1493 DemoRecorder.AddDemoMarker(Tick: pInfo->m_Info.m_aTimelineMarkers[i]);
1494 }
1495 }
1496
1497 DemoPlayer.Stop();
1498 DemoRecorder.Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
1499 return true;
1500}
1501