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