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(class 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(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetVersion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned Crc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser)
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_aLastSnapshotData, 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 + sizeof(int)];
351 m_pSnapshotDelta->SetStaticsize(ItemType: protocol7::NETEVENTTYPE_SOUNDWORLD, Size: true);
352 m_pSnapshotDelta->SetStaticsize(ItemType: protocol7::NETEVENTTYPE_DAMAGE, Size: true);
353 const int DeltaSize = m_pSnapshotDelta->CreateDelta(pFrom: (CSnapshot *)m_aLastSnapshotData, pTo: (CSnapshot *)pData, pDstData: &aDeltaData);
354 if(DeltaSize)
355 {
356 // record delta
357 Write(Type: CHUNKTYPE_DELTA, pData: aDeltaData, Size: DeltaSize);
358 mem_copy(dest: m_aLastSnapshotData, source: pData, size: Size);
359 }
360 }
361}
362
363void CDemoRecorder::RecordMessage(const void *pData, int Size)
364{
365 if(m_pfnFilter)
366 {
367 if(m_pfnFilter(pData, Size, m_pUser))
368 {
369 return;
370 }
371 }
372 Write(Type: CHUNKTYPE_MESSAGE, pData, Size);
373}
374
375int CDemoRecorder::Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename)
376{
377 if(!m_File)
378 return -1;
379
380 if(Mode == IDemoRecorder::EStopMode::KEEP_FILE)
381 {
382 // add the demo length to the header
383 io_seek(io: m_File, offsetof(CDemoHeader, m_aLength), origin: IOSEEK_START);
384 unsigned char aLength[sizeof(int32_t)];
385 uint_to_bytes_be(bytes: aLength, value: Length());
386 io_write(io: m_File, buffer: aLength, size: sizeof(aLength));
387
388 // add the timeline markers to the header
389 io_seek(io: m_File, offset: sizeof(CDemoHeader) + offsetof(CTimelineMarkers, m_aNumTimelineMarkers), origin: IOSEEK_START);
390 unsigned char aNumMarkers[sizeof(int32_t)];
391 uint_to_bytes_be(bytes: aNumMarkers, value: m_NumTimelineMarkers);
392 io_write(io: m_File, buffer: aNumMarkers, size: sizeof(aNumMarkers));
393 for(int i = 0; i < m_NumTimelineMarkers; i++)
394 {
395 unsigned char aMarker[sizeof(int32_t)];
396 uint_to_bytes_be(bytes: aMarker, value: m_aTimelineMarkers[i]);
397 io_write(io: m_File, buffer: aMarker, size: sizeof(aMarker));
398 }
399 }
400
401 io_close(io: m_File);
402 m_File = nullptr;
403
404 if(Mode == IDemoRecorder::EStopMode::REMOVE_FILE)
405 {
406 if(!m_pStorage->RemoveFile(pFilename: m_aCurrentFilename, Type: IStorage::TYPE_SAVE))
407 {
408 if(m_pConsole)
409 {
410 char aBuf[64 + IO_MAX_PATH_LENGTH];
411 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Could not remove demo file '%s'.", m_aCurrentFilename);
412 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
413 }
414 return -1;
415 }
416 }
417 else if(pTargetFilename[0] != '\0')
418 {
419 if(!m_pStorage->RenameFile(pOldFilename: m_aCurrentFilename, pNewFilename: pTargetFilename, Type: IStorage::TYPE_SAVE))
420 {
421 if(m_pConsole)
422 {
423 char aBuf[64 + 2 * IO_MAX_PATH_LENGTH];
424 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Could not move demo file '%s' to '%s'.", m_aCurrentFilename, pTargetFilename);
425 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
426 }
427 return -1;
428 }
429 }
430
431 if(m_pConsole)
432 {
433 char aBuf[64 + IO_MAX_PATH_LENGTH];
434 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Stopped recording to '%s'", m_aCurrentFilename);
435 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: aBuf, PrintColor: gs_DemoPrintColor);
436 }
437
438 return 0;
439}
440
441void CDemoRecorder::AddDemoMarker()
442{
443 if(m_LastTickMarker < 0)
444 return;
445 AddDemoMarker(Tick: m_LastTickMarker);
446}
447
448void CDemoRecorder::AddDemoMarker(int Tick)
449{
450 dbg_assert(Tick >= 0, "invalid marker tick");
451 if(m_NumTimelineMarkers >= MAX_TIMELINE_MARKERS)
452 {
453 if(m_pConsole)
454 {
455 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Too many timeline markers", PrintColor: gs_DemoPrintColor);
456 }
457 return;
458 }
459
460 // not more than 1 marker in a second
461 if(m_NumTimelineMarkers > 0)
462 {
463 const int Diff = Tick - m_aTimelineMarkers[m_NumTimelineMarkers - 1];
464 if(Diff < (float)SERVER_TICK_SPEED)
465 {
466 if(m_pConsole)
467 {
468 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Previous timeline marker too close", PrintColor: gs_DemoPrintColor);
469 }
470 return;
471 }
472 }
473
474 m_aTimelineMarkers[m_NumTimelineMarkers++] = Tick;
475
476 if(m_pConsole)
477 {
478 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Added timeline marker", PrintColor: gs_DemoPrintColor);
479 }
480}
481
482CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc)
483{
484 Construct(pSnapshotDelta, UseVideo);
485
486 m_UpdateIntraTimesFunc = UpdateIntraTimesFunc;
487}
488
489CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo)
490{
491 Construct(pSnapshotDelta, UseVideo);
492}
493
494CDemoPlayer::~CDemoPlayer()
495{
496 dbg_assert(m_File == 0, "Demo player not stopped");
497}
498
499void CDemoPlayer::Construct(class CSnapshotDelta *pSnapshotDelta, bool UseVideo)
500{
501 m_File = nullptr;
502 m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;
503
504 m_pSnapshotDelta = pSnapshotDelta;
505 m_LastSnapshotDataSize = -1;
506 m_pListener = nullptr;
507 m_UseVideo = UseVideo;
508
509 m_aFilename[0] = '\0';
510 m_aErrorMessage[0] = '\0';
511}
512
513void CDemoPlayer::SetListener(IListener *pListener)
514{
515 m_pListener = pListener;
516}
517
518CDemoPlayer::EReadChunkHeaderResult CDemoPlayer::ReadChunkHeader(int *pType, int *pSize, int *pTick)
519{
520 *pSize = 0;
521 *pType = 0;
522
523 unsigned char Chunk = 0;
524 if(io_read(io: m_File, buffer: &Chunk, size: sizeof(Chunk)) != sizeof(Chunk))
525 return CHUNKHEADER_EOF;
526
527 if(Chunk & CHUNKTYPEFLAG_TICKMARKER)
528 {
529 // decode tick marker
530 int TickdeltaLegacy = Chunk & CHUNKMASK_TICK_LEGACY; // compatibility
531 *pType = Chunk & (CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_KEYFRAME);
532
533 int NewTick;
534 if(m_Info.m_Header.m_Version < gs_VersionTickCompression && TickdeltaLegacy != 0)
535 {
536 if(*pTick < 0) // initial tick not initialized before a tick delta
537 return CHUNKHEADER_ERROR;
538 NewTick = *pTick + TickdeltaLegacy;
539 }
540 else if(Chunk & CHUNKTICKFLAG_TICK_COMPRESSED)
541 {
542 if(*pTick < 0) // initial tick not initialized before a tick delta
543 return CHUNKHEADER_ERROR;
544 int Tickdelta = Chunk & CHUNKMASK_TICK;
545 NewTick = *pTick + Tickdelta;
546 }
547 else
548 {
549 unsigned char aTickdata[sizeof(int32_t)];
550 if(io_read(io: m_File, buffer: aTickdata, size: sizeof(aTickdata)) != sizeof(aTickdata))
551 return CHUNKHEADER_ERROR;
552 NewTick = bytes_be_to_uint(bytes: aTickdata);
553 }
554 if(NewTick < MIN_TICK || NewTick >= MAX_TICK) // invalid tick
555 return CHUNKHEADER_ERROR;
556 *pTick = NewTick;
557 }
558 else
559 {
560 // decode normal chunk
561 *pType = (Chunk & CHUNKMASK_TYPE) >> 5;
562 *pSize = Chunk & CHUNKMASK_SIZE;
563
564 if(*pSize == 30)
565 {
566 unsigned char aSizedata[1];
567 if(io_read(io: m_File, buffer: aSizedata, size: sizeof(aSizedata)) != sizeof(aSizedata))
568 return CHUNKHEADER_ERROR;
569 *pSize = aSizedata[0];
570 }
571 else if(*pSize == 31)
572 {
573 unsigned char aSizedata[2];
574 if(io_read(io: m_File, buffer: aSizedata, size: sizeof(aSizedata)) != sizeof(aSizedata))
575 return CHUNKHEADER_ERROR;
576 *pSize = (aSizedata[1] << 8) | aSizedata[0];
577 }
578 }
579
580 return CHUNKHEADER_SUCCESS;
581}
582
583CDemoPlayer::EScanFileResult CDemoPlayer::ScanFile()
584{
585 const int64_t StartPos = io_tell(io: m_File);
586 if(StartPos < 0)
587 {
588 return EScanFileResult::ERROR_UNRECOVERABLE;
589 }
590
591 const auto &ResetToStartPosition = [&](EScanFileResult Result) -> EScanFileResult {
592 if(io_seek(io: m_File, offset: StartPos, origin: IOSEEK_START) != 0)
593 {
594 m_vKeyFrames.clear();
595 return EScanFileResult::ERROR_UNRECOVERABLE;
596 }
597 return Result;
598 };
599
600 int ChunkTick = -1;
601 if(!m_vKeyFrames.empty())
602 {
603 if(io_seek(io: m_File, offset: m_vKeyFrames.back().m_Filepos, origin: IOSEEK_START) != 0)
604 {
605 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
606 }
607 int ChunkType, ChunkSize;
608 const EReadChunkHeaderResult Result = ReadChunkHeader(pType: &ChunkType, pSize: &ChunkSize, pTick: &ChunkTick);
609 if(Result != CHUNKHEADER_SUCCESS ||
610 (ChunkSize > 0 && io_skip(io: m_File, size: ChunkSize) != 0))
611 {
612 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
613 }
614 }
615
616 while(true)
617 {
618 const int64_t CurrentPos = io_tell(io: m_File);
619 if(CurrentPos < 0)
620 {
621 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
622 }
623
624 int ChunkType, ChunkSize;
625 const EReadChunkHeaderResult Result = ReadChunkHeader(pType: &ChunkType, pSize: &ChunkSize, pTick: &ChunkTick);
626 if(Result == CHUNKHEADER_EOF)
627 {
628 break;
629 }
630 else if(Result == CHUNKHEADER_ERROR)
631 {
632 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
633 }
634
635 if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
636 {
637 if(ChunkType & CHUNKTICKFLAG_KEYFRAME)
638 {
639 m_vKeyFrames.emplace_back(args: CurrentPos, args&: ChunkTick);
640 }
641 if(m_Info.m_Info.m_FirstTick == -1)
642 {
643 m_Info.m_Info.m_FirstTick = ChunkTick;
644 }
645 m_Info.m_Info.m_LastTick = ChunkTick;
646 }
647 else if(ChunkSize)
648 {
649 if(io_skip(io: m_File, size: ChunkSize) != 0)
650 {
651 return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
652 }
653 }
654 }
655
656 // Cannot start playback without at least one keyframe
657 return ResetToStartPosition(m_vKeyFrames.empty() ? EScanFileResult::ERROR_UNRECOVERABLE : EScanFileResult::SUCCESS);
658}
659
660void CDemoPlayer::DoTick()
661{
662 // update ticks
663 m_Info.m_PreviousTick = m_Info.m_Info.m_CurrentTick;
664 m_Info.m_Info.m_CurrentTick = m_Info.m_NextTick;
665 int ChunkTick = m_Info.m_Info.m_CurrentTick;
666
667 UpdateTimes();
668
669 bool GotSnapshot = false;
670 while(true)
671 {
672 int ChunkType, ChunkSize;
673 const EReadChunkHeaderResult Result = ReadChunkHeader(pType: &ChunkType, pSize: &ChunkSize, pTick: &ChunkTick);
674 if(Result == CHUNKHEADER_EOF)
675 {
676 if(m_Info.m_PreviousTick == -1)
677 {
678 Stop(pErrorMessage: "Empty demo");
679 }
680 else
681 {
682 Pause();
683 // Stop rendering when reaching end of file
684#if defined(CONF_VIDEORECORDER)
685 if(m_UseVideo && IVideo::Current())
686 Stop();
687#endif
688 }
689 break;
690 }
691 else if(Result == CHUNKHEADER_ERROR)
692 {
693 Stop(pErrorMessage: "Error reading chunk header");
694 break;
695 }
696
697 // read the chunk
698 int DataSize = 0;
699 if(ChunkSize)
700 {
701 if(io_read(io: m_File, buffer: m_aCompressedSnapshotData, size: ChunkSize) != (unsigned)ChunkSize)
702 {
703 Stop(pErrorMessage: "Error reading chunk data");
704 break;
705 }
706
707 DataSize = CNetBase::Decompress(pData: m_aCompressedSnapshotData, DataSize: ChunkSize, pOutput: m_aDecompressedSnapshotData, OutputSize: sizeof(m_aDecompressedSnapshotData));
708 if(DataSize < 0)
709 {
710 Stop(pErrorMessage: "Error during network decompression");
711 break;
712 }
713
714 DataSize = CVariableInt::Decompress(pSrc: m_aDecompressedSnapshotData, SrcSize: DataSize, pDst: m_aChunkData, DstSize: sizeof(m_aChunkData));
715 if(DataSize < 0)
716 {
717 Stop(pErrorMessage: "Error during intpack decompression");
718 break;
719 }
720 }
721
722 if(ChunkType == CHUNKTYPE_DELTA)
723 {
724 // process delta snapshot
725 CSnapshot *pNewsnap = (CSnapshot *)m_aSnapshot;
726 DataSize = m_pSnapshotDelta->UnpackDelta(pFrom: (CSnapshot *)m_aLastSnapshotData, pTo: pNewsnap, pSrcData: m_aChunkData, DataSize, Sixup: IsSixup());
727
728 if(DataSize < 0)
729 {
730 if(m_pConsole)
731 {
732 char aBuf[64];
733 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Error unpacking snapshot delta. DataSize=%d", DataSize);
734 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
735 }
736 }
737 else if(!pNewsnap->IsValid(ActualSize: DataSize))
738 {
739 if(m_pConsole)
740 {
741 char aBuf[64];
742 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Snapshot delta invalid. DataSize=%d", DataSize);
743 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
744 }
745 }
746 else
747 {
748 if(m_pListener)
749 m_pListener->OnDemoPlayerSnapshot(pData: m_aSnapshot, Size: DataSize);
750
751 m_LastSnapshotDataSize = DataSize;
752 mem_copy(dest: m_aLastSnapshotData, source: m_aSnapshot, size: DataSize);
753 GotSnapshot = true;
754 }
755 }
756 else if(ChunkType == CHUNKTYPE_SNAPSHOT)
757 {
758 // process full snapshot
759 CSnapshot *pSnap = (CSnapshot *)m_aChunkData;
760 if(!pSnap->IsValid(ActualSize: DataSize))
761 {
762 if(m_pConsole)
763 {
764 char aBuf[64];
765 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Snapshot invalid. DataSize=%d", DataSize);
766 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: aBuf);
767 }
768 }
769 else
770 {
771 GotSnapshot = true;
772
773 m_LastSnapshotDataSize = DataSize;
774 mem_copy(dest: m_aLastSnapshotData, source: m_aChunkData, size: DataSize);
775 if(m_pListener)
776 m_pListener->OnDemoPlayerSnapshot(pData: m_aChunkData, Size: DataSize);
777 }
778 }
779 else
780 {
781 // if there were no snapshots in this tick, replay the last one
782 if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
783 {
784 GotSnapshot = true;
785 m_pListener->OnDemoPlayerSnapshot(pData: m_aLastSnapshotData, Size: m_LastSnapshotDataSize);
786 }
787
788 // check the remaining types
789 if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
790 {
791 m_Info.m_NextTick = ChunkTick;
792 break;
793 }
794 else if(ChunkType == CHUNKTYPE_MESSAGE)
795 {
796 if(m_pListener)
797 m_pListener->OnDemoPlayerMessage(pData: m_aChunkData, Size: DataSize);
798 }
799 }
800 }
801}
802
803void CDemoPlayer::Pause()
804{
805 m_Info.m_Info.m_Paused = true;
806#if defined(CONF_VIDEORECORDER)
807 if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
808 IVideo::Current()->Pause(Pause: true);
809#endif
810}
811
812void CDemoPlayer::Unpause()
813{
814 m_Info.m_Info.m_Paused = false;
815#if defined(CONF_VIDEORECORDER)
816 if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
817 IVideo::Current()->Pause(Pause: false);
818#endif
819}
820
821int CDemoPlayer::Load(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, int StorageType)
822{
823 dbg_assert(m_File == 0, "Demo player already playing");
824
825 m_pConsole = pConsole;
826 str_copy(dst&: m_aFilename, src: pFilename);
827 str_copy(dst&: m_aErrorMessage, src: "");
828
829 if(m_pConsole)
830 {
831 char aBuf[32 + IO_MAX_PATH_LENGTH];
832 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Loading demo '%s'", pFilename);
833 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
834 }
835
836 // clear the playback info
837 mem_zero(block: &m_Info, size: sizeof(m_Info));
838 m_Info.m_Info.m_FirstTick = -1;
839 m_Info.m_Info.m_LastTick = -1;
840 m_Info.m_NextTick = -1;
841 m_Info.m_Info.m_CurrentTick = -1;
842 m_Info.m_PreviousTick = -1;
843 m_Info.m_Info.m_Speed = 1;
844 m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;
845 m_LastSnapshotDataSize = -1;
846
847 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)))
848 {
849 str_copy(dst&: m_aFilename, src: "");
850 return -1;
851 }
852 m_Sixup = str_startswith(str: m_Info.m_Header.m_aNetversion, prefix: "0.7");
853
854 // save byte offset of map for later use
855 m_MapOffset = io_tell(io: m_File);
856 if(m_MapOffset < 0 || io_skip(io: m_File, size: m_MapInfo.m_Size) != 0)
857 {
858 Stop(pErrorMessage: "Error skipping map data");
859 return -1;
860 }
861
862 if(m_Info.m_Header.m_Version > gs_OldVersion)
863 {
864 // get timeline markers
865 int Num = bytes_be_to_uint(bytes: m_Info.m_TimelineMarkers.m_aNumTimelineMarkers);
866 m_Info.m_Info.m_NumTimelineMarkers = std::clamp<int>(val: Num, lo: 0, hi: MAX_TIMELINE_MARKERS);
867 for(int i = 0; i < m_Info.m_Info.m_NumTimelineMarkers; i++)
868 {
869 m_Info.m_Info.m_aTimelineMarkers[i] = bytes_be_to_uint(bytes: m_Info.m_TimelineMarkers.m_aTimelineMarkers[i]);
870 }
871 }
872
873 // Scan the file for interesting points
874 if(ScanFile() == EScanFileResult::ERROR_UNRECOVERABLE)
875 {
876 Stop(pErrorMessage: "Error scanning demo file");
877 return -1;
878 }
879 m_Info.m_LiveStateUpdating = true;
880
881 // reset slice markers
882 g_Config.m_ClDemoSliceBegin = -1;
883 g_Config.m_ClDemoSliceEnd = -1;
884
885 // ready for playback
886 return 0;
887}
888
889unsigned char *CDemoPlayer::GetMapData(class IStorage *pStorage)
890{
891 if(!m_MapInfo.m_Size)
892 return nullptr;
893
894 const int64_t CurSeek = io_tell(io: m_File);
895 if(CurSeek < 0 || io_seek(io: m_File, offset: m_MapOffset, origin: IOSEEK_START) != 0)
896 return nullptr;
897 unsigned char *pMapData = (unsigned char *)malloc(size: m_MapInfo.m_Size);
898 if(io_read(io: m_File, buffer: pMapData, size: m_MapInfo.m_Size) != m_MapInfo.m_Size ||
899 io_seek(io: m_File, offset: CurSeek, origin: IOSEEK_START) != 0)
900 {
901 free(ptr: pMapData);
902 return nullptr;
903 }
904 return pMapData;
905}
906
907bool CDemoPlayer::ExtractMap(class IStorage *pStorage)
908{
909 unsigned char *pMapData = GetMapData(pStorage);
910 if(!pMapData)
911 return false;
912
913 // handle sha256
914 std::optional<SHA256_DIGEST> Sha256;
915 if(m_Info.m_Header.m_Version >= gs_Sha256Version)
916 {
917 Sha256 = m_MapInfo.m_Sha256;
918 dbg_assert(Sha256.has_value(), "SHA256 missing for version %d demo", m_Info.m_Header.m_Version);
919 }
920 else
921 {
922 Sha256 = sha256(message: pMapData, message_len: m_MapInfo.m_Size);
923 m_MapInfo.m_Sha256 = Sha256;
924 }
925
926 // construct name
927 char aSha[SHA256_MAXSTRSIZE], aMapFilename[IO_MAX_PATH_LENGTH];
928 sha256_str(digest: Sha256.value(), str: aSha, max_len: sizeof(aSha));
929 str_format(buffer: aMapFilename, buffer_size: sizeof(aMapFilename), format: "downloadedmaps/%s_%s.map", m_Info.m_Header.m_aMapName, aSha);
930
931 // save map
932 IOHANDLE MapFile = pStorage->OpenFile(pFilename: aMapFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
933 if(!MapFile)
934 {
935 free(ptr: pMapData);
936 return false;
937 }
938
939 io_write(io: MapFile, buffer: pMapData, size: m_MapInfo.m_Size);
940 io_close(io: MapFile);
941
942 // free data
943 free(ptr: pMapData);
944 return true;
945}
946
947int64_t CDemoPlayer::Time()
948{
949#if defined(CONF_VIDEORECORDER)
950 if(m_UseVideo && IVideo::Current())
951 {
952 if(!m_WasRecording)
953 {
954 m_WasRecording = true;
955 m_Info.m_LastUpdate = IVideo::Time();
956 }
957 return IVideo::Time();
958 }
959 else
960 {
961 const int64_t Now = time_get();
962 if(m_WasRecording)
963 {
964 m_WasRecording = false;
965 m_Info.m_LastUpdate = Now;
966 }
967 return Now;
968 }
969#else
970 return time_get();
971#endif
972}
973
974void CDemoPlayer::Play()
975{
976 // Fill in previous and next tick
977 while(m_Info.m_PreviousTick == -1)
978 {
979 DoTick();
980 if(!IsPlaying())
981 {
982 // Empty demo or error playing tick
983 return;
984 }
985 }
986
987 // Initialize playback time. Using `set_new_tick` is essential so that `Time`
988 // returns the updated time, otherwise the delta between `m_LastUpdate` and
989 // the value that `Time` returns when called in the `Update` function can be
990 // very large depending on the time required to load the demo, which causes
991 // demo playback to start later. This ensures it always starts at 00:00.
992 set_new_tick();
993 m_Info.m_CurrentTime = m_Info.m_PreviousTick * time_freq() / SERVER_TICK_SPEED;
994 m_Info.m_LastUpdate = Time();
995 if(m_Info.m_LiveStateUpdating && m_Info.m_LastScan <= 0)
996 {
997 m_Info.m_LastScan = m_Info.m_LastUpdate;
998 }
999}
1000
1001bool CDemoPlayer::SeekPercent(float Percent)
1002{
1003 int WantedTick = m_Info.m_Info.m_FirstTick + round_truncate(f: (m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick) * Percent);
1004 return SetPos(WantedTick);
1005}
1006
1007bool CDemoPlayer::SeekTime(float Seconds)
1008{
1009 int WantedTick = m_Info.m_Info.m_CurrentTick + round_truncate(f: Seconds * (float)SERVER_TICK_SPEED);
1010 return SetPos(WantedTick);
1011}
1012
1013bool CDemoPlayer::SeekTick(ETickOffset TickOffset)
1014{
1015 int WantedTick;
1016 switch(TickOffset)
1017 {
1018 case TICK_CURRENT:
1019 // TODO: https://github.com/ddnet/ddnet/issues/11681
1020 WantedTick = m_Info.m_Info.m_CurrentTick;
1021 break;
1022 case TICK_PREVIOUS:
1023 WantedTick = m_Info.m_PreviousTick;
1024 break;
1025 case TICK_NEXT:
1026 WantedTick = m_Info.m_NextTick;
1027 break;
1028 default:
1029 dbg_assert_failed("Invalid TickOffset");
1030 }
1031
1032 // +1 because SetPos will seek until the given tick is the next tick that
1033 // will be played back, whereas we want the wanted tick to be played now.
1034 return SetPos(WantedTick + 1);
1035}
1036
1037bool CDemoPlayer::SetPos(int WantedTick)
1038{
1039 if(!m_File)
1040 return false;
1041
1042 // TODO: Early exit when WantedTick > m_Info.m_Info.m_CurrentTick && WantedTick <= m_Info.m_NextTick with https://github.com/ddnet/ddnet/issues/11681
1043
1044 int LastSeekableTick = m_Info.m_Info.m_LastTick;
1045 if(m_Info.m_Info.m_LiveDemo)
1046 {
1047 // 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.
1048 LastSeekableTick -= 2 * SERVER_TICK_SPEED;
1049 }
1050 if(LastSeekableTick < m_Info.m_Info.m_FirstTick)
1051 {
1052 WantedTick = m_Info.m_Info.m_FirstTick;
1053 }
1054 else
1055 {
1056 WantedTick = std::clamp(val: WantedTick, lo: m_Info.m_Info.m_FirstTick, hi: LastSeekableTick);
1057 }
1058
1059 // Just the next tick
1060 if(WantedTick == m_Info.m_NextTick + 1)
1061 {
1062 DoTick();
1063 Play();
1064 return true;
1065 }
1066
1067 const int KeyFrameWantedTick = WantedTick - 5; // -5 because we have to have a current tick and previous tick when we do the playback
1068 const float Percent = (KeyFrameWantedTick - m_Info.m_Info.m_FirstTick) / (float)(m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick);
1069
1070 // get correct key frame
1071 size_t KeyFrame = std::clamp<size_t>(val: m_vKeyFrames.size() * Percent, lo: 0, hi: m_vKeyFrames.size() - 1);
1072 while(KeyFrame < m_vKeyFrames.size() - 1 && m_vKeyFrames[KeyFrame].m_Tick < KeyFrameWantedTick)
1073 KeyFrame++;
1074 while(KeyFrame > 0 && m_vKeyFrames[KeyFrame].m_Tick > KeyFrameWantedTick)
1075 KeyFrame--;
1076
1077 // TODO Remove `WantedTick <= m_Info.m_NextTick` with https://github.com/ddnet/ddnet/issues/11681
1078 if(WantedTick <= m_Info.m_Info.m_CurrentTick || // if we are seeking backwards (must be <= for high bandwidth demos) OR
1079 WantedTick <= m_Info.m_NextTick || // if seeking to current tick OR
1080 m_Info.m_Info.m_CurrentTick < m_vKeyFrames[KeyFrame].m_Tick || // we are before the wanted KeyFrame OR
1081 (KeyFrame != m_vKeyFrames.size() - 1 && m_Info.m_Info.m_CurrentTick >= m_vKeyFrames[KeyFrame + 1].m_Tick)) // we are after the wanted KeyFrame
1082 {
1083 if(io_seek(io: m_File, offset: m_vKeyFrames[KeyFrame].m_Filepos, origin: IOSEEK_START) != 0)
1084 {
1085 Stop(pErrorMessage: "Error seeking keyframe position");
1086 return false;
1087 }
1088 m_Info.m_NextTick = -1;
1089 m_Info.m_Info.m_CurrentTick = -1;
1090 m_Info.m_PreviousTick = -1;
1091 }
1092
1093 // playback everything until we hit our tick
1094 while(m_Info.m_NextTick < WantedTick)
1095 {
1096 DoTick();
1097 if(!IsPlaying())
1098 {
1099 return false;
1100 }
1101 }
1102
1103 Play();
1104
1105 return true;
1106}
1107
1108void CDemoPlayer::SetSpeed(float Speed)
1109{
1110 m_Info.m_Info.m_Speed = std::clamp(val: Speed, lo: 0.f, hi: 256.f);
1111}
1112
1113void CDemoPlayer::SetSpeedIndex(int SpeedIndex)
1114{
1115 dbg_assert(SpeedIndex >= 0 && SpeedIndex < (int)std::size(DEMO_SPEEDS), "invalid SpeedIndex");
1116 m_SpeedIndex = SpeedIndex;
1117 SetSpeed(DEMO_SPEEDS[m_SpeedIndex]);
1118}
1119
1120void CDemoPlayer::AdjustSpeedIndex(int Offset)
1121{
1122 SetSpeedIndex(std::clamp(val: m_SpeedIndex + Offset, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1)));
1123}
1124
1125void CDemoPlayer::Update(bool RealTime)
1126{
1127 const int64_t Now = Time();
1128 const int64_t Freq = time_freq();
1129 const int64_t DeltaTime = Now - m_Info.m_LastUpdate;
1130 m_Info.m_LastUpdate = Now;
1131
1132 if(m_Info.m_LiveStateUpdating)
1133 {
1134 // Determine if demo is live and still being written to, by scanning
1135 // file again and checking if more ticks are available than before.
1136 if(Now - m_Info.m_LastScan > Freq)
1137 {
1138 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1139 const EScanFileResult ScanResult = ScanFile();
1140 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1141 {
1142 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning demo file to determine live state");
1143 return;
1144 }
1145 else if(ScanResult == EScanFileResult::SUCCESS)
1146 {
1147 // Live state is known when ScanFile succeeded.
1148 m_Info.m_LiveStateUpdating = false;
1149 }
1150 else
1151 {
1152 m_Info.m_LiveStateFailedCount++;
1153 if(m_Info.m_LiveStateFailedCount >= 15)
1154 {
1155 // ScanFile keeps failing, which should be unlikely, so this is probably
1156 // not a live demo but a regular demo that is truncated at the end.
1157 m_Info.m_LiveStateUpdating = false;
1158 }
1159 }
1160 // Check if we got more ticks also when ScanFile failed, because
1161 // it could still have found more ticks.
1162 if(m_Info.m_Info.m_LastTick > PreviousLastTick)
1163 {
1164 m_Info.m_Info.m_LiveDemo = true;
1165 m_Info.m_LiveStateUpdating = false;
1166 m_Info.m_LiveStateUnchangedCount = 0;
1167 }
1168 m_Info.m_LastScan = Now;
1169 // Try again later if ScanFile failed and no more ticks were found.
1170 }
1171 }
1172 else if(m_Info.m_Info.m_LiveDemo)
1173 {
1174 // Scan live demo at tick frequency to smoothly update total time.
1175 if(Now - m_Info.m_LastScan > Freq / SERVER_TICK_SPEED)
1176 {
1177 const int PreviousLastTick = m_Info.m_Info.m_LastTick;
1178 const EScanFileResult ScanResult = ScanFile();
1179 if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
1180 {
1181 Stop(pErrorMessage: "Unrecoverable error on incrementally scanning live demo file");
1182 return;
1183 }
1184 else if(ScanResult == EScanFileResult::SUCCESS &&
1185 m_Info.m_Info.m_LastTick == PreviousLastTick)
1186 {
1187 m_Info.m_LiveStateUnchangedCount++;
1188 if(m_Info.m_LiveStateUnchangedCount >= 2 * SERVER_TICK_SPEED)
1189 {
1190 // Assume demo stopped being live if we scanned the demo
1191 // successfully for 2 seconds without reading new ticks.
1192 m_Info.m_Info.m_LiveDemo = false;
1193 }
1194 }
1195 else
1196 {
1197 m_Info.m_LiveStateUnchangedCount = 0;
1198 }
1199 m_Info.m_LastScan = Now;
1200 }
1201 }
1202
1203 if(!IsPlaying())
1204 {
1205 return;
1206 }
1207
1208 if(!m_Info.m_Info.m_Paused)
1209 {
1210 if(m_Info.m_Info.m_LiveDemo &&
1211 m_Info.m_Info.m_Speed > 1.0f &&
1212 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)
1213 {
1214 // Reset to default speed if we are fast-forwarding to the end of a live demo,
1215 // to prevent playback error due to final demo chunk data still being written.
1216 SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
1217 }
1218
1219 m_Info.m_CurrentTime += (int64_t)(DeltaTime * (double)m_Info.m_Info.m_Speed);
1220
1221 // Do more ticks until we reach the current time.
1222 while(!m_Info.m_Info.m_Paused)
1223 {
1224 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1225 if(RealTime && CurrentTickStart > m_Info.m_CurrentTime)
1226 {
1227 break;
1228 }
1229 DoTick();
1230 if(!IsPlaying())
1231 {
1232 return;
1233 }
1234 }
1235 }
1236
1237 UpdateTimes();
1238}
1239
1240void CDemoPlayer::UpdateTimes()
1241{
1242 const int64_t Freq = time_freq();
1243 const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
1244 const int64_t PreviousTickStart = m_Info.m_PreviousTick * Freq / SERVER_TICK_SPEED;
1245 m_Info.m_IntraTick = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(CurrentTickStart - PreviousTickStart);
1246 m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(Freq / SERVER_TICK_SPEED);
1247 m_Info.m_TickTime = (m_Info.m_CurrentTime - PreviousTickStart) / (float)Freq;
1248 m_Info.m_Info.m_LivePlayback = m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick < 3 * SERVER_TICK_SPEED;
1249
1250 if(m_UpdateIntraTimesFunc)
1251 {
1252 m_UpdateIntraTimesFunc();
1253 }
1254}
1255
1256void CDemoPlayer::Stop(const char *pErrorMessage)
1257{
1258#if defined(CONF_VIDEORECORDER)
1259 if(m_UseVideo && IVideo::Current())
1260 IVideo::Current()->Stop();
1261 m_WasRecording = false;
1262#endif
1263
1264 if(!m_File)
1265 return;
1266
1267 if(m_pConsole)
1268 {
1269 char aBuf[256];
1270 if(pErrorMessage[0] == '\0')
1271 str_copy(dst&: aBuf, src: "Stopped playback");
1272 else
1273 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Stopped playback due to error: %s", pErrorMessage);
1274 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_player", pStr: aBuf);
1275 }
1276
1277 io_close(io: m_File);
1278 m_File = nullptr;
1279 m_vKeyFrames.clear();
1280 str_copy(dst&: m_aFilename, src: "");
1281 str_copy(dst&: m_aErrorMessage, src: pErrorMessage);
1282}
1283
1284void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const
1285{
1286 IStorage::StripPathAndExtension(pFilename: m_aFilename, pBuffer, BufferSize);
1287}
1288
1289bool 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
1290{
1291 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1292 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1293 pMapInfo->m_aName[0] = '\0';
1294 pMapInfo->m_Sha256 = std::nullopt;
1295 pMapInfo->m_Crc = 0;
1296 pMapInfo->m_Size = 0;
1297
1298 IOHANDLE File = pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType);
1299 if(!File)
1300 {
1301 if(pErrorMessage != nullptr)
1302 str_copy(dst: pErrorMessage, src: "Could not open demo file", dst_size: ErrorMessageSize);
1303 return false;
1304 }
1305
1306 if(io_read(io: File, buffer: pDemoHeader, size: sizeof(CDemoHeader)) != sizeof(CDemoHeader) || !pDemoHeader->Valid())
1307 {
1308 if(pErrorMessage != nullptr)
1309 str_copy(dst: pErrorMessage, src: "Error reading demo header", dst_size: ErrorMessageSize);
1310 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1311 io_close(io: File);
1312 return false;
1313 }
1314
1315 if(pDemoHeader->m_Version < gs_OldVersion)
1316 {
1317 if(pErrorMessage != nullptr)
1318 str_format(buffer: pErrorMessage, buffer_size: ErrorMessageSize, format: "Demo version '%d' is not supported", pDemoHeader->m_Version);
1319 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1320 io_close(io: File);
1321 return false;
1322 }
1323 else if(pDemoHeader->m_Version > gs_OldVersion)
1324 {
1325 if(io_read(io: File, buffer: pTimelineMarkers, size: sizeof(CTimelineMarkers)) != sizeof(CTimelineMarkers))
1326 {
1327 if(pErrorMessage != nullptr)
1328 str_copy(dst: pErrorMessage, src: "Error reading timeline markers", dst_size: ErrorMessageSize);
1329 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1330 io_close(io: File);
1331 return false;
1332 }
1333 }
1334
1335 std::optional<SHA256_DIGEST> Sha256;
1336 if(pDemoHeader->m_Version >= gs_Sha256Version)
1337 {
1338 CUuid ExtensionUuid = {};
1339 const unsigned ExtensionUuidSize = io_read(io: File, buffer: &ExtensionUuid.m_aData, size: sizeof(ExtensionUuid.m_aData));
1340 if(ExtensionUuidSize == sizeof(ExtensionUuid.m_aData) && ExtensionUuid == SHA256_EXTENSION)
1341 {
1342 SHA256_DIGEST ReadSha256;
1343 if(io_read(io: File, buffer: &ReadSha256, size: sizeof(SHA256_DIGEST)) != sizeof(SHA256_DIGEST))
1344 {
1345 if(pErrorMessage != nullptr)
1346 str_copy(dst: pErrorMessage, src: "Error reading SHA256", dst_size: ErrorMessageSize);
1347 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1348 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1349 io_close(io: File);
1350 return false;
1351 }
1352 Sha256 = ReadSha256;
1353 }
1354 else
1355 {
1356 // This hopes whatever happened during the version increment didn't add something here
1357 if(pConsole)
1358 {
1359 pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "demo_player", pStr: "Demo version incremented, but not by DDNet");
1360 }
1361 if(io_seek(io: File, offset: -(int64_t)ExtensionUuidSize, origin: IOSEEK_CUR) != 0)
1362 {
1363 if(pErrorMessage != nullptr)
1364 str_copy(dst: pErrorMessage, src: "Error rewinding SHA256 extension UUID", dst_size: ErrorMessageSize);
1365 mem_zero(block: pDemoHeader, size: sizeof(CDemoHeader));
1366 mem_zero(block: pTimelineMarkers, size: sizeof(CTimelineMarkers));
1367 io_close(io: File);
1368 return false;
1369 }
1370 }
1371 }
1372
1373 str_copy(dst&: pMapInfo->m_aName, src: pDemoHeader->m_aMapName);
1374 pMapInfo->m_Sha256 = Sha256;
1375 pMapInfo->m_Crc = bytes_be_to_uint(bytes: pDemoHeader->m_aMapCrc);
1376 pMapInfo->m_Size = bytes_be_to_uint(bytes: pDemoHeader->m_aMapSize);
1377
1378 if(pFile == nullptr)
1379 io_close(io: File);
1380 else
1381 *pFile = File;
1382
1383 return true;
1384}
1385
1386class CDemoRecordingListener : public CDemoPlayer::IListener
1387{
1388public:
1389 CDemoRecorder *m_pDemoRecorder;
1390 CDemoPlayer *m_pDemoPlayer;
1391 bool m_Stop;
1392 int m_StartTick;
1393 int m_EndTick;
1394
1395 void OnDemoPlayerSnapshot(void *pData, int Size) override
1396 {
1397 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1398
1399 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1400 m_Stop = true;
1401 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1402 m_pDemoRecorder->RecordSnapshot(Tick: pInfo->m_Info.m_CurrentTick, pData, Size);
1403 }
1404
1405 void OnDemoPlayerMessage(void *pData, int Size) override
1406 {
1407 const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();
1408
1409 if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
1410 m_Stop = true;
1411 else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
1412 m_pDemoRecorder->RecordMessage(pData, Size);
1413 }
1414};
1415
1416void CDemoEditor::Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage)
1417{
1418 m_pSnapshotDelta = pSnapshotDelta;
1419 m_pConsole = pConsole;
1420 m_pStorage = pStorage;
1421}
1422
1423bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser)
1424{
1425 CDemoPlayer DemoPlayer(m_pSnapshotDelta, false);
1426 if(DemoPlayer.Load(pStorage: m_pStorage, pConsole: m_pConsole, pFilename: pDemo, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
1427 return false;
1428
1429 const CMapInfo *pMapInfo = DemoPlayer.GetMapInfo();
1430 const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();
1431
1432 std::optional<SHA256_DIGEST> Sha256 = pMapInfo->m_Sha256;
1433 if(pInfo->m_Header.m_Version < gs_Sha256Version)
1434 {
1435 if(DemoPlayer.ExtractMap(pStorage: m_pStorage))
1436 {
1437 Sha256 = pMapInfo->m_Sha256;
1438 }
1439 }
1440 if(!Sha256.has_value())
1441 {
1442 log_error_color(DEMO_PRINT_COLOR, "demo/slice", "Failed to start demo slicing because map SHA256 could not be determined.");
1443 return false;
1444 }
1445
1446 CDemoRecorder DemoRecorder(m_pSnapshotDelta);
1447 unsigned char *pMapData = DemoPlayer.GetMapData(pStorage: m_pStorage);
1448 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;
1449 free(ptr: pMapData);
1450 if(Result != 0)
1451 {
1452 DemoPlayer.Stop();
1453 return false;
1454 }
1455
1456 CDemoRecordingListener Listener;
1457 Listener.m_pDemoRecorder = &DemoRecorder;
1458 Listener.m_pDemoPlayer = &DemoPlayer;
1459 Listener.m_Stop = false;
1460 Listener.m_StartTick = StartTick;
1461 Listener.m_EndTick = EndTick;
1462 DemoPlayer.SetListener(&Listener);
1463
1464 DemoPlayer.Play();
1465
1466 while(DemoPlayer.IsPlaying() && !Listener.m_Stop)
1467 {
1468 DemoPlayer.Update(RealTime: false);
1469
1470 if(pInfo->m_Info.m_Paused)
1471 break;
1472 }
1473
1474 // Copy timeline markers to sliced demo
1475 for(int i = 0; i < pInfo->m_Info.m_NumTimelineMarkers; i++)
1476 {
1477 if((StartTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] >= StartTick) && (EndTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] <= EndTick))
1478 {
1479 DemoRecorder.AddDemoMarker(Tick: pInfo->m_Info.m_aTimelineMarkers[i]);
1480 }
1481 }
1482
1483 DemoPlayer.Stop();
1484 DemoRecorder.Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
1485 return true;
1486}
1487