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 | |
20 | const 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}; |
21 | const CUuid SHA256_EXTENSION = |
22 | {.m_aData: {0x6b, 0xe6, 0xda, 0x4a, 0xce, 0xbd, 0x38, 0x0c, |
23 | 0x9b, 0x5b, 0x12, 0x89, 0xc8, 0x42, 0xd7, 0x80}}; |
24 | |
25 | static const unsigned char gs_CurVersion = 6; |
26 | static const unsigned char gs_OldVersion = 3; |
27 | static const unsigned char gs_Sha256Version = 6; |
28 | static const unsigned char gs_VersionTickCompression = 5; // demo files with this version or higher will use `CHUNKTICKFLAG_TICK_COMPRESSED` |
29 | static const int gs_LengthOffset = 152; |
30 | static const int gs_NumMarkersOffset = 176; |
31 | |
32 | static const ColorRGBA gs_DemoPrintColor{0.75f, 0.7f, 0.7f, 1.0f}; |
33 | |
34 | bool CDemoHeader::() 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 | |
44 | CDemoRecorder::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 | |
55 | CDemoRecorder::~CDemoRecorder() |
56 | { |
57 | dbg_assert(m_File == 0, "Demo recorder was not stopped" ); |
58 | } |
59 | |
60 | // Record |
61 | int 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 ; |
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 | |
208 | enum |
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 | |
224 | void 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 | |
249 | void 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 | |
299 | void 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 | |
329 | void 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 | |
341 | int 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 | |
407 | void CDemoRecorder::AddDemoMarker() |
408 | { |
409 | if(m_LastTickMarker < 0) |
410 | return; |
411 | AddDemoMarker(Tick: m_LastTickMarker); |
412 | } |
413 | |
414 | void 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 | |
448 | CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc) |
449 | { |
450 | Construct(pSnapshotDelta, UseVideo); |
451 | |
452 | m_UpdateIntraTimesFunc = UpdateIntraTimesFunc; |
453 | } |
454 | |
455 | CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo) |
456 | { |
457 | Construct(pSnapshotDelta, UseVideo); |
458 | } |
459 | |
460 | CDemoPlayer::~CDemoPlayer() |
461 | { |
462 | dbg_assert(m_File == 0, "Demo player not stopped" ); |
463 | } |
464 | |
465 | void 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 | |
479 | void CDemoPlayer::SetListener(IListener *pListener) |
480 | { |
481 | m_pListener = pListener; |
482 | } |
483 | |
484 | CDemoPlayer::EReadChunkHeaderResult CDemoPlayer::(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 TickdeltaLegacy = 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 && TickdeltaLegacy != 0) |
501 | { |
502 | if(*pTick < 0) // initial tick not initialized before a tick delta |
503 | return CHUNKHEADER_ERROR; |
504 | NewTick = *pTick + TickdeltaLegacy; |
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 | |
549 | bool 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 | |
607 | void 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 | |
757 | void 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 | |
766 | void 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 | |
775 | int 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 | |
841 | unsigned 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 | |
859 | bool CDemoPlayer::(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 | |
896 | int64_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 | |
923 | int 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 | |
935 | int 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 | |
941 | int 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 | |
947 | int 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 | |
972 | int 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 | |
1008 | void CDemoPlayer::SetSpeed(float Speed) |
1009 | { |
1010 | m_Info.m_Info.m_Speed = clamp(val: Speed, lo: 0.f, hi: 256.f); |
1011 | } |
1012 | |
1013 | void 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 | |
1020 | void CDemoPlayer::AdjustSpeedIndex(int Offset) |
1021 | { |
1022 | SetSpeedIndex(clamp(val: m_SpeedIndex + Offset, lo: 0, hi: (int)(std::size(g_aSpeeds) - 1))); |
1023 | } |
1024 | |
1025 | int 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 | |
1066 | void 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 | |
1093 | void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const |
1094 | { |
1095 | IStorage::StripPathAndExtension(pFilename: m_aFilename, pBuffer, BufferSize); |
1096 | } |
1097 | |
1098 | bool CDemoPlayer::(IStorage *pStorage, IConsole *pConsole, const char *pFilename, int StorageType, CDemoHeader *, 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 | |
1190 | class CDemoRecordingListener : public CDemoPlayer::IListener |
1191 | { |
1192 | public: |
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 | |
1220 | void 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 | |
1228 | bool 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 | |