| 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 | |
| 20 | const CUuid SHA256_EXTENSION = |
| 21 | {.m_aData: {0x6b, 0xe6, 0xda, 0x4a, 0xce, 0xbd, 0x38, 0x0c, |
| 22 | 0x9b, 0x5b, 0x12, 0x89, 0xc8, 0x42, 0xd7, 0x80}}; |
| 23 | |
| 24 | static const unsigned char gs_CurVersion = 6; |
| 25 | static const unsigned char gs_OldVersion = 3; |
| 26 | static const unsigned char gs_Sha256Version = 6; |
| 27 | static 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 |
| 30 | static constexpr ColorRGBA gs_DemoPrintColor{0.75f, 0.7f, 0.7f, 1.0f}; |
| 31 | static constexpr LOG_COLOR DEMO_PRINT_COLOR = {.r: 191, .g: 178, .b: 178}; |
| 32 | |
| 33 | bool CDemoHeader::() 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 | |
| 43 | CDemoRecorder::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 | |
| 54 | CDemoRecorder::~CDemoRecorder() |
| 55 | { |
| 56 | dbg_assert(m_File == 0, "Demo recorder was not stopped" ); |
| 57 | } |
| 58 | |
| 59 | // Record |
| 60 | 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) |
| 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 ; |
| 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 | |
| 234 | enum |
| 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 | |
| 250 | void 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 | |
| 275 | void 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 | |
| 325 | void 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 | |
| 357 | void 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 | |
| 369 | int 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 | |
| 435 | void CDemoRecorder::AddDemoMarker() |
| 436 | { |
| 437 | if(m_LastTickMarker < 0) |
| 438 | return; |
| 439 | AddDemoMarker(Tick: m_LastTickMarker); |
| 440 | } |
| 441 | |
| 442 | void 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 | |
| 476 | CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc) |
| 477 | { |
| 478 | Construct(pSnapshotDelta, UseVideo); |
| 479 | |
| 480 | m_UpdateIntraTimesFunc = UpdateIntraTimesFunc; |
| 481 | } |
| 482 | |
| 483 | CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo) |
| 484 | { |
| 485 | Construct(pSnapshotDelta, UseVideo); |
| 486 | } |
| 487 | |
| 488 | CDemoPlayer::~CDemoPlayer() |
| 489 | { |
| 490 | dbg_assert(m_File == 0, "Demo player not stopped" ); |
| 491 | } |
| 492 | |
| 493 | void 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 | |
| 507 | void CDemoPlayer::SetListener(IListener *pListener) |
| 508 | { |
| 509 | m_pListener = pListener; |
| 510 | } |
| 511 | |
| 512 | CDemoPlayer::EReadChunkHeaderResult CDemoPlayer::(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 | |
| 577 | CDemoPlayer::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 | |
| 654 | void 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 | |
| 797 | void 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 | |
| 806 | void 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 | |
| 815 | int 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 | |
| 883 | unsigned 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 | |
| 901 | bool CDemoPlayer::(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 | |
| 938 | int64_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 | |
| 965 | void 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 | |
| 992 | int 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 | |
| 998 | int 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 | |
| 1004 | int 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 | |
| 1027 | int 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 | |
| 1082 | void CDemoPlayer::SetSpeed(float Speed) |
| 1083 | { |
| 1084 | m_Info.m_Info.m_Speed = std::clamp(val: Speed, lo: 0.f, hi: 256.f); |
| 1085 | } |
| 1086 | |
| 1087 | void 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 | |
| 1094 | void CDemoPlayer::AdjustSpeedIndex(int Offset) |
| 1095 | { |
| 1096 | SetSpeedIndex(std::clamp(val: m_SpeedIndex + Offset, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1))); |
| 1097 | } |
| 1098 | |
| 1099 | void 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 | |
| 1214 | void 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 | |
| 1230 | void 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 | |
| 1258 | void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const |
| 1259 | { |
| 1260 | IStorage::StripPathAndExtension(pFilename: m_aFilename, pBuffer, BufferSize); |
| 1261 | } |
| 1262 | |
| 1263 | bool CDemoPlayer::(IStorage *pStorage, IConsole *pConsole, const char *pFilename, int StorageType, CDemoHeader *, 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 | |
| 1355 | class CDemoRecordingListener : public CDemoPlayer::IListener |
| 1356 | { |
| 1357 | public: |
| 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 | |
| 1385 | void 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 | |
| 1392 | bool 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 | |