BYTEARRAY CBNETProtocol :: SEND_SID_AUTH_ACCOUNTLOGONPROOF( BYTEARRAY clientPasswordProof ) { BYTEARRAY packet; if( clientPasswordProof.size( ) == 20 ) { packet.push_back( BNET_HEADER_CONSTANT ); // BNET header constant packet.push_back( SID_AUTH_ACCOUNTLOGONPROOF ); // SID_AUTH_ACCOUNTLOGONPROOF packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later UTIL_AppendByteArrayFast( packet, clientPasswordProof ); // Client Password Proof AssignLength( packet ); } else CONSOLE_Print( "[BNETPROTO] invalid parameters passed to SEND_SID_AUTH_ACCOUNTLOGON" ); // DEBUG_Print( "SENT SID_AUTH_ACCOUNTLOGONPROOF" ); // DEBUG_Print( packet ); return packet; }
BYTEARRAY CBNETProtocol :: SEND_SID_PING( BYTEARRAY pingValue ) { BYTEARRAY packet; if( pingValue.size( ) == 4 ) { packet.push_back( BNET_HEADER_CONSTANT ); // BNET header constant packet.push_back( SID_PING ); // SID_PING packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later UTIL_AppendByteArrayFast( packet, pingValue ); // Ping Value AssignLength( packet ); } else CONSOLE_Print( "[BNETPROTO] invalid parameters passed to SEND_SID_PING" ); // DEBUG_Print( "SENT SID_PING" ); // DEBUG_Print( packet ); return packet; }
BYTEARRAY CBNETProtocol :: SEND_SID_JOINCHANNEL( string channel ) { unsigned char NoCreateJoin[] = { 2, 0, 0, 0 }; unsigned char FirstJoin[] = { 1, 0, 0, 0 }; BYTEARRAY packet; packet.push_back( BNET_HEADER_CONSTANT ); // BNET header constant packet.push_back( SID_JOINCHANNEL ); // SID_JOINCHANNEL packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later if( channel.size( ) > 0 ) UTIL_AppendByteArray( packet, NoCreateJoin, 4 ); // flags for no create join else UTIL_AppendByteArray( packet, FirstJoin, 4 ); // flags for first join UTIL_AppendByteArrayFast( packet, channel ); AssignLength( packet ); // DEBUG_Print( "SENT SID_JOINCHANNEL" ); // DEBUG_Print( packet ); return packet; }
BYTEARRAY CBNETProtocol :: SEND_SID_AUTH_CHECK( bool TFT, BYTEARRAY clientToken, BYTEARRAY exeVersion, BYTEARRAY exeVersionHash, BYTEARRAY keyInfoROC, BYTEARRAY keyInfoTFT, string exeInfo, string keyOwnerName ) { uint32_t NumKeys = 0; if( TFT ) NumKeys = 2; else NumKeys = 1; BYTEARRAY packet; if( clientToken.size( ) == 4 && exeVersion.size( ) == 4 && exeVersionHash.size( ) == 4 && keyInfoROC.size( ) == 36 && ( !TFT || keyInfoTFT.size( ) == 36 ) ) { packet.push_back( BNET_HEADER_CONSTANT ); // BNET header constant packet.push_back( SID_AUTH_CHECK ); // SID_AUTH_CHECK packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later UTIL_AppendByteArrayFast( packet, clientToken ); // Client Token UTIL_AppendByteArrayFast( packet, exeVersion ); // EXE Version UTIL_AppendByteArrayFast( packet, exeVersionHash ); // EXE Version Hash UTIL_AppendByteArray( packet, NumKeys, false ); // number of keys in this packet UTIL_AppendByteArray( packet, (uint32_t)0, false ); // boolean Using Spawn (32 bit) UTIL_AppendByteArrayFast( packet, keyInfoROC ); // ROC Key Info if( TFT ) UTIL_AppendByteArrayFast( packet, keyInfoTFT ); // TFT Key Info UTIL_AppendByteArrayFast( packet, exeInfo ); // EXE Info UTIL_AppendByteArrayFast( packet, keyOwnerName ); // CD Key Owner Name AssignLength( packet ); } else { CONSOLE_Print( "[BNETPROTO] invalid parameters passed to SEND_SID_AUTH_CHECK" ); forward(new CFwdData(FWD_GENERAL, "Invalid parameters passed to SEND_SID_AUTH_CHECK", 6, 0)); } // DEBUG_Print( "SENT SID_AUTH_CHECK" ); // DEBUG_Print( packet ); return packet; }
void CReplay :: AddTimeSlot2( queue<CIncomingAction *> actions ) { BYTEARRAY Block; Block.push_back( REPLAY_TIMESLOT2 ); UTIL_AppendByteArray( Block, (uint16_t)0, false ); UTIL_AppendByteArray( Block, (uint16_t)0, false ); while( !actions.empty( ) ) { CIncomingAction *Action = actions.front( ); actions.pop( ); Block.push_back( Action->GetPID( ) ); UTIL_AppendByteArray( Block, (uint16_t)Action->GetAction( )->size( ), false ); UTIL_AppendByteArrayFast( Block, *Action->GetAction( ) ); } // assign length BYTEARRAY LengthBytes = UTIL_CreateByteArray( (uint16_t)( Block.size( ) - 3 ), false ); Block[1] = LengthBytes[0]; Block[2] = LengthBytes[1]; m_CompiledBlocks += string( Block.begin( ), Block.end( ) ); }
BYTEARRAY CBNETProtocol :: SEND_SID_GETADVLISTEX( string gameName ) { unsigned char MapFilter1[] = { 255, 3, 0, 0 }; unsigned char MapFilter2[] = { 255, 3, 0, 0 }; unsigned char MapFilter3[] = { 0, 0, 0, 0 }; unsigned char NumGames[] = { 1, 0, 0, 0 }; BYTEARRAY packet; packet.push_back( BNET_HEADER_CONSTANT ); // BNET header constant packet.push_back( SID_GETADVLISTEX ); // SID_GETADVLISTEX packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later UTIL_AppendByteArray( packet, MapFilter1, 4 ); // Map Filter UTIL_AppendByteArray( packet, MapFilter2, 4 ); // Map Filter UTIL_AppendByteArray( packet, MapFilter3, 4 ); // Map Filter UTIL_AppendByteArray( packet, NumGames, 4 ); // maximum number of games to list UTIL_AppendByteArrayFast( packet, gameName ); // Game Name packet.push_back( 0 ); // Game Password is NULL packet.push_back( 0 ); // Game Stats is NULL AssignLength( packet ); // DEBUG_Print( "SENT SID_GETADVLISTEX" ); // DEBUG_Print( packet ); return packet; }
BYTEARRAY CGameProtocol :: SEND_W3GS_GAMEINFO( bool TFT, unsigned char war3Version, BYTEARRAY mapGameType, BYTEARRAY mapFlags, BYTEARRAY mapWidth, BYTEARRAY mapHeight, string gameName, string hostName, uint32_t upTime, string mapPath, BYTEARRAY mapCRC, uint32_t slotsTotal, uint32_t slotsOpen, uint16_t port, uint32_t hostCounter, uint32_t entryKey ) { unsigned char ProductID_ROC[] = { 51, 82, 65, 87 }; // "WAR3" unsigned char ProductID_TFT[] = { 80, 88, 51, 87 }; // "W3XP" unsigned char Version[] = { war3Version, 0, 0, 0 }; unsigned char Unknown[] = { 1, 0, 0, 0 }; BYTEARRAY packet; if( mapGameType.size( ) == 4 && mapFlags.size( ) == 4 && mapWidth.size( ) == 2 && mapHeight.size( ) == 2 && !gameName.empty( ) && !hostName.empty( ) && !mapPath.empty( ) && mapCRC.size( ) == 4 ) { // make the stat string BYTEARRAY StatString; UTIL_AppendByteArrayFast( StatString, mapFlags ); StatString.push_back( 0 ); UTIL_AppendByteArrayFast( StatString, mapWidth ); UTIL_AppendByteArrayFast( StatString, mapHeight ); UTIL_AppendByteArrayFast( StatString, mapCRC ); UTIL_AppendByteArrayFast( StatString, mapPath ); UTIL_AppendByteArrayFast( StatString, hostName ); StatString.push_back( 0 ); StatString = UTIL_EncodeStatString( StatString ); // make the rest of the packet packet.push_back( W3GS_HEADER_CONSTANT ); // W3GS header constant packet.push_back( W3GS_GAMEINFO ); // W3GS_GAMEINFO packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later if( TFT ) UTIL_AppendByteArray( packet, ProductID_TFT, 4 ); // Product ID (TFT) else UTIL_AppendByteArray( packet, ProductID_ROC, 4 ); // Product ID (ROC) UTIL_AppendByteArray( packet, Version, 4 ); // Version UTIL_AppendByteArray( packet, hostCounter, false ); // Host Counter UTIL_AppendByteArray( packet, entryKey, false ); // Entry Key UTIL_AppendByteArrayFast( packet, gameName ); // Game Name packet.push_back( 0 ); // ??? (maybe game password) UTIL_AppendByteArrayFast( packet, StatString ); // Stat String packet.push_back( 0 ); // Stat String null terminator (the stat string is encoded to remove all even numbers i.e. zeros) UTIL_AppendByteArray( packet, slotsTotal, false ); // Slots Total UTIL_AppendByteArrayFast( packet, mapGameType ); // Game Type UTIL_AppendByteArray( packet, Unknown, 4 ); // ??? UTIL_AppendByteArray( packet, slotsOpen, false ); // Slots Open UTIL_AppendByteArray( packet, upTime, false ); // time since creation UTIL_AppendByteArray( packet, port, false ); // port AssignLength( packet ); } else { if(mapGameType.size( ) != 4) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (mapGameType)" ); else if(mapFlags.size( ) != 4) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (mapFlags)" ); else if(mapWidth.size( ) != 2) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (mapWidth)" ); else if(mapHeight.size( ) != 2) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (mapHeight)" ); else if(gameName.empty( )) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (gameName)" ); else if(hostName.empty( )) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (hostName)" ); else if(mapPath.empty( )) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (mapPath)" ); else if(mapCRC.size( ) != 4) CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (CRC)" ); else CONSOLE_Print( "[GAMEPROTO] invalid parameters passed to SEND_W3GS_GAMEINFO (unknown)" ); } // DEBUG_Print( "SENT W3GS_GAMEINFO" ); // DEBUG_Print( packet ); return packet; }
BYTEARRAY CBNETProtocol :: SEND_SID_STARTADVEX3( unsigned char state, BYTEARRAY mapGameType, BYTEARRAY mapFlags, BYTEARRAY mapWidth, BYTEARRAY mapHeight, string gameName, string hostName, uint32_t upTime, string mapPath, BYTEARRAY mapCRC, BYTEARRAY mapSHA1, uint32_t hostCounter ) { // todotodo: sort out how GameType works, the documentation is horrendous /* Game type tag: (read W3GS_GAMEINFO for this field) 0x00000001 - Custom 0x00000009 - Blizzard/Ladder Map author: (mask 0x00006000) can be combined *0x00002000 - Blizzard 0x00004000 - Custom Battle type: (mask 0x00018000) cant be combined 0x00000000 - Battle *0x00010000 - Scenario Map size: (mask 0x000E0000) can be combined with 2 nearest values 0x00020000 - Small 0x00040000 - Medium *0x00080000 - Huge Observers: (mask 0x00700000) cant be combined 0x00100000 - Allowed observers 0x00200000 - Observers on defeat *0x00400000 - No observers Flags: 0x00000800 - Private game flag (not used in game list) */ unsigned char Unknown[] = { 255, 3, 0, 0 }; unsigned char CustomGame[] = { 0, 0, 0, 0 }; string HostCounterString = UTIL_ToHexString( hostCounter ); if( HostCounterString.size( ) < 8 ) HostCounterString.insert( 0, 8 - HostCounterString.size( ), '0' ); HostCounterString = string( HostCounterString.rbegin( ), HostCounterString.rend( ) ); BYTEARRAY packet; // make the stat string BYTEARRAY StatString; UTIL_AppendByteArrayFast( StatString, mapFlags ); StatString.push_back( 0 ); UTIL_AppendByteArrayFast( StatString, mapWidth ); UTIL_AppendByteArrayFast( StatString, mapHeight ); UTIL_AppendByteArrayFast( StatString, mapCRC ); UTIL_AppendByteArrayFast( StatString, mapPath ); UTIL_AppendByteArrayFast( StatString, hostName ); StatString.push_back( 0 ); UTIL_AppendByteArrayFast( StatString, mapSHA1 ); StatString = UTIL_EncodeStatString( StatString ); if( mapGameType.size( ) == 4 && mapFlags.size( ) == 4 && mapWidth.size( ) == 2 && mapHeight.size( ) == 2 && !gameName.empty( ) && !hostName.empty( ) && !mapPath.empty( ) && mapCRC.size( ) == 4 && mapSHA1.size( ) == 20 && StatString.size( ) < 128 && HostCounterString.size( ) == 8 ) { // make the rest of the packet packet.push_back( BNET_HEADER_CONSTANT ); // BNET header constant packet.push_back( SID_STARTADVEX3 ); // SID_STARTADVEX3 packet.push_back( 0 ); // packet length will be assigned later packet.push_back( 0 ); // packet length will be assigned later packet.push_back( state ); // State (16 = public, 17 = private, 18 = close) packet.push_back( 0 ); // State continued... packet.push_back( 0 ); // State continued... packet.push_back( 0 ); // State continued... UTIL_AppendByteArray( packet, upTime, false ); // time since creation UTIL_AppendByteArrayFast( packet, mapGameType ); // Game Type, Parameter UTIL_AppendByteArray( packet, Unknown, 4 ); // ??? UTIL_AppendByteArray( packet, CustomGame, 4 ); // Custom Game UTIL_AppendByteArrayFast( packet, gameName ); // Game Name packet.push_back( 0 ); // Game Password is NULL packet.push_back( 98 ); // Slots Free (ascii 98 = char 'b' = 11 slots free) - note: do not reduce this as this is the # of PID's Warcraft III will allocate UTIL_AppendByteArrayFast( packet, HostCounterString, false ); // Host Counter UTIL_AppendByteArrayFast( packet, StatString ); // Stat String packet.push_back( 0 ); // Stat String null terminator (the stat string is encoded to remove all even numbers i.e. zeros) AssignLength( packet ); } else { CONSOLE_Print( "[BNETPROTO] invalid parameters passed to SEND_SID_STARTADVEX3" ); forward(new CFwdData(FWD_REALM, "Invalid parameters passed to SEND_SID_STARTADVEX3", 6, hostCounter)); } // DEBUG_Print( "SENT SID_STARTADVEX3" ); // DEBUG_Print( packet ); return packet; }
void CReplay :: BuildReplay( string gameName, string statString, uint32_t war3Version, uint16_t buildNumber ) { m_War3Version = war3Version; m_BuildNumber = buildNumber; m_Flags = 32768; CONSOLE_Print( "[REPLAY] building replay" ); uint32_t LanguageID = 0x0012F8B0; BYTEARRAY Replay; Replay.push_back( 16 ); // Unknown (4.0) Replay.push_back( 1 ); // Unknown (4.0) Replay.push_back( 0 ); // Unknown (4.0) Replay.push_back( 0 ); // Unknown (4.0) Replay.push_back( 0 ); // Host RecordID (4.1) Replay.push_back( m_HostPID ); // Host PlayerID (4.1) UTIL_AppendByteArrayFast( Replay, m_HostName ); // Host PlayerName (4.1) Replay.push_back( 1 ); // Host AdditionalSize (4.1) Replay.push_back( 0 ); // Host AdditionalData (4.1) UTIL_AppendByteArrayFast( Replay, gameName ); // GameName (4.2) Replay.push_back( 0 ); // Null (4.0) UTIL_AppendByteArrayFast( Replay, statString ); // StatString (4.3) UTIL_AppendByteArray( Replay, (uint32_t)m_Slots.size( ), false ); // PlayerCount (4.6) UTIL_AppendByteArray( Replay, m_MapGameType, false ); // GameType (4.7) UTIL_AppendByteArray( Replay, LanguageID, false ); // LanguageID (4.8) // PlayerList (4.9) for( vector<PIDPlayer> :: iterator i = m_Players.begin( ); i != m_Players.end( ); ++i ) { if( (*i).first != m_HostPID ) { Replay.push_back( 22 ); // Player RecordID (4.1) Replay.push_back( (*i).first ); // Player PlayerID (4.1) UTIL_AppendByteArrayFast( Replay, (*i).second ); // Player PlayerName (4.1) Replay.push_back( 1 ); // Player AdditionalSize (4.1) Replay.push_back( 0 ); // Player AdditionalData (4.1) UTIL_AppendByteArray( Replay, (uint32_t)0, false ); // Unknown } } // GameStartRecord (4.10) Replay.push_back( 25 ); // RecordID (4.10) UTIL_AppendByteArray( Replay, (uint16_t)( 7 + m_Slots.size( ) * 9 ), false ); // Size (4.10) Replay.push_back( m_Slots.size( ) ); // NumSlots (4.10) for( unsigned char i = 0; i < m_Slots.size( ); ++i ) UTIL_AppendByteArray( Replay, m_Slots[i].GetByteArray( ) ); UTIL_AppendByteArray( Replay, m_RandomSeed, false ); // RandomSeed (4.10) Replay.push_back( m_SelectMode ); // SelectMode (4.10) Replay.push_back( m_StartSpotCount ); // StartSpotCount (4.10) // ReplayData (5.0) Replay.push_back( REPLAY_FIRSTSTARTBLOCK ); UTIL_AppendByteArray( Replay, (uint32_t)1, false ); Replay.push_back( REPLAY_SECONDSTARTBLOCK ); UTIL_AppendByteArray( Replay, (uint32_t)1, false ); // leavers during loading need to be stored between the second and third start blocks while( !m_LoadingBlocks.empty( ) ) { UTIL_AppendByteArray( Replay, m_LoadingBlocks.front( ) ); m_LoadingBlocks.pop( ); } Replay.push_back( REPLAY_THIRDSTARTBLOCK ); UTIL_AppendByteArray( Replay, (uint32_t)1, false ); // done m_Decompressed = string( Replay.begin( ), Replay.end( ) ); m_Decompressed += m_CompiledBlocks; }
void CReplay :: BuildReplay( string gameName, string statString, uint32_t war3Version, uint16_t buildNumber ) { m_War3Version = war3Version; m_BuildNumber = buildNumber; m_Flags = 32768; CONSOLE_Print( "[REPLAY] building replay" ); uint32_t LanguageID = 0x0012F8B0; BYTEARRAY Replay; Replay.push_back( 16 ); // Unknown (4.0) Replay.push_back( 1 ); // Unknown (4.0) Replay.push_back( 0 ); // Unknown (4.0) Replay.push_back( 0 ); // Unknown (4.0) Replay.push_back( 0 ); // Host RecordID (4.1) Replay.push_back( m_HostPID ); // Host PlayerID (4.1) UTIL_AppendByteArrayFast( Replay, m_HostName ); // Host PlayerName (4.1) Replay.push_back( 1 ); // Host AdditionalSize (4.1) Replay.push_back( 0 ); // Host AdditionalData (4.1) UTIL_AppendByteArrayFast( Replay, gameName ); // GameName (4.2) Replay.push_back( 0 ); // Null (4.0) UTIL_AppendByteArrayFast( Replay, statString ); // StatString (4.3) UTIL_AppendByteArray( Replay, (uint32_t)m_Slots.size( ), false ); // PlayerCount (4.6) Replay.push_back( m_MapGameType ); // GameType (4.7) Replay.push_back( 32 ); // GameType (4.7) Replay.push_back( 73 ); // GameType (4.7) Replay.push_back( 0 ); // GameType (4.7) UTIL_AppendByteArray( Replay, LanguageID, false ); // LanguageID (4.8) // PlayerList (4.9) for( vector<PIDPlayer> :: iterator i = m_Players.begin( ); i != m_Players.end( ); i++ ) { if( (*i).first != m_HostPID ) { Replay.push_back( 22 ); // Player RecordID (4.1) Replay.push_back( (*i).first ); // Player PlayerID (4.1) UTIL_AppendByteArrayFast( Replay, (*i).second ); // Player PlayerName (4.1) Replay.push_back( 1 ); // Player AdditionalSize (4.1) Replay.push_back( 0 ); // Player AdditionalData (4.1) UTIL_AppendByteArray( Replay, (uint32_t)0, false ); // Unknown } } // GameStartRecord (4.10) Replay.push_back( 25 ); // RecordID (4.10) UTIL_AppendByteArray( Replay, (uint16_t)( 7 + m_Slots.size( ) * 9 ), false ); // Size (4.10) Replay.push_back( m_Slots.size( ) ); // NumSlots (4.10) for( unsigned char i = 0; i < m_Slots.size( ); i++ ) UTIL_AppendByteArray( Replay, m_Slots[i].GetByteArray( ) ); UTIL_AppendByteArray( Replay, m_RandomSeed, false ); // RandomSeed (4.10) Replay.push_back( m_SelectMode ); // SelectMode (4.10) Replay.push_back( m_StartSpotCount ); // StartSpotCount (4.10) // ReplayData (5.0) Replay.push_back( REPLAY_FIRSTSTARTBLOCK ); UTIL_AppendByteArray( Replay, (uint32_t)1, false ); Replay.push_back( REPLAY_SECONDSTARTBLOCK ); UTIL_AppendByteArray( Replay, (uint32_t)1, false ); // leavers during loading need to be stored between the second and third start blocks while( !m_LoadingBlocks.empty( ) ) { UTIL_AppendByteArray( Replay, m_LoadingBlocks.front( ) ); m_LoadingBlocks.pop( ); } Replay.push_back( REPLAY_THIRDSTARTBLOCK ); UTIL_AppendByteArray( Replay, (uint32_t)1, false ); // initialize replay length to zero // we'll accumulate the replay length as we iterate through the timeslots // this is necessary because we might be discarding some timeslots due to not enough checksums and the replay length needs to be accurate m_ReplayLength = 0; uint32_t TimeSlotsDiscarded = 0; bool EndOfTimeSlots = false; while( !m_Blocks.empty( ) ) { BYTEARRAY Block = m_Blocks.front( ); m_Blocks.pop( ); if( Block.size( ) >= 5 && Block[0] == REPLAY_TIMESLOT ) { uint16_t TimeIncrement = UTIL_ByteArrayToUInt16( Block, false, 3 ); if( TimeIncrement != 0 && m_CheckSums.empty( ) ) EndOfTimeSlots = true; if( EndOfTimeSlots ) { TimeSlotsDiscarded++; continue; } // append timeslot UTIL_AppendByteArrayFast( Replay, Block ); // append checksum // todotodo: after experimenting, Strilanc discovered that checksums are NOT required in replays // we could optimize saving of replays by building a complete stream without waiting for checksums as the game progresses // alternatively, we could build that stream as the checksums were added if we wanted to keep them // rather than building it in one go right now, which might take a few hundred ms and cause a spike in other games if( TimeIncrement != 0 ) { BYTEARRAY CheckSum; CheckSum.reserve( 6 ); CheckSum.push_back( REPLAY_CHECKSUM ); CheckSum.push_back( 4 ); UTIL_AppendByteArray( CheckSum, m_CheckSums.front( ), false ); m_CheckSums.pop( ); UTIL_AppendByteArrayFast( Replay, CheckSum ); } // accumulate replay length m_ReplayLength += TimeIncrement; } else UTIL_AppendByteArrayFast( Replay, Block ); } if( TimeSlotsDiscarded > 0 ) CONSOLE_Print( "[REPLAY] ran out of checksums, discarded " + UTIL_ToString( TimeSlotsDiscarded ) + " timeslots" ); // done m_Decompressed = string( Replay.begin( ), Replay.end( ) ); }