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