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