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