Example #1
0
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;
}
Example #2
0
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;
}
Example #5
0
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;
}
Example #7
0
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;
}
Example #9
0
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;
}
Example #10
0
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( ) );
}