private DataBlock CreateParsedDataBlock(byte[] replayBytes, int currIndex, out int movedIndex) { DataBlock dataBlock = new DataBlock(); if (currIndex >= replayBytes.Length) { movedIndex = currIndex; return null; } ushort compressedDataBlockSize = ByteUtility.ReadWord(replayBytes, currIndex); currIndex += 2; ushort decompressedDataBlockSize = ByteUtility.ReadWord(replayBytes, currIndex); currIndex += 2; uint checkSum = ByteUtility.ReadDoubleWord(replayBytes, currIndex); currIndex += 4; byte[] compressedDataBlockBytes = replayBytes.SubArray(currIndex, compressedDataBlockSize); byte[] decompressedDataBlockBytes = Ionic.Zlib.ZlibStream.UncompressBuffer(compressedDataBlockBytes); dataBlock.CompressedDataBlockSize = compressedDataBlockSize; dataBlock.DecompressedDataBlockSize = decompressedDataBlockSize; dataBlock.CheckSum = checkSum; dataBlock.CompressedDataBlockBytes = compressedDataBlockBytes; dataBlock.DecompressedDataBlockBytes = decompressedDataBlockBytes; currIndex += compressedDataBlockSize; movedIndex = currIndex; return dataBlock; }
private string GetChatMessage(ReplayData replayData, ref int currIndex, byte[] gameData) { int playerId = gameData[currIndex]; currIndex++; PlayerInfo playerInfo = replayData.GetPlayerInfoByPlayerReplayId(playerId); if (playerInfo == null) throw new InvalidDataException( String.Format("Player Id Not found in method ParseGameReplayDataFromBlock. Input : {0}", playerId)); ushort numberOfBytes = ByteUtility.ReadWord(gameData, currIndex); currIndex += 2; byte checkFlag = gameData[currIndex]; currIndex++; uint chatMode = ByteUtility.ReadDoubleWord(gameData, currIndex); currIndex += 4; string chatMessage = String.Empty; //Mix Teams messes up team orientation //For now, not appending all or allied chat until better solution is found //if (chatMode == 0x00) // chatMessage += "[A]"; //else if (chatMode == 0x01) // chatMessage += "[T" + (playerInfo.Team + 1) + "]"; chatMessage += "[" + playerInfo.PlayerName + "]"; List<byte> encodedChatMessage = new List<byte>(); while (gameData[currIndex] != 0x00) { encodedChatMessage.Add(gameData[currIndex]); currIndex++; } chatMessage += Encoding.UTF8.GetString(encodedChatMessage.ToArray()); currIndex++; return chatMessage; }
private void ParseTimeSlotBlock(ReplayData replayData, ref int currIndex, ref uint accumulatedReplayTime, byte[] gameData) { ushort commandByteCount = ByteUtility.ReadWord(gameData, currIndex); currIndex += 2; if (commandByteCount < 2) throw new InvalidDataException(String.Format("Unexpected Command Byte Count (Min. 2). Input: {0}", commandByteCount)); accumulatedReplayTime += ByteUtility.ReadWord(gameData, currIndex); currIndex += 2; //Command Data not present if n = 2 int commandByteEndIndex = currIndex + commandByteCount - 2; while (currIndex < commandByteEndIndex) //A single command block may contain multiple actions { byte playerId = gameData[currIndex]; currIndex++; ushort actionBlockLength = ByteUtility.ReadWord(gameData, currIndex); currIndex += 2; ParseActionBlock(actionBlockLength, ref currIndex, gameData,replayData); } }
public ReplayHeader ParseReplayHeader(byte[] totalReplayBytes, out int currIndex) { currIndex = 0; byte[] replayHeaderBytes = totalReplayBytes.SubArray(0, HEADER_SIZE); ReplayHeader parsedReplayHeader = new ReplayHeader(replayHeaderBytes); //Check if the replay file starts with the expected header string of "Warcraft III recorded game" byte[] replayStartHeader = replayHeaderBytes.SubArray(0, REPLAY_HEADER_ST_BYTES.Length); if (!replayStartHeader.SequenceEqual(REPLAY_HEADER_ST_BYTES)) { throw new InvalidDataException(String.Format(ERROR_INVALID_HEADER_START_STRING, Encoding.UTF8.GetString(replayStartHeader))); } currIndex += REPLAY_HEADER_ST_BYTES.Length; //Move 26 characters forward //Get file offset uint replayFileOffset = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); if (replayFileOffset != FILE_OFFSET_FIRST_COMPRESSED_DATA_BLOCK) { throw new InvalidDataException(String.Format(ERROR_INVALID_REPLAY_FILE_OFFSET, BitConverter.ToString(BitConverter.GetBytes(replayFileOffset)))); } currIndex += 4; //Get overall size of compressed file uint compressedFileSize = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); parsedReplayHeader.CompressedFileSize = compressedFileSize; currIndex += 4; //Get replay version uint replayHeaderVersion = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); if (replayHeaderVersion != SUPPORTED_REPLAY_HEADER_VERSION) { throw new InvalidDataException(String.Format(ERROR_INVALID_REPLAY_HEADER_VERSION, BitConverter.ToString(BitConverter.GetBytes(replayHeaderVersion)))); } currIndex += 4; //Get overall size of decompressed file uint decompressedFileSize = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); parsedReplayHeader.DecompressedFileSize = decompressedFileSize; currIndex += 4; //Get total number of compressed data blocks in file uint compressedDataBlockCount = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); parsedReplayHeader.CompressedDataBlockCount = (int)compressedDataBlockCount; currIndex += 4; //Start SubHeaderParsing (Version 1) //Get version identifier (Classic, TFT) string clientType = ByteUtility.ReadDoubleWordString(replayHeaderBytes, currIndex); if (clientType != SUPPORTED_WC3_CLIENT_TYPE) { throw new InvalidDataException(String.Format(ERROR_INVALID_REPLAY_CLIENT_TYPE, clientType)); } currIndex += 4; //Get Client Version Number string replayVersion = ByteUtility.ReadDoubleWordString(replayHeaderBytes, currIndex); double replayVersionValue = 0; Double.TryParse(replayVersion, out replayVersionValue); //For some reason, replays generated by GHost doesn't follow the standard replay format //This part is commented out until I figure out what's going on //if (replayVersionValue < 1.0 && replayVersionValue > 2.0) //{ // throw new InvalidDataException(String.Format(ERROR_INVALID_REPLAY_VERSION, replayVersion)); //} parsedReplayHeader.ReplayVersion = replayVersion; currIndex += 4; ushort buildNumber = ByteUtility.ReadWord(replayHeaderBytes, currIndex); parsedReplayHeader.BuildNumber = buildNumber; currIndex += 2; ushort flag = ByteUtility.ReadWord(replayHeaderBytes, currIndex); if (flag != FLAG_MULTIPLAYER) { //throw new InvalidDataException(String.Format(ERROR_INVALID_GAME_TYPE, BitConverter.ToString(BitConverter.GetBytes(flag)))); } currIndex += 2; uint replayLength = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); parsedReplayHeader.ReplayLength = replayLength; currIndex += 4; uint checksum = ByteUtility.ReadDoubleWord(replayHeaderBytes, currIndex); parsedReplayHeader.CRC32 = checksum; currIndex += 4; return(parsedReplayHeader); }
//Precondition: datablock cannot be null //Parses Gamename, playerNames and the team indices of the player. private void ParseGameHeaderBlock(DataBlock dataBlock, ReplayData replayData, out int endIndex) { //Skip 4 (According to specification, first 4 bytes is unknown) int currIndex = 4; byte[] gameHeaderData = dataBlock.DecompressedDataBlockBytes; byte recordId = gameHeaderData[currIndex]; currIndex++; byte playerId = gameHeaderData[currIndex]; currIndex++; string playerName = ByteUtility.GetNullTerminatedString(gameHeaderData, currIndex, out currIndex); replayData.AddPlayerInfo(new PlayerInfo(playerName,playerId,recordId)); //Custom data byte. We can safely ignore this. Debug.Assert(gameHeaderData[currIndex] == 0x01); currIndex++; //Null byte. Ignore this as well Debug.Assert(gameHeaderData[currIndex] == 0x00); currIndex++; replayData.GameName = ByteUtility.GetNullTerminatedString(gameHeaderData, currIndex, out currIndex); //Null byte. Debug.Assert(gameHeaderData[currIndex] == 0x00); currIndex++; //Refers to Section 4.4, 4.5 (Game Settings, Map&Creator Name) //We don't actually need this information, but it's kept just in case we need it in the future string encodedString = ByteUtility.GetReplayEncodedString(gameHeaderData, currIndex, out currIndex); //According to spec, this is player count but in reality, it's mapslotcount uint mapSlotCount = ByteUtility.ReadDoubleWord(gameHeaderData, currIndex); currIndex += 4; //Game Type byte. Safely skip (Section 4.7) currIndex++; //Private Flag. Safely Skip (Section 4.7) currIndex++; //Unknown Word. Safely Skip (Section 4.7) currIndex += 2; //Language ID. Safely Skip (Section 4.8) currIndex += 4; //Loop until we find all players while (gameHeaderData[currIndex] == 0x16) { recordId = gameHeaderData[currIndex]; currIndex++; playerId = gameHeaderData[currIndex]; currIndex++; playerName = ByteUtility.GetNullTerminatedString(gameHeaderData, currIndex, out currIndex); replayData.AddPlayerInfo(new PlayerInfo(playerName, playerId, recordId)); //Custom data byte. We can safely ignore this. Debug.Assert(gameHeaderData[currIndex] == 0x01); currIndex++; //Skip 4 unknown bytes (Section 4.9) currIndex += 4; //Skip null byte while (gameHeaderData[currIndex] == 0x00) { currIndex++; } } //Skip Record Id (Section 4.10, always 0x19) currIndex++; //Number of data bytes following ushort dataByteCount = ByteUtility.ReadWord(gameHeaderData, currIndex); currIndex += 2; //number of available slots (For fate, always 12) int slotCount = gameHeaderData[currIndex]; currIndex++; //int slotRecordIndex = 0; for (int i = 0; i < slotCount; i++ ) { playerId = gameHeaderData[currIndex]; currIndex++; if (playerId == 0x00) //Computer player. Skip to next one { currIndex += 8; continue; } //Skip map download percent currIndex++; byte slotStatus = gameHeaderData[currIndex]; if (slotStatus == 0x00) //Empty slot. Skip to next one. { currIndex += 7; continue; } currIndex++; //Skip computer player flag currIndex++; byte teamNumber = gameHeaderData[currIndex]; PlayerInfo player = replayData.GetPlayerInfoByPlayerReplayId(playerId); if (player == null) throw new InvalidDataException("Player Id not found! ID: " + playerId); player.Team = teamNumber; replayData.PlayerCount++; currIndex++; //Skip rest of bytes (color, raceflags, AI strength, handicap) currIndex += 4; } //Skip randomseed (Section 4.12) currIndex += 4; //Skip selectMode byte selectMode = gameHeaderData[currIndex]; //For fate, Team & Race is not selectable. currIndex++; //Skip StartSpotCount currIndex++; endIndex = currIndex; }
private void ParseActionBlock(int actionBlockLength, ref int currIndex, byte[] gameData, ReplayData replayData) { actionBlockLength += currIndex; while(currIndex < actionBlockLength) { byte actionId = gameData[currIndex]; currIndex++; switch (actionId) { case 0x01: //Pause Game break; case 0x02: //Resume Game break; case 0x03: //Set game speed (Single Player) currIndex++; break; case 0x04: //Increase game speed (Single Player) break; case 0x05: //Decrease game speed (Single Player) break; case 0x06: //Save game string saveName = ByteUtility.GetNullTerminatedString(gameData, currIndex, out currIndex); break; case 0x07: //save game finished currIndex += 4; break; case 0x10: //Unit/Building Ability currIndex += 14; break; case 0x11: //Unit/Building Ability with target position currIndex += 22; break; case 0x12: //Unit/Building ability (With target position and target object id) currIndex += 30; break; case 0x13: //Give item to Unit / Drop item on ground currIndex += 38; break; case 0x14: //Unit building ability (With two target positions and two item ids) currIndex += 43; break; case 0x16: case 0x17: //Change selection, assign group hotkey currIndex++; //Select Mode ushort selectionCount = ByteUtility.ReadWord(gameData, currIndex); currIndex += 2; currIndex += selectionCount*8; //n * 8 bytes (Two DWORD repeated); break; case 0x18: //Select Group Hotkey currIndex += 2; break; case 0x19: //Select Subgroup currIndex += 12; break; case 0x1A: //Pre Subselection break; case 0x1B: //Unknown currIndex += 9; break; case 0x1C: //Select Ground Item currIndex += 9; break; case 0x1D: //Cancel Hero Revival currIndex += 8; break; case 0x1E: //Remove unit from building queue currIndex += 5; break; case 0x21: //unknown currIndex += 8; break; case 0x50: //Change ally options currIndex += 5; break; case 0x51: //Transfer Resources currIndex += 9; break; case 0x60: //Map Trigger Chat Command currIndex += 8; //Two Unknown Double Words List<byte> encodedChatBytes = new List<byte>(); while (gameData[currIndex] != 0x00) { encodedChatBytes.Add(gameData[currIndex]); currIndex++; } currIndex++; string chatString = Encoding.UTF8.GetString(encodedChatBytes.ToArray()); break; case 0x61: //ESC Pressed break; case 0x62: //Scenario Trigger currIndex += 12; break; case 0x66: //Choose hero skill submenu break; case 0x67: //Choose building submenu break; case 0x68: //Minimap Ping currIndex += 12; break; case 0x69: //Continue Game (Block B) currIndex += 16; break; case 0x6A: //Continue Game (Block A) currIndex += 16; break; case 0x75: //Unknown currIndex++; break; case 0x20: //Cheats case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x29: case 0x2A: case 0x2B: case 0x2C: case 0x2F: case 0x30: case 0x31: case 0x32: break; case 0x70: //SyncStoredInteger. The most important part. string gameCacheName = ByteUtility.GetNullTerminatedString(gameData, currIndex, out currIndex); string eventCategory = ByteUtility.GetNullTerminatedString(gameData, currIndex, out currIndex); string[] eventDetailId = ByteUtility.GetNullTerminatedString(gameData, currIndex, out currIndex).Split(new[] { "/" }, StringSplitOptions.None); string eventId = eventDetailId[0]; string eventDetail = string.Join("/",eventDetailId.Skip(1).Take(4)); _frsEventCallList.Add(new FRSEvent(eventId, gameCacheName,eventCategory,eventDetail)); break; default: throw new Exception(String.Format("Unexpected Action ID found at currIndex: {0} Input: {1}", currIndex-1, actionId)); } } }